mirror of
https://github.com/supabase/supabase.git
synced 2026-10-18 15:49:09 +03:00
1 parent
7185112fdd
commit
9cfeb44b77
7 files changed
+180
-219
No files matched your search
@@ -34,9 +34,9 @@ export const ForeignKeyFormatter = (
|
||||
|
||||
return (
|
||||
<div className="sb-grid-foreign-key-formatter flex justify-between">
|
||||
<p className="sb-grid-foreign-key-formatter__text">
|
||||
<span className="sb-grid-foreign-key-formatter__text">
|
||||
{value === null ? <NullValue /> : value}
|
||||
</p>
|
||||
</span>
|
||||
<ForeignTableModal columnName={p.column.key} defaultValue={value} onChange={onChange} />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -134,7 +134,11 @@ export function ColumnHeader<R>({
|
||||
function renderColumnIcon(type: ColumnType) {
|
||||
switch (type) {
|
||||
case 'foreign_key':
|
||||
return <IconLink size="tiny" strokeWidth={2} />
|
||||
return (
|
||||
<div>
|
||||
<IconLink size="tiny" strokeWidth={2} />
|
||||
</div>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,141 +1,128 @@
|
||||
import * as React from 'react';
|
||||
import { useDispatch, useTrackedState } from '../../store';
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
IconDownload,
|
||||
IconPlus,
|
||||
IconX,
|
||||
IconTrash,
|
||||
Typography,
|
||||
} from '@supabase/ui';
|
||||
import FileSaver from 'file-saver';
|
||||
import FilterDropdown from './filter';
|
||||
import SortPopover from './sort';
|
||||
import StatusLabel from './StatusLabel';
|
||||
import RefreshButton from './RefreshButton';
|
||||
import { exportRowsToCsv } from '../../utils';
|
||||
import { showConfirmAlert } from '../common';
|
||||
import * as React from 'react'
|
||||
import { useDispatch, useTrackedState } from '../../store'
|
||||
import { Button, Divider, IconDownload, IconPlus, IconX, IconTrash, Typography } from '@supabase/ui'
|
||||
import FileSaver from 'file-saver'
|
||||
import FilterDropdown from './filter'
|
||||
import SortPopover from './sort'
|
||||
import StatusLabel from './StatusLabel'
|
||||
import RefreshButton from './RefreshButton'
|
||||
import { exportRowsToCsv } from '../../utils'
|
||||
import { showConfirmAlert } from '../common'
|
||||
|
||||
type HeaderProps = {
|
||||
onAddColumn?: () => void;
|
||||
onAddRow?: () => void;
|
||||
headerActions?: React.ReactNode;
|
||||
};
|
||||
onAddColumn?: () => void
|
||||
onAddRow?: () => void
|
||||
headerActions?: React.ReactNode
|
||||
}
|
||||
|
||||
const Header: React.FC<HeaderProps> = ({
|
||||
onAddColumn,
|
||||
onAddRow,
|
||||
headerActions,
|
||||
}) => {
|
||||
const state = useTrackedState();
|
||||
const { selectedRows } = state;
|
||||
const Header: React.FC<HeaderProps> = ({ onAddColumn, onAddRow, headerActions }) => {
|
||||
const state = useTrackedState()
|
||||
const { selectedRows } = state
|
||||
|
||||
return (
|
||||
<div className="sb-grid-header">
|
||||
<div className="sb-grid-header__inner">
|
||||
{selectedRows.size > 0 ? (
|
||||
<RowHeader />
|
||||
) : (
|
||||
<DefaultHeader onAddColumn={onAddColumn} onAddRow={onAddRow} />
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-scale-100 dark:bg-scale-300 flex h-10 justify-between px-5">
|
||||
{selectedRows.size > 0 ? (
|
||||
<RowHeader />
|
||||
) : (
|
||||
<DefaultHeader onAddColumn={onAddColumn} onAddRow={onAddRow} />
|
||||
)}
|
||||
<div className="sb-grid-header__inner">
|
||||
{headerActions}
|
||||
<StatusLabel />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Header;
|
||||
)
|
||||
}
|
||||
export default Header
|
||||
|
||||
type DefaultHeaderProps = {
|
||||
onAddColumn?: () => void;
|
||||
onAddRow?: () => void;
|
||||
};
|
||||
const DefaultHeader: React.FC<DefaultHeaderProps> = ({
|
||||
onAddColumn,
|
||||
onAddRow,
|
||||
}) => {
|
||||
onAddColumn?: () => void
|
||||
onAddRow?: () => void
|
||||
}
|
||||
const DefaultHeader: React.FC<DefaultHeaderProps> = ({ onAddColumn, onAddRow }) => {
|
||||
const renderNewColumn = (onAddColumn?: () => void) => {
|
||||
if (!onAddColumn) return null;
|
||||
return (
|
||||
<Button type="text" onClick={onAddColumn} style={{ padding: '4px 8px' }}>
|
||||
New Column
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const renderAddRow = (onAddRow?: () => void) => {
|
||||
if (!onAddRow) return null;
|
||||
if (!onAddColumn) return null
|
||||
return (
|
||||
<Button
|
||||
style={{ padding: '4px 8px' }}
|
||||
icon={<IconPlus size="tiny" />}
|
||||
type="text"
|
||||
onClick={onAddColumn}
|
||||
// style={{ padding: '4px 8px' }}
|
||||
>
|
||||
New Column
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
const renderAddRow = (onAddRow?: () => void) => {
|
||||
if (!onAddRow) return null
|
||||
return (
|
||||
<Button
|
||||
size="tiny"
|
||||
// style={{ padding: '4px 8px' }}
|
||||
icon={<IconPlus size={14} strokeWidth={2} />}
|
||||
onClick={onAddRow}
|
||||
>
|
||||
Insert row
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<RefreshButton />
|
||||
<FilterDropdown />
|
||||
<SortPopover />
|
||||
<Divider
|
||||
light
|
||||
type="vertical"
|
||||
className="sb-grid-header__inner__divider"
|
||||
/>
|
||||
{renderNewColumn(onAddColumn)}
|
||||
{renderAddRow(onAddRow)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-1">
|
||||
<RefreshButton />
|
||||
<FilterDropdown />
|
||||
<SortPopover />
|
||||
</div>
|
||||
<div className="bg-scale-600 h-[50%] w-px"></div>
|
||||
<div className="flex items-center gap-2">
|
||||
{renderNewColumn(onAddColumn)}
|
||||
{renderAddRow(onAddRow)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type RowHeaderProps = {};
|
||||
type RowHeaderProps = {}
|
||||
const RowHeader: React.FC<RowHeaderProps> = ({}) => {
|
||||
const state = useTrackedState();
|
||||
const dispatch = useDispatch();
|
||||
const state = useTrackedState()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const { selectedRows, rows: allRows, editable } = state;
|
||||
const { selectedRows, rows: allRows, editable } = state
|
||||
|
||||
const onRowsDelete = () => {
|
||||
showConfirmAlert({
|
||||
title: 'Confirm to delete',
|
||||
message:
|
||||
'Are you sure you want to delete the selected rows? This action cannot be undone.',
|
||||
message: 'Are you sure you want to delete the selected rows? This action cannot be undone.',
|
||||
onConfirm: async () => {
|
||||
const rowIdxs = Array.from(selectedRows) as number[];
|
||||
const rows = allRows.filter((x) => rowIdxs.includes(x.idx));
|
||||
const { error } = state.rowService!.delete(rows);
|
||||
const rowIdxs = Array.from(selectedRows) as number[]
|
||||
const rows = allRows.filter((x) => rowIdxs.includes(x.idx))
|
||||
const { error } = state.rowService!.delete(rows)
|
||||
if (error) {
|
||||
if (state.onError) state.onError(error);
|
||||
if (state.onError) state.onError(error)
|
||||
} else {
|
||||
dispatch({ type: 'REMOVE_ROWS', payload: { rowIdxs } });
|
||||
dispatch({ type: 'REMOVE_ROWS', payload: { rowIdxs } })
|
||||
dispatch({
|
||||
type: 'SELECTED_ROWS_CHANGE',
|
||||
payload: { selectedRows: new Set() },
|
||||
});
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
function onRowsExportCsv() {
|
||||
const rows = allRows.filter((x) => selectedRows.has(x.idx));
|
||||
const csv = exportRowsToCsv(state.table!.columns, rows);
|
||||
const csvData = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
FileSaver.saveAs(csvData, `${state.table!.name}_rows.csv`);
|
||||
const rows = allRows.filter((x) => selectedRows.has(x.idx))
|
||||
const csv = exportRowsToCsv(state.table!.columns, rows)
|
||||
const csvData = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
|
||||
FileSaver.saveAs(csvData, `${state.table!.name}_rows.csv`)
|
||||
}
|
||||
|
||||
function deselectRows() {
|
||||
dispatch({
|
||||
type: 'SELECTED_ROWS_CHANGE',
|
||||
payload: { selectedRows: new Set() },
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -147,11 +134,7 @@ const RowHeader: React.FC<RowHeaderProps> = ({}) => {
|
||||
onClick={deselectRows}
|
||||
/>
|
||||
<div>
|
||||
<Typography.Text
|
||||
small
|
||||
type="secondary"
|
||||
className="row_header__selected-rows"
|
||||
>
|
||||
<Typography.Text small type="secondary" className="row_header__selected-rows">
|
||||
{selectedRows.size > 1
|
||||
? `${selectedRows.size} rows selected`
|
||||
: `${selectedRows.size} row selected`}
|
||||
@@ -160,7 +143,8 @@ const RowHeader: React.FC<RowHeaderProps> = ({}) => {
|
||||
<Button
|
||||
type="primary"
|
||||
size="tiny"
|
||||
style={{ padding: '4px 8px' }}
|
||||
// style={{ padding: '4px 8px' }}
|
||||
|
||||
icon={<IconDownload />}
|
||||
onClick={onRowsExportCsv}
|
||||
>
|
||||
@@ -168,15 +152,12 @@ const RowHeader: React.FC<RowHeaderProps> = ({}) => {
|
||||
</Button>
|
||||
{editable && (
|
||||
<>
|
||||
<Divider
|
||||
type="vertical"
|
||||
className="sb-grid-header__inner__divider"
|
||||
light
|
||||
/>
|
||||
<Divider type="vertical" className="sb-grid-header__inner__divider" light />
|
||||
<Button
|
||||
type="default"
|
||||
size="tiny"
|
||||
style={{ padding: '4px 8px' }}
|
||||
// style={{ padding: '4px 8px' }}
|
||||
|
||||
icon={<IconTrash size="tiny" />}
|
||||
onClick={onRowsDelete}
|
||||
>
|
||||
@@ -187,5 +168,5 @@ const RowHeader: React.FC<RowHeaderProps> = ({}) => {
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
@@ -1,32 +1,36 @@
|
||||
import * as React from 'react';
|
||||
import { Button, IconRefreshCw } from '@supabase/ui';
|
||||
import { useDispatch, useTrackedState } from '../../store';
|
||||
import { fetchCount, fetchPage } from '../../utils';
|
||||
import * as React from 'react'
|
||||
import { Button, IconRefreshCw } from '@supabase/ui'
|
||||
import { useDispatch, useTrackedState } from '../../store'
|
||||
import { fetchCount, fetchPage } from '../../utils'
|
||||
|
||||
type RefreshButtonProps = {};
|
||||
type RefreshButtonProps = {}
|
||||
|
||||
const RefreshButton: React.FC<RefreshButtonProps> = ({}) => {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const state = useTrackedState();
|
||||
const dispatch = useDispatch();
|
||||
const [loading, setLoading] = React.useState(false)
|
||||
const state = useTrackedState()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
async function onClick() {
|
||||
setLoading(true);
|
||||
await fetchCount(state, dispatch);
|
||||
await fetchPage(state, dispatch);
|
||||
setLoading(false);
|
||||
setLoading(true)
|
||||
await fetchCount(state, dispatch)
|
||||
await fetchPage(state, dispatch)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="text"
|
||||
style={{ padding: '4px 8px' }}
|
||||
icon={<IconRefreshCw />}
|
||||
// style={{ padding: '4px 8px' }}
|
||||
icon={
|
||||
<div className="text-scale-900">
|
||||
<IconRefreshCw strokeWidth={1.5} />
|
||||
</div>
|
||||
}
|
||||
onClick={onClick}
|
||||
loading={loading}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
export default RefreshButton;
|
||||
)
|
||||
}
|
||||
export default RefreshButton
|
||||
@@ -1,40 +1,37 @@
|
||||
import React, { FC } from 'react';
|
||||
import { Button, IconPlus, IconFilter, Popover } from '@supabase/ui';
|
||||
import { useDispatch, useTrackedState } from '../../../store';
|
||||
import FilterRow from './FilterRow';
|
||||
import React, { FC } from 'react'
|
||||
import { Button, IconPlus, IconFilter, Popover } from '@supabase/ui'
|
||||
import { useDispatch, useTrackedState } from '../../../store'
|
||||
import FilterRow from './FilterRow'
|
||||
|
||||
const FilterPopover: FC = () => {
|
||||
const state = useTrackedState();
|
||||
const state = useTrackedState()
|
||||
const btnText =
|
||||
state.filters.length > 0
|
||||
? `Filtered by ${state.filters.length} rule${
|
||||
state.filters.length > 1 ? 's' : ''
|
||||
}`
|
||||
: 'Filter';
|
||||
? `Filtered by ${state.filters.length} rule${state.filters.length > 1 ? 's' : ''}`
|
||||
: 'Filter'
|
||||
|
||||
return (
|
||||
<Popover
|
||||
size="large"
|
||||
align="start"
|
||||
className="sb-grid-filter-popover"
|
||||
overlay={<Filter />}
|
||||
>
|
||||
<Popover size="large" align="start" className="sb-grid-filter-popover" overlay={<Filter />}>
|
||||
<Button
|
||||
as={'span'}
|
||||
type="text"
|
||||
icon={<IconFilter />}
|
||||
style={{ padding: '4px 8px' }}
|
||||
icon={
|
||||
<div className="text-scale-900">
|
||||
<IconFilter strokeWidth={1.5} />
|
||||
</div>
|
||||
}
|
||||
// style={{ padding: '4px 8px' }}
|
||||
>
|
||||
{btnText}
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
export default FilterPopover;
|
||||
)
|
||||
}
|
||||
export default FilterPopover
|
||||
|
||||
const Filter: FC = () => {
|
||||
const state = useTrackedState();
|
||||
const dispatch = useDispatch();
|
||||
const state = useTrackedState()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
function onAddFilter() {
|
||||
dispatch({
|
||||
@@ -44,27 +41,19 @@ const Filter: FC = () => {
|
||||
operator: '=',
|
||||
value: '',
|
||||
},
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2 py-2">
|
||||
<div className="space-y-2">
|
||||
{state.filters.map((_, index) => (
|
||||
<FilterRow
|
||||
key={`filter-${index}`}
|
||||
filterIdx={index}
|
||||
now={Date.now()}
|
||||
/>
|
||||
<FilterRow key={`filter-${index}`} filterIdx={index} now={Date.now()} />
|
||||
))}
|
||||
{state.filters.length == 0 && (
|
||||
<div className="space-y-1 px-3">
|
||||
<h5 className="text-sm text-scale-1100">
|
||||
No filters applied to this view
|
||||
</h5>
|
||||
<p className="text-xs text-scale-900">
|
||||
Add a column below to filter the view
|
||||
</p>
|
||||
<h5 className="text-scale-1100 text-sm">No filters applied to this view</h5>
|
||||
<p className="text-scale-900 text-xs">Add a column below to filter the view</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -75,5 +64,5 @@ const Filter: FC = () => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
@@ -1,70 +1,63 @@
|
||||
import React, { FC } from 'react';
|
||||
import { Button, IconList, IconChevronDown, Popover } from '@supabase/ui';
|
||||
import { DropdownControl } from '../../common';
|
||||
import { useDispatch, useTrackedState } from '../../../store';
|
||||
import SortRow from './SortRow';
|
||||
import React, { FC } from 'react'
|
||||
import { Button, IconList, IconChevronDown, Popover } from '@supabase/ui'
|
||||
import { DropdownControl } from '../../common'
|
||||
import { useDispatch, useTrackedState } from '../../../store'
|
||||
import SortRow from './SortRow'
|
||||
|
||||
const SortPopover: FC = () => {
|
||||
const state = useTrackedState();
|
||||
const state = useTrackedState()
|
||||
const btnText =
|
||||
state.sorts.length > 0
|
||||
? `Sorted by ${state.sorts.length} rule${
|
||||
state.sorts.length > 1 ? 's' : ''
|
||||
}`
|
||||
: 'Sort';
|
||||
? `Sorted by ${state.sorts.length} rule${state.sorts.length > 1 ? 's' : ''}`
|
||||
: 'Sort'
|
||||
|
||||
return (
|
||||
<Popover
|
||||
size="large"
|
||||
align="start"
|
||||
className="sb-grid-sort-popover"
|
||||
overlay={<Sort />}
|
||||
>
|
||||
<Popover size="large" align="start" className="sb-grid-sort-popover" overlay={<Sort />}>
|
||||
<Button
|
||||
as={'span'}
|
||||
type="text"
|
||||
icon={<IconList />}
|
||||
style={{ padding: '4px 8px' }}
|
||||
icon={
|
||||
<div className="text-scale-900">
|
||||
<IconList strokeWidth={1.5} />
|
||||
</div>
|
||||
}
|
||||
// style={{ padding: '4px 8px' }}
|
||||
>
|
||||
{btnText}
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
export default SortPopover;
|
||||
)
|
||||
}
|
||||
export default SortPopover
|
||||
|
||||
const Sort: FC = () => {
|
||||
const state = useTrackedState();
|
||||
const dispatch = useDispatch();
|
||||
const state = useTrackedState()
|
||||
const dispatch = useDispatch()
|
||||
const columns = state?.table?.columns!.filter((x) => {
|
||||
const found = state.sorts.find((y) => y.column == x.name);
|
||||
return !found;
|
||||
});
|
||||
const found = state.sorts.find((y) => y.column == x.name)
|
||||
return !found
|
||||
})
|
||||
const dropdownOptions =
|
||||
columns?.map((x) => {
|
||||
return { value: x.name, label: x.name };
|
||||
}) || [];
|
||||
return { value: x.name, label: x.name }
|
||||
}) || []
|
||||
|
||||
function onAddSort(columnName: string | number) {
|
||||
dispatch({
|
||||
type: 'ADD_SORT',
|
||||
payload: { column: columnName, ascending: true },
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="py-2 space-y-2">
|
||||
<div className="space-y-2 py-2">
|
||||
{state.sorts.map((x, index) => (
|
||||
<SortRow key={x.column} columnName={x.column} index={index} />
|
||||
))}
|
||||
{state.sorts.length == 0 && (
|
||||
<div className="space-y-1 px-3">
|
||||
<h5 className="text-sm text-scale-1100">
|
||||
No sorts applied to this view
|
||||
</h5>
|
||||
<p className="text-xs text-scale-900">
|
||||
Add a column below to sort the view
|
||||
</p>
|
||||
<h5 className="text-scale-1100 text-sm">No sorts applied to this view</h5>
|
||||
<p className="text-scale-900 text-xs">Add a column below to sort the view</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -83,15 +76,13 @@ const Sort: FC = () => {
|
||||
iconRight={<IconChevronDown />}
|
||||
className="sb-grid-dropdown__item-trigger"
|
||||
>
|
||||
{`Pick ${
|
||||
state.sorts.length > 1 ? 'another' : 'a'
|
||||
} column to sort by`}
|
||||
{`Pick ${state.sorts.length > 1 ? 'another' : 'a'} column to sort by`}
|
||||
</Button>
|
||||
</DropdownControl>
|
||||
) : (
|
||||
<p className="text-sm text-scale-1100">All columns have been added</p>
|
||||
<p className="text-scale-1100 text-sm">All columns have been added</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
+5
-13
@@ -10,7 +10,7 @@
|
||||
.rdg-cell {
|
||||
@apply flex;
|
||||
@apply border-scale-400 dark:border-scale-500 border-r border-b text-sm;
|
||||
@apply text-typography-body-light dark:text-typography-body-dark;
|
||||
@apply text-scale-1200;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -129,12 +129,6 @@
|
||||
@apply hover:bg-bg-secondary-light dark:hover:bg-bg-secondary-dark;
|
||||
}
|
||||
|
||||
.rdg-row .rdg-cell span,
|
||||
/* .rdg-row .rdg-cell svg, */
|
||||
.rdg-row .rdg-cell div {
|
||||
@apply text-typography-body-light dark:text-typography-body-dark;
|
||||
}
|
||||
|
||||
.tab-cursor {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
@@ -466,18 +460,17 @@
|
||||
}
|
||||
|
||||
.sb-grid-column-header__inner {
|
||||
@apply flex items-center space-x-2 overflow-hidden overflow-ellipsis;
|
||||
@apply flex items-center gap-2 overflow-hidden overflow-ellipsis;
|
||||
}
|
||||
|
||||
.sb-grid-column-header__inner__name {
|
||||
@apply overflow-hidden overflow-ellipsis text-sm;
|
||||
@apply text-typography-body-light dark:text-typography-body-dark;
|
||||
@apply text-scale-1200 overflow-hidden overflow-ellipsis text-xs;
|
||||
}
|
||||
|
||||
.sb-grid-column-header__inner__format {
|
||||
@apply text-xs;
|
||||
@apply overflow-hidden overflow-ellipsis font-normal;
|
||||
@apply text-typography-body-secondary-light dark:text-typography-body-secondary-dark;
|
||||
@apply text-scale-1000;
|
||||
}
|
||||
|
||||
.sb-grid-column-header__inner__primary-key {
|
||||
@@ -485,8 +478,7 @@
|
||||
}
|
||||
|
||||
.sb-grid-column-header__inner svg {
|
||||
@apply mr-1;
|
||||
@apply border-green-900 text-green-900 dark:border-green-900 dark:text-green-900;
|
||||
@apply border-brand-900 text-brand-900 dark:border-brand-900 dark:text-brand-900;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Reference in new issue
Block a user