@adea-ai/ui 0.122.3 → 0.123.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/components/layout/split-layout/resize-controller-core-retry.d.ts +13 -0
  2. package/dist/components/layout/split-layout/resize-controller-core-retry.d.ts.map +1 -0
  3. package/dist/components/layout/split-layout/resize-controller-core-retry.js +63 -0
  4. package/dist/components/layout/split-layout/resize-controller-core-retry.js.map +1 -0
  5. package/dist/components/layout/split-layout/resize-controller-core.d.ts +13 -0
  6. package/dist/components/layout/split-layout/resize-controller-core.d.ts.map +1 -0
  7. package/dist/components/layout/split-layout/resize-controller-core.js +63 -0
  8. package/dist/components/layout/split-layout/resize-controller-core.js.map +1 -0
  9. package/dist/components/layout/split-layout/resize-controller-retry-2.d.ts +3 -0
  10. package/dist/components/layout/split-layout/resize-controller-retry-2.d.ts.map +1 -0
  11. package/dist/components/layout/split-layout/resize-controller-retry-2.js +2 -0
  12. package/dist/components/layout/split-layout/resize-controller-retry.d.ts +2 -0
  13. package/dist/components/layout/split-layout/resize-controller-retry.d.ts.map +1 -0
  14. package/dist/components/layout/split-layout/resize-controller-retry.js +2 -0
  15. package/dist/components/layout/split-layout/resize-controller.d.ts +2 -0
  16. package/dist/components/layout/split-layout/resize-controller.d.ts.map +1 -0
  17. package/dist/components/layout/split-layout/resize-controller.js +2 -0
  18. package/dist/components/layout/split-layout/split-layout.d.ts.map +1 -1
  19. package/dist/components/layout/split-layout/split-layout.js +171 -60
  20. package/dist/components/layout/split-layout/split-layout.js.map +1 -1
  21. package/dist/lib/themes.d.ts +8 -7
  22. package/dist/lib/themes.d.ts.map +1 -1
  23. package/dist/lib/themes.js +53 -55
  24. package/dist/lib/themes.js.map +1 -1
  25. package/dist/r/appearance-editor.json +1 -1
  26. package/dist/r/lib.json +1 -1
  27. package/dist/r/registry.json +29 -3
  28. package/dist/r/split-layout.json +26 -0
  29. package/dist/r/src/components/layout/split-layout/resize-controller-core-retry.tsx +66 -0
  30. package/dist/r/src/components/layout/split-layout/resize-controller-core.tsx +66 -0
  31. package/dist/r/src/components/layout/split-layout/resize-controller-retry-2.tsx +2 -0
  32. package/dist/r/src/components/layout/split-layout/resize-controller-retry.tsx +1 -0
  33. package/dist/r/src/components/layout/split-layout/resize-controller.tsx +1 -0
  34. package/dist/r/src/components/layout/split-layout/split-layout.tsx +197 -49
  35. package/dist/r/src/lib/themes.ts +56 -73
  36. package/dist/r/theming.json +1 -1
  37. package/package.json +2 -2
  38. package/registry.json +29 -3
  39. package/src/components/layout/split-layout/resize-controller-core-retry.tsx +66 -0
  40. package/src/components/layout/split-layout/resize-controller-core.tsx +66 -0
  41. package/src/components/layout/split-layout/resize-controller-retry-2.tsx +2 -0
  42. package/src/components/layout/split-layout/resize-controller-retry.tsx +1 -0
  43. package/src/components/layout/split-layout/resize-controller.tsx +1 -0
  44. package/src/components/layout/split-layout/split-layout.tsx +197 -49
  45. package/src/lib/themes.ts +56 -73
@@ -1,17 +1,21 @@
1
- import Resizable from '@corvu/resizable'
2
1
  import {
3
2
  For,
4
- createMemo,
3
+ createEffect,
5
4
  createComputed,
5
+ createMemo,
6
6
  createUniqueId,
7
7
  createSignal,
8
+ onMount,
8
9
  onCleanup,
9
10
  on,
11
+ Show,
10
12
  type Accessor,
11
13
  type JSX,
12
14
  } from 'solid-js'
15
+ import { Dynamic } from 'solid-js/web'
13
16
  import { X, GripVertical } from 'lucide-solid'
14
17
  import { ActionButton } from '../../composites/action-button'
18
+ import { Button } from '../../ui/button'
15
19
  import { cn } from '#lib/utils'
16
20
  import { computeLayoutFrames, type LayoutRect } from './geometry'
17
21
  import { paneDropIntent, type PaneDropIntent } from './drop'
@@ -21,12 +25,16 @@ const DRAG_PREVIEW_MAX_WIDTH = 384
21
25
  const DRAG_PREVIEW_MAX_HEIGHT = 256
22
26
  import {
23
27
  listLeaves,
24
- MIN_SPLIT_RATIO,
25
- MAX_SPLIT_RATIO,
26
28
  type SplitLayoutBranch,
27
29
  type SplitLayoutLeaf,
28
30
  type SplitLayoutState,
29
31
  } from './model'
32
+ import type { SplitLayoutResizeControllerProps } from './resize-controller-core'
33
+
34
+ type ResizeController<L extends SplitLayoutLeaf> = (
35
+ props: SplitLayoutResizeControllerProps<L>
36
+ ) => JSX.Element
37
+ type ResizeControllerState<L extends SplitLayoutLeaf> = ResizeController<L> | false | 'error'
30
38
 
31
39
  export type SplitLayoutProps<L extends SplitLayoutLeaf> = {
32
40
  state: SplitLayoutState<L>
@@ -59,6 +67,16 @@ function rectStyle(rect: LayoutRect): JSX.CSSProperties {
59
67
  height: `${rect.height * 100}%`,
60
68
  }
61
69
  }
70
+ function separatorPlaceholderStyle(branch: SplitLayoutBranch, rect: LayoutRect): JSX.CSSProperties {
71
+ const horizontal = branch.direction === 'row'
72
+ return {
73
+ left: `${(rect.x + (horizontal ? rect.width * branch.ratio : 0)) * 100}%`,
74
+ top: `${(rect.y + (horizontal ? 0 : rect.height * branch.ratio)) * 100}%`,
75
+ width: horizontal ? '1px' : `${rect.width * 100}%`,
76
+ height: horizontal ? `${rect.height * 100}%` : '1px',
77
+ transform: horizontal ? 'translateX(-50%)' : 'translateY(-50%)',
78
+ }
79
+ }
62
80
  function dragPreviewPlaceholder(source: HTMLElement, message: string) {
63
81
  const placeholder = document.createElement('div')
64
82
  placeholder.className = cn(
@@ -171,14 +189,40 @@ function cloneDragPreviewNode(source: Node): Node | undefined {
171
189
  export function SplitLayout<L extends SplitLayoutLeaf>(props: SplitLayoutProps<L>) {
172
190
  let root: HTMLDivElement | undefined
173
191
  let pendingFrame: number | undefined
192
+ let disposed = false
193
+ let resizeLoadGeneration = 0
194
+ let retryWithAlternateImport = false
195
+ let alternateImportAttempts = 0
196
+ let focusHandleAfterRetry = false
197
+ let focusRecoveryButtonAfterError = false
198
+ let focusRestoreEpoch = 0
199
+ let focusEpoch = 0
200
+ let pendingFocusFrame: number | undefined
174
201
  const refs = new Map<string, HTMLElement>()
175
202
  const domIds = new Map<string, string>()
176
203
  const prefix = createUniqueId()
204
+ const resizeReloadWarningId = `${prefix}-resize-reload-warning`
177
205
  let nextDomId = 0
178
206
  let nextDragId = 0
179
207
  let dragPreview: HTMLElement | undefined
180
208
  const [drag, setDrag] = createSignal<{ id: string; token: string }>()
181
209
  const [drop, setDrop] = createSignal<{ id: string; intent: PaneDropIntent }>()
210
+ const [resizeController, setResizeController] = createSignal<ResizeControllerState<L>>()
211
+ const cancelPendingFocusRestore = () => {
212
+ if (pendingFocusFrame !== undefined) cancelAnimationFrame(pendingFocusFrame)
213
+ pendingFocusFrame = undefined
214
+ }
215
+ const trackFocusChange = (event: FocusEvent) => {
216
+ if (event.target === document.body) return
217
+ focusEpoch += 1
218
+ focusHandleAfterRetry = false
219
+ focusRecoveryButtonAfterError = false
220
+ cancelPendingFocusRestore()
221
+ }
222
+ onMount(() => {
223
+ document.addEventListener('focusin', trackFocusChange)
224
+ onCleanup(() => document.removeEventListener('focusin', trackFocusChange))
225
+ })
182
226
  const clearDrag = () => {
183
227
  setDrag(undefined)
184
228
  setDrop(undefined)
@@ -221,6 +265,79 @@ export function SplitLayout<L extends SplitLayoutLeaf>(props: SplitLayoutProps<L
221
265
  frame.node.kind === 'split' ? [frame.node] : []
222
266
  )
223
267
  )
268
+ createEffect(() => {
269
+ if (branches().length === 0) {
270
+ resizeLoadGeneration += 1
271
+ focusHandleAfterRetry = false
272
+ focusRecoveryButtonAfterError = false
273
+ cancelPendingFocusRestore()
274
+ // Keep a loaded controller so a later split does not re-import a facade
275
+ // that may have failed before recovery. Clear only transient UI state.
276
+ if (resizeController() === false || resizeController() === 'error')
277
+ setResizeController(undefined)
278
+ return
279
+ }
280
+ if (resizeController() !== undefined) return
281
+ const retry = retryWithAlternateImport
282
+ const generation = ++resizeLoadGeneration
283
+ setResizeController(false)
284
+ const controllerImport = retry
285
+ ? alternateImportAttempts++ === 0
286
+ ? import('./resize-controller-retry')
287
+ : import('./resize-controller-retry-2')
288
+ : import('./resize-controller')
289
+ void controllerImport.then(
290
+ ({ default: Controller }) => {
291
+ if (disposed || generation !== resizeLoadGeneration || branches().length === 0) return
292
+ const shouldFocusHandle = focusHandleAfterRetry && focusEpoch === focusRestoreEpoch
293
+ focusHandleAfterRetry = false
294
+ focusRecoveryButtonAfterError = false
295
+ retryWithAlternateImport = false
296
+ alternateImportAttempts = 0
297
+ setResizeController(() => Controller)
298
+ if (shouldFocusHandle)
299
+ pendingFocusFrame = requestAnimationFrame(() => {
300
+ pendingFocusFrame = undefined
301
+ if (
302
+ disposed ||
303
+ generation !== resizeLoadGeneration ||
304
+ branches().length === 0 ||
305
+ focusEpoch !== focusRestoreEpoch
306
+ )
307
+ return
308
+ const currentFocus = root?.ownerDocument.activeElement
309
+ if (currentFocus && currentFocus !== root?.ownerDocument.body && currentFocus !== root)
310
+ return
311
+ root?.querySelector<HTMLElement>('[data-corvu-resizable-handle]')?.focus({
312
+ preventScroll: true,
313
+ })
314
+ })
315
+ },
316
+ () => {
317
+ // A rejected dynamic import can remain cached under its specifier.
318
+ // The next split must start from the alternate module entry as well.
319
+ retryWithAlternateImport = true
320
+ if (!disposed && generation === resizeLoadGeneration && branches().length > 0) {
321
+ setResizeController('error')
322
+ const shouldFocusRetry = focusRecoveryButtonAfterError && focusEpoch === focusRestoreEpoch
323
+ if (shouldFocusRetry)
324
+ pendingFocusFrame = requestAnimationFrame(() => {
325
+ pendingFocusFrame = undefined
326
+ if (
327
+ disposed ||
328
+ generation !== resizeLoadGeneration ||
329
+ branches().length === 0 ||
330
+ focusEpoch !== focusRestoreEpoch
331
+ )
332
+ return
333
+ root?.querySelector<HTMLButtonElement>('[data-resize-retry]')?.focus({
334
+ preventScroll: true,
335
+ })
336
+ })
337
+ }
338
+ }
339
+ )
340
+ })
224
341
  createComputed(() => {
225
342
  const current = drag()
226
343
  if (current && (!props.onMove || !leafMap().has(current.id))) clearDrag()
@@ -288,6 +405,10 @@ export function SplitLayout<L extends SplitLayoutLeaf>(props: SplitLayoutProps<L
288
405
  )
289
406
  )
290
407
  onCleanup(() => {
408
+ disposed = true
409
+ focusHandleAfterRetry = false
410
+ focusRecoveryButtonAfterError = false
411
+ cancelPendingFocusRestore()
291
412
  if (pendingFrame !== undefined) cancelAnimationFrame(pendingFrame)
292
413
  clearDrag()
293
414
  refs.clear()
@@ -452,62 +573,89 @@ export function SplitLayout<L extends SplitLayoutLeaf>(props: SplitLayoutProps<L
452
573
  }}
453
574
  </For>
454
575
  <span class="sr-only" role="status">
455
- {dropLabel()}
576
+ {dropLabel() ||
577
+ (resizeController() === 'error'
578
+ ? alternateImportAttempts >= 2
579
+ ? 'Resize controls did not load after repeated attempts. Reloading may discard unsaved pane content.'
580
+ : 'Resize controls did not load.'
581
+ : '')}
456
582
  </span>
457
583
  <For each={branches().map((branch) => branch.id)}>
458
584
  {(id) => {
459
585
  const initial = branchMap().get(id)!
460
586
  const branch: Accessor<SplitLayoutBranch<L>> = () => branchMap().get(id) ?? initial
587
+ const rect = () => frames().get(id)?.rect ?? { x: 0, y: 0, width: 0, height: 0 }
461
588
  return (
462
- <Resizable
463
- orientation={branch().direction === 'row' ? 'horizontal' : 'vertical'}
464
- sizes={[branch().ratio, 1 - branch().ratio]}
465
- keyboardDelta={0.05}
466
- onSizesChange={(sizes) => {
467
- // Corvu also reports panel registration/unregistration while
468
- // mounting or disposing a controller. Those incomplete or
469
- // hidden-host sizes are not user resize intent.
470
- if (!root?.isConnected || root.getClientRects().length === 0) return
471
- if (
472
- sizes.length !== 2 ||
473
- sizes.some((size) => !Number.isFinite(size)) ||
474
- Math.abs(sizes[0]! + sizes[1]! - 1) > 0.000001
475
- )
476
- return
477
- if (sizes[0] !== undefined && Math.abs(sizes[0] - branch().ratio) > 0.000001)
478
- props.onResize(id, sizes[0])
479
- }}
480
- style={rectStyle(frames().get(id)?.rect ?? { x: 0, y: 0, width: 0, height: 0 })}
481
- class="pointer-events-none absolute flex min-h-0 min-w-0 data-[orientation=vertical]:flex-col"
589
+ <Show
590
+ when={typeof resizeController() === 'function'}
591
+ fallback={
592
+ <div
593
+ role="separator"
594
+ aria-label={
595
+ props.labelForSeparator?.(branch()) ??
596
+ (branch().direction === 'row' ? 'Resize pane columns' : 'Resize pane rows')
597
+ }
598
+ aria-orientation={branch().direction === 'row' ? 'vertical' : 'horizontal'}
599
+ aria-controls={listLeaves(branch())
600
+ .map((leaf) => domId(leaf.id))
601
+ .join(' ')}
602
+ aria-valuemin={10}
603
+ aria-valuemax={90}
604
+ aria-valuenow={Math.round(branch().ratio * 100)}
605
+ aria-disabled="true"
606
+ tabIndex={-1}
607
+ data-direction={branch().direction}
608
+ style={separatorPlaceholderStyle(branch(), rect())}
609
+ class="pointer-events-none absolute bg-border"
610
+ />
611
+ }
482
612
  >
483
- <Resizable.Panel
484
- minSize={MIN_SPLIT_RATIO}
485
- maxSize={MAX_SPLIT_RATIO}
486
- aria-hidden="true"
487
- />
488
- <Resizable.Handle
489
- aria-label={
490
- props.labelForSeparator?.(branch()) ??
491
- (branch().direction === 'row' ? 'Resize pane columns' : 'Resize pane rows')
492
- }
493
- aria-orientation={branch().direction === 'row' ? 'vertical' : 'horizontal'}
494
- aria-controls={listLeaves(branch())
495
- .map((leaf) => domId(leaf.id))
496
- .join(' ')}
497
- aria-valuemin={10}
498
- aria-valuemax={90}
499
- aria-valuenow={Math.round(branch().ratio * 100)}
500
- class="pointer-events-auto relative w-px shrink-0 bg-border after:absolute after:inset-y-0 after:-inset-x-1 after:w-3 focus-visible:bg-primary focus-visible:outline-none hover:bg-primary data-[orientation=vertical]:h-px data-[orientation=vertical]:w-full data-[orientation=vertical]:after:inset-x-0 data-[orientation=vertical]:after:-inset-y-1 data-[orientation=vertical]:after:h-3 data-[orientation=vertical]:after:w-full"
501
- />
502
- <Resizable.Panel
503
- minSize={MIN_SPLIT_RATIO}
504
- maxSize={MAX_SPLIT_RATIO}
505
- aria-hidden="true"
613
+ <Dynamic
614
+ component={resizeController() as ResizeController<L>}
615
+ branch={branch}
616
+ rect={() => rectStyle(rect())}
617
+ domId={domId}
618
+ root={() => root}
619
+ labelForSeparator={props.labelForSeparator}
620
+ onResize={props.onResize}
506
621
  />
507
- </Resizable>
622
+ </Show>
508
623
  )
509
624
  }}
510
625
  </For>
626
+ {resizeController() === 'error' ? (
627
+ <div class="absolute top-2 right-2 z-(--z-menu) flex max-w-sm flex-col items-end gap-2">
628
+ {alternateImportAttempts >= 2 ? (
629
+ <p
630
+ id={resizeReloadWarningId}
631
+ role="alert"
632
+ class="rounded-md border bg-background p-2 text-sm shadow-md"
633
+ >
634
+ Reloading may discard unsaved pane content.
635
+ </p>
636
+ ) : null}
637
+ <Button
638
+ variant="outline"
639
+ size="sm"
640
+ data-resize-retry
641
+ aria-describedby={alternateImportAttempts >= 2 ? resizeReloadWarningId : undefined}
642
+ onClick={(event) => {
643
+ if (alternateImportAttempts >= 2) {
644
+ window.location.reload()
645
+ return
646
+ }
647
+ const ownsFocus = document.activeElement === event.currentTarget
648
+ focusHandleAfterRetry = ownsFocus
649
+ focusRecoveryButtonAfterError = ownsFocus
650
+ focusRestoreEpoch = focusEpoch
651
+ retryWithAlternateImport = true
652
+ setResizeController(undefined)
653
+ }}
654
+ >
655
+ {alternateImportAttempts >= 2 ? 'Reload page to retry resizing' : 'Retry pane resizing'}
656
+ </Button>
657
+ </div>
658
+ ) : null}
511
659
  </div>
512
660
  )
513
661
  }
package/src/lib/themes.ts CHANGED
@@ -24,13 +24,13 @@
24
24
  *
25
25
  * ## The mapping, and the entry that is not a rename
26
26
  *
27
- * {@link ROLE_FOR} maps canonical roles onto this system's variable vocabulary. One
28
- * entry will trip a reader: the catalogue's `accent` becomes this system's
29
- * `primary`, because in the shadcn vocabulary `--primary` is the action colour — the
30
- * fill of a primary button, the thing a focus ring is drawn in — while `--accent` is
31
- * a low-contrast hover wash. The catalogue's `accent` is the former, so it goes to
32
- * `--primary`, and `--accent` is filled from `surfaceHover`, which is what shadcn
33
- * components actually use it for.
27
+ * The package-owned `shadcnVariables` adapter maps canonical roles onto this
28
+ * system's vocabulary. One entry will trip a reader: the catalogue's `accent`
29
+ * becomes this system's `primary`, because in the shadcn vocabulary `--primary` is
30
+ * the action colour — the fill of a primary button, the thing a focus ring is drawn
31
+ * in — while `--accent` is a low-contrast hover wash. The catalogue's `accent` is
32
+ * the former, so it goes to `--primary`, and `--accent` is filled from
33
+ * `surfaceHover`, which is what shadcn components actually use it for.
34
34
  */
35
35
 
36
36
  import {
@@ -56,6 +56,7 @@ import {
56
56
  type AdeaThemeRecord,
57
57
  type ContrastFinding,
58
58
  } from '@adea-ai/themes'
59
+ import { shadcnVariables } from '@adea-ai/themes/adapters/shadcn'
59
60
  import { isThemeAccentId } from './theme-accents'
60
61
 
61
62
  export { isThemeAccentId } from './theme-accents'
@@ -106,6 +107,7 @@ export type ThemeColors = {
106
107
  sidebarForeground: string
107
108
  sidebarAccent: string
108
109
  sidebarBorder: string
110
+ sidebarMutedForeground: string
109
111
  }
110
112
 
111
113
  /** The sixteen ANSI colours a terminal needs, plus its cursor and selection. */
@@ -179,75 +181,56 @@ export type ThemeVariant = {
179
181
  provenance: ThemeProvenance
180
182
  }
181
183
 
182
- /**
183
- * Canonical role → this system's `ThemeColors` key.
184
- *
185
- * Read by {@link toVariant}. The two derived destructive-action presentation values
186
- * are intentionally excluded; the shared Themes adapter owns their color math.
187
- */
188
- type ThemeRole = Exclude<keyof ThemeColors, 'destructiveAction' | 'destructiveActionForeground'>
189
-
190
- const ROLE_FOR: Readonly<Record<ThemeRole, keyof AdeaThemeRecord['colors']>> = {
191
- background: 'background',
192
- foreground: 'text',
193
- card: 'surface',
194
- cardForeground: 'text',
195
- popover: 'surfaceElevated',
196
- popoverForeground: 'text',
197
- // Not a rename — see this module's header.
198
- primary: 'accent',
199
- primaryForeground: 'accentForeground',
200
- // shadcn's `secondary` and `muted` are both the first surface rung used as a fill.
201
- secondary: 'surface',
202
- secondaryForeground: 'text',
203
- muted: 'surface',
204
- mutedForeground: 'textMuted',
205
- // shadcn's `accent` is a hover wash, which is exactly `surfaceHover`.
206
- accent: 'surfaceHover',
207
- accentForeground: 'text',
208
- destructive: 'error',
209
- // Overwritten below from the measured pairing; nominal here so the table is total.
210
- destructiveForeground: 'error',
211
- destructiveSubtle: 'error',
212
- success: 'success',
213
- successForeground: 'success',
214
- successSubtle: 'success',
215
- warning: 'warning',
216
- warningForeground: 'warning',
217
- warningSubtle: 'warning',
218
- info: 'info',
219
- infoForeground: 'info',
220
- infoSubtle: 'info',
221
- border: 'border',
222
- input: 'border',
223
- ring: 'accent',
224
- sidebar: 'surface',
225
- sidebarForeground: 'text',
226
- sidebarAccent: 'surfaceHover',
227
- sidebarBorder: 'border',
228
- }
229
-
230
184
  /** Builds the design system's view of one catalogue entry. */
231
185
  function toVariant(theme: AdeaThemeRecord): ThemeVariant {
232
- const colors = {} as ThemeColors
233
- for (const key of Object.keys(ROLE_FOR) as ThemeRole[]) {
234
- colors[key] = theme.colors[ROLE_FOR[key]]
186
+ const projected = shadcnVariables(theme)
187
+ const token = (name: string): string => {
188
+ const value = projected[`--${name}`]
189
+ if (!value) throw new Error(`${theme.id} has no shared shadcn projection for --${name}`)
190
+ return value
235
191
  }
236
- // Keep the semantic foreground paired with the canonical error role. Filled
237
- // destructive actions use the distinct projection below so their /90 hover state
238
- // remains readable without changing icons, borders, or tinted status surfaces.
239
- colors.destructiveForeground = statusForeground(theme, 'error')
240
- colors.destructiveSubtle = tint(theme.colors.error, theme.colors.background)
241
192
  const destructiveAction = shadcnDestructiveProjection(theme)
242
- colors.destructiveAction = destructiveAction.fill
243
- colors.destructiveActionForeground = destructiveAction.foreground
244
- colors.successForeground = statusForeground(theme, 'success')
245
- colors.successSubtle = tint(theme.colors.success, theme.colors.background)
246
- colors.warningForeground = statusForeground(theme, 'warning')
247
- colors.warningSubtle = tint(theme.colors.warning, theme.colors.background)
248
- colors.infoForeground = statusForeground(theme, 'info')
249
- colors.infoSubtle = tint(theme.colors.info, theme.colors.background)
250
-
193
+ const colors: ThemeColors = {
194
+ background: token('background'),
195
+ foreground: token('foreground'),
196
+ card: token('card'),
197
+ cardForeground: token('card-foreground'),
198
+ popover: token('popover'),
199
+ popoverForeground: token('popover-foreground'),
200
+ primary: token('primary'),
201
+ primaryForeground: token('primary-foreground'),
202
+ secondary: token('secondary'),
203
+ secondaryForeground: token('secondary-foreground'),
204
+ muted: token('muted'),
205
+ mutedForeground: token('muted-foreground'),
206
+ accent: token('accent'),
207
+ accentForeground: token('accent-foreground'),
208
+ // Preserve the UI's semantic error token. The Themes projection uses its
209
+ // contrast-safe solid-action fill for --destructive; UI keeps that fill in
210
+ // the separate destructiveAction pair below.
211
+ destructive: theme.colors.error,
212
+ destructiveForeground: statusForeground(theme, 'error'),
213
+ destructiveSubtle: tint(theme.colors.error, theme.colors.background),
214
+ destructiveAction: destructiveAction.fill,
215
+ destructiveActionForeground: destructiveAction.foreground,
216
+ success: token('success'),
217
+ successForeground: statusForeground(theme, 'success'),
218
+ successSubtle: tint(theme.colors.success, theme.colors.background),
219
+ warning: token('warning'),
220
+ warningForeground: statusForeground(theme, 'warning'),
221
+ warningSubtle: tint(theme.colors.warning, theme.colors.background),
222
+ info: token('info'),
223
+ infoForeground: statusForeground(theme, 'info'),
224
+ infoSubtle: tint(theme.colors.info, theme.colors.background),
225
+ border: token('border'),
226
+ input: token('input'),
227
+ ring: token('ring'),
228
+ sidebar: token('sidebar'),
229
+ sidebarForeground: token('sidebar-foreground'),
230
+ sidebarAccent: token('sidebar-accent'),
231
+ sidebarBorder: token('sidebar-border'),
232
+ sidebarMutedForeground: token('sidebar-muted-foreground'),
233
+ }
251
234
  const syntax = syntaxRoles(theme)
252
235
 
253
236
  return {
@@ -556,7 +539,7 @@ export function themeCssVariables(theme: ThemeVariant): Record<string, string> {
556
539
  '--sidebar-primary': c.primary,
557
540
  '--sidebar-primary-foreground': c.primaryForeground,
558
541
  '--sidebar-ring': c.ring,
559
- '--sidebar-muted-foreground': c.mutedForeground,
542
+ '--sidebar-muted-foreground': c.sidebarMutedForeground,
560
543
  '--surface-sunken': c.muted,
561
544
  '--surface-hover': c.accent,
562
545
  '--surface-active': c.border,