@teamofsilicons/silicon-ui 0.2.0 → 0.3.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 (112) hide show
  1. package/README.md +4 -3
  2. package/check.mjs +14 -5
  3. package/components/activity-heatmap/activity-heatmap.module.css +4 -4
  4. package/components/alert/alert.module.css +1 -1
  5. package/components/announcement-bar/announcement-bar.module.css +6 -6
  6. package/components/avatar/avatar.d.ts +2 -2
  7. package/components/avatar/avatar.js +1 -1
  8. package/components/avatar/avatar.module.css +5 -2
  9. package/components/avatar-group/avatar-group.module.css +4 -4
  10. package/components/badge/badge.module.css +4 -4
  11. package/components/bar-chart/bar-chart.module.css +4 -4
  12. package/components/billing-toggle/billing-toggle.module.css +10 -10
  13. package/components/bottom-sheet/bottom-sheet.module.css +4 -5
  14. package/components/breadcrumb/breadcrumb.d.ts +8 -2
  15. package/components/breadcrumb/breadcrumb.js +21 -9
  16. package/components/breadcrumb/breadcrumb.module.css +7 -3
  17. package/components/brush-chart/brush-chart.module.css +9 -9
  18. package/components/button/button.d.ts +7 -2
  19. package/components/button/button.js +3 -1
  20. package/components/button/button.module.css +10 -1
  21. package/components/button-group/button-group.module.css +6 -6
  22. package/components/calendar/calendar.module.css +7 -7
  23. package/components/card/card.module.css +2 -2
  24. package/components/card-stack/card-stack.module.css +2 -3
  25. package/components/carousel/carousel.module.css +2 -2
  26. package/components/chat-thread/chat-thread.module.css +41 -41
  27. package/components/checkbox/checkbox.module.css +1 -1
  28. package/components/chip-group/chip-group.module.css +2 -2
  29. package/components/code-block/code-block.module.css +7 -14
  30. package/components/color-picker/color-picker.module.css +20 -21
  31. package/components/combobox/combobox.module.css +2 -2
  32. package/components/command-palette/command-palette.module.css +18 -18
  33. package/components/comment-thread/comment-thread.module.css +35 -35
  34. package/components/confirm-morph/confirm-morph.module.css +9 -9
  35. package/components/context-menu/context-menu.module.css +3 -3
  36. package/components/countdown/countdown.module.css +4 -4
  37. package/components/date-picker/date-picker.module.css +6 -6
  38. package/components/date-range-picker/date-range-picker.module.css +21 -21
  39. package/components/dialog/dialog.module.css +2 -2
  40. package/components/donut-chart/donut-chart.module.css +4 -4
  41. package/components/drawer/drawer.module.css +5 -5
  42. package/components/dropdown-menu/dropdown-menu.d.ts +71 -1
  43. package/components/dropdown-menu/dropdown-menu.js +100 -6
  44. package/components/dropdown-menu/dropdown-menu.module.css +15 -5
  45. package/components/elastic-slider/elastic-slider.module.css +4 -4
  46. package/components/expandable-card/expandable-card.module.css +1 -1
  47. package/components/expanding-button-group/expanding-button-group.module.css +1 -1
  48. package/components/expanding-search/expanding-search.module.css +9 -9
  49. package/components/file-dropzone/file-dropzone.module.css +21 -21
  50. package/components/file-upload/file-upload.module.css +7 -7
  51. package/components/filter-toolbar/filter-toolbar.module.css +9 -9
  52. package/components/floating-button-group/floating-button-group.module.css +6 -6
  53. package/components/gauge/gauge.module.css +2 -2
  54. package/components/hover-card/hover-card.module.css +2 -2
  55. package/components/image-compare/image-compare.module.css +4 -4
  56. package/components/inline-edit/inline-edit.module.css +12 -12
  57. package/components/json-viewer/json-viewer.module.css +12 -12
  58. package/components/line-chart/line-chart.module.css +8 -8
  59. package/components/mention-input/mention-input.module.css +8 -8
  60. package/components/metric-card/metric-card.module.css +1 -1
  61. package/components/money-input/money-input.module.css +9 -9
  62. package/components/morph-select/morph-select.module.css +13 -13
  63. package/components/multi-select/multi-select.module.css +6 -6
  64. package/components/nav-list/nav-list.d.ts +64 -0
  65. package/components/nav-list/nav-list.js +103 -0
  66. package/components/nav-list/nav-list.module.css +25 -0
  67. package/components/notification-center/notification-center.module.css +27 -27
  68. package/components/number-field/number-field.module.css +2 -2
  69. package/components/otp-input/otp-input.module.css +4 -4
  70. package/components/password-field/password-field.module.css +1 -1
  71. package/components/password-strength/password-strength.module.css +7 -7
  72. package/components/phone-input/phone-input.module.css +19 -19
  73. package/components/popover/popover.module.css +2 -2
  74. package/components/progress/progress.module.css +1 -1
  75. package/components/radio-cards/radio-cards.module.css +8 -8
  76. package/components/radio-group/radio-group.module.css +1 -1
  77. package/components/resizable-panels/resizable-panels.module.css +5 -5
  78. package/components/rich-text-editor/rich-text-editor.module.css +12 -12
  79. package/components/ridgeline/ridgeline.module.css +7 -7
  80. package/components/scroll-area/scroll-area.module.css +8 -8
  81. package/components/segmented-control/segmented-control.d.ts +3 -1
  82. package/components/segmented-control/segmented-control.js +2 -2
  83. package/components/segmented-control/segmented-control.module.css +7 -4
  84. package/components/select/select.module.css +5 -5
  85. package/components/shortcut-recorder/shortcut-recorder.module.css +15 -15
  86. package/components/signature-pad/signature-pad.module.css +11 -11
  87. package/components/slider/slider.module.css +4 -4
  88. package/components/slope-chart/slope-chart.module.css +7 -7
  89. package/components/sortable-data-table/sortable-data-table.module.css +4 -4
  90. package/components/split-button/split-button.module.css +5 -5
  91. package/components/stepper/stepper.module.css +3 -3
  92. package/components/streamgraph/streamgraph.module.css +7 -7
  93. package/components/swipe-actions/swipe-actions.module.css +5 -5
  94. package/components/switch/switch.module.css +1 -1
  95. package/components/tabs/tabs.d.ts +9 -2
  96. package/components/tabs/tabs.js +4 -2
  97. package/components/tabs/tabs.module.css +7 -2
  98. package/components/tag-input/tag-input.module.css +4 -4
  99. package/components/time-picker/time-picker.module.css +3 -3
  100. package/components/timeline/timeline.module.css +3 -3
  101. package/components/toast/toast.module.css +5 -5
  102. package/components/toast-stack/toast-stack.module.css +5 -5
  103. package/components/tooltip/tooltip.module.css +1 -1
  104. package/components/tree-view/tree-view.module.css +7 -7
  105. package/components/treemap/treemap.module.css +13 -13
  106. package/components/usage-meter/usage-meter.module.css +5 -5
  107. package/components/user-menu/user-menu.module.css +22 -22
  108. package/components/waffle-chart/waffle-chart.module.css +7 -7
  109. package/components.json +416 -1
  110. package/eslint.js +35 -3
  111. package/package.json +1 -1
  112. package/styles.css +44 -4
@@ -1,16 +1,86 @@
1
- import type { ReactNode } from "react";
1
+ import type { ComponentPropsWithoutRef, ReactNode } from "react";
2
+ import * as DropdownPrimitive from "@radix-ui/react-dropdown-menu";
3
+ /** The menu's state. Uncontrolled by default; pass `open` and `onOpenChange` to control it. */
4
+ export declare function DropdownMenuRoot(props: ComponentPropsWithoutRef<typeof DropdownPrimitive.Root>): import("react").JSX.Element;
5
+ export interface DropdownMenuTriggerProps extends ComponentPropsWithoutRef<typeof DropdownPrimitive.Trigger> {
6
+ /** Render your own trigger (an icon-only Button with an aria-label, an avatar and name) instead of the default one. */
7
+ asChild?: boolean;
8
+ }
9
+ /** What opens the menu. Without `asChild` it is the default outlined trigger with a chevron. */
10
+ export declare const DropdownMenuTrigger: import("react").ForwardRefExoticComponent<DropdownMenuTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
11
+ export interface DropdownMenuContentProps extends ComponentPropsWithoutRef<typeof DropdownPrimitive.Content> {
12
+ /** Which side of the trigger it opens on (default bottom). */
13
+ side?: "top" | "right" | "bottom" | "left";
14
+ /** How it lines up with the trigger (default end). */
15
+ align?: "start" | "center" | "end";
16
+ }
17
+ /** The menu itself. One highlight glides between items for the pointer and jumps instantly for the keyboard. */
18
+ export declare const DropdownMenuContent: import("react").ForwardRefExoticComponent<DropdownMenuContentProps & import("react").RefAttributes<HTMLDivElement>>;
19
+ export interface DropdownMenuItemProps extends ComponentPropsWithoutRef<typeof DropdownPrimitive.Item> {
20
+ /** A 16px icon before the label. */
21
+ icon?: ReactNode;
22
+ /** A keyboard shortcut hint after the label, such as "⌘D". Shown only; bind the keys yourself. */
23
+ shortcut?: string;
24
+ /** A destructive action (delete, remove): shown in the danger colour. */
25
+ destructive?: boolean;
26
+ /** Make your own element the item, such as a link: `<DropdownMenuItem asChild><a href="/q3">Q3 2026</a></DropdownMenuItem>`.
27
+ * Its children become the label, and Enter follows the link. */
28
+ asChild?: boolean;
29
+ }
30
+ /** One action, or with `asChild` one link. */
31
+ export declare const DropdownMenuItem: import("react").ForwardRefExoticComponent<DropdownMenuItemProps & import("react").RefAttributes<HTMLDivElement>>;
32
+ export interface DropdownMenuCheckboxItemProps extends ComponentPropsWithoutRef<typeof DropdownPrimitive.CheckboxItem> {
33
+ /** A keyboard shortcut hint after the label. */
34
+ shortcut?: string;
35
+ }
36
+ /** An on/off setting, such as "Full width". A check shows in the icon column while it is on. */
37
+ export declare const DropdownMenuCheckboxItem: import("react").ForwardRefExoticComponent<DropdownMenuCheckboxItemProps & import("react").RefAttributes<HTMLDivElement>>;
38
+ /** One choice out of several (the current space, a sort order). Wrap DropdownMenuRadioItems in it. */
39
+ export declare const DropdownMenuRadioGroup: import("react").ForwardRefExoticComponent<DropdownPrimitive.DropdownMenuRadioGroupProps & import("react").RefAttributes<HTMLDivElement>>;
40
+ export interface DropdownMenuRadioItemProps extends ComponentPropsWithoutRef<typeof DropdownPrimitive.RadioItem> {
41
+ /** A 16px icon or avatar before the label; the check shows at the end while this is the current choice. */
42
+ icon?: ReactNode;
43
+ }
44
+ /** One of the choices in a DropdownMenuRadioGroup. The current choice shows a check at the end. */
45
+ export declare const DropdownMenuRadioItem: import("react").ForwardRefExoticComponent<DropdownMenuRadioItemProps & import("react").RefAttributes<HTMLDivElement>>;
46
+ /** A section heading inside the menu, such as "Spaces". Not focusable. */
47
+ export declare const DropdownMenuLabel: import("react").ForwardRefExoticComponent<Omit<DropdownPrimitive.DropdownMenuLabelProps & import("react").RefAttributes<HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
48
+ /** A hairline between groups of items. */
49
+ export declare const DropdownMenuSeparator: import("react").ForwardRefExoticComponent<Omit<DropdownPrimitive.DropdownMenuSeparatorProps & import("react").RefAttributes<HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
50
+ /** Groups items for assistive technology, without visual change. */
51
+ export declare const DropdownMenuGroup: import("react").ForwardRefExoticComponent<DropdownPrimitive.DropdownMenuGroupProps & import("react").RefAttributes<HTMLDivElement>>;
52
+ /** A nested menu: a DropdownMenuSubTrigger and its DropdownMenuSubContent. */
53
+ export declare const DropdownMenuSub: import("react").FC<DropdownPrimitive.DropdownMenuSubProps>;
54
+ export interface DropdownMenuSubTriggerProps extends ComponentPropsWithoutRef<typeof DropdownPrimitive.SubTrigger> {
55
+ /** A 16px icon before the label. */
56
+ icon?: ReactNode;
57
+ }
58
+ /** The item that opens a submenu (on hover, click, or the right arrow key). */
59
+ export declare const DropdownMenuSubTrigger: import("react").ForwardRefExoticComponent<DropdownMenuSubTriggerProps & import("react").RefAttributes<HTMLDivElement>>;
60
+ /** The submenu's own menu, opening beside its trigger. */
61
+ export declare const DropdownMenuSubContent: import("react").ForwardRefExoticComponent<Omit<DropdownPrimitive.DropdownMenuSubContentProps & import("react").RefAttributes<HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
2
62
  export interface DropdownItem {
63
+ /** The item's text. */
3
64
  label: string;
65
+ /** Runs when the item is chosen. */
4
66
  onSelect?: () => void;
67
+ /** Shown but not choosable. */
5
68
  disabled?: boolean;
69
+ /** A 16px icon before the label. */
6
70
  icon?: ReactNode;
71
+ /** A destructive action, shown in the danger colour. */
7
72
  destructive?: boolean;
73
+ /** Draws a separator above this item. */
8
74
  separatorBefore?: boolean;
9
75
  }
10
76
  export interface DropdownMenuProps {
77
+ /** The trigger's text; when it names the current choice, a new label rises in and the trigger springs to its width. */
11
78
  label: string;
79
+ /** The actions, in order. */
12
80
  items: DropdownItem[];
81
+ /** An icon before the trigger's text. */
13
82
  icon?: ReactNode;
14
83
  }
84
+ /** The shorthand: a labelled trigger and a flat list of actions, built from the parts above. */
15
85
  export declare function DropdownMenu({ label, items, icon }: DropdownMenuProps): import("react").JSX.Element;
16
86
  export default DropdownMenu;
@@ -1,12 +1,107 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { Fragment, useLayoutEffect, useRef, useState } from "react";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ /**
4
+ * Dropdown menu: a list of actions behind a trigger.
5
+ *
6
+ * Two ways to use it:
7
+ *
8
+ * // Shorthand: a labelled trigger and a flat list.
9
+ * <DropdownMenu label="Manage silicon" items={[{ label: "Rename", onSelect: rename }, …]} />
10
+ *
11
+ * // Composable (experimental): any trigger, labels, checked and radio items, shortcuts and submenus.
12
+ * <DropdownMenuRoot>
13
+ * <DropdownMenuTrigger asChild><Button variant="ghost" aria-label="Page options"><Ellipsis /></Button></DropdownMenuTrigger>
14
+ * <DropdownMenuContent align="end">
15
+ * <DropdownMenuLabel>Page</DropdownMenuLabel>
16
+ * <DropdownMenuItem icon={<Copy />} shortcut="⌘D" onSelect={duplicate}>Duplicate</DropdownMenuItem>
17
+ * <DropdownMenuCheckboxItem checked={wide} onCheckedChange={setWide}>Full width</DropdownMenuCheckboxItem>
18
+ * <DropdownMenuSub>
19
+ * <DropdownMenuSubTrigger icon={<FolderInput />}>Move to</DropdownMenuSubTrigger>
20
+ * <DropdownMenuSubContent>…</DropdownMenuSubContent>
21
+ * </DropdownMenuSub>
22
+ * <DropdownMenuSeparator />
23
+ * <DropdownMenuItem destructive icon={<Trash2 />} onSelect={remove}>Delete</DropdownMenuItem>
24
+ * </DropdownMenuContent>
25
+ * </DropdownMenuRoot>
26
+ *
27
+ * Every part shares one look: the same surface, the gliding highlight, the entrance that grows from the trigger.
28
+ */
29
+ import { Fragment, cloneElement, forwardRef, isValidElement, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
4
30
  import * as DropdownPrimitive from "@radix-ui/react-dropdown-menu";
5
31
  import { AnimatePresence, motion, useReducedMotion } from "motion/react";
6
- import { ChevronDown } from "lucide-react";
32
+ import { Check, ChevronDown, ChevronRight } from "lucide-react";
7
33
  import { MenuHighlight, useMenuHighlight } from "../../lib/menu-highlight.js";
8
34
  import { motionTokens } from "../../lib/motion-tokens.js";
9
35
  import styles from "./dropdown-menu.module.css";
36
+ const ITEMS = '[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]';
37
+ const cx = (...names) => names.filter(Boolean).join(" ");
38
+ /** A menu surface's own ref, also handed to the caller's ref. It numbers the items in order, so their entrance
39
+ * staggers whatever groups and parts they sit in. */
40
+ function useSurfaceRef(forwarded) {
41
+ const own = useRef(null);
42
+ useImperativeHandle(forwarded, () => own.current, []);
43
+ useLayoutEffect(() => {
44
+ own.current?.querySelectorAll(ITEMS).forEach((item, index) => item.style.setProperty("--i", String(index)));
45
+ });
46
+ return own;
47
+ }
48
+ /* ------------------------------------------------------------------------------------------------------------------ */
49
+ /* Composable parts */
50
+ /* ------------------------------------------------------------------------------------------------------------------ */
51
+ /** The menu's state. Uncontrolled by default; pass `open` and `onOpenChange` to control it. */
52
+ export function DropdownMenuRoot(props) {
53
+ return _jsx(DropdownPrimitive.Root, { ...props });
54
+ }
55
+ /** What opens the menu. Without `asChild` it is the default outlined trigger with a chevron. */
56
+ export const DropdownMenuTrigger = forwardRef(function DropdownMenuTrigger({ asChild, className, children, ...props }, ref) {
57
+ if (asChild)
58
+ return _jsx(DropdownPrimitive.Trigger, { ...props, ref: ref, asChild: true, children: children });
59
+ return (_jsxs(DropdownPrimitive.Trigger, { ...props, ref: ref, type: "button", className: cx(styles.trigger, className), children: [children, _jsx(ChevronDown, { className: styles.chevron, size: 15, strokeWidth: 1.8, "aria-hidden": "true" })] }));
60
+ });
61
+ /** The menu itself. One highlight glides between items for the pointer and jumps instantly for the keyboard. */
62
+ export const DropdownMenuContent = forwardRef(function DropdownMenuContent({ className, children, side = "bottom", align = "end", sideOffset = 6, collisionPadding = 12, ...props }, ref) {
63
+ const { highlight, contentProps } = useMenuHighlight(ITEMS);
64
+ const surface = useSurfaceRef(ref);
65
+ return (_jsx(DropdownPrimitive.Portal, { children: _jsxs(DropdownPrimitive.Content, { ...props, ...contentProps, ref: surface, side: side, align: align, sideOffset: sideOffset, collisionPadding: collisionPadding, loop: true, className: cx(styles.menu, className), children: [_jsx(MenuHighlight, { state: highlight, className: styles.highlight }), children] }) }));
66
+ });
67
+ /** One action, or with `asChild` one link. */
68
+ export const DropdownMenuItem = forwardRef(function DropdownMenuItem({ icon, shortcut, destructive, asChild, className, children, ...props }, ref) {
69
+ const content = (label) => _jsxs(_Fragment, { children: [icon ? _jsx("span", { className: styles.icon, "aria-hidden": "true", children: icon }) : null, _jsx("span", { className: styles.itemLabel, children: label }), shortcut ? _jsx("kbd", { className: styles.shortcut, children: shortcut }) : null] });
70
+ const own = asChild && isValidElement(children) ? children : null;
71
+ return (_jsx(DropdownPrimitive.Item, { ...props, ref: ref, asChild: !!own, className: cx(styles.item, destructive && styles.destructive, className), "data-tone": destructive ? "danger" : undefined, children: own ? cloneElement(own, undefined, content(own.props.children)) : content(children) }));
72
+ });
73
+ /** An on/off setting, such as "Full width". A check shows in the icon column while it is on. */
74
+ export const DropdownMenuCheckboxItem = forwardRef(function DropdownMenuCheckboxItem({ shortcut, className, children, ...props }, ref) {
75
+ return (_jsxs(DropdownPrimitive.CheckboxItem, { ...props, ref: ref, className: cx(styles.item, className), children: [_jsx("span", { className: styles.icon, "aria-hidden": "true", children: _jsx(DropdownPrimitive.ItemIndicator, { children: _jsx(Check, { size: 16, strokeWidth: 2 }) }) }), _jsx("span", { className: styles.itemLabel, children: children }), shortcut ? _jsx("kbd", { className: styles.shortcut, children: shortcut }) : null] }));
76
+ });
77
+ /** One choice out of several (the current space, a sort order). Wrap DropdownMenuRadioItems in it. */
78
+ export const DropdownMenuRadioGroup = DropdownPrimitive.RadioGroup;
79
+ /** One of the choices in a DropdownMenuRadioGroup. The current choice shows a check at the end. */
80
+ export const DropdownMenuRadioItem = forwardRef(function DropdownMenuRadioItem({ icon, className, children, ...props }, ref) {
81
+ return (_jsxs(DropdownPrimitive.RadioItem, { ...props, ref: ref, className: cx(styles.item, className), children: [icon ? _jsx("span", { className: styles.icon, "aria-hidden": "true", children: icon }) : null, _jsx("span", { className: styles.itemLabel, children: children }), _jsx("span", { className: styles.indicator, "aria-hidden": "true", children: _jsx(DropdownPrimitive.ItemIndicator, { children: _jsx(Check, { size: 16, strokeWidth: 2 }) }) })] }));
82
+ });
83
+ /** A section heading inside the menu, such as "Spaces". Not focusable. */
84
+ export const DropdownMenuLabel = forwardRef(function DropdownMenuLabel({ className, ...props }, ref) {
85
+ return _jsx(DropdownPrimitive.Label, { ...props, ref: ref, className: cx(styles.sectionLabel, className) });
86
+ });
87
+ /** A hairline between groups of items. */
88
+ export const DropdownMenuSeparator = forwardRef(function DropdownMenuSeparator({ className, ...props }, ref) {
89
+ return _jsx(DropdownPrimitive.Separator, { ...props, ref: ref, className: cx(styles.separator, className) });
90
+ });
91
+ /** Groups items for assistive technology, without visual change. */
92
+ export const DropdownMenuGroup = DropdownPrimitive.Group;
93
+ /** A nested menu: a DropdownMenuSubTrigger and its DropdownMenuSubContent. */
94
+ export const DropdownMenuSub = DropdownPrimitive.Sub;
95
+ /** The item that opens a submenu (on hover, click, or the right arrow key). */
96
+ export const DropdownMenuSubTrigger = forwardRef(function DropdownMenuSubTrigger({ icon, className, children, ...props }, ref) {
97
+ return (_jsxs(DropdownPrimitive.SubTrigger, { ...props, ref: ref, className: cx(styles.item, styles.subTrigger, className), children: [icon ? _jsx("span", { className: styles.icon, "aria-hidden": "true", children: icon }) : null, _jsx("span", { className: styles.itemLabel, children: children }), _jsx(ChevronRight, { className: styles.subChevron, size: 15, strokeWidth: 1.8, "aria-hidden": "true" })] }));
98
+ });
99
+ /** The submenu's own menu, opening beside its trigger. */
100
+ export const DropdownMenuSubContent = forwardRef(function DropdownMenuSubContent({ className, children, sideOffset = 6, collisionPadding = 12, ...props }, ref) {
101
+ const { highlight, contentProps } = useMenuHighlight(ITEMS);
102
+ const surface = useSurfaceRef(ref);
103
+ return (_jsx(DropdownPrimitive.Portal, { children: _jsxs(DropdownPrimitive.SubContent, { ...props, ...contentProps, ref: surface, sideOffset: sideOffset, collisionPadding: collisionPadding, loop: true, className: cx(styles.menu, className), children: [_jsx(MenuHighlight, { state: highlight, className: styles.highlight }), children] }) }));
104
+ });
10
105
  /** A new trigger label rises in while the old one leaves, and the trigger width springs to the measured text instead of snapping. */
11
106
  function TriggerLabel({ text }) {
12
107
  const reduced = useReducedMotion();
@@ -31,9 +126,8 @@ function TriggerLabel({ text }) {
31
126
  }, []);
32
127
  return _jsxs(motion.span, { className: styles.label, initial: false, animate: { width: size.width }, transition: size.animate && !reduced ? motionTokens.spring.morph : { duration: 0 }, children: [_jsx("span", { ref: measure, className: styles.labelMeasure, "aria-hidden": "true", children: text }), _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(motion.span, { className: styles.labelText, initial: reduced ? false : { opacity: 0, y: "0.3em", filter: `blur(${motionTokens.blur.soft}px)` }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, exit: reduced ? { opacity: 0, transition: { duration: 0 } } : { opacity: 0, y: "-0.3em", filter: `blur(${motionTokens.blur.subtle}px)`, transition: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } }, transition: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] }, children: text }, text) })] });
33
128
  }
129
+ /** The shorthand: a labelled trigger and a flat list of actions, built from the parts above. */
34
130
  export function DropdownMenu({ label, items, icon }) {
35
- const { highlight, reset, contentProps } = useMenuHighlight();
36
- return _jsxs(DropdownPrimitive.Root, { onOpenChange: open => { if (open)
37
- reset(); }, children: [_jsxs(DropdownPrimitive.Trigger, { className: styles.trigger, type: "button", children: [icon && _jsx("span", { className: styles.triggerIcon, "aria-hidden": "true", children: icon }), _jsx(TriggerLabel, { text: label }), _jsx(ChevronDown, { className: styles.chevron, size: 15, strokeWidth: 1.8, "aria-hidden": "true" })] }), _jsx(DropdownPrimitive.Portal, { children: _jsxs(DropdownPrimitive.Content, { className: styles.menu, sideOffset: 6, align: "end", collisionPadding: 12, loop: true, ...contentProps, children: [_jsx(MenuHighlight, { state: highlight, className: styles.highlight }), items.map((item, index) => _jsxs(Fragment, { children: [item.separatorBefore && _jsx(DropdownPrimitive.Separator, { className: styles.separator }), _jsxs(DropdownPrimitive.Item, { className: [styles.item, item.destructive ? styles.destructive : ""].filter(Boolean).join(" "), "data-tone": item.destructive ? "danger" : undefined, style: { "--i": index }, disabled: item.disabled, onSelect: item.onSelect, children: [item.icon && _jsx("span", { className: styles.icon, "aria-hidden": "true", children: item.icon }), item.label] })] }, item.label))] }) })] });
131
+ return (_jsxs(DropdownMenuRoot, { children: [_jsxs(DropdownMenuTrigger, { children: [icon ? _jsx("span", { className: styles.triggerIcon, "aria-hidden": "true", children: icon }) : null, _jsx(TriggerLabel, { text: label })] }), _jsx(DropdownMenuContent, { children: items.map(item => (_jsxs(Fragment, { children: [item.separatorBefore ? _jsx(DropdownMenuSeparator, {}) : null, _jsx(DropdownMenuItem, { icon: item.icon, destructive: item.destructive, disabled: item.disabled, onSelect: item.onSelect, children: item.label })] }, item.label))) })] }));
38
132
  }
39
133
  export default DropdownMenu;
@@ -12,7 +12,7 @@
12
12
  .trigger[data-state="open"] .chevron { transform: rotate(180deg); }
13
13
  /* The menu grows from the trigger edge: offset toward the trigger, scale from the Radix origin, fade quickly.
14
14
  Transitions instead of keyframes, so a close that interrupts the open (or a reopen during the close) reverses from where it is. */
15
- .menu { --menu-x: 0px; --menu-y: -5px; position: relative; z-index: 60; min-width: min(12rem, var(--radix-dropdown-menu-content-available-width)); max-width: var(--radix-dropdown-menu-content-available-width); padding: 5px; border: 1px solid var(--border); border-radius: var(--radius-container); background: var(--surface-raised); box-shadow: var(--shadow-floating); transform-origin: var(--radix-dropdown-menu-content-transform-origin); transition: opacity var(--duration-fast) var(--ease-enter), transform var(--duration-spring) var(--ease-spring); }
15
+ .menu { --menu-x: 0px; --menu-y: -5px; position: relative; z-index: var(--layer-dropdown); min-width: min(12rem, var(--radix-dropdown-menu-content-available-width)); max-width: var(--radix-dropdown-menu-content-available-width); padding: var(--space-tight); border: 1px solid var(--border); border-radius: var(--radius-container); background: var(--surface-raised); box-shadow: var(--shadow-floating); transform-origin: var(--radix-dropdown-menu-content-transform-origin); transition: opacity var(--duration-fast) var(--ease-enter), transform var(--duration-spring) var(--ease-spring); }
16
16
  .menu[data-side="top"] { --menu-y: 5px; }
17
17
  .menu[data-side="left"] { --menu-x: 5px; --menu-y: 0px; }
18
18
  .menu[data-side="right"] { --menu-x: -5px; --menu-y: 0px; }
@@ -21,11 +21,21 @@
21
21
  .menu[data-state="open"] .item { opacity: 0; transform: translate(calc(var(--menu-x) * .4), calc(var(--menu-y) * .4)); }
22
22
  }
23
23
  /* Radix unmounts when the exit animation ends, so a no-op keyframe times the exit while the transition does the visual work. */
24
- .menu[data-state="closed"] { opacity: 0; transform: translate(calc(var(--menu-x) * .5), calc(var(--menu-y) * .5)) scale(.985); transition: opacity 130ms var(--ease-standard), transform 130ms var(--ease-standard); animation: menu-exit 130ms linear both; pointer-events: none; }
25
- .highlight { position: absolute; top: 0; right: 5px; left: 5px; border-radius: calc(var(--radius-container) - 6px); background: var(--surface-muted); opacity: 0; pointer-events: none; transition: background-color var(--duration-fast) var(--ease-standard); }
24
+ .menu[data-state="closed"] { opacity: 0; transform: translate(calc(var(--menu-x) * .5), calc(var(--menu-y) * .5)) scale(.985); transition: opacity var(--duration-instant) var(--ease-standard), transform var(--duration-instant) var(--ease-standard); animation: menu-exit var(--duration-instant) linear both; pointer-events: none; }
25
+ .highlight { position: absolute; top: 0; right: var(--space-tight); left: var(--space-tight); border-radius: calc(var(--radius-container) - var(--space-related)); background: var(--surface-muted); opacity: 0; pointer-events: none; transition: background-color var(--duration-fast) var(--ease-standard); }
26
26
  .highlight[data-tone="danger"] { background: color-mix(in oklab, var(--danger) 8%, var(--surface)); }
27
- .item { position: relative; display: flex; min-height: 36px; align-items: center; gap: 10px; padding: 0 11px; border-radius: calc(var(--radius-container) - 6px); color: var(--foreground); font-size: var(--text-sm); cursor: pointer; outline: none; transition: color var(--duration-fast) var(--ease-standard), opacity var(--duration-standard) var(--ease-enter) calc(min(var(--i, 0), 4) * 35ms), transform var(--duration-standard) var(--ease-enter) calc(min(var(--i, 0), 4) * 35ms); }
28
- .icon { display: inline-flex; width: 17px; color: var(--text-secondary); }
27
+ /* An item may be a link (asChild), which keeps the item's look. */
28
+ .item { position: relative; display: flex; min-height: 36px; align-items: center; gap: var(--space-group); padding: 0 var(--space-group); border-radius: calc(var(--radius-container) - var(--space-related)); color: var(--foreground); font-size: var(--text-sm); text-decoration: none; cursor: pointer; outline: none; transition: color var(--duration-fast) var(--ease-standard), opacity var(--duration-standard) var(--ease-enter) calc(min(var(--i, 0), 4) * var(--stagger-item)), transform var(--duration-standard) var(--ease-enter) calc(min(var(--i, 0), 4) * var(--stagger-item)); }
29
+ /* At least the 16px icon's column; wider content (an avatar) takes the room it needs. */
30
+ .icon { display: inline-flex; min-width: 17px; flex: none; color: var(--text-secondary); }
31
+ /* The composable parts. The label takes the free width, so a shortcut or check sits at the end of the row. */
32
+ .itemLabel { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
33
+ .shortcut { flex: none; padding-left: var(--space-group); color: var(--text-muted); font: inherit; font-size: var(--text-xs); letter-spacing: var(--tracking-body); }
34
+ .indicator { display: inline-flex; flex: none; color: var(--foreground); }
35
+ .sectionLabel { padding: var(--space-related) var(--space-group) var(--space-tight); color: var(--text-muted); font-size: var(--text-xs); font-weight: 500; }
36
+ .subChevron { flex: none; color: var(--text-muted); }
37
+ /* While its submenu is open, the item that opened it keeps a quiet fill, so the path stays visible. */
38
+ .subTrigger[data-state="open"] { background: var(--surface-muted); }
29
39
  .separator { height: 1px; margin: var(--space-tight) calc(var(--space-tight) * -1); background: var(--border-subtle); }
30
40
  .destructive { color: var(--danger); }
31
41
  .destructive .icon { color: var(--danger); }
@@ -15,7 +15,7 @@
15
15
  /* Only the band stretches. Ticks, thumb, and tag ride along without distorting. */
16
16
  .track { position: absolute; inset: 0; border-radius: var(--radius-full); corner-shape: round; background: var(--control-track); will-change: transform; }
17
17
  .fill { position: absolute; inset: 0; border-radius: inherit; corner-shape: inherit; background: var(--control-fill); }
18
- .tick { position: absolute; top: 50%; width: 4px; height: 4px; margin: -2px 0 0 -2px; border-radius: var(--radius-full); corner-shape: round; background: color-mix(in oklab, var(--foreground) 32%, transparent); pointer-events: none; transition: background-color var(--duration-fast) var(--ease-standard); }
18
+ .tick { position: absolute; top: 50%; width: 4px; height: 4px; margin: calc(var(--space-hair) * -1) 0 0 calc(var(--space-hair) * -1); border-radius: var(--radius-full); corner-shape: round; background: color-mix(in oklab, var(--foreground) 32%, transparent); pointer-events: none; transition: background-color var(--duration-fast) var(--ease-standard); }
19
19
  .tick[data-on] { background: color-mix(in oklab, var(--control-glyph) 60%, transparent); }
20
20
  .thumbs { position: absolute; inset: 0 calc(var(--elastic-thumb) / 2); container-type: inline-size; pointer-events: none; }
21
21
  /* Each layer is as wide as the rail and slides by a percentage of itself, so placing the thumb needs no measuring. */
@@ -23,11 +23,11 @@
23
23
  .shift { position: absolute; inset: 0; }
24
24
  .thumb { position: absolute; top: 50%; right: 0; box-sizing: border-box; width: var(--elastic-thumb); height: var(--elastic-thumb); translate: 50% -50%; border: 0; border-radius: var(--radius-full); corner-shape: round; background: var(--control-thumb); box-shadow: var(--control-thumb-shadow); pointer-events: auto; cursor: grab; outline: none; transition: box-shadow var(--duration-fast) var(--ease-standard); }
25
25
  /* A generous invisible hit area for fingertips. */
26
- .thumb::after { content: ""; position: absolute; inset: -12px; border-radius: inherit; corner-shape: inherit; }
26
+ .thumb::after { content: ""; position: absolute; inset: calc(var(--space-group) * -1); border-radius: inherit; corner-shape: inherit; }
27
27
  .root[data-dragging], .root[data-dragging] .control, .root[data-dragging] .thumb { cursor: grabbing; }
28
28
  /* The tag centers on the thumb until it would pass the component's edge, then holds there. It hangs from its bottom edge, so a swing pivots on the thumb. */
29
- .bubbleAnchor { --edge: calc(var(--elastic-thumb) / 2); --x: calc(var(--at, 0) * 1cqw); position: absolute; right: 0; bottom: calc(50% + var(--elastic-thumb) / 2 + 10px); display: flex; translate: clamp(calc(100% - var(--edge) - var(--x)), 50%, calc(100cqw + var(--edge) - var(--x))) 0; pointer-events: none; }
30
- @media (pointer: coarse) { .bubbleAnchor { bottom: calc(50% + var(--elastic-thumb) / 2 + 20px); } }
29
+ .bubbleAnchor { --edge: calc(var(--elastic-thumb) / 2); --x: calc(var(--at, 0) * 1cqw); position: absolute; right: 0; bottom: calc(50% + var(--elastic-thumb) / 2 + var(--space-group)); display: flex; translate: clamp(calc(100% - var(--edge) - var(--x)), 50%, calc(100cqw + var(--edge) - var(--x))) 0; pointer-events: none; }
30
+ @media (pointer: coarse) { .bubbleAnchor { bottom: calc(50% + var(--elastic-thumb) / 2 + var(--space-comfort)); } }
31
31
  .bubble { display: inline-flex; min-height: 1.75rem; align-items: center; padding: 0 var(--space-group); border-radius: var(--radius-full); corner-shape: round; background: var(--foreground); color: var(--background); box-shadow: var(--shadow-raised); font-size: var(--text-sm); font-weight: 500; font-variant-numeric: tabular-nums; white-space: nowrap; transform-origin: 50% 130%; }
32
32
  .marks { position: relative; grid-area: 2 / 2; height: 1.25rem; margin: 0 calc(var(--elastic-thumb) / 2); color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
33
33
  .markLabel { position: absolute; top: 0; translate: -50% 0; white-space: nowrap; cursor: pointer; transition: color var(--duration-fast) var(--ease-standard); }
@@ -10,7 +10,7 @@
10
10
  .trigger:active { background: var(--surface-muted); }
11
11
  .copy { min-width: 0; }
12
12
  .copy strong { display: block; font-size: var(--text-sm); font-weight: 500; }
13
- .description { display: block; margin-top: 4px; color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
13
+ .description { display: block; margin-top: var(--space-tight); color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
14
14
  .roll { position: relative; display: block; }
15
15
  .word { display: inline-block; white-space: pre; }
16
16
  .srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
@@ -1,5 +1,5 @@
1
1
  /* A flat pill on the surface. Geometry comes from the component as --ebg-* variables, so the measured widths and the CSS agree. */
2
- .group { --ebg-h: 36px; --ebg-icon: 16px; --ebg-start: 10px; --ebg-gap: 7px; --ebg-end: 14px; --ebg-pad: 3px; position: relative; isolation: isolate; display: inline-flex; max-width: 100%; flex: none; align-items: center; gap: 2px; padding: var(--ebg-pad); border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface); }
2
+ .group { --ebg-h: 36px; --ebg-icon: 16px; --ebg-start: 10px; --ebg-gap: 7px; --ebg-end: 14px; --ebg-pad: 3px; position: relative; isolation: isolate; display: inline-flex; max-width: 100%; flex: none; align-items: center; gap: var(--space-hair); padding: var(--ebg-pad); border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface); }
3
3
 
4
4
  /* The box never scales: its width is the animation, so a press answers with the icon and with color. The label overflows to the
5
5
  right of the icon and the clipped width reveals it. min-width keeps the spring's settle from pinching a collapsed button. */
@@ -16,24 +16,24 @@
16
16
  .trigger { position: absolute; z-index: 1; inset: 0; margin: 0; padding: 0; border: 0; border-radius: inherit; corner-shape: inherit; background: transparent; cursor: pointer; -webkit-tap-highlight-color: transparent; }
17
17
  .trigger:focus-visible { outline: none; }
18
18
  /* The glass sits at the leading edge, so it is centered in the circle and rides that edge to its place in the open field. */
19
- .icon { position: absolute; top: calc(50% - 9px); left: calc(var(--control-height-md) / 2 - 10px); pointer-events: none; }
20
- .input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; padding: 0 40px 0 38px; border: 0; background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-sm); letter-spacing: var(--tracking-body); text-overflow: ellipsis; outline: none; }
19
+ .icon { position: absolute; top: calc(50% - var(--space-related)); left: calc(var(--control-height-md) / 2 - var(--space-group)); pointer-events: none; }
20
+ .input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; padding: 0 var(--space-page) 0 var(--space-block); border: 0; background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-sm); letter-spacing: var(--tracking-body); text-overflow: ellipsis; outline: none; }
21
21
  .input:focus-visible { outline: none; }
22
22
  .input::placeholder { color: var(--text-muted); opacity: 1; }
23
23
  .input[inert] { pointer-events: none; }
24
- .clear { position: absolute; z-index: 1; top: calc(50% - 14px); right: 7px; display: grid; width: 28px; height: 28px; place-items: center; margin: 0; padding: 0; border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; color: var(--text-muted); cursor: pointer; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard); -webkit-tap-highlight-color: transparent; }
24
+ .clear { position: absolute; z-index: 1; top: calc(50% - var(--space-inset)); right: var(--space-related); display: grid; width: 28px; height: 28px; place-items: center; margin: 0; padding: 0; border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; color: var(--text-muted); cursor: pointer; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard); -webkit-tap-highlight-color: transparent; }
25
25
 
26
26
  /* A floating layer: its width is the field's, so it unfolds from the field on first open; its content keeps the final width and rides the leading edge. */
27
- .panel { position: absolute; z-index: 40; top: calc(100% + 8px); right: 0; overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius-container); corner-shape: var(--corner-shape); background: var(--surface-raised); box-shadow: var(--shadow-floating); pointer-events: auto; }
27
+ .panel { position: absolute; z-index: var(--layer-dropdown); top: calc(100% + var(--space-related)); right: 0; overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius-container); corner-shape: var(--corner-shape); background: var(--surface-raised); box-shadow: var(--shadow-floating); pointer-events: auto; }
28
28
  .root[data-anchor="start"] .panel { right: auto; left: 0; }
29
- .body { position: relative; width: calc(var(--es-width, 360px) - 2px); max-height: min(22rem, 60vh); overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; padding: 6px; scrollbar-width: thin; }
29
+ .body { position: relative; width: calc(var(--es-width, 360px) - 2px); max-height: min(22rem, 60vh); overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; padding: var(--space-related); scrollbar-width: thin; }
30
30
  .view { position: relative; }
31
31
  /* A flex gap, not margins: a group popping out while it leaves never lets the next one's margin collapse through the list. */
32
- .listbox { position: relative; isolation: isolate; display: flex; flex-direction: column; gap: 4px; }
32
+ .listbox { position: relative; isolation: isolate; display: flex; flex-direction: column; gap: var(--space-tight); }
33
33
  .group { position: relative; }
34
- .groupLabel { padding: 8px 12px 4px; color: var(--text-muted); font-size: var(--text-xs); font-weight: 500; line-height: var(--leading-body); }
34
+ .groupLabel { padding: var(--space-related) var(--space-group) var(--space-tight); color: var(--text-muted); font-size: var(--text-xs); font-weight: 500; line-height: var(--leading-body); }
35
35
  /* Nested corners step down from the panel by its inset. The highlight paints under every row's content, so text never hides while it travels. */
36
- .option { position: relative; display: flex; min-height: 48px; align-items: center; gap: 12px; padding: 6px 12px; border-radius: calc(var(--radius-container) - 6px); corner-shape: var(--corner-shape); color: var(--foreground); cursor: pointer; user-select: none; -webkit-tap-highlight-color: transparent; }
36
+ .option { position: relative; display: flex; min-height: 48px; align-items: center; gap: var(--space-group); padding: var(--space-related) var(--space-group); border-radius: calc(var(--radius-container) - var(--space-related)); corner-shape: var(--corner-shape); color: var(--foreground); cursor: pointer; user-select: none; -webkit-tap-highlight-color: transparent; }
37
37
  .highlight { position: absolute; z-index: -1; inset: 0; border-radius: inherit; corner-shape: inherit; background: color-mix(in oklab, var(--foreground) 6%, transparent); pointer-events: none; }
38
38
  .optionIcon { display: grid; flex: none; width: 18px; place-items: center; color: var(--text-muted); transition: color var(--duration-fast) var(--ease-standard); }
39
39
  .option[data-active] .optionIcon { color: var(--foreground); }
@@ -42,7 +42,7 @@
42
42
  .optionTitle { color: var(--foreground); font-size: var(--text-sm); }
43
43
  .optionMeta { color: var(--text-muted); font-size: var(--text-xs); }
44
44
  .mark { background: none; color: var(--foreground); font-weight: 500; }
45
- .empty { display: grid; gap: 2px; padding: 12px 12px 14px; }
45
+ .empty { display: grid; gap: var(--space-hair); padding: var(--space-group) var(--space-group) var(--space-inset); }
46
46
  .emptyTitle { margin: 0; overflow-wrap: anywhere; color: var(--foreground); font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); }
47
47
  .emptyHint { margin: 0; color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); }
48
48
  .srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
@@ -4,9 +4,9 @@
4
4
  /* A folded prompt is a slimmer surface, so its corners step down to the panel radius. */
5
5
  .dropzone.compact { border-radius: var(--radius-container); corner-shape: var(--corner-shape); }
6
6
  /* The edge is a drawn stroke, fitted to the perimeter in script, so a drag can close its dashes into one line. */
7
- .edge { position: absolute; top: .5px; left: .5px; width: calc(100% - 1px); height: calc(100% - 1px); overflow: visible; pointer-events: none; }
7
+ .edge { position: absolute; top: var(--space-hair); left: var(--space-hair); width: calc(100% - 1px); height: calc(100% - 1px); overflow: visible; pointer-events: none; }
8
8
  .edge rect { fill: none; stroke: var(--border-strong); stroke-width: 1; stroke-dasharray: 3.5 3.5; stroke-dashoffset: 0; transition: stroke var(--duration-fast) var(--ease-standard), stroke-dasharray var(--duration-standard) var(--ease-standard), stroke-dashoffset var(--duration-standard) var(--ease-standard); }
9
- .trigger { display: flex; width: 100%; flex-direction: column; align-items: stretch; justify-content: center; border: 0; border-radius: inherit; corner-shape: inherit; padding: 34px var(--space-comfort); background: transparent; color: inherit; font: inherit; letter-spacing: var(--tracking-body); text-align: center; cursor: pointer; }
9
+ .trigger { display: flex; width: 100%; flex-direction: column; align-items: stretch; justify-content: center; border: 0; border-radius: inherit; corner-shape: inherit; padding: var(--space-block) var(--space-comfort); background: transparent; color: inherit; font: inherit; letter-spacing: var(--tracking-body); text-align: center; cursor: pointer; }
10
10
  .trigger > * { pointer-events: none; }
11
11
  @media (hover: hover) and (pointer: fine) {
12
12
  .dropzone:has(.trigger:hover) { background: var(--surface-muted); }
@@ -19,24 +19,24 @@
19
19
  @keyframes breathe { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.012); } }
20
20
  /* Light that follows the pointer: a wash inside, and a brighter stretch of the edge nearest the file. */
21
21
  .wash, .glow { position: absolute; inset: 0; border-radius: inherit; corner-shape: inherit; opacity: 0; pointer-events: none; transition: opacity var(--duration-standard) var(--ease-standard); }
22
- .glow { padding: 1.5px; -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); -webkit-mask-composite: xor; mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0); }
22
+ .glow { padding: var(--space-hair); -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); -webkit-mask-composite: xor; mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0); }
23
23
  .dragging .wash, .dragging .glow { opacity: 1; }
24
24
  .dragging .edge rect { stroke: color-mix(in oklab, var(--accent) 40%, var(--border)); stroke-dasharray: 7 0; stroke-dashoffset: 1.75; }
25
25
  /* No gaps between the prompt's parts: each slot carries its own space, so a folding slot takes its space with it. */
26
26
  .iconSlot, .noteSlot { display: flex; flex-direction: column; overflow: hidden; }
27
27
  /* Three sheets of paper, the target's one illustration. The slot's top margin is room for them to lift without clipping. */
28
- .sheets { position: relative; display: block; width: 36px; height: 44px; align-self: center; margin: 10px 0 14px; }
28
+ .sheets { position: relative; display: block; width: 36px; height: 44px; align-self: center; margin: var(--space-group) 0 var(--space-inset); }
29
29
  .sheet {
30
30
  --r: 0deg; --x: 0px; --y: 0px;
31
- position: absolute; inset: 0; display: grid; place-items: end center; padding: 0 0 5px;
31
+ position: absolute; inset: 0; display: grid; place-items: end center; padding: 0 0 var(--space-tight);
32
32
  border: 1px solid var(--border); border-radius: var(--radius-detail); corner-shape: var(--corner-shape); background: var(--surface-raised); color: var(--text-muted);
33
- box-shadow: 0 1px 2px oklch(0% 0 0 / .05), 0 3px 8px oklch(0% 0 0 / .04);
33
+ box-shadow: var(--shadow-thumb);
34
34
  transform: translate(var(--x), var(--y)) rotate(var(--r));
35
35
  transform-origin: 50% 100%;
36
36
  transition: transform var(--duration-spring) var(--ease-spring), color var(--duration-standard) var(--ease-standard), border-color var(--duration-standard) var(--ease-standard);
37
37
  }
38
- .sheet::before, .sheet::after { position: absolute; top: 8px; left: 7px; width: 16px; height: 2px; border-radius: var(--radius-full); corner-shape: round; background: currentColor; content: ""; opacity: .3; }
39
- .sheet::after { top: 13px; width: 11px; }
38
+ .sheet::before, .sheet::after { position: absolute; top: var(--space-related); left: var(--space-related); width: 16px; height: 2px; border-radius: var(--radius-full); corner-shape: round; background: currentColor; content: ""; opacity: .3; }
39
+ .sheet::after { top: var(--space-group); width: 11px; }
40
40
  .sheet > svg { transition: transform var(--duration-spring) var(--ease-spring); }
41
41
  .dragging .sheet > svg { transform: translateY(-3px); }
42
42
  .sheet[data-sheet="back"] { --r: -10deg; --x: -9px; --y: 2px; }
@@ -53,31 +53,31 @@
53
53
  /* Full width and centered text: the label box never moves, so an outgoing label leaves from exactly where it was. */
54
54
  .label { position: relative; display: block; font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); }
55
55
  .labelText { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; vertical-align: top; white-space: nowrap; }
56
- .hint { margin-top: 6px; color: var(--text-secondary); font-size: var(--text-sm); line-height: var(--leading-body); }
57
- .note { padding-top: 6px; color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); }
56
+ .hint { margin-top: var(--space-related); color: var(--text-secondary); font-size: var(--text-sm); line-height: var(--leading-body); }
57
+ .note { padding-top: var(--space-related); color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); }
58
58
  .input { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
59
59
  .messageSlot { overflow: hidden; }
60
- .error { display: flex; align-items: flex-start; gap: 6px; padding-top: var(--space-group); color: var(--danger); font-size: var(--text-sm); line-height: var(--leading-body); }
60
+ .error { display: flex; align-items: flex-start; gap: var(--space-related); padding-top: var(--space-group); color: var(--danger); font-size: var(--text-sm); line-height: var(--leading-body); }
61
61
  /* The icon sits on the first line's optical center, so a wrapped message keeps it at the top. */
62
- .errorIcon { flex: 0 0 auto; margin-top: 3px; }
62
+ .errorIcon { flex: 0 0 auto; margin-top: var(--space-tight); }
63
63
  .errorCopy { position: relative; min-width: 0; flex: 1; }
64
64
 
65
65
  .list { display: grid; min-width: 0; margin: 0; padding: 0; list-style: none; container: dropzone-list / inline-size; }
66
- .inside .list { padding-inline: 12px; }
66
+ .inside .list { padding-inline: var(--space-group); }
67
67
  /* Rows animate height; the gap lives inside the clipped row as margin, so a closing row reaches a true zero. */
68
68
  .item { min-width: 0; overflow: hidden; }
69
- .row { position: relative; display: grid; grid-template-columns: 40px minmax(0, 1fr) auto; align-items: center; column-gap: 12px; min-height: 58px; margin-top: 8px; border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); padding: 8px 8px 8px 9px; background: var(--surface); transition: border-color var(--duration-standard) var(--ease-standard); }
69
+ .row { position: relative; display: grid; grid-template-columns: 40px minmax(0, 1fr) auto; align-items: center; column-gap: var(--space-group); min-height: 58px; margin-top: var(--space-related); border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); padding: var(--space-related) var(--space-related) var(--space-related) var(--space-related); background: var(--surface); transition: border-color var(--duration-standard) var(--ease-standard); }
70
70
  .row.isFailed { border-color: color-mix(in oklab, var(--danger) 26%, var(--border)); }
71
71
  .fileIcon { display: grid; width: 40px; height: 40px; place-items: center; color: var(--text-secondary); }
72
- .thumb { display: block; width: 40px; height: 40px; overflow: hidden; border-radius: var(--radius-detail); corner-shape: var(--corner-shape); background: var(--surface-muted); box-shadow: inset 0 0 0 1px oklch(0% 0 0 / .06); }
72
+ .thumb { display: block; width: 40px; height: 40px; overflow: hidden; border-radius: var(--radius-detail); corner-shape: var(--corner-shape); background: var(--surface-muted); box-shadow: var(--edge-inset); }
73
73
  .thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }
74
74
  .copy { display: grid; min-width: 0; }
75
75
  .name { overflow: hidden; font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); text-overflow: ellipsis; white-space: nowrap; }
76
76
  .meta { display: flex; min-width: 0; align-items: center; margin-top: 1px; color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); font-variant-numeric: tabular-nums; white-space: nowrap; }
77
- .dot { padding-inline: 6px; }
77
+ .dot { padding-inline: var(--space-related); }
78
78
  /* The outgoing status is measured against this box when it pops out to leave. */
79
79
  .phase { position: relative; display: flex; min-width: 0; flex: 1; }
80
- .state { display: inline-flex; min-width: 0; align-items: center; gap: 5px; }
80
+ .state { display: inline-flex; min-width: 0; align-items: center; gap: var(--space-tight); }
81
81
  .uploaded { color: var(--text-secondary); }
82
82
  .uploaded .stateIcon { color: var(--success); }
83
83
  .failed { color: var(--danger); }
@@ -85,18 +85,18 @@
85
85
  .reason { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
86
86
  .percent { font-variant-numeric: tabular-nums; }
87
87
  .barFrame { display: block; overflow: hidden; }
88
- .bar { display: block; height: 4px; margin: 7px 0 2px; overflow: hidden; border-radius: var(--radius-full); corner-shape: round; background: var(--border); }
88
+ .bar { display: block; height: 4px; margin: var(--space-related) 0 var(--space-hair); overflow: hidden; border-radius: var(--radius-full); corner-shape: round; background: var(--border); }
89
89
  /* The fill slides in from the left instead of scaling, so its rounded end keeps its shape at every value. */
90
90
  .barFill { display: block; width: 100%; height: 100%; border-radius: inherit; corner-shape: inherit; background: var(--accent); }
91
91
  .actions { display: flex; align-items: center; }
92
92
  .retrySlot { display: flex; overflow: hidden; }
93
- .retryInner { display: flex; padding: 3px 6px 3px 3px; }
93
+ .retryInner { display: flex; padding: var(--space-tight) var(--space-related) var(--space-tight) var(--space-tight); }
94
94
  .retry, .remove { display: grid; place-items: center; border: 0; color: var(--text-secondary); font: inherit; cursor: pointer; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), transform var(--duration-spring) var(--ease-spring); }
95
- .retry { display: flex; height: 30px; align-items: center; gap: 5px; border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; padding: 0 12px 0 10px; background: var(--surface); color: var(--foreground); font-size: var(--text-xs); font-weight: 500; letter-spacing: var(--tracking-body); white-space: nowrap; }
95
+ .retry { display: flex; height: 30px; align-items: center; gap: var(--space-tight); border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; padding: 0 var(--space-group) 0 var(--space-group); background: var(--surface); color: var(--foreground); font-size: var(--text-xs); font-weight: 500; letter-spacing: var(--tracking-body); white-space: nowrap; }
96
96
  /* On narrow lists Retry keeps only its icon, so the failure reason keeps the room to be read in full. */
97
97
  @container dropzone-list (max-width: 440px) { .retry { width: 30px; justify-content: center; padding: 0; } .retryLabel { display: none; } }
98
98
  /* On the narrowest lists the type icon steps aside for the name and status. */
99
- @container dropzone-list (max-width: 300px) { .row { grid-template-columns: minmax(0, 1fr) auto; padding-left: 14px; } .fileIcon, .thumb { display: none; } }
99
+ @container dropzone-list (max-width: 300px) { .row { grid-template-columns: minmax(0, 1fr) auto; padding-left: var(--space-inset); } .fileIcon, .thumb { display: none; } }
100
100
  .remove { width: 32px; height: 32px; border-radius: var(--radius-full); corner-shape: round; background: transparent; }
101
101
  @media (hover: hover) and (pointer: fine) {
102
102
  .retry:hover { border-color: var(--border-strong); background: var(--surface-muted); }
@@ -1,38 +1,38 @@
1
1
  .root { display: grid; width: 100%; min-width: 0; color: var(--foreground); }
2
2
  .input { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; clip-path: inset(50%); }
3
3
  /* A full-width drop target: press feedback is color only, never a scale. */
4
- .dropzone { display: flex; min-height: 92px; align-items: center; gap: 12px; padding: 16px; border: 1px dashed var(--border-strong); border-radius: var(--radius-container); corner-shape: var(--corner-shape); background: var(--surface); cursor: pointer; transition: border-color var(--duration-standard) var(--ease-standard), background-color var(--duration-standard) var(--ease-standard); }
4
+ .dropzone { display: flex; min-height: 92px; align-items: center; gap: var(--space-group); padding: var(--space-inset); border: 1px dashed var(--border-strong); border-radius: var(--radius-container); corner-shape: var(--corner-shape); background: var(--surface); cursor: pointer; transition: border-color var(--duration-standard) var(--ease-standard), background-color var(--duration-standard) var(--ease-standard); }
5
5
  .dropzone.dragging, .dropzone:not(.disabled):active { border-color: var(--accent); background: var(--accent-subtle); }
6
6
  .dropzone.disabled { cursor: not-allowed; opacity: .52; }
7
7
  .uploadIcon, .fileIcon { display: grid; flex: 0 0 auto; place-items: center; color: var(--accent-strong); }
8
8
  /* The cloud lifts toward the file while it hovers over the drop target. */
9
9
  .uploadIcon { width: 24px; height: 24px; transition: transform var(--duration-spring) var(--ease-spring); }
10
10
  .dragging .uploadIcon { transform: translateY(-3px); }
11
- .copy { display: grid; min-width: 0; gap: 3px; flex: 1; }
11
+ .copy { display: grid; min-width: 0; gap: var(--space-tight); flex: 1; }
12
12
  .copy strong { font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); }
13
13
  .label { position: relative; display: block; min-width: 0; }
14
14
  .labelText { display: block; }
15
15
  .copy > span { color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); overflow-wrap: anywhere; }
16
16
  .browse { flex: 0 0 auto; color: var(--accent-strong); font-size: var(--text-xs); font-weight: 500; }
17
17
  .statusFrame { overflow: hidden; }
18
- .status { position: relative; min-height: 16px; margin: 0; padding-top: 8px; color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); }
18
+ .status { position: relative; min-height: 16px; margin: 0; padding-top: var(--space-related); color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); }
19
19
  .statusText { display: block; }
20
20
  .fileList { display: grid; margin: 0; padding: 0; list-style: none; }
21
21
  /* The gap lives inside the clipped row, so a collapsing row reaches zero height instead of stopping at its padding. */
22
22
  .fileRow { overflow: hidden; }
23
- .fileItem { display: flex; margin-top: 6px; min-width: 0; align-items: center; gap: 12px; padding: 10px 10px 10px 12px; border: 1px solid var(--border-subtle); border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: var(--surface-muted); }
23
+ .fileItem { display: flex; margin-top: var(--space-related); min-width: 0; align-items: center; gap: var(--space-group); padding: var(--space-group) var(--space-group) var(--space-group) var(--space-group); border: 1px solid var(--border-subtle); border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: var(--surface-muted); }
24
24
  .fileIcon { width: 24px; height: 24px; color: var(--text-secondary); }
25
- .fileCopy { display: grid; min-width: 0; flex: 1; gap: 2px; }
25
+ .fileCopy { display: grid; min-width: 0; flex: 1; gap: var(--space-hair); }
26
26
  .fileCopy strong { overflow: hidden; font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); text-overflow: ellipsis; white-space: nowrap; }
27
27
  .fileCopy > span { color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); }
28
28
  .meta { display: flex; min-width: 0; white-space: nowrap; font-variant-numeric: tabular-nums; }
29
- .dot { padding-inline: 5px; }
29
+ .dot { padding-inline: var(--space-tight); }
30
30
  .phase { position: relative; display: inline-block; }
31
31
  .phaseText { display: block; }
32
32
  .percent { display: inline-block; min-width: 4ch; }
33
33
  .fileCopy .error { color: var(--danger); }
34
34
  .barFrame { display: block; }
35
- .bar { display: block; height: 3px; margin: 5px 0 1px; overflow: hidden; border-radius: var(--radius-full); corner-shape: round; background: var(--border); }
35
+ .bar { display: block; height: 3px; margin: var(--space-tight) 0 1px; overflow: hidden; border-radius: var(--radius-full); corner-shape: round; background: var(--border); }
36
36
  .barFill { display: block; width: 100%; height: 100%; border-radius: inherit; corner-shape: inherit; background: var(--accent); }
37
37
  .statusSlot { position: relative; display: grid; width: 26px; height: 26px; flex: 0 0 auto; place-items: center; }
38
38
  .retrySlot { display: grid; place-items: center; }
@@ -24,7 +24,7 @@
24
24
  /* Add filter: the trigger is transparent and the surface behind it is the button's rounded rectangle.
25
25
  Opening springs that same surface to the panel size, so the button itself becomes the menu. */
26
26
  .menu { position: relative; display: inline-flex; flex: none; isolation: isolate; }
27
- .menu:not([data-state="closed"]) { z-index: 30; }
27
+ .menu:not([data-state="closed"]) { z-index: var(--layer-dropdown); }
28
28
  .trigger { position: relative; z-index: 1; display: inline-flex; min-height: var(--control-height-sm); align-items: center; padding: 0 var(--space-group); border: 1px solid transparent; border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); letter-spacing: var(--tracking-body); white-space: nowrap; cursor: pointer; -webkit-tap-highlight-color: transparent; }
29
29
  .menu[data-state="open"] .trigger { pointer-events: none; }
30
30
  .face { display: inline-flex; align-items: center; gap: var(--space-related); }
@@ -38,24 +38,24 @@
38
38
  @media (hover: hover) and (pointer: fine) {
39
39
  .menu[data-state="closed"] .trigger:hover + .surface { background: var(--surface-muted); box-shadow: var(--shadow-resting); }
40
40
  }
41
- .panel { position: absolute; top: 0; left: 0; width: min(var(--filter-menu-width, 16rem), calc(100vw - var(--space-block))); padding: 6px 6px 6px; outline: none; }
41
+ .panel { position: absolute; top: 0; left: 0; width: min(var(--filter-menu-width, 16rem), calc(100vw - var(--space-block))); padding: var(--space-related) var(--space-related) var(--space-related); outline: none; }
42
42
  .menu[data-y="up"] .panel { top: auto; bottom: 0; }
43
43
  .menu[data-state="closed"] .panel { visibility: hidden; }
44
- .head { display: flex; height: 32px; align-items: center; padding: 0 var(--space-related) 0 10px; }
45
- .menu[data-y="up"] .head { margin-top: 4px; }
46
- .back { display: grid; width: 28px; height: 28px; flex: none; place-items: center; margin-left: -6px; padding: 0; border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; color: var(--text-secondary); cursor: pointer; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard); }
44
+ .head { display: flex; height: 32px; align-items: center; padding: 0 var(--space-related) 0 var(--space-group); }
45
+ .menu[data-y="up"] .head { margin-top: var(--space-tight); }
46
+ .back { display: grid; width: 28px; height: 28px; flex: none; place-items: center; margin-left: calc(var(--space-related) * -1); padding: 0; border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; color: var(--text-secondary); cursor: pointer; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard); }
47
47
  .back:active { background: var(--surface-muted); color: var(--foreground); }
48
48
  .title { position: relative; display: flex; min-width: 0; color: var(--foreground); font-size: var(--text-sm); font-weight: 500; letter-spacing: var(--tracking-body); white-space: nowrap; }
49
49
  .titleText { display: block; }
50
- .stage { position: relative; margin-top: 2px; }
50
+ .stage { position: relative; margin-top: var(--space-hair); }
51
51
  .step { width: 100%; }
52
52
  .list { position: relative; display: grid; max-height: min(20rem, 55vh); overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; }
53
- .highlight { position: absolute; top: 0; right: 0; left: 0; border-radius: calc(var(--radius-container) - 6px); corner-shape: var(--corner-shape); background: color-mix(in oklab, var(--foreground) 6%, transparent); opacity: 0; pointer-events: none; }
53
+ .highlight { position: absolute; top: 0; right: 0; left: 0; border-radius: calc(var(--radius-container) - var(--space-related)); corner-shape: var(--corner-shape); background: color-mix(in oklab, var(--foreground) 6%, transparent); opacity: 0; pointer-events: none; }
54
54
  .item:focus-visible { outline: none; }
55
- .item { position: relative; display: flex; min-width: 0; min-height: var(--control-height-sm); align-items: center; gap: 10px; padding: 0 10px; border: 0; border-radius: calc(var(--radius-container) - 6px); corner-shape: var(--corner-shape); background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-sm); letter-spacing: var(--tracking-body); text-align: left; cursor: pointer; outline: none; -webkit-tap-highlight-color: transparent; }
55
+ .item { position: relative; display: flex; min-width: 0; min-height: var(--control-height-sm); align-items: center; gap: var(--space-group); padding: 0 var(--space-group); border: 0; border-radius: calc(var(--radius-container) - var(--space-related)); corner-shape: var(--corner-shape); background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-sm); letter-spacing: var(--tracking-body); text-align: left; cursor: pointer; outline: none; -webkit-tap-highlight-color: transparent; }
56
56
  .itemIcon { display: inline-flex; min-width: 16px; flex: none; align-items: center; justify-content: center; color: var(--text-secondary); }
57
57
  .itemLabel { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
58
- .itemMeta { display: inline-flex; min-width: 0; flex: none; align-items: center; gap: 6px; max-width: 50%; color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
58
+ .itemMeta { display: inline-flex; min-width: 0; flex: none; align-items: center; gap: var(--space-related); max-width: 50%; color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
59
59
  .metaValue { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
60
60
  .hint { min-width: 2ch; text-align: right; }
61
61
  .check { display: inline-flex; width: 15px; color: var(--foreground); opacity: 0; }