release: bump version to 0.65.0
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
release / images (push) Has been cancelled
ci / delivery (push) Has been cancelled
ci / backend (pull_request) Has been cancelled
ci / frontend (pull_request) Has been cancelled
ci / delivery (pull_request) Has been cancelled

This commit is contained in:
rayd1o
2026-05-21 05:41:49 +08:00
parent 37e92e7572
commit 65e6a96c0d
27 changed files with 459 additions and 86 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "planet-frontend",
"version": "0.64.0",
"version": "0.65.0",
"private": true,
"packageManager": "bun@1",
"dependencies": {

View File

@@ -16,6 +16,13 @@ interface DataTableProps<TData> {
data: TData[]
getRowId?: (row: TData, index: number) => string
getRowClassName?: (row: TData) => string | undefined
selection?: {
selectedRowIds: Set<string>
onToggleAllVisible: (rowIds: string[]) => void
onToggleRow: (rowId: string, row: TData) => void
getCheckboxLabel?: (row: TData) => string
isRowSelectable?: (row: TData) => boolean
}
loading?: boolean
emptyText?: string
className?: string
@@ -28,6 +35,7 @@ export function DataTable<TData>({
data,
getRowId,
getRowClassName,
selection,
loading = false,
emptyText = '暂无数据',
className = '',
@@ -46,6 +54,13 @@ export function DataTable<TData>({
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
})
const visibleSelectableRows = selection
? table.getRowModel().rows.filter((row) => selection.isRowSelectable?.(row.original) ?? true)
: []
const visibleSelectableIds = visibleSelectableRows.map((row) => row.id)
const allVisibleSelected = visibleSelectableIds.length > 0 && visibleSelectableIds.every((rowId) => selection?.selectedRowIds.has(rowId))
const someVisibleSelected = visibleSelectableIds.some((rowId) => selection?.selectedRowIds.has(rowId))
const columnCount = columns.length + (selection ? 1 : 0)
return (
<div className={`an-data-table ${className}`}>
@@ -55,6 +70,20 @@ export function DataTable<TData>({
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{selection ? (
<th className="an-data-table__selection-cell">
<input
type="checkbox"
aria-label="选择当前可见数据"
checked={allVisibleSelected}
disabled={!visibleSelectableIds.length}
ref={(element) => {
if (element) element.indeterminate = someVisibleSelected && !allVisibleSelected
}}
onChange={() => selection.onToggleAllVisible(visibleSelectableIds)}
/>
</th>
) : null}
{headerGroup.headers.map((header) => {
const sorted = header.column.getIsSorted()
const stickyEnd = header.column.id === 'actions' || header.column.id === 'action'
@@ -82,7 +111,7 @@ export function DataTable<TData>({
<tbody>
{loading ? (
<tr>
<td colSpan={columns.length}>
<td colSpan={columnCount}>
<div className="an-data-table__state">
<span className="an-spinner" />
@@ -97,6 +126,18 @@ export function DataTable<TData>({
onClick={onRowClick ? () => onRowClick(row.original) : undefined}
data-clickable={onRowClick ? 'true' : undefined}
>
{selection ? (
<td className="an-data-table__selection-cell">
<input
type="checkbox"
aria-label={selection.getCheckboxLabel?.(row.original) || '选择行'}
checked={selection.selectedRowIds.has(row.id)}
disabled={selection.isRowSelectable ? !selection.isRowSelectable(row.original) : false}
onClick={(event) => event.stopPropagation()}
onChange={() => selection.onToggleRow(row.id, row.original)}
/>
</td>
) : null}
{row.getVisibleCells().map((cell) => (
<td key={cell.id} data-sticky-end={cell.column.id === 'actions' || cell.column.id === 'action' || undefined}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
@@ -106,7 +147,7 @@ export function DataTable<TData>({
))
) : (
<tr>
<td colSpan={columns.length}>
<td colSpan={columnCount}>
<div className="an-data-table__state">{emptyText}</div>
</td>
</tr>

View File

@@ -42,6 +42,14 @@ export function AdminThemeProvider({ children }: { children: ReactNode }) {
return () => mediaQuery.removeEventListener('change', handleChange)
}, [])
useEffect(() => {
if (typeof document === 'undefined') return
document.body.dataset.adminNextTheme = theme
return () => {
delete document.body.dataset.adminNextTheme
}
}, [theme])
const value = useMemo(() => ({ mode, theme, setMode }), [mode, setMode, theme])
return (

View File

@@ -11,6 +11,7 @@ import {
FileText,
Globe2,
ImageUp,
ListChecks,
Radio,
Redo2,
RefreshCw,
@@ -527,12 +528,20 @@ function ModuleTable({
selected,
onSelect,
columns,
selection,
loading,
}: {
rows: TableRecord[]
selected: TableRecord | null
onSelect: (record: TableRecord) => void
columns?: Array<ColumnDef<TableRecord>>
selection?: {
selectedRowIds: Set<string>
onToggleAllVisible: (rowIds: string[]) => void
onToggleRow: (rowId: string, row: TableRecord) => void
getCheckboxLabel?: (row: TableRecord) => string
isRowSelectable?: (row: TableRecord) => boolean
}
loading?: boolean
}) {
const tableColumns = useMemo(() => columns || defaultColumns(onSelect), [columns, onSelect])
@@ -546,6 +555,7 @@ function ModuleTable({
getRowId={(row) => row.__rowId}
loading={loading}
onRowClick={onSelect}
selection={selection}
/>
)
}
@@ -2025,6 +2035,8 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
const [rowActionLoading, setRowActionLoading] = useState<Record<string, boolean>>({})
const [collectionQueue, setCollectionQueue] = useState<CollectionQueueItem[]>([])
const [collectionQueueOpen, setCollectionQueueOpen] = useState(false)
const collectionQueueRef = useRef<HTMLDivElement>(null)
const [datasourceSelectedRowIds, setDatasourceSelectedRowIds] = useState<Set<string>>(() => new Set())
const pendingDatasourceTasksRef = useRef<Record<string, { sourceId: string; source?: string; name: string; taskId?: number | string | null; completed?: boolean }>>({})
const completedDatasourceTasksRef = useRef<Set<string>>(new Set())
const datasourcePollTimersRef = useRef<Record<string, number>>({})
@@ -2038,11 +2050,8 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
const [visibleSecretFields, setVisibleSecretFields] = useState<Record<string, boolean>>({})
const [smtpTestEmail, setSmtpTestEmail] = useState('')
const [brandUploadFile, setBrandUploadFile] = useState<File | null>(null)
const [batchTriggerOpen, setBatchTriggerOpen] = useState(false)
const [resolveTarget, setResolveTarget] = useState<TableRecord | null>(null)
const [resolutionText, setResolutionText] = useState('已处理')
const [batchTriggerIds, setBatchTriggerIds] = useState('')
const [batchTriggerForce, setBatchTriggerForce] = useState(false)
const [credentialGuide, setCredentialGuide] = useState<AnyRecord | null>(null)
const [credentialGuideOpen, setCredentialGuideOpen] = useState(false)
const [credentialGuidePosition, setCredentialGuidePosition] = useState<{ x: number; y: number } | null>(null)
@@ -2095,6 +2104,23 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
window.addEventListener('pointerup', stopMove, { once: true })
}
useEffect(() => {
if (!collectionQueueOpen) return
const handlePointerDown = (event: MouseEvent) => {
if (collectionQueueRef.current?.contains(event.target as Node)) return
setCollectionQueueOpen(false)
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') setCollectionQueueOpen(false)
}
document.addEventListener('mousedown', handlePointerDown)
document.addEventListener('keydown', handleKeyDown)
return () => {
document.removeEventListener('mousedown', handlePointerDown)
document.removeEventListener('keydown', handleKeyDown)
}
}, [collectionQueueOpen])
useEffect(() => {
datasourceFiltersRef.current = datasourceFilters
if (config === configs.datasources) {
@@ -2105,19 +2131,19 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
useEffect(() => {
if (config !== configs.datasources) return
const next = datasourceFiltersFromSearch(location.search)
setDatasourceFilters((current) => (
current.product === next.product &&
const current = datasourceFiltersRef.current
const unchanged = current.product === next.product &&
current.module === next.module &&
current.isActive === next.isActive &&
current.runStatus === next.runStatus &&
current.dataStatus === next.dataStatus
? current
: next
))
if (!unchanged) setDatasourceSelectedRowIds(new Set())
setDatasourceFilters((filters) => unchanged ? filters : next)
}, [config, location.search])
const setDatasourceFilter = (key: keyof DatasourceFilters, value: string) => {
const next = { ...datasourceFiltersRef.current, [key]: value }
setDatasourceSelectedRowIds(new Set())
setDatasourceFilters(next)
const params = new URLSearchParams(location.search)
const queryKeyByFilter: Record<keyof DatasourceFilters, string> = {
@@ -2150,6 +2176,9 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
const load = useCallback(async (sectionKey = activeSectionKey) => {
const section = config.sections.find((item) => item.key === sectionKey) ?? config.sections[0]
if (!section) return
if (config === configs.datasources && section.key === 'builtin') {
setDatasourceSelectedRowIds(new Set())
}
setLoading(true)
try {
const result = await (async (): Promise<SectionState> => {
@@ -2460,6 +2489,42 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
const activeSection = useMemo(() => config.sections.find((section) => section.key === activeSectionKey) ?? config.sections[0], [activeSectionKey, config.sections])
const activeState = useMemo(() => states.find((state) => state.section.key === activeSection?.key), [activeSection?.key, states])
const rows = activeState?.rows ?? []
const isDatasourceBuiltinSection = config === configs.datasources && activeSection?.key === 'builtin'
const selectedDatasourceRows = useMemo(
() => isDatasourceBuiltinSection ? rows.filter((row) => datasourceSelectedRowIds.has(row.__rowId)) : [],
[datasourceSelectedRowIds, isDatasourceBuiltinSection, rows],
)
const selectedDatasourceIds = useMemo(
() => selectedDatasourceRows
.map((row) => Number(row.id))
.filter((id) => Number.isFinite(id) && id > 0),
[selectedDatasourceRows],
)
const toggleDatasourceSelection = useCallback((rowId: string) => {
setDatasourceSelectedRowIds((current) => {
const next = new Set(current)
if (next.has(rowId)) {
next.delete(rowId)
} else {
next.add(rowId)
}
return next
})
}, [])
const toggleAllVisibleDatasourceSelection = useCallback((rowIds: string[]) => {
setDatasourceSelectedRowIds((current) => {
const next = new Set(current)
const allSelected = rowIds.length > 0 && rowIds.every((rowId) => next.has(rowId))
rowIds.forEach((rowId) => {
if (allSelected) {
next.delete(rowId)
} else {
next.add(rowId)
}
})
return next
})
}, [])
const summary = sectionSummary(states)
const collectionQueueSummary = useMemo(() => {
const running = collectionQueue.filter((item) => item.status === 'queued' || item.status === 'running').length
@@ -2485,6 +2550,7 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
const handleSectionChange = (key: string) => {
setActiveSectionKey(key)
setDatasourceSelectedRowIds(new Set())
setActiveGroupKey('')
setHierarchyDraft('')
setTvDraftGroup(null)
@@ -2637,20 +2703,17 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
}
}
const submitBatchTrigger = async () => {
const sourceIds = batchTriggerIds
.split(/[\s,;]+/)
.map((item) => Number(item.trim()))
.filter((item) => Number.isFinite(item) && item > 0)
const triggerSelectedDatasources = async () => {
const sourceIds = selectedDatasourceIds
if (!sourceIds.length) {
toast({ title: '请输入数据源 ID', description: '可以用逗号、空格或换行分隔。', tone: 'error' })
toast({ title: '请先勾选数据源', description: '勾选内置源后,主触发按钮会只触发所选数据源。', tone: 'error' })
return
}
setActionLoading(true)
try {
const response = await axios.post(apiPath('/datasources/trigger-batch'), {
source_ids: sourceIds,
force: batchTriggerForce,
force: false,
})
addBatchQueueResult(response.data)
arrayAt(response.data, 'triggered').forEach((item) => {
@@ -2665,15 +2728,23 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
__status: '已提交',
__metric: `${sourceIds.length} 个数据源`,
}])
setBatchTriggerOpen(false)
toast({ title: '批量触发已提交', tone: 'success' })
setDatasourceSelectedRowIds(new Set())
toast({ title: '已触发所选数据源', description: `${sourceIds.length} 个数据源已提交。`, tone: 'success' })
} catch (error) {
toast({ title: '批量触发失败', description: error instanceof Error ? error.message : '接口请求失败', tone: 'error' })
toast({ title: '触发已选失败', description: error instanceof Error ? error.message : '接口请求失败', tone: 'error' })
} finally {
setActionLoading(false)
}
}
const triggerDatasourcePrimaryAction = async () => {
if (selectedDatasourceIds.length) {
await triggerSelectedDatasources()
return
}
await triggerAllDatasources()
}
const triggerAllDatasources = async () => {
setActionLoading(true)
try {
@@ -4880,12 +4951,17 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
const renderModuleActions = () => {
const actions: ReactNode[] = []
if (config === configs.datasources) {
const selectedCount = selectedDatasourceIds.length
actions.push(
<Button key="trigger-all" variant="primary" icon="trigger" onClick={() => void triggerAllDatasources()} loading={actionLoading} title="触发全部数据源">
</Button>,
<Button key="trigger-batch" size="icon" variant="subtle" onClick={() => setBatchTriggerOpen(true)} loading={actionLoading} title="批量触发数据源" aria-label="批量触发数据源">
<DatabaseZap size={15} />
<Button
key="trigger-primary"
variant="primary"
icon="trigger"
onClick={() => void triggerDatasourcePrimaryAction()}
loading={actionLoading}
title={selectedCount ? `触发已选 ${selectedCount} 个数据源` : '触发全部数据源'}
>
{selectedCount ? `触发已选 ${selectedCount}` : '触发全部'}
</Button>,
)
}
@@ -5035,8 +5111,8 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
void triggerDatasourceWithPrecheck(record)
}
const renderCollectionQueue = () => {
if (config !== configs.datasources || !collectionQueueSummary.total) return null
const renderCollectionQueuePanel = () => {
if (config !== configs.datasources) return null
const groups: Array<{ key: string; title: string; items: CollectionQueueItem[] }> = [
{ key: 'running', title: '运行中', items: collectionQueue.filter((item) => item.status === 'queued' || item.status === 'running') },
{ key: 'failed', title: '失败', items: collectionQueue.filter((item) => item.status === 'failed') },
@@ -5055,9 +5131,6 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
<span> {collectionQueueSummary.skipped}</span>
</div>
<div className="an-collection-queue__actions">
<Button size="sm" variant="subtle" onClick={() => setCollectionQueueOpen((open) => !open)}>
{collectionQueueOpen ? '收起队列' : '查看队列'}
</Button>
{collectionQueueSummary.running === 0 ? (
<Button size="icon" variant="subtle" title="清空已结束队列项" aria-label="清空已结束队列项" onClick={() => setCollectionQueue((items) => items.filter((item) => item.status === 'queued' || item.status === 'running'))}>
<X size={14} />
@@ -5068,7 +5141,7 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
<div className="an-collection-queue__track" aria-hidden="true">
<span style={{ width: `${collectionQueueSummary.progress}%` }} />
</div>
{collectionQueueOpen ? (
{collectionQueueSummary.total ? (
<div className="an-collection-queue__panel">
{groups.map((group) => (
<section key={group.key} className="an-collection-queue__group">
@@ -5097,6 +5170,43 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
</section>
))}
</div>
) : (
<p className="an-collection-queue__empty an-collection-queue__empty--panel"></p>
)}
</div>
)
}
const renderCollectionQueueAction = () => {
if (config !== configs.datasources) return null
const hasQueue = collectionQueueSummary.total > 0
const queueLabel = hasQueue
? `采集队列 ${collectionQueueSummary.progress}%,运行 ${collectionQueueSummary.running},完成 ${collectionQueueSummary.completed},失败 ${collectionQueueSummary.failed},跳过 ${collectionQueueSummary.skipped}`
: '采集队列,暂无采集任务'
return (
<div className="an-collection-queue-anchor" ref={collectionQueueRef}>
<Button
size="icon"
variant="subtle"
title={queueLabel}
aria-label={queueLabel}
aria-expanded={collectionQueueOpen}
onClick={() => setCollectionQueueOpen((open) => !open)}
>
{hasQueue ? (
<span
className="an-collection-queue-trigger an-collection-queue-trigger--progress"
style={{ '--queue-progress': `${collectionQueueSummary.progress}%` } as CSSProperties}
aria-hidden="true"
/>
) : (
<ListChecks size={15} />
)}
</Button>
{collectionQueueOpen ? (
<div className="an-collection-queue-popover">
{renderCollectionQueuePanel()}
</div>
) : null}
</div>
)
@@ -5126,6 +5236,7 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
actions={(
<>
<Button size="icon" variant="subtle" onClick={() => void load()} loading={loading} title="刷新" aria-label="刷新"><RefreshCw size={15} /></Button>
{renderCollectionQueueAction()}
{moduleActions}
{config.actions.map((action) => (
<Button key={action.label} asChild size="icon" variant="subtle" title={action.label} aria-label={action.label}>
@@ -5144,7 +5255,6 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
<SectionTabs sections={config.sections} states={states} activeKey={activeSection?.key || ''} onChange={handleSectionChange} />
{renderDatasourceFilters()}
{renderCollectionQueue()}
</div>
{isPlaygroundSection ? (
@@ -5163,7 +5273,19 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
</div>
<div className="an-panel-body">
{loading || rows.length > 0 ? (
<ModuleTable rows={rows} selected={selected} onSelect={openResourceDetail} columns={config.columns} loading={loading} />
<ModuleTable
rows={rows}
selected={selected}
onSelect={openResourceDetail}
columns={config.columns}
loading={loading}
selection={isDatasourceBuiltinSection ? {
selectedRowIds: datasourceSelectedRowIds,
onToggleAllVisible: toggleAllVisibleDatasourceSelection,
onToggleRow: toggleDatasourceSelection,
getCheckboxLabel: (row) => `选择${recordTitle(row)}`,
} : undefined}
/>
) : (
<EmptyState title={loading ? '正在加载数据' : '当前分区暂无记录'} description="切换上方分区可精准查看不同配置和接口。" />
)}
@@ -5291,31 +5413,6 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
</div>
) : null}
<Dialog
open={batchTriggerOpen}
onOpenChange={setBatchTriggerOpen}
title="批量触发数据源"
description="输入内置数据源 ID提交到 trigger-batch。"
width={560}
footer={(
<>
<Button variant="subtle" onClick={() => setBatchTriggerOpen(false)} disabled={actionLoading}></Button>
<Button variant="primary" onClick={submitBatchTrigger} loading={actionLoading}></Button>
</>
)}
>
<div className="an-form">
<label className="an-field">
<span> ID</span>
<Textarea value={batchTriggerIds} onChange={(event) => setBatchTriggerIds(event.target.value)} placeholder="例如1, 2, 3" />
</label>
<label className="an-checkbox-row">
<input type="checkbox" checked={batchTriggerForce} onChange={(event) => setBatchTriggerForce(event.target.checked)} />
</label>
</div>
</Dialog>
<Dialog
open={Boolean(resolveTarget)}
onOpenChange={(open) => {

View File

@@ -365,6 +365,46 @@ body:has(.admin-next-theme-root[data-theme='dark']) .an-toast {
transition: width 180ms ease;
}
.an-collection-queue-anchor {
position: relative;
display: inline-flex;
}
.an-collection-queue-trigger {
position: relative;
width: 20px;
height: 20px;
display: inline-grid;
place-items: center;
color: currentColor;
}
.an-collection-queue-trigger--progress::before {
content: "";
position: absolute;
inset: 0;
border-radius: 999px;
background: conic-gradient(var(--an-accent) var(--queue-progress, 0%), var(--an-soft) 0);
mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 0);
-webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 0);
}
.an-collection-queue-popover {
position: absolute;
top: calc(100% + 8px);
right: 0;
z-index: 40;
width: min(960px, calc(100vw - 32px));
}
.an-collection-queue-popover .an-collection-queue {
max-height: min(560px, calc(100vh - 96px));
}
.an-collection-queue-popover .an-collection-queue__panel {
max-height: min(430px, calc(100vh - 210px));
}
.an-collection-queue__panel {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
@@ -433,6 +473,15 @@ body:has(.admin-next-theme-root[data-theme='dark']) .an-toast {
font-size: 12px;
}
.an-collection-queue__empty--panel {
min-height: 96px;
display: grid;
place-items: center;
border: 1px dashed var(--an-border);
border-radius: 8px;
background: var(--an-surface-alt);
}
.an-collection-queue__item > span {
min-width: 38px;
color: var(--an-muted);
@@ -711,6 +760,20 @@ body:has(.admin-next-theme-root[data-theme='dark']) .an-toast {
gap: 8px;
}
.an-toolbar > .tui-button {
align-self: stretch;
height: 34px;
}
.an-toolbar .an-status-pill {
align-self: stretch;
height: 34px;
min-width: 0;
padding: 0 12px;
border-radius: 7px;
line-height: 1;
}
.an-inline-field {
height: 34px;
display: inline-flex;
@@ -2378,9 +2441,11 @@ body:has(.admin-next-theme-root[data-theme='dark']) .an-toast {
}
.an-toolbar .an-badge {
height: 32px;
padding: 0 10px;
border-radius: 6px;
align-self: stretch;
height: 34px;
padding: 0 12px;
border-radius: 7px;
line-height: 1;
}
.an-data-table {
@@ -2431,6 +2496,22 @@ body:has(.admin-next-theme-root[data-theme='dark']) .an-toast {
white-space: nowrap;
}
.an-data-table__selection-cell {
width: 42px;
min-width: 42px;
max-width: 42px;
padding: 0;
text-align: center;
}
.an-data-table__selection-cell input {
width: 15px;
height: 15px;
margin: 0;
accent-color: var(--an-accent);
cursor: pointer;
}
.an-data-table th {
position: sticky;
top: 0;
@@ -3775,6 +3856,14 @@ body:has(.admin-next-theme-root[data-theme='dark']) .an-toast {
flex-direction: column;
}
.an-collection-queue-popover {
position: fixed;
top: 92px;
right: 12px;
left: 12px;
width: auto;
}
.an-collection-queue__panel {
grid-template-columns: 1fr;
max-height: 46vh;

View File

@@ -69,10 +69,10 @@ export function tactileButtonPreset(
return {
...base,
background: 'var(--tui-primary)',
backgroundHover: 'var(--tui-primary-hover)',
backgroundHover: 'color-mix(in srgb, var(--tui-primary) 86%, white)',
backgroundActive: 'var(--tui-primary-active)',
borderColor: 'var(--tui-primary)',
borderColorHover: 'var(--tui-primary-hover)',
borderColor: 'color-mix(in srgb, var(--tui-primary) 64%, white)',
borderColorHover: 'color-mix(in srgb, var(--tui-primary) 52%, white)',
color: '#fff',
darkBackground: 'var(--tui-primary)',
}
@@ -81,10 +81,10 @@ export function tactileButtonPreset(
return {
...base,
background: 'var(--tui-danger)',
backgroundHover: 'var(--tui-danger-hover)',
backgroundHover: 'color-mix(in srgb, var(--tui-danger) 86%, white)',
backgroundActive: 'var(--tui-danger-active)',
borderColor: 'var(--tui-danger)',
borderColorHover: 'var(--tui-danger-hover)',
borderColor: 'color-mix(in srgb, var(--tui-danger) 64%, white)',
borderColorHover: 'color-mix(in srgb, var(--tui-danger) 52%, white)',
color: '#fff',
darkBackground: 'var(--tui-danger)',
}

View File

@@ -37,7 +37,13 @@
[data-theme='dark'] .tui-control-group,
[data-theme='dark'] .tui-scrollbar,
[data-theme='dark'] .tui-table-scroll-region,
[data-theme='dark'] .tui-tooltip {
[data-theme='dark'] .tui-tooltip,
body[data-admin-next-theme='dark'] .tui-button,
body[data-admin-next-theme='dark'] .tui-switch,
body[data-admin-next-theme='dark'] .tui-control-group,
body[data-admin-next-theme='dark'] .tui-scrollbar,
body[data-admin-next-theme='dark'] .tui-table-scroll-region,
body[data-admin-next-theme='dark'] .tui-tooltip {
--tui-bg: #0f1724;
--tui-surface: #172033;
--tui-surface-soft: #202b3d;