@duro-app/cli 5.12.0 → 5.14.0

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 (2) hide show
  1. package/package.json +5 -5
  2. package/registry.json +551 -54
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duro-app/cli",
3
- "version": "5.12.0",
3
+ "version": "5.14.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Machine-queryable docs for the Duro design system: components, props, recipes, tokens and rules as CLI output, JSON, or an MCP server.",
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@modelcontextprotocol/sdk": ">=1.0.0",
48
- "@duro-app/ui": "^5.12.0"
48
+ "@duro-app/ui": "^5.14.0"
49
49
  },
50
50
  "peerDependenciesMeta": {
51
51
  "@duro-app/ui": {
@@ -60,9 +60,9 @@
60
60
  "@types/node": "^24.0.0",
61
61
  "ts-morph": "^26.0.0",
62
62
  "typescript": "^5.7.0",
63
- "@duro-app/eslint-plugin": "^5.12.0",
64
- "@duro-app/tokens": "^5.12.0",
65
- "@duro-app/ui": "^5.12.0"
63
+ "@duro-app/eslint-plugin": "^5.14.0",
64
+ "@duro-app/tokens": "^5.14.0",
65
+ "@duro-app/ui": "^5.14.0"
66
66
  },
67
67
  "scripts": {
68
68
  "registry": "node scripts/build-registry.mjs",
package/registry.json CHANGED
@@ -190,7 +190,7 @@
190
190
  "Main"
191
191
  ]
192
192
  },
193
- "description": "An application's frame: a navigation rail (a labelled nav, the brand at its top, an optional footer at its foot) beside the main content (main), with an optional Header. Below collapseBelow (sm default, md or lg), measured on the shell's own width, the rail is hidden and one sticky bar (a banner, appBarH tall) holds a Menu button that opens the same navigation in a left Drawer, the brand, and the Header's content when there is one — a shell without a Header still has its navigation on a phone. Above it the Header is a row that scrolls with the page and the rail stays sticky. CSS makes the switch: the server renders one HTML for every width with the brand in both places, and display: none hides the inactive copy, so nothing moves after hydration. A skip link (the first tab stop) focuses Main; an Aside inside Main sticks below the bar. Labels come from props. Compound component — Root is required.",
193
+ "description": "An application's frame: a navigation rail (a labelled nav, the brand at its top, an optional footer at its foot) beside the main content (main), with an optional Header. Below collapseBelow (sm default, md or lg), measured on the shell's own width, the rail is hidden and one sticky bar (a banner, appBarH tall) holds a Menu button that opens the same navigation in a left Drawer, the brand, and the Header's content when there is one — a shell without a Header still has its navigation on a phone. Above it the Header is a row that scrolls with the page and the rail stays sticky. CSS makes the switch: the server renders one HTML for every width with the brand in both places, and display: none hides the inactive copy, so nothing moves after hydration. A skip link (the first tab stop) focuses Main; an Aside inside Main sticks below the bar. Labels come from props. Compound component — Root is required. On React Native it is always the narrow shape: the bar below the status bar, Main under it, and the Rail in a left Drawer; there is no skip link.",
194
194
  "example": "<AppShell.Root\n menuLabel=\"Menu\"\n closeLabel=\"Close navigation\"\n skipLabel=\"Skip to content\"\n brand={<TextLink href=\"/\">Acme</TextLink>}\n collapseBelow=\"md\"\n>\n <AppShell.Rail aria-label=\"Navigation\" footer={<Text variant=\"bodySm\">{user.name}</Text>}>\n {({close}) => (\n <SideNav.Root value={pathname} onValueChange={(v) => { close(); navigate(v) }}>\n <SideNav.Item value=\"/docs\">Docs</SideNav.Item>\n </SideNav.Root>\n )}\n </AppShell.Rail>\n <AppShell.Header>{search}</AppShell.Header>\n <AppShell.Main>{children}</AppShell.Main>\n</AppShell.Root>",
195
195
  "relatedTo": [
196
196
  {
@@ -225,7 +225,7 @@
225
225
  ]
226
226
  },
227
227
  "name": "AppShell",
228
- "native": false,
228
+ "native": true,
229
229
  "parts": {
230
230
  "Header": {
231
231
  "name": "Header",
@@ -1341,7 +1341,7 @@
1341
1341
  "ColorModeProvider"
1342
1342
  ]
1343
1343
  },
1344
- "description": "Color-mode controller + toggle. ColorModeProvider owns the preference (system/light/dark/high-contrast), resolves \"system\" against the OS scheme, persists it to localStorage, and applies the resolved theme via ThemeProvider. ColorModeToggle is an icon button that cycles the preference; useColorMode() reads it.",
1344
+ "description": "Color-mode controller + toggle. ColorModeProvider owns the preference (system/light/dark/high-contrast), resolves \"system\" against the OS scheme, persists it to localStorage, and applies the resolved theme via ThemeProvider. ColorModeToggle is an icon button that cycles the preference; useColorMode() reads it. On React Native \"system\" follows the OS appearance (useColorScheme) and nothing is stored: persist the preference yourself through preference and onPreferenceChange.",
1345
1345
  "example": "// At the app root — replaces a bare <ThemeProvider>:\n<ColorModeProvider defaultPreference=\"system\" modes={['light', 'dark']}>\n <App />\n</ColorModeProvider>\n\n// Anywhere inside (e.g. the top bar):\n<ColorModeToggle />\n\n// Or read the resolved theme:\nconst {theme, preference, setPreference} = useColorMode()",
1346
1346
  "relatedTo": [
1347
1347
  {
@@ -1366,7 +1366,7 @@
1366
1366
  ]
1367
1367
  },
1368
1368
  "name": "ColorModeProvider",
1369
- "native": false,
1369
+ "native": true,
1370
1370
  "props": [
1371
1371
  {
1372
1372
  "name": "children",
@@ -1401,7 +1401,7 @@
1401
1401
  },
1402
1402
  {
1403
1403
  "default": "'duro-color-mode'",
1404
- "description": "localStorage key for persistence; `null` disables it. Default `'duro-color-mode'`.",
1404
+ "description": "localStorage key for persistence; `null` disables it. Default `'duro-color-mode'`. Native has no localStorage: persist through `preference` and `onPreferenceChange`.",
1405
1405
  "name": "storageKey",
1406
1406
  "required": false,
1407
1407
  "type": "string | null"
@@ -1429,7 +1429,7 @@
1429
1429
  "ColorModeProvider"
1430
1430
  ]
1431
1431
  },
1432
- "description": "Color-mode controller + toggle. ColorModeProvider owns the preference (system/light/dark/high-contrast), resolves \"system\" against the OS scheme, persists it to localStorage, and applies the resolved theme via ThemeProvider. ColorModeToggle is an icon button that cycles the preference; useColorMode() reads it.",
1432
+ "description": "Color-mode controller + toggle. ColorModeProvider owns the preference (system/light/dark/high-contrast), resolves \"system\" against the OS scheme, persists it to localStorage, and applies the resolved theme via ThemeProvider. ColorModeToggle is an icon button that cycles the preference; useColorMode() reads it. On React Native \"system\" follows the OS appearance (useColorScheme) and nothing is stored: persist the preference yourself through preference and onPreferenceChange.",
1433
1433
  "example": "// At the app root — replaces a bare <ThemeProvider>:\n<ColorModeProvider defaultPreference=\"system\" modes={['light', 'dark']}>\n <App />\n</ColorModeProvider>\n\n// Anywhere inside (e.g. the top bar):\n<ColorModeToggle />\n\n// Or read the resolved theme:\nconst {theme, preference, setPreference} = useColorMode()",
1434
1434
  "relatedTo": [
1435
1435
  {
@@ -1454,7 +1454,7 @@
1454
1454
  ]
1455
1455
  },
1456
1456
  "name": "ColorModeToggle",
1457
- "native": false,
1457
+ "native": true,
1458
1458
  "props": [
1459
1459
  {
1460
1460
  "default": "'md'",
@@ -1494,7 +1494,7 @@
1494
1494
  "Item"
1495
1495
  ]
1496
1496
  },
1497
- "description": "Searchable dropdown for selecting a value from a filterable list. Combines a text input with a popup listbox. Compound component — Root is required.",
1497
+ "description": "Searchable dropdown for selecting a value from a filterable list. Combines a text input with a popup listbox. Compound component — Root is required. Combobox.Popup takes label (the list's accessible name). On React Native the field opens a bottom sheet at its large detent, above the keyboard, titled by label with a Cancel text button (cancelLabel): a search field is pinned under the header and focused, the list below filters as you type, Combobox.Empty shows when nothing matches, and a press picks an option and closes the sheet. The sheet's search text starts empty each time; Combobox.Trigger renders nothing on native.",
1498
1498
  "example": "<Combobox.Root name=\"country\" defaultValue=\"us\" initialLabels={{us: 'United States'}}>\n <Combobox.Input placeholder=\"Search countries…\" />\n <Combobox.Popup>\n <Combobox.Item value=\"us\">United States</Combobox.Item>\n <Combobox.Item value=\"gb\">United Kingdom</Combobox.Item>\n <Combobox.Item value=\"fr\">France</Combobox.Item>\n <Combobox.Item value=\"de\">Germany</Combobox.Item>\n <Combobox.Empty>No countries found</Combobox.Empty>\n </Combobox.Popup>\n</Combobox.Root>",
1499
1499
  "relatedTo": [
1500
1500
  {
@@ -1530,7 +1530,7 @@
1530
1530
  ]
1531
1531
  },
1532
1532
  "name": "Combobox",
1533
- "native": false,
1533
+ "native": true,
1534
1534
  "parts": {
1535
1535
  "Empty": {
1536
1536
  "name": "Empty",
@@ -1593,6 +1593,18 @@
1593
1593
  "name": "children",
1594
1594
  "required": true,
1595
1595
  "type": "ReactNode"
1596
+ },
1597
+ {
1598
+ "description": "The list's accessible name; on native also the sheet's title.",
1599
+ "name": "label",
1600
+ "required": false,
1601
+ "type": "string"
1602
+ },
1603
+ {
1604
+ "description": "Native only: the sheet header's text button that closes it unchanged. Default: 'Cancel'",
1605
+ "name": "cancelLabel",
1606
+ "required": false,
1607
+ "type": "string"
1596
1608
  }
1597
1609
  ],
1598
1610
  "sourcePath": "packages/ui/src/components/Combobox/Combobox.tsx"
@@ -2503,7 +2515,7 @@
2503
2515
  "Portal"
2504
2516
  ]
2505
2517
  },
2506
- "description": "Modal sliding panel from a screen edge (right, left, or bottom). Slides in with backdrop overlay. Supports swipe-to-dismiss gestures on mobile, backdrop click dismiss, and Escape key. Swipe yields to an inner Duro ScrollArea: a drag there scrolls it, and dismisses only toward the edge from the scroll start (a native overflow:auto child is not covered — use ScrollArea). Use for secondary workflows, settings panels, or mobile-friendly detail views.",
2518
+ "description": "Modal sliding panel from a screen edge (right, left, or bottom). Slides in with backdrop overlay. Supports swipe-to-dismiss gestures on mobile, backdrop click dismiss, and Escape key. Swipe yields to an inner Duro ScrollArea: a drag there scrolls it, and dismisses only toward the edge from the scroll start (a native overflow:auto child is not covered — use ScrollArea). A bottom drawer with snapPoints (medium and large) is a phone sheet: it rests at its detents, a grabber steps through them on a tap, a drag moves between them and dismisses below the lowest; its Body is the scroll the drag defers to. Header titleAlign=\"center\" centres the title with a text close at the end. Body flush drops its side padding, so rows run to the panel's edges (a picker's list); on React Native a bottom drawer rides above the on-screen keyboard. Use for secondary workflows, settings panels, or mobile-friendly detail views.",
2507
2519
  "example": "<Drawer.Root anchor=\"right\" open={open} onOpenChange={setOpen}>\n <Drawer.Trigger>\n <Button>Open drawer</Button>\n </Drawer.Trigger>\n <Drawer.Portal size=\"md\">\n <Drawer.Header>\n <Drawer.Title>Settings</Drawer.Title>\n <Drawer.Close />\n </Drawer.Header>\n <Drawer.Body>\n <Stack gap=\"md\">\n <Text>Drawer content goes here.</Text>\n </Stack>\n </Drawer.Body>\n <Drawer.Footer>\n <Button variant=\"secondary\" onClick={() => setOpen(false)}>Done</Button>\n </Drawer.Footer>\n </Drawer.Portal>\n</Drawer.Root>",
2508
2520
  "relatedTo": [
2509
2521
  {
@@ -2531,12 +2543,13 @@
2531
2543
  "whenToUse": [
2532
2544
  "Settings or configuration panels that slide in from the side",
2533
2545
  "Mobile-friendly detail views (bottom sheet pattern)",
2546
+ "Phone pickers and lists that need more room on demand (snapPoints)",
2534
2547
  "Secondary workflows that need more space than a dialog",
2535
2548
  "Filters, navigation drawers, or form panels"
2536
2549
  ]
2537
2550
  },
2538
2551
  "name": "Drawer",
2539
- "native": false,
2552
+ "native": true,
2540
2553
  "parts": {
2541
2554
  "Body": {
2542
2555
  "name": "Body",
@@ -2545,6 +2558,13 @@
2545
2558
  "name": "children",
2546
2559
  "required": true,
2547
2560
  "type": "ReactNode"
2561
+ },
2562
+ {
2563
+ "default": "false",
2564
+ "description": "No side padding: rows that run to the panel's edges, as a picker's list.",
2565
+ "name": "flush",
2566
+ "required": false,
2567
+ "type": "boolean"
2548
2568
  }
2549
2569
  ],
2550
2570
  "sourcePath": "packages/ui/src/components/Drawer/Drawer.tsx"
@@ -2575,7 +2595,7 @@
2575
2595
  "type": "ReactNode"
2576
2596
  }
2577
2597
  ],
2578
- "sourcePath": "packages/ui/src/components/Drawer/Drawer.tsx"
2598
+ "sourcePath": "packages/ui/src/components/Drawer/parts.tsx"
2579
2599
  },
2580
2600
  "Footer": {
2581
2601
  "name": "Footer",
@@ -2586,7 +2606,7 @@
2586
2606
  "type": "ReactNode"
2587
2607
  }
2588
2608
  ],
2589
- "sourcePath": "packages/ui/src/components/Drawer/Drawer.tsx"
2609
+ "sourcePath": "packages/ui/src/components/Drawer/parts.tsx"
2590
2610
  },
2591
2611
  "Header": {
2592
2612
  "name": "Header",
@@ -2595,9 +2615,16 @@
2595
2615
  "name": "children",
2596
2616
  "required": true,
2597
2617
  "type": "ReactNode"
2618
+ },
2619
+ {
2620
+ "default": "'start'",
2621
+ "description": "'center' puts the Title in the middle and the other children at the end, as a phone sheet does (a text close button on the right). Default: 'start'",
2622
+ "name": "titleAlign",
2623
+ "required": false,
2624
+ "type": "'start' | 'center'"
2598
2625
  }
2599
2626
  ],
2600
- "sourcePath": "packages/ui/src/components/Drawer/Drawer.tsx"
2627
+ "sourcePath": "packages/ui/src/components/Drawer/parts.tsx"
2601
2628
  },
2602
2629
  "Portal": {
2603
2630
  "name": "Portal",
@@ -2617,6 +2644,13 @@
2617
2644
  "md",
2618
2645
  "lg"
2619
2646
  ]
2647
+ },
2648
+ {
2649
+ "default": "DEFAULT_GRABBER_LABELS",
2650
+ "description": "The grabber's accessible names, by what a press does next. Snap sheets only",
2651
+ "name": "grabberLabels",
2652
+ "required": false,
2653
+ "type": "GrabberLabels"
2620
2654
  }
2621
2655
  ],
2622
2656
  "sourcePath": "packages/ui/src/components/Drawer/Drawer.tsx"
@@ -2636,7 +2670,6 @@
2636
2670
  "type": "boolean"
2637
2671
  },
2638
2672
  {
2639
- "default": "false",
2640
2673
  "description": "Default open state (uncontrolled)",
2641
2674
  "name": "defaultOpen",
2642
2675
  "required": false,
@@ -2649,7 +2682,6 @@
2649
2682
  "type": "(open: boolean) => void"
2650
2683
  },
2651
2684
  {
2652
- "default": "'right'",
2653
2685
  "description": "Which edge the drawer slides from. Default: 'right'",
2654
2686
  "name": "anchor",
2655
2687
  "required": false,
@@ -2661,21 +2693,18 @@
2661
2693
  ]
2662
2694
  },
2663
2695
  {
2664
- "default": "true",
2665
- "description": "Whether clicking backdrop or pressing Escape closes the drawer. Default: true",
2696
+ "description": "Whether clicking backdrop or pressing Escape (Android's back button on native) closes the drawer. Default: true",
2666
2697
  "name": "dismissable",
2667
2698
  "required": false,
2668
2699
  "type": "boolean"
2669
2700
  },
2670
2701
  {
2671
- "default": "true",
2672
2702
  "description": "Whether swipe gesture dismisses the drawer. Default: true",
2673
2703
  "name": "swipeDismiss",
2674
2704
  "required": false,
2675
2705
  "type": "boolean"
2676
2706
  },
2677
2707
  {
2678
- "default": "'fast'",
2679
2708
  "description": "Motion token for the close animation. Default: 'fast' (150ms)",
2680
2709
  "name": "closeAnimationDuration",
2681
2710
  "required": false,
@@ -2689,6 +2718,22 @@
2689
2718
  "base",
2690
2719
  "slow"
2691
2720
  ]
2721
+ },
2722
+ {
2723
+ "description": "Bottom anchor only: the heights the sheet rests at. 'medium' is half the window, 'large' the window less its top safe-area inset. A drag moves between them and dismisses below the lowest; the grabber steps through them on a tap. Without it, a bottom drawer is as tall as its content.",
2724
+ "name": "snapPoints",
2725
+ "required": false,
2726
+ "type": "readonly SnapPoint[]"
2727
+ },
2728
+ {
2729
+ "description": "The detent the sheet opens at. Default: the lowest of snapPoints",
2730
+ "name": "defaultSnapPoint",
2731
+ "required": false,
2732
+ "type": "SnapPoint",
2733
+ "union": [
2734
+ "medium",
2735
+ "large"
2736
+ ]
2692
2737
  }
2693
2738
  ],
2694
2739
  "sourcePath": "packages/ui/src/components/Drawer/Drawer.tsx"
@@ -2702,7 +2747,7 @@
2702
2747
  "type": "ReactNode"
2703
2748
  }
2704
2749
  ],
2705
- "sourcePath": "packages/ui/src/components/Drawer/Drawer.tsx"
2750
+ "sourcePath": "packages/ui/src/components/Drawer/parts.tsx"
2706
2751
  },
2707
2752
  "Trigger": {
2708
2753
  "name": "Trigger",
@@ -3086,7 +3131,7 @@
3086
3131
  "importPath": "@duro-app/ui/form",
3087
3132
  "kind": "component",
3088
3133
  "meta": {
3089
- "description": "Form wrapper with Effect Schema validation and react-hook-form integration. Provides FormContext to child Field components.",
3134
+ "description": "Form wrapper with Effect Schema validation and react-hook-form integration. Provides FormContext to child Field components. On React Native, @duro-app/ui/form resolves to a native Form (a view, not a <form>): a Button type=\"submit\" inside it runs the validated submit, since there is no DOM submit event.",
3090
3135
  "example": "import {Schema} from 'effect'\n\nconst MySchema = Schema.Struct({\n email: Schema.String.pipe(\n Schema.pattern(/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/, {message: () => 'Invalid email'}),\n ),\n})\n\n<Form schema={MySchema} defaultValues={{email: ''}} onSubmit={handleSubmit}>\n {({formState}) => (\n <Fieldset.Root gap=\"md\">\n <Field.Root name=\"email\">\n <Field.Label>Email</Field.Label>\n <Input type=\"email\" />\n <Field.Error />\n </Field.Root>\n <Button type=\"submit\" disabled={!formState.isValid}>Submit</Button>\n </Fieldset.Root>\n )}\n</Form>",
3091
3136
  "relatedTo": [
3092
3137
  {
@@ -4089,7 +4134,7 @@
4089
4134
  "Option"
4090
4135
  ]
4091
4136
  },
4092
- "description": "Popup list of options for an input that keeps focus — an editor typeahead (mentions, slash commands) or a custom combobox. The anchor owns focus and the keyboard: it sets the controlled highlightedId and spreads Listbox.getAnchorProps() for aria-controls / aria-expanded / aria-activedescendant. A press on an option never takes focus from the anchor. Portalled and placed against anchor() (an input rect or a caret rect), capped at a size-token maxHeight (default listMaxH).",
4137
+ "description": "Popup list of options for an input that keeps focus — an editor typeahead (mentions, slash commands) or a custom combobox. The anchor owns focus and the keyboard: it sets the controlled highlightedId and spreads Listbox.getAnchorProps() for aria-controls / aria-expanded / aria-activedescendant. A press on an option never takes focus from the anchor. Portalled and placed against anchor() (an input rect or a caret rect), capped at a size-token maxHeight (default listMaxH). On React Native it is a plain list laid out where it is rendered (inside a Select or Combobox sheet): nothing is anchored or highlighted, the selected option is filled with a trailing check, and a press calls onSelect.",
4093
4138
  "example": "const id = useId()\nconst [highlightedId, setHighlightedId] = useState<string | null>(null)\nconst optionId = (value: string) => `${id}-${value}`\n// On the editor root: Object.entries(Listbox.getAnchorProps({id, open, highlightedId}))\n<Listbox.Root\n id={id}\n open={open}\n anchor={() => caretRect()}\n highlightedId={highlightedId}\n onHighlight={setHighlightedId}\n onSelect={(value) => insertMention(value)}\n getOptionId={optionId}\n aria-label=\"Mentions\"\n>\n {people.map((p) => (\n <Listbox.Option key={p.id} value={p.id}>{p.name}</Listbox.Option>\n ))}\n {people.length === 0 && <Listbox.Empty>No match</Listbox.Empty>}\n</Listbox.Root>",
4094
4139
  "relatedTo": [
4095
4140
  {
@@ -4114,7 +4159,7 @@
4114
4159
  ]
4115
4160
  },
4116
4161
  "name": "Listbox",
4117
- "native": false,
4162
+ "native": true,
4118
4163
  "parts": {
4119
4164
  "Empty": {
4120
4165
  "name": "Empty",
@@ -4347,7 +4392,7 @@
4347
4392
  "Item"
4348
4393
  ]
4349
4394
  },
4350
- "description": "Dropdown action menu. Triggers actions (not value selection). Compound component — Root is required (throws without it). Menu.Trigger renders the button itself: give it the label (text, an Icon), never a Button; an icon-only trigger takes aria-label, and variant=\"ghost\" suits toolbars; size=\"small\" matches small Toggles and Buttons (a small ghost trigger is iconButtonSm). The popup is portalled and placed against the trigger (never clipped by a sticky or overflow ancestor), capped at sizes.listMaxH with scrolling, and takes focus on open (WAI-ARIA menu button); Escape closes only the menu, not an enclosing Popover or Dialog.",
4395
+ "description": "Dropdown action menu. Triggers actions (not value selection). Compound component — Root is required (throws without it). Menu.Trigger renders the button itself: give it the label (text, an Icon), never a Button; an icon-only trigger takes aria-label, and variant=\"ghost\" suits toolbars; size=\"small\" matches small Toggles and Buttons (a small ghost trigger is iconButtonSm). The popup is portalled and placed against the trigger (never clipped by a sticky or overflow ancestor), capped at sizes.listMaxH with scrolling, and takes focus on open (WAI-ARIA menu button); Escape closes only the menu, not an enclosing Popover or Dialog. On React Native the popup is placed against the trigger in a modal overlay; a press outside or the Android back button closes it, and an item runs its onClick after the menu has gone (so an item can open a Dialog). Menu.LinkItem takes onNavigate(href, event) on both platforms (ADR-0002); on native, without it or without preventDefault, the link opens with the system. Native has no arrow keys: each item is its own screen-reader stop.",
4351
4396
  "example": "<Menu.Root>\n <Menu.Trigger>Actions</Menu.Trigger>\n <Menu.Popup>\n <Menu.Item onClick={() => console.log('edit')}>Edit</Menu.Item>\n <Menu.Item onClick={() => console.log('duplicate')}>Duplicate</Menu.Item>\n <Menu.Separator />\n <Menu.LinkItem href=\"/settings\">Settings</Menu.LinkItem>\n </Menu.Popup>\n</Menu.Root>",
4352
4397
  "relatedTo": [
4353
4398
  {
@@ -4374,7 +4419,7 @@
4374
4419
  ]
4375
4420
  },
4376
4421
  "name": "Menu",
4377
- "native": false,
4422
+ "native": true,
4378
4423
  "parts": {
4379
4424
  "Item": {
4380
4425
  "name": "Item",
@@ -4400,6 +4445,12 @@
4400
4445
  "required": true,
4401
4446
  "type": "string"
4402
4447
  },
4448
+ {
4449
+ "description": "Called for a plain primary click (ADR-0002): call `event.preventDefault()`, then your router's navigate. Without it the browser follows the link.",
4450
+ "name": "onNavigate",
4451
+ "required": false,
4452
+ "type": "OnNavigate"
4453
+ },
4403
4454
  {
4404
4455
  "name": "children",
4405
4456
  "required": true,
@@ -4964,7 +5015,7 @@
4964
5015
  "Popup"
4965
5016
  ]
4966
5017
  },
4967
- "description": "Non-modal anchored overlay for small interactive content. Compound component — Root is required (throws without it). Anchored to Popover.Trigger, or to a virtual anchor (a () => DOMRect) when there is no Trigger; call reposition() through a ref when the anchor moves. Popover.Trigger takes size=\"small\" beside small Toggles. Popover.Popup raised draws it over another floating surface (floatingRaised, popoverRaised in a modal), never over a modal.",
5018
+ "description": "Non-modal anchored overlay for small interactive content. Compound component — Root is required (throws without it). Anchored to Popover.Trigger, or to a virtual anchor (a () => DOMRect) when there is no Trigger; call reposition() through a ref when the anchor moves. Popover.Trigger takes size=\"small\" beside small Toggles. Popover.Popup raised draws it over another floating surface (floatingRaised, popoverRaised in a modal), never over a modal. On React Native the popup is modal: it is placed against Popover.Trigger in an overlay, a press outside or the Android back button closes it, and screen-reader focus moves in on open and back to the trigger on close; anchor, ignore and raised are web only.",
4968
5019
  "example": "const ref = useRef<PopoverHandle>(null)\nconst anchor = () => new DOMRect(point.x, point.y, 0, 0)\n\n<Popover.Root ref={ref} open={open} onOpenChange={setOpen} anchor={anchor} side=\"bottom\" align=\"start\">\n <Popover.Popup label=\"Edit label\">\n <Field.Root>\n <Field.Label>Label</Field.Label>\n <Input value={label} onChange={(e) => setLabel(e.target.value)} />\n </Field.Root>\n <Popover.Close>Done</Popover.Close>\n </Popover.Popup>\n</Popover.Root>\n// after the anchor moves: ref.current?.reposition()",
4969
5020
  "relatedTo": [
4970
5021
  {
@@ -5018,7 +5069,7 @@
5018
5069
  ]
5019
5070
  },
5020
5071
  "name": "Popover",
5021
- "native": false,
5072
+ "native": true,
5022
5073
  "parts": {
5023
5074
  "Close": {
5024
5075
  "name": "Close",
@@ -5325,7 +5376,7 @@
5325
5376
  "Thumb"
5326
5377
  ]
5327
5378
  },
5328
- "description": "Custom scrollbar region with draggable thumb. Supports vertical and horizontal scrolling. Compound component — Root is required. Inside a Drawer, a drag in the viewport scrolls it; the drawer only takes a drag toward its edge from the scroll start.",
5379
+ "description": "Custom scrollbar region with draggable thumb. Supports vertical and horizontal scrolling. Compound component — Root is required. Inside a Drawer, a drag in the viewport scrolls it; the drawer only takes a drag toward its edge from the scroll start. On React Native the Viewport is an RN ScrollView with the platform scroll indicator, capped by the same maxHeight token key; Scrollbar and Thumb render nothing.",
5329
5380
  "example": "<ScrollArea.Root>\n <ScrollArea.Viewport maxHeight=\"listMaxH\">\n <ScrollArea.Content>\n {/* Long content here */}\n </ScrollArea.Content>\n </ScrollArea.Viewport>\n <ScrollArea.Scrollbar orientation=\"vertical\">\n <ScrollArea.Thumb orientation=\"vertical\" />\n </ScrollArea.Scrollbar>\n</ScrollArea.Root>",
5330
5381
  "whenNotToUse": [
5331
5382
  "Simple page-level scrolling — browser scroll is fine",
@@ -5337,7 +5388,7 @@
5337
5388
  ]
5338
5389
  },
5339
5390
  "name": "ScrollArea",
5340
- "native": false,
5391
+ "native": true,
5341
5392
  "parts": {
5342
5393
  "Content": {
5343
5394
  "name": "Content",
@@ -5440,7 +5491,7 @@
5440
5491
  "Item"
5441
5492
  ]
5442
5493
  },
5443
- "description": "Dropdown select for choosing one value from a list. Compound component — Root is required (throws without it). Select.Trigger takes size=\"small\" to match small Toggles and Buttons in a toolbar.",
5494
+ "description": "Dropdown select for choosing one value from a list. Compound component — Root is required (throws without it). Select.Trigger takes size=\"small\" to match small Toggles and Buttons in a toolbar. Select.Popup takes label (the list's accessible name) and empty (shown in place of the list when it has no items). On React Native the trigger opens a bottom sheet (a Drawer at the medium and large detents) titled by label, with a Cancel text button (cancelLabel); the chosen option is filled with a trailing check, and a press picks an option and closes the sheet. Native has no arrow keys: each option is its own screen-reader stop.",
5444
5495
  "example": "<Select.Root name=\"role\" defaultValue=\"viewer\">\n <Select.Trigger>\n <Select.Value placeholder=\"Select role\" />\n <Select.Icon />\n </Select.Trigger>\n <Select.Popup>\n <Select.Item value=\"admin\">Admin</Select.Item>\n <Select.Item value=\"editor\">Editor</Select.Item>\n <Select.Item value=\"viewer\">Viewer</Select.Item>\n </Select.Popup>\n</Select.Root>",
5445
5496
  "relatedTo": [
5446
5497
  {
@@ -5470,7 +5521,7 @@
5470
5521
  ]
5471
5522
  },
5472
5523
  "name": "Select",
5473
- "native": false,
5524
+ "native": true,
5474
5525
  "parts": {
5475
5526
  "Icon": {
5476
5527
  "name": "Icon",
@@ -5517,6 +5568,24 @@
5517
5568
  "name": "children",
5518
5569
  "required": true,
5519
5570
  "type": "ReactNode"
5571
+ },
5572
+ {
5573
+ "description": "The list's accessible name; on native also the sheet's title.",
5574
+ "name": "label",
5575
+ "required": false,
5576
+ "type": "string"
5577
+ },
5578
+ {
5579
+ "description": "Native only: the sheet header's text button that closes it unchanged. Default: 'Cancel'",
5580
+ "name": "cancelLabel",
5581
+ "required": false,
5582
+ "type": "string"
5583
+ },
5584
+ {
5585
+ "description": "Shown in place of the list when there are no items.",
5586
+ "name": "empty",
5587
+ "required": false,
5588
+ "type": "ReactNode"
5520
5589
  }
5521
5590
  ],
5522
5591
  "sourcePath": "packages/ui/src/components/Select/Select.tsx"
@@ -5613,7 +5682,7 @@
5613
5682
  "Item"
5614
5683
  ]
5615
5684
  },
5616
- "description": "Vertical side navigation. `Section` is a labelled block that is always open — the default for primary navigation. `Group` is the same block behind a chevron (a disclosure) — reach for it only for a rare/advanced region, an unbounded data-driven list, or a rail long enough that flat stops reading as an overview; mixing the two in one rail is the healthy shape. `Item`s take an optional `icon` and show an active left-marker. Supports controlled and uncontrolled active item. Compound component — Root is required.",
5685
+ "description": "Vertical side navigation. `Section` is a labelled block that is always open — the default for primary navigation. `Group` is the same block behind a chevron (a disclosure) — reach for it only for a rare/advanced region, an unbounded data-driven list, or a rail long enough that flat stops reading as an overview; mixing the two in one rail is the healthy shape. `Item`s take an optional `icon` and show an active left-marker. Supports controlled and uncontrolled active item. Compound component — Root is required. On React Native the same parts render, and a Group knows whether it holds the active item from its Items, not from the DOM.",
5617
5686
  "example": "<SideNav.Root defaultValue=\"identities\">\n {/* Default: always-open sections. Nothing is a click away from being seen. */}\n <SideNav.Section label=\"People & access\">\n <SideNav.Item value=\"identities\" icon={<Icon name=\"users\" size=\"md\" />}>Identities</SideNav.Item>\n <SideNav.Item value=\"grants\" icon={<Icon name=\"key\" size=\"md\" />}>Grants</SideNav.Item>\n </SideNav.Section>\n {/* Disclosure, earned: rarely visited, so it starts collapsed. */}\n <SideNav.Group label=\"Advanced\" defaultExpanded={false}>\n <SideNav.Item value=\"plugins\" icon={<Icon name=\"plug\" size=\"md\" />}>Plugins</SideNav.Item>\n </SideNav.Group>\n</SideNav.Root>",
5618
5687
  "relatedTo": [
5619
5688
  {
@@ -5646,7 +5715,7 @@
5646
5715
  ]
5647
5716
  },
5648
5717
  "name": "SideNav",
5649
- "native": false,
5718
+ "native": true,
5650
5719
  "parts": {
5651
5720
  "Group": {
5652
5721
  "name": "Group",
@@ -5787,6 +5856,130 @@
5787
5856
  ],
5788
5857
  "sourcePath": "packages/ui/src/components/Spinner/Spinner.tsx"
5789
5858
  },
5859
+ "SplitPane": {
5860
+ "importPath": "@duro-app/ui",
5861
+ "kind": "compound",
5862
+ "meta": {
5863
+ "anatomy": {
5864
+ "optional": [
5865
+ "Handle"
5866
+ ],
5867
+ "required": [
5868
+ "Root",
5869
+ "Pane"
5870
+ ]
5871
+ },
5872
+ "description": "Two panes side by side with a resizable separator: the first Pane is sized by the Handle (pointer drag, arrow keys, Shift for larger steps, Home and End), the second takes the rest. Min, max and starting widths are size tokens. Below collapseBelow, measured on its own width, the panes stack in one column and the handle goes; on native they always stack. The Handle is a hairline that thickens to the accent on hover, keyboard focus or drag.",
5873
+ "example": "<SplitPane.Root aria-label=\"Resize the session list\" minSize=\"sidebarW\" maxSize=\"panelLg\">\n <SplitPane.Pane>\n <List.Root aria-label=\"Sessions\">…</List.Root>\n </SplitPane.Pane>\n <SplitPane.Handle />\n <SplitPane.Pane>\n <Panel.Root>…</Panel.Root>\n </SplitPane.Pane>\n</SplitPane.Root>",
5874
+ "relatedTo": [
5875
+ {
5876
+ "component": "Grid",
5877
+ "kind": "contrast",
5878
+ "relationship": "Grid layout=\"split\" is a fixed split; SplitPane lets people resize it"
5879
+ },
5880
+ {
5881
+ "component": "DetailPanel",
5882
+ "kind": "contrast",
5883
+ "relationship": "DetailPanel overlays the list; SplitPane keeps both side by side"
5884
+ }
5885
+ ],
5886
+ "whenNotToUse": [
5887
+ "A fixed list/detail layout nobody resizes — use Grid layout=\"split\"",
5888
+ "Navigation beside content — use AppShell or the page-with-sidenav recipe",
5889
+ "A short-lived inspection over a list — use DetailPanel"
5890
+ ],
5891
+ "whenToUse": [
5892
+ "A list beside its detail where people need more room on either side (a session list and its transcript)",
5893
+ "An editor beside its preview"
5894
+ ]
5895
+ },
5896
+ "name": "SplitPane",
5897
+ "native": true,
5898
+ "parts": {
5899
+ "Handle": {
5900
+ "name": "Handle",
5901
+ "props": [],
5902
+ "sourcePath": "packages/ui/src/components/SplitPane/SplitPane.tsx"
5903
+ },
5904
+ "Pane": {
5905
+ "name": "Pane",
5906
+ "props": [
5907
+ {
5908
+ "name": "children",
5909
+ "required": true,
5910
+ "type": "ReactNode"
5911
+ }
5912
+ ],
5913
+ "sourcePath": "packages/ui/src/components/SplitPane/SplitPane.tsx"
5914
+ },
5915
+ "Root": {
5916
+ "name": "Root",
5917
+ "props": [
5918
+ {
5919
+ "name": "children",
5920
+ "required": true,
5921
+ "type": "ReactNode"
5922
+ },
5923
+ {
5924
+ "default": "'sidebarW'",
5925
+ "description": "The primary (first) pane's starting width, a size token. Default: `sidebarW`",
5926
+ "name": "defaultSize",
5927
+ "required": false,
5928
+ "type": "SizeToken"
5929
+ },
5930
+ {
5931
+ "description": "Controlled width of the primary pane, in px (from `onSizeChange`)",
5932
+ "name": "size",
5933
+ "required": false,
5934
+ "type": "number"
5935
+ },
5936
+ {
5937
+ "description": "Called with the primary pane's new width while the handle moves",
5938
+ "name": "onSizeChange",
5939
+ "required": false,
5940
+ "type": "(px: number) => void"
5941
+ },
5942
+ {
5943
+ "default": "'sidebarW'",
5944
+ "description": "The primary pane's narrowest width, a size token. Default: `sidebarW`",
5945
+ "name": "minSize",
5946
+ "required": false,
5947
+ "type": "SizeToken"
5948
+ },
5949
+ {
5950
+ "default": "'panelLg'",
5951
+ "description": "The primary pane's widest width, a size token. Default: `panelLg`",
5952
+ "name": "maxSize",
5953
+ "required": false,
5954
+ "type": "SizeToken"
5955
+ },
5956
+ {
5957
+ "default": "'sm'",
5958
+ "description": "Below this breakpoint, measured on the split's own width, the panes stack. Default: `sm`",
5959
+ "name": "collapseBelow",
5960
+ "required": false,
5961
+ "type": "Breakpoint",
5962
+ "union": [
5963
+ "xs",
5964
+ "sm",
5965
+ "md",
5966
+ "lg",
5967
+ "xl"
5968
+ ]
5969
+ },
5970
+ {
5971
+ "description": "The handle's accessible name (\"Resize list\").",
5972
+ "name": "aria-label",
5973
+ "required": true,
5974
+ "type": "string"
5975
+ }
5976
+ ],
5977
+ "sourcePath": "packages/ui/src/components/SplitPane/SplitPane.tsx"
5978
+ }
5979
+ },
5980
+ "props": null,
5981
+ "sourcePath": "packages/ui/src/components/SplitPane/SplitPane.tsx"
5982
+ },
5790
5983
  "Stack": {
5791
5984
  "importPath": "@duro-app/ui",
5792
5985
  "kind": "component",
@@ -6680,7 +6873,7 @@
6680
6873
  "Panel"
6681
6874
  ]
6682
6875
  },
6683
- "description": "Tabbed interface with keyboard navigation. Supports horizontal and vertical orientation. Compound component — Root is required.",
6876
+ "description": "Tabbed interface with keyboard navigation. Supports horizontal and vertical orientation. Compound component — Root is required. On React Native the indicator follows each tab layout; there is no arrow-key navigation there, each tab is its own screen-reader stop.",
6684
6877
  "example": "<Tabs.Root defaultValue=\"general\">\n <Tabs.List>\n <Tabs.Tab value=\"general\">General</Tabs.Tab>\n <Tabs.Tab value=\"security\">Security</Tabs.Tab>\n <Tabs.Tab value=\"billing\" disabled>Billing</Tabs.Tab>\n </Tabs.List>\n <Tabs.Panel value=\"general\">General settings content</Tabs.Panel>\n <Tabs.Panel value=\"security\">Security settings content</Tabs.Panel>\n <Tabs.Panel value=\"billing\">Billing content</Tabs.Panel>\n</Tabs.Root>",
6685
6878
  "relatedTo": [
6686
6879
  {
@@ -6705,7 +6898,7 @@
6705
6898
  ]
6706
6899
  },
6707
6900
  "name": "Tabs",
6708
- "native": false,
6901
+ "native": true,
6709
6902
  "parts": {
6710
6903
  "List": {
6711
6904
  "name": "List",
@@ -6796,7 +6989,7 @@
6796
6989
  "importPath": "@duro-app/ui",
6797
6990
  "kind": "component",
6798
6991
  "meta": {
6799
- "description": "Interactive tag/chip with optional remove button. Works standalone or inside TagGroup for collection management. Visually similar to Badge but supports interaction.",
6992
+ "description": "Interactive tag/chip with optional remove button. Works standalone or inside TagGroup for collection management. Visually similar to Badge but supports interaction. On React Native a Tag stands alone (TagGroup is web only): the label and, when removable, a remove button named from a string label.",
6800
6993
  "example": "<Tag removable onRemove={() => console.log('removed')}>\n user@example.com\n</Tag>",
6801
6994
  "relatedTo": [
6802
6995
  {
@@ -6821,7 +7014,7 @@
6821
7014
  ]
6822
7015
  },
6823
7016
  "name": "Tag",
6824
- "native": false,
7017
+ "native": true,
6825
7018
  "props": [
6826
7019
  {
6827
7020
  "description": "Value identifier — required when used inside TagGroup",
@@ -7340,7 +7533,7 @@
7340
7533
  "importPath": "@duro-app/ui",
7341
7534
  "kind": "provider",
7342
7535
  "meta": {
7343
- "description": "Theme context provider. Must wrap the entire app at the root level. Sets dark, light, or high-contrast theme for all child components via CSS custom properties.",
7536
+ "description": "Theme context provider. Must wrap the entire app at the root level. Sets dark, light, or high-contrast theme for all child components via CSS custom properties. overrides replaces colours by token key ({accent: \"#ff5a1f\"}) and updates in place, without a re-mount: a scoped rule on the web (values that are not a colour are dropped), a runtime theme on React Native, where useResolvedColors() follows too. Overrides replace a colour whatever the theme, so pass per-mode values when the mode can change. A nested provider on the same theme keeps the outer overrides and lays its own over them; on another theme it starts from that theme's colours.",
7344
7537
  "example": "<ThemeProvider theme=\"dark\">\n <App />\n</ThemeProvider>",
7345
7538
  "whenNotToUse": [
7346
7539
  "Never omit — all Duro components require ThemeProvider"
@@ -7364,6 +7557,12 @@
7364
7557
  "high-contrast"
7365
7558
  ]
7366
7559
  },
7560
+ {
7561
+ "description": "Colours that replace the theme's own, by token key (`{accent: '#ff5a1f'}`). They update in place, without a re-mount. A nested provider on the same theme keeps these and lays its own over them.",
7562
+ "name": "overrides",
7563
+ "required": false,
7564
+ "type": "ColorOverrides"
7565
+ },
7367
7566
  {
7368
7567
  "name": "children",
7369
7568
  "required": true,
@@ -7621,7 +7820,7 @@
7621
7820
  "importPath": "@duro-app/ui",
7622
7821
  "kind": "provider",
7623
7822
  "meta": {
7624
- "description": "Transient, self-dismissing notification for the RESULT of an action (saved, sent, revoked, failed). Mount <ToastProvider> once at the app root and call useToast().toast() from anywhere. Toasts stack and portal above dialogs. Variants: success, info, warning, error. The countdown pauses while a toast is hovered or holds focus, and every toast has a close button (`dismissLabel` on the provider names it).",
7823
+ "description": "Transient, self-dismissing notification for the RESULT of an action (saved, sent, revoked, failed). Mount <ToastProvider> once at the app root and call useToast().toast() from anywhere. Toasts stack and portal above dialogs. Variants: success, info, warning, error. The countdown pauses while a toast is hovered or holds focus, and every toast has a close button (`dismissLabel` on the provider names it). On React Native the same API draws through the portal host above every open overlay and the keyboard, as a card with a status-coloured edge, its action and its close in one row; a sideways swipe dismisses, a press holds the countdown, and so does an active screen reader (WCAG 2.2.1).",
7625
7824
  "example": "function Example() {\n const {toast} = useToast()\n return (\n <Button onClick={() => toast({variant: 'success', message: 'Grant revoked', action: {label: 'Undo', onClick: restore}})}>\n Revoke\n </Button>\n )\n}",
7626
7825
  "relatedTo": [
7627
7826
  {
@@ -7648,7 +7847,7 @@
7648
7847
  ]
7649
7848
  },
7650
7849
  "name": "ToastProvider",
7651
- "native": false,
7850
+ "native": true,
7652
7851
  "props": [
7653
7852
  {
7654
7853
  "default": "'Dismiss'",
@@ -7954,7 +8153,7 @@
7954
8153
  "Trigger"
7955
8154
  ]
7956
8155
  },
7957
- "description": "Hover/focus tooltip that shows supplementary content. Compound component — Root is required (throws without it).",
8156
+ "description": "Hover/focus tooltip that shows supplementary content. Compound component — Root is required (throws without it). On touch (React Native) no bubble is drawn: a string content becomes the accessibility hint of the Duro trigger inside (Button, Toggle, Menu.Trigger, Popover.Trigger), read after its name. Keep tooltip text non-essential, and keep aria-label on icon-only triggers.",
7958
8157
  "example": "<Tooltip.Root content=\"Delete this item permanently\" placement=\"top\">\n <Tooltip.Trigger>\n <Button variant=\"danger\">Delete</Button>\n </Tooltip.Trigger>\n</Tooltip.Root>",
7959
8158
  "whenNotToUse": [
7960
8159
  "Critical information that must always be visible — use Text or Callout",
@@ -7966,7 +8165,7 @@
7966
8165
  ]
7967
8166
  },
7968
8167
  "name": "Tooltip",
7969
- "native": false,
8168
+ "native": true,
7970
8169
  "parts": {
7971
8170
  "Root": {
7972
8171
  "name": "Root",
@@ -8021,7 +8220,7 @@
8021
8220
  "Item"
8022
8221
  ]
8023
8222
  },
8024
- "description": "Hierarchy of items with single selection and expandable branches (the WAI-ARIA tree pattern): one tab stop, arrow keys, Home/End, typeahead. An Item with href renders its row as a real link (cmd-click opens a tab) outside the tab order; Enter/Space follows it, through onNavigate(href, event) when given. Compound component — Root is required.",
8223
+ "description": "Hierarchy of items with single selection and expandable branches (the WAI-ARIA tree pattern): one tab stop, arrow keys, Home/End, typeahead. An Item with href renders its row as a real link (cmd-click opens a tab) outside the tab order; Enter/Space follows it, through onNavigate(href, event) when given. Compound component — Root is required. On React Native selection, expansion and aria-level are the same; the keyboard model (arrows, Home/End, typeahead) is web only.",
8025
8224
  "example": "<Tree.Root aria-label=\"Blocs\" defaultExpanded={['energy']} onValueChange={setSelected}>\n <Tree.Item value=\"energy\" label=\"Core business — Energy\">\n <Tree.Item value=\"generation\" label=\"Generation\" meta=\"4 apps\" />\n <Tree.Item value=\"network\" label=\"Network operations\">\n <Tree.Item value=\"scada\" label=\"SCADA supervision\" />\n </Tree.Item>\n </Tree.Item>\n <Tree.Item value=\"finance\" label=\"Finance & steering\" />\n</Tree.Root>",
8026
8225
  "relatedTo": [
8027
8226
  {
@@ -8047,7 +8246,7 @@
8047
8246
  ]
8048
8247
  },
8049
8248
  "name": "Tree",
8050
- "native": false,
8249
+ "native": true,
8051
8250
  "parts": {
8052
8251
  "Item": {
8053
8252
  "name": "Item",
@@ -8157,6 +8356,92 @@
8157
8356
  "props": null,
8158
8357
  "sourcePath": "packages/ui/src/components/Tree/Tree.tsx"
8159
8358
  },
8359
+ "VirtualList": {
8360
+ "importPath": "@duro-app/ui",
8361
+ "kind": "component",
8362
+ "meta": {
8363
+ "anatomy": {
8364
+ "optional": [],
8365
+ "required": []
8366
+ },
8367
+ "description": "A windowed list on @tanstack/react-virtual: only the rows in view (plus overscan) are mounted, and rows are measured so they may differ in height. role=\"list\" with listitem rows carrying aria-setsize and aria-posinset. anchor=\"end\" opens at the last item and stays pinned to the bottom while new items arrive and the reader is at the bottom (a transcript, a log). It fills its parent, or stops at maxHeight. Web only: on native, host FlashList or Legend List behind your own .native.tsx leaf.",
8368
+ "example": "<VirtualList\n aria-label=\"Transcript\"\n items={messages}\n getKey={(m) => m.id}\n estimateSize=\"controlLg\"\n anchor=\"end\"\n renderItem={(m) => <Text>{m.text}</Text>}\n/>",
8369
+ "relatedTo": [
8370
+ {
8371
+ "component": "VirtualTable",
8372
+ "kind": "contrast",
8373
+ "relationship": "VirtualTable windows table rows with columns; VirtualList windows any row"
8374
+ },
8375
+ {
8376
+ "component": "List",
8377
+ "kind": "composition",
8378
+ "relationship": "Render a List.Item-like row in renderItem for the list look"
8379
+ }
8380
+ ],
8381
+ "whenNotToUse": [
8382
+ "Tabular data with columns and sorting — use VirtualTable",
8383
+ "A few dozen rows — use List; windowing only adds cost there",
8384
+ "Selectable options in a picker — use Listbox"
8385
+ ],
8386
+ "whenToUse": [
8387
+ "Hundreds to tens of thousands of rows: sessions, a chat transcript, log lines",
8388
+ "A list that grows at the bottom while the reader follows it (anchor=\"end\")"
8389
+ ]
8390
+ },
8391
+ "name": "VirtualList",
8392
+ "native": false,
8393
+ "props": [
8394
+ {
8395
+ "name": "items",
8396
+ "required": true,
8397
+ "type": "readonly T[]"
8398
+ },
8399
+ {
8400
+ "description": "Each row's starting height estimate, a size token; rows are then measured.",
8401
+ "name": "estimateSize",
8402
+ "required": true,
8403
+ "type": "SizeToken"
8404
+ },
8405
+ {
8406
+ "name": "renderItem",
8407
+ "required": true,
8408
+ "type": "(item: T, index: number) => ReactNode"
8409
+ },
8410
+ {
8411
+ "description": "A stable key per item (an id), so rows keep their measurement as items change.",
8412
+ "name": "getKey",
8413
+ "required": true,
8414
+ "type": "(item: T, index: number) => string | number"
8415
+ },
8416
+ {
8417
+ "default": "5",
8418
+ "description": "Rows rendered beyond each edge of the view. Default 5",
8419
+ "name": "overscan",
8420
+ "required": false,
8421
+ "type": "number"
8422
+ },
8423
+ {
8424
+ "default": "'start'",
8425
+ "description": "`end` starts at the last item and stays pinned to the bottom while new items arrive, as long as the reader is at the bottom (a transcript, a log). Default `start`.",
8426
+ "name": "anchor",
8427
+ "required": false,
8428
+ "type": "'start' | 'end'"
8429
+ },
8430
+ {
8431
+ "description": "Cap the list's height; without it, it fills its parent.",
8432
+ "name": "maxHeight",
8433
+ "required": false,
8434
+ "type": "Length"
8435
+ },
8436
+ {
8437
+ "description": "The list's accessible name.",
8438
+ "name": "aria-label",
8439
+ "required": true,
8440
+ "type": "string"
8441
+ }
8442
+ ],
8443
+ "sourcePath": "packages/ui/src/components/VirtualList/VirtualList.tsx"
8444
+ },
8160
8445
  "VirtualTable": {
8161
8446
  "importPath": "@duro-app/ui/table",
8162
8447
  "kind": "component",
@@ -8366,7 +8651,7 @@
8366
8651
  "ColorModeProvider"
8367
8652
  ]
8368
8653
  },
8369
- "description": "Color-mode controller + toggle. ColorModeProvider owns the preference (system/light/dark/high-contrast), resolves \"system\" against the OS scheme, persists it to localStorage, and applies the resolved theme via ThemeProvider. ColorModeToggle is an icon button that cycles the preference; useColorMode() reads it.",
8654
+ "description": "Color-mode controller + toggle. ColorModeProvider owns the preference (system/light/dark/high-contrast), resolves \"system\" against the OS scheme, persists it to localStorage, and applies the resolved theme via ThemeProvider. ColorModeToggle is an icon button that cycles the preference; useColorMode() reads it. On React Native \"system\" follows the OS appearance (useColorScheme) and nothing is stored: persist the preference yourself through preference and onPreferenceChange.",
8370
8655
  "example": "// At the app root — replaces a bare <ThemeProvider>:\n<ColorModeProvider defaultPreference=\"system\" modes={['light', 'dark']}>\n <App />\n</ColorModeProvider>\n\n// Anywhere inside (e.g. the top bar):\n<ColorModeToggle />\n\n// Or read the resolved theme:\nconst {theme, preference, setPreference} = useColorMode()",
8371
8656
  "relatedTo": [
8372
8657
  {
@@ -8391,7 +8676,7 @@
8391
8676
  ]
8392
8677
  },
8393
8678
  "name": "useColorMode",
8394
- "native": false,
8679
+ "native": true,
8395
8680
  "props": [],
8396
8681
  "sourcePath": "packages/ui/src/components/ColorMode/ColorMode.tsx"
8397
8682
  },
@@ -8524,7 +8809,7 @@
8524
8809
  "importPath": "@duro-app/ui",
8525
8810
  "kind": "hook",
8526
8811
  "meta": {
8527
- "description": "Theme context provider. Must wrap the entire app at the root level. Sets dark, light, or high-contrast theme for all child components via CSS custom properties.",
8812
+ "description": "Theme context provider. Must wrap the entire app at the root level. Sets dark, light, or high-contrast theme for all child components via CSS custom properties. overrides replaces colours by token key ({accent: \"#ff5a1f\"}) and updates in place, without a re-mount: a scoped rule on the web (values that are not a colour are dropped), a runtime theme on React Native, where useResolvedColors() follows too. Overrides replace a colour whatever the theme, so pass per-mode values when the mode can change. A nested provider on the same theme keeps the outer overrides and lays its own over them; on another theme it starts from that theme's colours.",
8528
8813
  "example": "<ThemeProvider theme=\"dark\">\n <App />\n</ThemeProvider>",
8529
8814
  "whenNotToUse": [
8530
8815
  "Never omit — all Duro components require ThemeProvider"
@@ -8543,7 +8828,7 @@
8543
8828
  "importPath": "@duro-app/ui",
8544
8829
  "kind": "hook",
8545
8830
  "meta": {
8546
- "description": "Transient, self-dismissing notification for the RESULT of an action (saved, sent, revoked, failed). Mount <ToastProvider> once at the app root and call useToast().toast() from anywhere. Toasts stack and portal above dialogs. Variants: success, info, warning, error. The countdown pauses while a toast is hovered or holds focus, and every toast has a close button (`dismissLabel` on the provider names it).",
8831
+ "description": "Transient, self-dismissing notification for the RESULT of an action (saved, sent, revoked, failed). Mount <ToastProvider> once at the app root and call useToast().toast() from anywhere. Toasts stack and portal above dialogs. Variants: success, info, warning, error. The countdown pauses while a toast is hovered or holds focus, and every toast has a close button (`dismissLabel` on the provider names it). On React Native the same API draws through the portal host above every open overlay and the keyboard, as a card with a status-coloured edge, its action and its close in one row; a sideways swipe dismisses, a press holds the countdown, and so does an active screen reader (WCAG 2.2.1).",
8547
8832
  "example": "function Example() {\n const {toast} = useToast()\n return (\n <Button onClick={() => toast({variant: 'success', message: 'Grant revoked', action: {label: 'Undo', onClick: restore}})}>\n Revoke\n </Button>\n )\n}",
8548
8833
  "relatedTo": [
8549
8834
  {
@@ -8570,7 +8855,7 @@
8570
8855
  ]
8571
8856
  },
8572
8857
  "name": "useToast",
8573
- "native": false,
8858
+ "native": true,
8574
8859
  "props": [],
8575
8860
  "sourcePath": "packages/ui/src/components/Toast/ToastProvider.tsx"
8576
8861
  }
@@ -8963,7 +9248,7 @@
8963
9248
  "split-pane": {
8964
9249
  "export": "SplitPaneRecipe",
8965
9250
  "meta": {
8966
- "description": "List/detail split: a selectable List beside a Panel showing the selection, on Grid layout=\"split\" (list ≥ 240px, one column when the board is narrower than sm).",
9251
+ "description": "List/detail split: a selectable List beside a Panel showing the selection, in a SplitPane (the list starts at sidebarW, resizes up to panelLg with the handle, and the panes stack when the split is narrower than sm).",
8967
9252
  "example": "<SplitPaneRecipe />",
8968
9253
  "whenNotToUse": [
8969
9254
  "The detail is a short-lived inspection — use DetailPanel over the list instead",
@@ -8976,18 +9261,18 @@
8976
9261
  },
8977
9262
  "name": "split-pane",
8978
9263
  "peerDeps": [],
8979
- "source": "import {useState} from 'react'\nimport {Grid, List, Panel, Stack, Inline, Heading, Text, Badge, Button, EmptyState} from '@duro-app/ui'\n\nconst ITEMS = [\n {id: 'grafana', name: 'Grafana', owner: 'platform', status: 'active'},\n {id: 'vault', name: 'Vault', owner: 'security', status: 'active'},\n {id: 'legacy-crm', name: 'Legacy CRM', owner: 'sales', status: 'retiring'},\n] as const\n\nexport function SplitPaneRecipe() {\n const [selected, setSelected] = useState<string | null>(ITEMS[0].id)\n const item = ITEMS.find((candidate) => candidate.id === selected)\n\n return (\n <Grid layout=\"split\" gap=\"lg\">\n <List.Root selectionMode=\"single\" aria-label=\"Applications\">\n {ITEMS.map((candidate) => (\n <List.Item\n key={candidate.id}\n selected={candidate.id === selected}\n onClick={() => setSelected(candidate.id)}\n >\n <List.Content>\n <List.Text>{candidate.name}</List.Text>\n <List.Description>Owned by {candidate.owner}</List.Description>\n </List.Content>\n <List.Actions>\n <Badge variant={candidate.status === 'active' ? 'success' : 'warning'}>\n {candidate.status}\n </Badge>\n </List.Actions>\n </List.Item>\n ))}\n <List.Empty>No applications.</List.Empty>\n </List.Root>\n\n <Panel.Root bordered>\n {item ? (\n <>\n <Panel.Header>\n <Heading level={2}>{item.name}</Heading>\n <Inline gap=\"sm\">\n <Button variant=\"secondary\" size=\"small\">\n Edit\n </Button>\n <Button variant=\"danger\" size=\"small\">\n Retire\n </Button>\n </Inline>\n </Panel.Header>\n <Panel.Body>\n <Stack gap=\"md\">\n <Text>\n Owner: <Text weight=\"medium\">{item.owner}</Text>\n </Text>\n <Text color=\"muted\">\n Everything about the selection lives here: fields, history, related records.\n </Text>\n </Stack>\n </Panel.Body>\n </>\n ) : (\n <Panel.Body>\n <EmptyState message=\"Select an application to see its details.\" />\n </Panel.Body>\n )}\n </Panel.Root>\n </Grid>\n )\n}\n",
9264
+ "source": "import {useState} from 'react'\nimport {SplitPane, List, Panel, Stack, Inline, Heading, Text, Badge, Button, EmptyState} from '@duro-app/ui'\n\nconst ITEMS = [\n {id: 'grafana', name: 'Grafana', owner: 'platform', status: 'active'},\n {id: 'vault', name: 'Vault', owner: 'security', status: 'active'},\n {id: 'legacy-crm', name: 'Legacy CRM', owner: 'sales', status: 'retiring'},\n] as const\n\nexport function SplitPaneRecipe() {\n const [selected, setSelected] = useState<string | null>(ITEMS[0].id)\n const item = ITEMS.find((candidate) => candidate.id === selected)\n\n return (\n <SplitPane.Root aria-label=\"Resize the application list\" minSize=\"sidebarW\" maxSize=\"panelLg\">\n <SplitPane.Pane>\n <List.Root selectionMode=\"single\" aria-label=\"Applications\">\n {ITEMS.map((candidate) => (\n <List.Item\n key={candidate.id}\n selected={candidate.id === selected}\n onClick={() => setSelected(candidate.id)}\n >\n <List.Content>\n <List.Text>{candidate.name}</List.Text>\n <List.Description>Owned by {candidate.owner}</List.Description>\n </List.Content>\n <List.Actions>\n <Badge variant={candidate.status === 'active' ? 'success' : 'warning'}>\n {candidate.status}\n </Badge>\n </List.Actions>\n </List.Item>\n ))}\n <List.Empty>No applications.</List.Empty>\n </List.Root>\n </SplitPane.Pane>\n <SplitPane.Handle />\n <SplitPane.Pane>\n <Panel.Root bordered>\n {item ? (\n <>\n <Panel.Header>\n <Heading level={2}>{item.name}</Heading>\n <Inline gap=\"sm\">\n <Button variant=\"secondary\" size=\"small\">\n Edit\n </Button>\n <Button variant=\"danger\" size=\"small\">\n Retire\n </Button>\n </Inline>\n </Panel.Header>\n <Panel.Body>\n <Stack gap=\"md\">\n <Text>\n Owner: <Text weight=\"medium\">{item.owner}</Text>\n </Text>\n <Text color=\"muted\">\n Everything about the selection lives here: fields, history, related records.\n </Text>\n </Stack>\n </Panel.Body>\n </>\n ) : (\n <Panel.Body>\n <EmptyState message=\"Select an application to see its details.\" />\n </Panel.Body>\n )}\n </Panel.Root>\n </SplitPane.Pane>\n </SplitPane.Root>\n )\n}\n",
8980
9265
  "sourcePath": "docs/ai/recipes/split-pane.recipe.tsx",
8981
9266
  "title": "Split Pane",
8982
9267
  "usesComponents": [
8983
9268
  "Badge",
8984
9269
  "Button",
8985
9270
  "EmptyState",
8986
- "Grid",
8987
9271
  "Heading",
8988
9272
  "Inline",
8989
9273
  "List",
8990
9274
  "Panel",
9275
+ "SplitPane",
8991
9276
  "Stack",
8992
9277
  "Text"
8993
9278
  ]
@@ -9486,6 +9771,206 @@
9486
9771
  "highContrast": "rgba(0, 0, 0, 0.42)",
9487
9772
  "light": "rgba(255, 255, 255, 0.33)"
9488
9773
  }
9774
+ },
9775
+ {
9776
+ "key": "bgInset",
9777
+ "values": {
9778
+ "dark": "#0a0a0a",
9779
+ "highContrast": "#0a0a0a",
9780
+ "light": "#f0f0f0"
9781
+ }
9782
+ },
9783
+ {
9784
+ "key": "bgElevated",
9785
+ "values": {
9786
+ "dark": "#242424",
9787
+ "highContrast": "#1a1a1a",
9788
+ "light": "#ffffff"
9789
+ }
9790
+ },
9791
+ {
9792
+ "key": "bgSelected",
9793
+ "values": {
9794
+ "dark": "rgba(106, 175, 252, 0.15)",
9795
+ "highContrast": "rgba(96, 165, 250, 0.25)",
9796
+ "light": "rgba(30, 64, 175, 0.12)"
9797
+ }
9798
+ },
9799
+ {
9800
+ "key": "pressOverlay",
9801
+ "values": {
9802
+ "dark": "rgba(255, 255, 255, 0.08)",
9803
+ "highContrast": "rgba(255, 255, 255, 0.15)",
9804
+ "light": "rgba(0, 0, 0, 0.08)"
9805
+ }
9806
+ },
9807
+ {
9808
+ "key": "ripple",
9809
+ "values": {
9810
+ "dark": "rgba(255, 255, 255, 0.12)",
9811
+ "highContrast": "rgba(255, 255, 255, 0.2)",
9812
+ "light": "rgba(0, 0, 0, 0.12)"
9813
+ }
9814
+ },
9815
+ {
9816
+ "key": "bgSectionTint",
9817
+ "values": {
9818
+ "dark": "rgba(255, 255, 255, 0.03)",
9819
+ "highContrast": "rgba(255, 255, 255, 0.06)",
9820
+ "light": "rgba(0, 0, 0, 0.03)"
9821
+ }
9822
+ },
9823
+ {
9824
+ "key": "borderSubtle",
9825
+ "values": {
9826
+ "dark": "#242424",
9827
+ "highContrast": "#333333",
9828
+ "light": "#ebebeb"
9829
+ }
9830
+ },
9831
+ {
9832
+ "key": "borderStrong",
9833
+ "values": {
9834
+ "dark": "#555555",
9835
+ "highContrast": "#888888",
9836
+ "light": "#a3a3a3"
9837
+ }
9838
+ },
9839
+ {
9840
+ "key": "surfaceHighlight",
9841
+ "values": {
9842
+ "dark": "rgba(255, 255, 255, 0.06)",
9843
+ "highContrast": "rgba(255, 255, 255, 0.15)",
9844
+ "light": "rgba(255, 255, 255, 0.7)"
9845
+ }
9846
+ },
9847
+ {
9848
+ "key": "textSubtle",
9849
+ "values": {
9850
+ "dark": "#8a8a8a",
9851
+ "highContrast": "#a3a3a3",
9852
+ "light": "#6b6b6b"
9853
+ }
9854
+ },
9855
+ {
9856
+ "key": "textDisabled",
9857
+ "values": {
9858
+ "dark": "#6b6b6b",
9859
+ "highContrast": "#808080",
9860
+ "light": "#a3a3a3"
9861
+ }
9862
+ },
9863
+ {
9864
+ "key": "neutral",
9865
+ "values": {
9866
+ "dark": "#a3a3a3",
9867
+ "highContrast": "#d4d4d4",
9868
+ "light": "#525252"
9869
+ }
9870
+ },
9871
+ {
9872
+ "key": "neutralBg",
9873
+ "values": {
9874
+ "dark": "rgba(163, 163, 163, 0.1)",
9875
+ "highContrast": "rgba(212, 212, 212, 0.15)",
9876
+ "light": "rgba(82, 82, 82, 0.08)"
9877
+ }
9878
+ },
9879
+ {
9880
+ "key": "neutralBorder",
9881
+ "values": {
9882
+ "dark": "rgba(163, 163, 163, 0.3)",
9883
+ "highContrast": "rgba(212, 212, 212, 0.5)",
9884
+ "light": "rgba(82, 82, 82, 0.3)"
9885
+ }
9886
+ },
9887
+ {
9888
+ "key": "neutralText",
9889
+ "values": {
9890
+ "dark": "#d4d4d4",
9891
+ "highContrast": "#e5e5e5",
9892
+ "light": "#404040"
9893
+ }
9894
+ },
9895
+ {
9896
+ "key": "active",
9897
+ "values": {
9898
+ "dark": "#6aaffc",
9899
+ "highContrast": "#60a5fa",
9900
+ "light": "#1e40af"
9901
+ }
9902
+ },
9903
+ {
9904
+ "key": "activeBg",
9905
+ "values": {
9906
+ "dark": "rgba(106, 175, 252, 0.1)",
9907
+ "highContrast": "rgba(96, 165, 250, 0.15)",
9908
+ "light": "rgba(30, 64, 175, 0.08)"
9909
+ }
9910
+ },
9911
+ {
9912
+ "key": "activeBorder",
9913
+ "values": {
9914
+ "dark": "rgba(106, 175, 252, 0.3)",
9915
+ "highContrast": "rgba(96, 165, 250, 0.5)",
9916
+ "light": "rgba(30, 64, 175, 0.3)"
9917
+ }
9918
+ },
9919
+ {
9920
+ "key": "activeText",
9921
+ "values": {
9922
+ "dark": "#93c5fd",
9923
+ "highContrast": "#bfdbfe",
9924
+ "light": "#1e40af"
9925
+ }
9926
+ },
9927
+ {
9928
+ "key": "accentDisabled",
9929
+ "values": {
9930
+ "dark": "rgba(106, 175, 252, 0.4)",
9931
+ "highContrast": "rgba(96, 165, 250, 0.5)",
9932
+ "light": "rgba(30, 64, 175, 0.4)"
9933
+ }
9934
+ },
9935
+ {
9936
+ "key": "segmentTrack",
9937
+ "values": {
9938
+ "dark": "rgba(255, 255, 255, 0.06)",
9939
+ "highContrast": "rgba(255, 255, 255, 0.12)",
9940
+ "light": "rgba(0, 0, 0, 0.06)"
9941
+ }
9942
+ },
9943
+ {
9944
+ "key": "segmentActive",
9945
+ "values": {
9946
+ "dark": "#333333",
9947
+ "highContrast": "#333333",
9948
+ "light": "#ffffff"
9949
+ }
9950
+ },
9951
+ {
9952
+ "key": "scrimSoft",
9953
+ "values": {
9954
+ "dark": "rgba(0, 0, 0, 0.2)",
9955
+ "highContrast": "rgba(0, 0, 0, 0.3)",
9956
+ "light": "rgba(0, 0, 0, 0.2)"
9957
+ }
9958
+ },
9959
+ {
9960
+ "key": "scrimStrong",
9961
+ "values": {
9962
+ "dark": "rgba(0, 0, 0, 0.6)",
9963
+ "highContrast": "rgba(0, 0, 0, 0.7)",
9964
+ "light": "rgba(0, 0, 0, 0.6)"
9965
+ }
9966
+ },
9967
+ {
9968
+ "key": "onScrimMuted",
9969
+ "values": {
9970
+ "dark": "rgba(255, 255, 255, 0.72)",
9971
+ "highContrast": "rgba(255, 255, 255, 0.72)",
9972
+ "light": "rgba(255, 255, 255, 0.72)"
9973
+ }
9489
9974
  }
9490
9975
  ],
9491
9976
  "exportName": "colors",
@@ -10089,6 +10574,14 @@
10089
10574
  "key": "barH",
10090
10575
  "value": "4px"
10091
10576
  },
10577
+ {
10578
+ "key": "sheetGrabberW",
10579
+ "value": "40px"
10580
+ },
10581
+ {
10582
+ "key": "sheetGrabberH",
10583
+ "value": "4px"
10584
+ },
10092
10585
  {
10093
10586
  "key": "readoutW",
10094
10587
  "value": "40px"
@@ -10278,7 +10771,7 @@
10278
10771
  "importPath": "@duro-app/tokens/tokens/typography.css"
10279
10772
  }
10280
10773
  },
10281
- "mockupCss": "/*\n * GENERATED by scripts/generate-mockup-css.mjs — do not edit.\n *\n * The design tokens as resolved values, for HTML that loads no stylesheet\n * (mockup artboards, static previews). Same names as vars.css; where vars.css\n * follows the live theme, this file carries every theme and switches on\n * <html data-theme=\"light|high-contrast\">.\n *\n * Inside the component library and the apps, never use this: go through\n * css.create() and the token imports, or vars.css from plain CSS.\n */\n:root {\n color-scheme: dark;\n /* colors */\n --duro-color-accent: #6aaffc;\n --duro-color-accent-contrast: #000000;\n --duro-color-accent-hover: #93c5fd;\n --duro-color-bg: #0f0f0f;\n --duro-color-bg-card: #1a1a1a;\n --duro-color-bg-card-hover: #242424;\n --duro-color-border: #333333;\n --duro-color-contrast-border: rgba(0, 0, 0, 0.42);\n --duro-color-contrast-surface: #f5f5f5;\n --duro-color-error: #f87171;\n --duro-color-error-bg: rgba(248, 113, 113, 0.1);\n --duro-color-error-border: rgba(248, 113, 113, 0.3);\n --duro-color-error-contrast: #000000;\n --duro-color-error-hover: #fca5a5;\n --duro-color-error-text: #fca5a5;\n --duro-color-fixed-dark: #000000;\n --duro-color-fixed-light: #ffffff;\n --duro-color-highlight: #c084fc;\n --duro-color-highlight-bg: rgba(192, 132, 252, 0.1);\n --duro-color-highlight-border: rgba(192, 132, 252, 0.3);\n --duro-color-highlight-text: #d8b4fe;\n --duro-color-info: #6aaffc;\n --duro-color-info-bg: rgba(106, 175, 252, 0.1);\n --duro-color-info-border: rgba(106, 175, 252, 0.3);\n --duro-color-info-text: #93c5fd;\n --duro-color-inverse-border: rgba(0, 0, 0, 0.55);\n --duro-color-inverse-border-hover: rgba(0, 0, 0, 0.70);\n --duro-color-inverse-fill: rgba(0, 0, 0, 0.10);\n --duro-color-inverse-fill-hover: rgba(0, 0, 0, 0.18);\n --duro-color-on-contrast-surface: #1a1a1a;\n --duro-color-overlay-light: rgba(255, 255, 255, 0.78);\n --duro-color-scrim: rgba(0, 0, 0, 0.4);\n --duro-color-success: #22c55e;\n --duro-color-success-bg: rgba(34, 197, 94, 0.1);\n --duro-color-success-border: rgba(34, 197, 94, 0.3);\n --duro-color-success-text: #86efac;\n --duro-color-text: #e5e5e5;\n --duro-color-text-muted: #b0b0b0;\n --duro-color-warning: #fbbf24;\n --duro-color-warning-bg: rgba(251, 191, 36, 0.1);\n --duro-color-warning-border: rgba(251, 191, 36, 0.3);\n --duro-color-warning-text: #fde68a;\n /* shadows */\n --duro-shadow-drop-over: inset 0 0 0 2px #6aaffc;\n --duro-shadow-drop-ready: inset 0 0 0 1px #333333;\n --duro-shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.5);\n --duro-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);\n --duro-shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.3);\n /* spacing */\n --duro-spacing-lg: 24px;\n --duro-spacing-md: 16px;\n --duro-spacing-ms: 12px;\n --duro-spacing-sm: 8px;\n --duro-spacing-xl: 32px;\n --duro-spacing-xs: 4px;\n --duro-spacing-xxl: 48px;\n --duro-spacing-xxxl: 64px;\n /* microSpacing */\n --duro-micro-spacing-px1: 1px;\n --duro-micro-spacing-px2: 2px;\n --duro-micro-spacing-px3: 3px;\n --duro-micro-spacing-px5: 5px;\n --duro-micro-spacing-px6: 6px;\n /* radii */\n --duro-radius-full: 9999px;\n --duro-radius-lg: 16px;\n --duro-radius-md: 12px;\n --duro-radius-px6: 6px;\n --duro-radius-sm: 8px;\n --duro-radius-xs: 4px;\n /* sizes */\n --duro-size-app-bar-h: 61px;\n --duro-size-aside-w: 320px;\n --duro-size-bar-h: 4px;\n --duro-size-canvas-min-h: 480px;\n --duro-size-check-mark-h: 9px;\n --duro-size-check-mark-w: 5px;\n --duro-size-chip: 24px;\n --duro-size-color-area-h: 150px;\n --duro-size-color-picker-w: 196px;\n --duro-size-color-preview-h: 22px;\n --duro-size-color-swatch: 20px;\n --duro-size-color-track-h: 12px;\n --duro-size-control-lg: 40px;\n --duro-size-control-md: 39px;\n --duro-size-control-sm: 28px;\n --duro-size-day-header-h: 46px;\n --duro-size-device-bar-w: 96px;\n --duro-size-dialog-lg: 680px;\n --duro-size-dialog-md: 520px;\n --duro-size-dialog-sm: 400px;\n --duro-size-divider: 1px;\n --duro-size-drag-thumb-h: 99px;\n --duro-size-drag-thumb-w: 132px;\n --duro-size-drop-zone-min-h: 128px;\n --duro-size-edge-fade: 32px;\n --duro-size-editor-min-h: 160px;\n --duro-size-embed-w: 550px;\n --duro-size-field-min-w: 160px;\n --duro-size-field-min-w-sm: 80px;\n --duro-size-glyph-clamp-max: 160px;\n --duro-size-glyph-md: 16px;\n --duro-size-glyph-sm: 12px;\n --duro-size-glyph-xs: 10px;\n --duro-size-grid-col-md: 280px;\n --duro-size-grid-col-sm: 240px;\n --duro-size-grid-col-xs: 200px;\n --duro-size-handle: 16px;\n --duro-size-icon-button: 32px;\n --duro-size-icon-button-sm: 28px;\n --duro-size-icon-lg: 24px;\n --duro-size-icon-md: 18px;\n --duro-size-icon-sm: 16px;\n --duro-size-icon-xl: 36px;\n --duro-size-icon-xxl: 48px;\n --duro-size-indicator: 18px;\n --duro-size-indicator-dot: 8px;\n --duro-size-label-min-w: 120px;\n --duro-size-list-max-h: 280px;\n --duro-size-list-max-h-sm: 200px;\n --duro-size-media-tile-min-w: 108px;\n --duro-size-meter-h: 6px;\n --duro-size-nav-marker-h: 18px;\n --duro-size-nav-marker-w: 3px;\n --duro-size-page-lg: 1200px;\n --duro-size-page-md: 800px;\n --duro-size-page-sm: 600px;\n --duro-size-page-xl: 1440px;\n --duro-size-page-xs: 480px;\n --duro-size-palette-min-w: 96px;\n --duro-size-panel-lg: 640px;\n --duro-size-panel-md: 480px;\n --duro-size-panel-sm: 360px;\n --duro-size-placeholder-min-h: 80px;\n --duro-size-popover-w: 320px;\n --duro-size-popover-w-sm: 240px;\n --duro-size-popup-max-w: 280px;\n --duro-size-popup-min-w: 160px;\n --duro-size-preview-max-h: 320px;\n --duro-size-readout-w: 40px;\n --duro-size-scrollbar: 8px;\n --duro-size-sidebar-w: 240px;\n --duro-size-skeleton-chip-w: 96px;\n --duro-size-slider-w: 96px;\n --duro-size-spinner-lg: 40px;\n --duro-size-spinner-md: 24px;\n --duro-size-spinner-sm: 16px;\n --duro-size-swatch-h: 34px;\n --duro-size-swatch-w: 44px;\n --duro-size-switch-thumb: 16px;\n --duro-size-switch-track-h: 20px;\n --duro-size-switch-track-w: 36px;\n --duro-size-tab-indicator: 2px;\n --duro-size-thumb-sm: 40px;\n --duro-size-time-gutter-w: 64px;\n --duro-size-timeline-label-w: 180px;\n --duro-size-time-readout-w: 74px;\n --duro-size-toast-max-w: 440px;\n --duro-size-toolbar-h: 36px;\n --duro-size-touch-target: 44px;\n /* borders */\n --duro-border-accent: 3px;\n --duro-border-focus-offset: 2px;\n --duro-border-focus-offset-sm: 1px;\n --duro-border-focus-ring: 2px;\n --duro-border-hairline: 1px;\n --duro-border-strong: 2px;\n /* layers */\n --duro-layer-floating: 50;\n --duro-layer-floating-raised: 60;\n --duro-layer-modal: 1001;\n --duro-layer-modal-raised: 1002;\n --duro-layer-overlay: 1000;\n --duro-layer-popover: 1040;\n --duro-layer-popover-raised: 1041;\n --duro-layer-popup: 1050;\n --duro-layer-popup-backdrop: 1049;\n --duro-layer-portal: 1100;\n --duro-layer-raised: 1;\n --duro-layer-toast: 1060;\n /* effects */\n --duro-effect-overlay-blur: blur(2px);\n --duro-effect-surface-blur: blur(6px);\n /* layoutSpacing */\n --duro-layout-spacing-container-lg: 32px;\n --duro-layout-spacing-container-md: 24px;\n --duro-layout-spacing-container-sm: 16px;\n --duro-layout-spacing-inline-lg: 24px;\n --duro-layout-spacing-inline-md: 16px;\n --duro-layout-spacing-inline-sm: 8px;\n --duro-layout-spacing-inline-xs: 4px;\n --duro-layout-spacing-stack-lg: 24px;\n --duro-layout-spacing-stack-md: 16px;\n --duro-layout-spacing-stack-sm: 8px;\n --duro-layout-spacing-stack-xl: 48px;\n --duro-layout-spacing-stack-xs: 4px;\n /* typography */\n --duro-typography-font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n --duro-typography-font-family-mono: \"SF Mono\", \"Fira Code\", \"Fira Mono\", \"Roboto Mono\", \"Courier New\", monospace;\n --duro-typography-font-size-heading: 1.5rem;\n --duro-typography-font-size-lg: 1.125rem;\n --duro-typography-font-size-md: 1rem;\n --duro-typography-font-size-sm: 0.875rem;\n --duro-typography-font-size-xl: 1.25rem;\n --duro-typography-font-size-xs: 0.75rem;\n --duro-typography-font-weight-bold: 700;\n --duro-typography-font-weight-medium: 500;\n --duro-typography-font-weight-normal: 400;\n --duro-typography-font-weight-semibold: 600;\n --duro-typography-line-height: 1.5;\n /* typeScale */\n --duro-type-scale-display-lg: clamp(3.5rem, 2rem + 3.5vw, 4.5rem);\n --duro-type-scale-display-md: clamp(2.75rem, 1.75rem + 2.5vw, 3.75rem);\n --duro-type-scale-display-sm: clamp(2.25rem, 1.5rem + 2vw, 3rem);\n --duro-type-scale-font-size1: 0.75rem;\n --duro-type-scale-font-size2: 0.8125rem;\n --duro-type-scale-font-size3: 0.875rem;\n --duro-type-scale-font-size4: 1rem;\n --duro-type-scale-font-size5: 1.125rem;\n --duro-type-scale-font-size6: 1.25rem;\n --duro-type-scale-font-size7: 1.5rem;\n --duro-type-scale-font-size8: 1.875rem;\n --duro-type-scale-font-size9: 2.25rem;\n --duro-type-scale-letter-spacing-normal: 0;\n --duro-type-scale-letter-spacing-tight: -0.02em;\n --duro-type-scale-letter-spacing-wide: 0.04em;\n --duro-type-scale-line-height1: 1rem;\n --duro-type-scale-line-height2: 1.25rem;\n --duro-type-scale-line-height3: 1.25rem;\n --duro-type-scale-line-height4: 1.5rem;\n --duro-type-scale-line-height5: 1.5rem;\n --duro-type-scale-line-height6: 1.75rem;\n --duro-type-scale-line-height7: 2rem;\n --duro-type-scale-line-height8: 2.25rem;\n --duro-type-scale-line-height9: 2.75rem;\n /* duration */\n --duro-duration-base: 200ms;\n --duro-duration-brisk: 160ms;\n --duro-duration-fast: 150ms;\n --duro-duration-instant: 0ms;\n --duro-duration-minimal: 1ms;\n --duro-duration-quick: 120ms;\n --duro-duration-slow: 280ms;\n /* easing */\n --duro-easing-ease-in: cubic-bezier(0.72, 0, 0.68, 0.28);\n --duro-easing-ease-out: cubic-bezier(0.32, 0.72, 0, 1);\n --duro-easing-linear: linear;\n --duro-easing-standard: ease;\n /* breakpoints */\n --duro-breakpoint-lg: 1024px;\n --duro-breakpoint-md: 768px;\n --duro-breakpoint-sm: 640px;\n --duro-breakpoint-xl: 1280px;\n --duro-breakpoint-xs: 480px;\n}\n\n:root[data-theme=\"light\"] {\n color-scheme: light;\n /* colors */\n --duro-color-accent: #1e40af;\n --duro-color-accent-contrast: #ffffff;\n --duro-color-accent-hover: #1a3799;\n --duro-color-bg: #ffffff;\n --duro-color-bg-card: #f5f5f5;\n --duro-color-bg-card-hover: #ebebeb;\n --duro-color-border: #d4d4d4;\n --duro-color-contrast-border: rgba(255, 255, 255, 0.33);\n --duro-color-contrast-surface: #1a1a1a;\n --duro-color-error: #991b1b;\n --duro-color-error-bg: rgba(153, 27, 27, 0.08);\n --duro-color-error-border: rgba(153, 27, 27, 0.3);\n --duro-color-error-contrast: #ffffff;\n --duro-color-error-hover: #7f1d1d;\n --duro-color-error-text: #7f1d1d;\n --duro-color-fixed-dark: #000000;\n --duro-color-fixed-light: #ffffff;\n --duro-color-highlight: #6b21a8;\n --duro-color-highlight-bg: rgba(107, 33, 168, 0.08);\n --duro-color-highlight-border: rgba(107, 33, 168, 0.3);\n --duro-color-highlight-text: #581c87;\n --duro-color-info: #1e40af;\n --duro-color-info-bg: rgba(30, 64, 175, 0.08);\n --duro-color-info-border: rgba(30, 64, 175, 0.3);\n --duro-color-info-text: #1e40af;\n --duro-color-inverse-border: rgba(0, 0, 0, 0.55);\n --duro-color-inverse-border-hover: rgba(0, 0, 0, 0.70);\n --duro-color-inverse-fill: rgba(0, 0, 0, 0.10);\n --duro-color-inverse-fill-hover: rgba(0, 0, 0, 0.18);\n --duro-color-on-contrast-surface: #e5e5e5;\n --duro-color-overlay-light: rgba(255, 255, 255, 0.78);\n --duro-color-scrim: rgba(0, 0, 0, 0.4);\n --duro-color-success: #166534;\n --duro-color-success-bg: rgba(22, 101, 52, 0.08);\n --duro-color-success-border: rgba(22, 101, 52, 0.3);\n --duro-color-success-text: #14532d;\n --duro-color-text: #1a1a1a;\n --duro-color-text-muted: #4a4a4a;\n --duro-color-warning: #92400e;\n --duro-color-warning-bg: rgba(146, 64, 14, 0.08);\n --duro-color-warning-border: rgba(146, 64, 14, 0.3);\n --duro-color-warning-text: #78350f;\n /* shadows */\n --duro-shadow-drop-over: inset 0 0 0 2px #1e40af;\n --duro-shadow-drop-ready: inset 0 0 0 1px #d4d4d4;\n --duro-shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.16);\n --duro-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.12);\n --duro-shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.08);\n}\n\n:root[data-theme=\"high-contrast\"] {\n color-scheme: dark;\n /* colors */\n --duro-color-accent: #60a5fa;\n --duro-color-accent-contrast: #000000;\n --duro-color-accent-hover: #93c5fd;\n --duro-color-bg: #000000;\n --duro-color-bg-card: #111111;\n --duro-color-bg-card-hover: #1a1a1a;\n --duro-color-border: #555555;\n --duro-color-contrast-border: rgba(0, 0, 0, 0.42);\n --duro-color-contrast-surface: #ffffff;\n --duro-color-error: #f87171;\n --duro-color-error-bg: rgba(248, 113, 113, 0.15);\n --duro-color-error-border: rgba(248, 113, 113, 0.5);\n --duro-color-error-contrast: #000000;\n --duro-color-error-hover: #fca5a5;\n --duro-color-error-text: #fca5a5;\n --duro-color-fixed-dark: #000000;\n --duro-color-fixed-light: #ffffff;\n --duro-color-highlight: #d8b4fe;\n --duro-color-highlight-bg: rgba(216, 180, 254, 0.15);\n --duro-color-highlight-border: rgba(216, 180, 254, 0.5);\n --duro-color-highlight-text: #e9d5ff;\n --duro-color-info: #60a5fa;\n --duro-color-info-bg: rgba(96, 165, 250, 0.15);\n --duro-color-info-border: rgba(96, 165, 250, 0.5);\n --duro-color-info-text: #bfdbfe;\n --duro-color-inverse-border: rgba(0, 0, 0, 0.55);\n --duro-color-inverse-border-hover: rgba(0, 0, 0, 0.70);\n --duro-color-inverse-fill: rgba(0, 0, 0, 0.10);\n --duro-color-inverse-fill-hover: rgba(0, 0, 0, 0.18);\n --duro-color-on-contrast-surface: #000000;\n --duro-color-overlay-light: rgba(255, 255, 255, 0.78);\n --duro-color-scrim: rgba(0, 0, 0, 0.4);\n --duro-color-success: #4ade80;\n --duro-color-success-bg: rgba(74, 222, 128, 0.15);\n --duro-color-success-border: rgba(74, 222, 128, 0.5);\n --duro-color-success-text: #86efac;\n --duro-color-text: #ffffff;\n --duro-color-text-muted: #b0b0b0;\n --duro-color-warning: #fcd34d;\n --duro-color-warning-bg: rgba(252, 211, 77, 0.15);\n --duro-color-warning-border: rgba(252, 211, 77, 0.5);\n --duro-color-warning-text: #fef08a;\n /* shadows */\n --duro-shadow-drop-over: inset 0 0 0 2px #60a5fa;\n --duro-shadow-drop-ready: inset 0 0 0 1px #555555;\n --duro-shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.8);\n --duro-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.7);\n --duro-shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.6);\n}\n"
10774
+ "mockupCss": "/*\n * GENERATED by scripts/generate-mockup-css.mjs — do not edit.\n *\n * The design tokens as resolved values, for HTML that loads no stylesheet\n * (mockup artboards, static previews). Same names as vars.css; where vars.css\n * follows the live theme, this file carries every theme and switches on\n * <html data-theme=\"light|high-contrast\">.\n *\n * Inside the component library and the apps, never use this: go through\n * css.create() and the token imports, or vars.css from plain CSS.\n */\n:root {\n color-scheme: dark;\n /* colors */\n --duro-color-accent: #6aaffc;\n --duro-color-accent-contrast: #000000;\n --duro-color-accent-disabled: rgba(106, 175, 252, 0.4);\n --duro-color-accent-hover: #93c5fd;\n --duro-color-active: #6aaffc;\n --duro-color-active-bg: rgba(106, 175, 252, 0.1);\n --duro-color-active-border: rgba(106, 175, 252, 0.3);\n --duro-color-active-text: #93c5fd;\n --duro-color-bg: #0f0f0f;\n --duro-color-bg-card: #1a1a1a;\n --duro-color-bg-card-hover: #242424;\n --duro-color-bg-elevated: #242424;\n --duro-color-bg-inset: #0a0a0a;\n --duro-color-bg-section-tint: rgba(255, 255, 255, 0.03);\n --duro-color-bg-selected: rgba(106, 175, 252, 0.15);\n --duro-color-border: #333333;\n --duro-color-border-strong: #555555;\n --duro-color-border-subtle: #242424;\n --duro-color-contrast-border: rgba(0, 0, 0, 0.42);\n --duro-color-contrast-surface: #f5f5f5;\n --duro-color-error: #f87171;\n --duro-color-error-bg: rgba(248, 113, 113, 0.1);\n --duro-color-error-border: rgba(248, 113, 113, 0.3);\n --duro-color-error-contrast: #000000;\n --duro-color-error-hover: #fca5a5;\n --duro-color-error-text: #fca5a5;\n --duro-color-fixed-dark: #000000;\n --duro-color-fixed-light: #ffffff;\n --duro-color-highlight: #c084fc;\n --duro-color-highlight-bg: rgba(192, 132, 252, 0.1);\n --duro-color-highlight-border: rgba(192, 132, 252, 0.3);\n --duro-color-highlight-text: #d8b4fe;\n --duro-color-info: #6aaffc;\n --duro-color-info-bg: rgba(106, 175, 252, 0.1);\n --duro-color-info-border: rgba(106, 175, 252, 0.3);\n --duro-color-info-text: #93c5fd;\n --duro-color-inverse-border: rgba(0, 0, 0, 0.55);\n --duro-color-inverse-border-hover: rgba(0, 0, 0, 0.70);\n --duro-color-inverse-fill: rgba(0, 0, 0, 0.10);\n --duro-color-inverse-fill-hover: rgba(0, 0, 0, 0.18);\n --duro-color-neutral: #a3a3a3;\n --duro-color-neutral-bg: rgba(163, 163, 163, 0.1);\n --duro-color-neutral-border: rgba(163, 163, 163, 0.3);\n --duro-color-neutral-text: #d4d4d4;\n --duro-color-on-contrast-surface: #1a1a1a;\n --duro-color-on-scrim-muted: rgba(255, 255, 255, 0.72);\n --duro-color-overlay-light: rgba(255, 255, 255, 0.78);\n --duro-color-press-overlay: rgba(255, 255, 255, 0.08);\n --duro-color-ripple: rgba(255, 255, 255, 0.12);\n --duro-color-scrim: rgba(0, 0, 0, 0.4);\n --duro-color-scrim-soft: rgba(0, 0, 0, 0.2);\n --duro-color-scrim-strong: rgba(0, 0, 0, 0.6);\n --duro-color-segment-active: #333333;\n --duro-color-segment-track: rgba(255, 255, 255, 0.06);\n --duro-color-success: #22c55e;\n --duro-color-success-bg: rgba(34, 197, 94, 0.1);\n --duro-color-success-border: rgba(34, 197, 94, 0.3);\n --duro-color-success-text: #86efac;\n --duro-color-surface-highlight: rgba(255, 255, 255, 0.06);\n --duro-color-text: #e5e5e5;\n --duro-color-text-disabled: #6b6b6b;\n --duro-color-text-muted: #b0b0b0;\n --duro-color-text-subtle: #8a8a8a;\n --duro-color-warning: #fbbf24;\n --duro-color-warning-bg: rgba(251, 191, 36, 0.1);\n --duro-color-warning-border: rgba(251, 191, 36, 0.3);\n --duro-color-warning-text: #fde68a;\n /* shadows */\n --duro-shadow-drop-over: inset 0 0 0 2px #6aaffc;\n --duro-shadow-drop-ready: inset 0 0 0 1px #333333;\n --duro-shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.5);\n --duro-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);\n --duro-shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.3);\n /* spacing */\n --duro-spacing-lg: 24px;\n --duro-spacing-md: 16px;\n --duro-spacing-ms: 12px;\n --duro-spacing-sm: 8px;\n --duro-spacing-xl: 32px;\n --duro-spacing-xs: 4px;\n --duro-spacing-xxl: 48px;\n --duro-spacing-xxxl: 64px;\n /* microSpacing */\n --duro-micro-spacing-px1: 1px;\n --duro-micro-spacing-px2: 2px;\n --duro-micro-spacing-px3: 3px;\n --duro-micro-spacing-px5: 5px;\n --duro-micro-spacing-px6: 6px;\n /* radii */\n --duro-radius-full: 9999px;\n --duro-radius-lg: 16px;\n --duro-radius-md: 12px;\n --duro-radius-px6: 6px;\n --duro-radius-sm: 8px;\n --duro-radius-xs: 4px;\n /* sizes */\n --duro-size-app-bar-h: 61px;\n --duro-size-aside-w: 320px;\n --duro-size-bar-h: 4px;\n --duro-size-canvas-min-h: 480px;\n --duro-size-check-mark-h: 9px;\n --duro-size-check-mark-w: 5px;\n --duro-size-chip: 24px;\n --duro-size-color-area-h: 150px;\n --duro-size-color-picker-w: 196px;\n --duro-size-color-preview-h: 22px;\n --duro-size-color-swatch: 20px;\n --duro-size-color-track-h: 12px;\n --duro-size-control-lg: 40px;\n --duro-size-control-md: 39px;\n --duro-size-control-sm: 28px;\n --duro-size-day-header-h: 46px;\n --duro-size-device-bar-w: 96px;\n --duro-size-dialog-lg: 680px;\n --duro-size-dialog-md: 520px;\n --duro-size-dialog-sm: 400px;\n --duro-size-divider: 1px;\n --duro-size-drag-thumb-h: 99px;\n --duro-size-drag-thumb-w: 132px;\n --duro-size-drop-zone-min-h: 128px;\n --duro-size-edge-fade: 32px;\n --duro-size-editor-min-h: 160px;\n --duro-size-embed-w: 550px;\n --duro-size-field-min-w: 160px;\n --duro-size-field-min-w-sm: 80px;\n --duro-size-glyph-clamp-max: 160px;\n --duro-size-glyph-md: 16px;\n --duro-size-glyph-sm: 12px;\n --duro-size-glyph-xs: 10px;\n --duro-size-grid-col-md: 280px;\n --duro-size-grid-col-sm: 240px;\n --duro-size-grid-col-xs: 200px;\n --duro-size-handle: 16px;\n --duro-size-icon-button: 32px;\n --duro-size-icon-button-sm: 28px;\n --duro-size-icon-lg: 24px;\n --duro-size-icon-md: 18px;\n --duro-size-icon-sm: 16px;\n --duro-size-icon-xl: 36px;\n --duro-size-icon-xxl: 48px;\n --duro-size-indicator: 18px;\n --duro-size-indicator-dot: 8px;\n --duro-size-label-min-w: 120px;\n --duro-size-list-max-h: 280px;\n --duro-size-list-max-h-sm: 200px;\n --duro-size-media-tile-min-w: 108px;\n --duro-size-meter-h: 6px;\n --duro-size-nav-marker-h: 18px;\n --duro-size-nav-marker-w: 3px;\n --duro-size-page-lg: 1200px;\n --duro-size-page-md: 800px;\n --duro-size-page-sm: 600px;\n --duro-size-page-xl: 1440px;\n --duro-size-page-xs: 480px;\n --duro-size-palette-min-w: 96px;\n --duro-size-panel-lg: 640px;\n --duro-size-panel-md: 480px;\n --duro-size-panel-sm: 360px;\n --duro-size-placeholder-min-h: 80px;\n --duro-size-popover-w: 320px;\n --duro-size-popover-w-sm: 240px;\n --duro-size-popup-max-w: 280px;\n --duro-size-popup-min-w: 160px;\n --duro-size-preview-max-h: 320px;\n --duro-size-readout-w: 40px;\n --duro-size-scrollbar: 8px;\n --duro-size-sheet-grabber-h: 4px;\n --duro-size-sheet-grabber-w: 40px;\n --duro-size-sidebar-w: 240px;\n --duro-size-skeleton-chip-w: 96px;\n --duro-size-slider-w: 96px;\n --duro-size-spinner-lg: 40px;\n --duro-size-spinner-md: 24px;\n --duro-size-spinner-sm: 16px;\n --duro-size-swatch-h: 34px;\n --duro-size-swatch-w: 44px;\n --duro-size-switch-thumb: 16px;\n --duro-size-switch-track-h: 20px;\n --duro-size-switch-track-w: 36px;\n --duro-size-tab-indicator: 2px;\n --duro-size-thumb-sm: 40px;\n --duro-size-time-gutter-w: 64px;\n --duro-size-timeline-label-w: 180px;\n --duro-size-time-readout-w: 74px;\n --duro-size-toast-max-w: 440px;\n --duro-size-toolbar-h: 36px;\n --duro-size-touch-target: 44px;\n /* borders */\n --duro-border-accent: 3px;\n --duro-border-focus-offset: 2px;\n --duro-border-focus-offset-sm: 1px;\n --duro-border-focus-ring: 2px;\n --duro-border-hairline: 1px;\n --duro-border-strong: 2px;\n /* layers */\n --duro-layer-floating: 50;\n --duro-layer-floating-raised: 60;\n --duro-layer-modal: 1001;\n --duro-layer-modal-raised: 1002;\n --duro-layer-overlay: 1000;\n --duro-layer-popover: 1040;\n --duro-layer-popover-raised: 1041;\n --duro-layer-popup: 1050;\n --duro-layer-popup-backdrop: 1049;\n --duro-layer-portal: 1100;\n --duro-layer-raised: 1;\n --duro-layer-toast: 1060;\n /* effects */\n --duro-effect-overlay-blur: blur(2px);\n --duro-effect-surface-blur: blur(6px);\n /* layoutSpacing */\n --duro-layout-spacing-container-lg: 32px;\n --duro-layout-spacing-container-md: 24px;\n --duro-layout-spacing-container-sm: 16px;\n --duro-layout-spacing-inline-lg: 24px;\n --duro-layout-spacing-inline-md: 16px;\n --duro-layout-spacing-inline-sm: 8px;\n --duro-layout-spacing-inline-xs: 4px;\n --duro-layout-spacing-stack-lg: 24px;\n --duro-layout-spacing-stack-md: 16px;\n --duro-layout-spacing-stack-sm: 8px;\n --duro-layout-spacing-stack-xl: 48px;\n --duro-layout-spacing-stack-xs: 4px;\n /* typography */\n --duro-typography-font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n --duro-typography-font-family-mono: \"SF Mono\", \"Fira Code\", \"Fira Mono\", \"Roboto Mono\", \"Courier New\", monospace;\n --duro-typography-font-size-heading: 1.5rem;\n --duro-typography-font-size-lg: 1.125rem;\n --duro-typography-font-size-md: 1rem;\n --duro-typography-font-size-sm: 0.875rem;\n --duro-typography-font-size-xl: 1.25rem;\n --duro-typography-font-size-xs: 0.75rem;\n --duro-typography-font-weight-bold: 700;\n --duro-typography-font-weight-medium: 500;\n --duro-typography-font-weight-normal: 400;\n --duro-typography-font-weight-semibold: 600;\n --duro-typography-line-height: 1.5;\n /* typeScale */\n --duro-type-scale-display-lg: clamp(3.5rem, 2rem + 3.5vw, 4.5rem);\n --duro-type-scale-display-md: clamp(2.75rem, 1.75rem + 2.5vw, 3.75rem);\n --duro-type-scale-display-sm: clamp(2.25rem, 1.5rem + 2vw, 3rem);\n --duro-type-scale-font-size1: 0.75rem;\n --duro-type-scale-font-size2: 0.8125rem;\n --duro-type-scale-font-size3: 0.875rem;\n --duro-type-scale-font-size4: 1rem;\n --duro-type-scale-font-size5: 1.125rem;\n --duro-type-scale-font-size6: 1.25rem;\n --duro-type-scale-font-size7: 1.5rem;\n --duro-type-scale-font-size8: 1.875rem;\n --duro-type-scale-font-size9: 2.25rem;\n --duro-type-scale-letter-spacing-normal: 0;\n --duro-type-scale-letter-spacing-tight: -0.02em;\n --duro-type-scale-letter-spacing-wide: 0.04em;\n --duro-type-scale-line-height1: 1rem;\n --duro-type-scale-line-height2: 1.25rem;\n --duro-type-scale-line-height3: 1.25rem;\n --duro-type-scale-line-height4: 1.5rem;\n --duro-type-scale-line-height5: 1.5rem;\n --duro-type-scale-line-height6: 1.75rem;\n --duro-type-scale-line-height7: 2rem;\n --duro-type-scale-line-height8: 2.25rem;\n --duro-type-scale-line-height9: 2.75rem;\n /* duration */\n --duro-duration-base: 200ms;\n --duro-duration-brisk: 160ms;\n --duro-duration-fast: 150ms;\n --duro-duration-instant: 0ms;\n --duro-duration-minimal: 1ms;\n --duro-duration-quick: 120ms;\n --duro-duration-slow: 280ms;\n /* easing */\n --duro-easing-ease-in: cubic-bezier(0.72, 0, 0.68, 0.28);\n --duro-easing-ease-out: cubic-bezier(0.32, 0.72, 0, 1);\n --duro-easing-linear: linear;\n --duro-easing-standard: ease;\n /* breakpoints */\n --duro-breakpoint-lg: 1024px;\n --duro-breakpoint-md: 768px;\n --duro-breakpoint-sm: 640px;\n --duro-breakpoint-xl: 1280px;\n --duro-breakpoint-xs: 480px;\n}\n\n:root[data-theme=\"light\"] {\n color-scheme: light;\n /* colors */\n --duro-color-accent: #1e40af;\n --duro-color-accent-contrast: #ffffff;\n --duro-color-accent-disabled: rgba(30, 64, 175, 0.4);\n --duro-color-accent-hover: #1a3799;\n --duro-color-active: #1e40af;\n --duro-color-active-bg: rgba(30, 64, 175, 0.08);\n --duro-color-active-border: rgba(30, 64, 175, 0.3);\n --duro-color-active-text: #1e40af;\n --duro-color-bg: #ffffff;\n --duro-color-bg-card: #f5f5f5;\n --duro-color-bg-card-hover: #ebebeb;\n --duro-color-bg-elevated: #ffffff;\n --duro-color-bg-inset: #f0f0f0;\n --duro-color-bg-section-tint: rgba(0, 0, 0, 0.03);\n --duro-color-bg-selected: rgba(30, 64, 175, 0.12);\n --duro-color-border: #d4d4d4;\n --duro-color-border-strong: #a3a3a3;\n --duro-color-border-subtle: #ebebeb;\n --duro-color-contrast-border: rgba(255, 255, 255, 0.33);\n --duro-color-contrast-surface: #1a1a1a;\n --duro-color-error: #991b1b;\n --duro-color-error-bg: rgba(153, 27, 27, 0.08);\n --duro-color-error-border: rgba(153, 27, 27, 0.3);\n --duro-color-error-contrast: #ffffff;\n --duro-color-error-hover: #7f1d1d;\n --duro-color-error-text: #7f1d1d;\n --duro-color-fixed-dark: #000000;\n --duro-color-fixed-light: #ffffff;\n --duro-color-highlight: #6b21a8;\n --duro-color-highlight-bg: rgba(107, 33, 168, 0.08);\n --duro-color-highlight-border: rgba(107, 33, 168, 0.3);\n --duro-color-highlight-text: #581c87;\n --duro-color-info: #1e40af;\n --duro-color-info-bg: rgba(30, 64, 175, 0.08);\n --duro-color-info-border: rgba(30, 64, 175, 0.3);\n --duro-color-info-text: #1e40af;\n --duro-color-inverse-border: rgba(0, 0, 0, 0.55);\n --duro-color-inverse-border-hover: rgba(0, 0, 0, 0.70);\n --duro-color-inverse-fill: rgba(0, 0, 0, 0.10);\n --duro-color-inverse-fill-hover: rgba(0, 0, 0, 0.18);\n --duro-color-neutral: #525252;\n --duro-color-neutral-bg: rgba(82, 82, 82, 0.08);\n --duro-color-neutral-border: rgba(82, 82, 82, 0.3);\n --duro-color-neutral-text: #404040;\n --duro-color-on-contrast-surface: #e5e5e5;\n --duro-color-on-scrim-muted: rgba(255, 255, 255, 0.72);\n --duro-color-overlay-light: rgba(255, 255, 255, 0.78);\n --duro-color-press-overlay: rgba(0, 0, 0, 0.08);\n --duro-color-ripple: rgba(0, 0, 0, 0.12);\n --duro-color-scrim: rgba(0, 0, 0, 0.4);\n --duro-color-scrim-soft: rgba(0, 0, 0, 0.2);\n --duro-color-scrim-strong: rgba(0, 0, 0, 0.6);\n --duro-color-segment-active: #ffffff;\n --duro-color-segment-track: rgba(0, 0, 0, 0.06);\n --duro-color-success: #166534;\n --duro-color-success-bg: rgba(22, 101, 52, 0.08);\n --duro-color-success-border: rgba(22, 101, 52, 0.3);\n --duro-color-success-text: #14532d;\n --duro-color-surface-highlight: rgba(255, 255, 255, 0.7);\n --duro-color-text: #1a1a1a;\n --duro-color-text-disabled: #a3a3a3;\n --duro-color-text-muted: #4a4a4a;\n --duro-color-text-subtle: #6b6b6b;\n --duro-color-warning: #92400e;\n --duro-color-warning-bg: rgba(146, 64, 14, 0.08);\n --duro-color-warning-border: rgba(146, 64, 14, 0.3);\n --duro-color-warning-text: #78350f;\n /* shadows */\n --duro-shadow-drop-over: inset 0 0 0 2px #1e40af;\n --duro-shadow-drop-ready: inset 0 0 0 1px #d4d4d4;\n --duro-shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.16);\n --duro-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.12);\n --duro-shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.08);\n}\n\n:root[data-theme=\"high-contrast\"] {\n color-scheme: dark;\n /* colors */\n --duro-color-accent: #60a5fa;\n --duro-color-accent-contrast: #000000;\n --duro-color-accent-disabled: rgba(96, 165, 250, 0.5);\n --duro-color-accent-hover: #93c5fd;\n --duro-color-active: #60a5fa;\n --duro-color-active-bg: rgba(96, 165, 250, 0.15);\n --duro-color-active-border: rgba(96, 165, 250, 0.5);\n --duro-color-active-text: #bfdbfe;\n --duro-color-bg: #000000;\n --duro-color-bg-card: #111111;\n --duro-color-bg-card-hover: #1a1a1a;\n --duro-color-bg-elevated: #1a1a1a;\n --duro-color-bg-inset: #0a0a0a;\n --duro-color-bg-section-tint: rgba(255, 255, 255, 0.06);\n --duro-color-bg-selected: rgba(96, 165, 250, 0.25);\n --duro-color-border: #555555;\n --duro-color-border-strong: #888888;\n --duro-color-border-subtle: #333333;\n --duro-color-contrast-border: rgba(0, 0, 0, 0.42);\n --duro-color-contrast-surface: #ffffff;\n --duro-color-error: #f87171;\n --duro-color-error-bg: rgba(248, 113, 113, 0.15);\n --duro-color-error-border: rgba(248, 113, 113, 0.5);\n --duro-color-error-contrast: #000000;\n --duro-color-error-hover: #fca5a5;\n --duro-color-error-text: #fca5a5;\n --duro-color-fixed-dark: #000000;\n --duro-color-fixed-light: #ffffff;\n --duro-color-highlight: #d8b4fe;\n --duro-color-highlight-bg: rgba(216, 180, 254, 0.15);\n --duro-color-highlight-border: rgba(216, 180, 254, 0.5);\n --duro-color-highlight-text: #e9d5ff;\n --duro-color-info: #60a5fa;\n --duro-color-info-bg: rgba(96, 165, 250, 0.15);\n --duro-color-info-border: rgba(96, 165, 250, 0.5);\n --duro-color-info-text: #bfdbfe;\n --duro-color-inverse-border: rgba(0, 0, 0, 0.55);\n --duro-color-inverse-border-hover: rgba(0, 0, 0, 0.70);\n --duro-color-inverse-fill: rgba(0, 0, 0, 0.10);\n --duro-color-inverse-fill-hover: rgba(0, 0, 0, 0.18);\n --duro-color-neutral: #d4d4d4;\n --duro-color-neutral-bg: rgba(212, 212, 212, 0.15);\n --duro-color-neutral-border: rgba(212, 212, 212, 0.5);\n --duro-color-neutral-text: #e5e5e5;\n --duro-color-on-contrast-surface: #000000;\n --duro-color-on-scrim-muted: rgba(255, 255, 255, 0.72);\n --duro-color-overlay-light: rgba(255, 255, 255, 0.78);\n --duro-color-press-overlay: rgba(255, 255, 255, 0.15);\n --duro-color-ripple: rgba(255, 255, 255, 0.2);\n --duro-color-scrim: rgba(0, 0, 0, 0.4);\n --duro-color-scrim-soft: rgba(0, 0, 0, 0.3);\n --duro-color-scrim-strong: rgba(0, 0, 0, 0.7);\n --duro-color-segment-active: #333333;\n --duro-color-segment-track: rgba(255, 255, 255, 0.12);\n --duro-color-success: #4ade80;\n --duro-color-success-bg: rgba(74, 222, 128, 0.15);\n --duro-color-success-border: rgba(74, 222, 128, 0.5);\n --duro-color-success-text: #86efac;\n --duro-color-surface-highlight: rgba(255, 255, 255, 0.15);\n --duro-color-text: #ffffff;\n --duro-color-text-disabled: #808080;\n --duro-color-text-muted: #b0b0b0;\n --duro-color-text-subtle: #a3a3a3;\n --duro-color-warning: #fcd34d;\n --duro-color-warning-bg: rgba(252, 211, 77, 0.15);\n --duro-color-warning-border: rgba(252, 211, 77, 0.5);\n --duro-color-warning-text: #fef08a;\n /* shadows */\n --duro-shadow-drop-over: inset 0 0 0 2px #60a5fa;\n --duro-shadow-drop-ready: inset 0 0 0 1px #555555;\n --duro-shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.8);\n --duro-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.7);\n --duro-shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.6);\n}\n"
10282
10775
  },
10283
10776
  "unions": {
10284
10777
  "AlertVariant": [
@@ -10529,6 +11022,10 @@
10529
11022
  "dropReady",
10530
11023
  "dropOver"
10531
11024
  ],
11025
+ "SnapPoint": [
11026
+ "medium",
11027
+ "large"
11028
+ ],
10532
11029
  "SpacingKey": [
10533
11030
  "xs",
10534
11031
  "sm",