@teamofsilicons/silicon-ui 0.1.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 +21 -2
  2. package/check.mjs +245 -0
  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 +85 -0
  111. package/package.json +12 -2
  112. package/styles.css +44 -4
@@ -1,19 +1,31 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { ChevronRight as NavArrowRight } from "lucide-react";
3
+ import { Ellipsis, ChevronRight as NavArrowRight } from "lucide-react";
4
4
  import { AnimatePresence, motion, useReducedMotion } from "motion/react";
5
+ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuRoot, DropdownMenuTrigger } from "../dropdown-menu/dropdown-menu.js";
5
6
  import { SiliconLink } from "../../lib/silicon-provider.js";
6
7
  import { motionTokens } from "../../lib/motion-tokens.js";
7
8
  import styles from "./breadcrumb.module.css";
8
- /** Crumbs present on first render stay still; crumbs added later slide in from the path before them. */
9
- export function Breadcrumb({ items, ariaLabel = "Breadcrumb" }) {
9
+ /** Crumbs present on first render stay still; crumbs added later slide in from the path before them.
10
+ * With maxItems, the levels a long path hides move into a "…" menu, where they stay links. */
11
+ export function Breadcrumb({ items, ariaLabel = "Breadcrumb", maxItems }) {
10
12
  const reduced = useReducedMotion() ?? false;
11
13
  const still = { duration: 0 };
12
14
  const path = items.map(item => item.label).join("/");
13
- return _jsx("nav", { "aria-label": ariaLabel, children: _jsx("ol", { className: styles.list, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: items.map((item, index) => {
14
- const current = index === items.length - 1;
15
- return _jsxs(motion.li, { layout: reduced ? false : "position", layoutDependency: path, initial: reduced ? false : { opacity: 0, x: -8, filter: `blur(${motionTokens.blur.subtle}px)` }, animate: { opacity: 1, x: 0, filter: "blur(0px)" }, exit: reduced ? { opacity: 0, transition: still } : { opacity: 0, x: -4, filter: `blur(${motionTokens.blur.subtle}px)`, transition: { duration: motionTokens.duration.instant, ease: [...motionTokens.ease.standard] } }, transition: reduced ? still : { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter], layout: motionTokens.spring.smooth }, children: [index > 0 && _jsx(NavArrowRight, { width: 14, height: 14, "aria-hidden": "true" }), !current && item.href ? _jsx(SiliconLink, { href: item.href, "data-label": item.label, onClick: item.onClick, children: item.label })
16
- : !current && item.onClick ? _jsx("button", { type: "button", "data-label": item.label, onClick: item.onClick, children: item.label })
17
- : _jsx("span", { "aria-current": current ? "page" : undefined, "data-label": item.label, children: item.label })] }, `${item.label}-${index}`);
18
- }) }) }) });
15
+ // The first crumb and the current page always show. Crumbs keep their index in `items` as their key, so the ones
16
+ // that stay visible never remount when the path collapses or grows.
17
+ const shown = Math.max(2, Math.floor(maxItems ?? Infinity));
18
+ const hidden = items.length > shown ? items.slice(1, items.length - shown + 1) : [];
19
+ const crumb = (key, first, children) => _jsxs(motion.li, { layout: reduced ? false : "position", layoutDependency: path, initial: reduced ? false : { opacity: 0, x: -8, filter: `blur(${motionTokens.blur.subtle}px)` }, animate: { opacity: 1, x: 0, filter: "blur(0px)" }, exit: reduced ? { opacity: 0, transition: still } : { opacity: 0, x: -4, filter: `blur(${motionTokens.blur.subtle}px)`, transition: { duration: motionTokens.duration.instant, ease: [...motionTokens.ease.standard] } }, transition: reduced ? still : { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter], layout: motionTokens.spring.smooth }, children: [!first && _jsx(NavArrowRight, { width: 14, height: 14, "aria-hidden": "true" }), children] }, key);
20
+ const crumbs = items.map((item, index) => {
21
+ const current = index === items.length - 1;
22
+ return crumb(`${item.label}-${index}`, index === 0, !current && item.href ? _jsx(SiliconLink, { href: item.href, "data-label": item.label, onClick: item.onClick, children: item.label })
23
+ : !current && item.onClick ? _jsx("button", { type: "button", "data-label": item.label, onClick: item.onClick, children: item.label })
24
+ : _jsx("span", { "aria-current": current ? "page" : undefined, "data-label": item.label, children: item.label }));
25
+ });
26
+ if (hidden.length)
27
+ crumbs.splice(1, hidden.length, crumb("more", false, _jsxs(DropdownMenuRoot, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx("button", { type: "button", className: styles.more, "aria-label": `Show ${hidden.length} more ${hidden.length === 1 ? "level" : "levels"}`, children: _jsx(Ellipsis, { width: 16, height: 16, "aria-hidden": "true" }) }) }), _jsx(DropdownMenuContent, { align: "start", children: hidden.map((item, index) => item.href
28
+ ? _jsx(DropdownMenuItem, { asChild: true, onClick: item.onClick, children: _jsx(SiliconLink, { href: item.href, children: item.label }) }, `${item.label}-${index + 1}`)
29
+ : _jsx(DropdownMenuItem, { disabled: !item.onClick, onClick: item.onClick, children: item.label }, `${item.label}-${index + 1}`)) })] })));
30
+ return _jsx("nav", { "aria-label": ariaLabel, children: _jsx("ol", { className: styles.list, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: crumbs }) }) });
19
31
  }
@@ -1,12 +1,16 @@
1
1
  /* The gap between crumbs matches the gap inside one, so every chevron sits centered between its two labels. */
2
- .list { position: relative; display: flex; flex-wrap: wrap; align-items: center; gap: 2px 6px; margin: 0; padding: 0; list-style: none; }
3
- .list li { display: inline-flex; align-items: center; gap: 6px; color: var(--text-muted); font-size: var(--text-sm); line-height: var(--leading-body); white-space: nowrap; }
2
+ .list { position: relative; display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-hair) var(--space-related); margin: 0; padding: 0; list-style: none; }
3
+ .list li { display: inline-flex; align-items: center; gap: var(--space-related); color: var(--text-muted); font-size: var(--text-sm); line-height: var(--leading-body); white-space: nowrap; }
4
4
  .list li > svg { width: 16px; height: 16px; flex: 0 0 auto; color: var(--border-strong); stroke-width: 1.75; }
5
5
  /* Each crumb reserves the width of its medium weight label, so becoming the current page never shifts the path. */
6
- .list a, .list li > span, .list li > button { display: inline-flex; flex-direction: column; border-radius: var(--radius-detail); padding: 4px 2px; }
6
+ .list a, .list li > span, .list li > button { display: inline-flex; flex-direction: column; border-radius: var(--radius-detail); padding: var(--space-tight) var(--space-hair); }
7
7
  .list a::after, .list li > span::after, .list li > button::after { height: 0; overflow: hidden; content: attr(data-label); font-weight: 500; visibility: hidden; user-select: none; pointer-events: none; }
8
8
  .list li > button { border: 0; margin: 0; background: none; font: inherit; line-height: inherit; text-align: start; cursor: pointer; }
9
9
  .list a, .list li > button { color: var(--text-secondary); text-decoration: underline 1px; text-decoration-color: transparent; text-underline-offset: 4px; transition: color var(--duration-fast) var(--ease-standard), text-decoration-color var(--duration-fast) var(--ease-standard); }
10
10
  .list [aria-current="page"] { color: var(--accent-strong); font-weight: 500; transition: color var(--duration-fast) var(--ease-standard); }
11
+ /* The "…" of a collapsed path: a 24px square target, accented while its menu of hidden levels is open. */
12
+ .list .more { padding: var(--space-tight); }
13
+ .list .more svg { width: 16px; height: 16px; stroke-width: 1.75; }
14
+ .list .more[data-state="open"] { color: var(--accent-strong); }
11
15
  @media (hover: hover) and (pointer: fine) { .list a:hover, .list li > button:hover { color: var(--accent-strong); text-decoration-color: color-mix(in oklab, currentColor 45%, transparent); } }
12
16
  @media (prefers-reduced-motion: reduce) { .list a, .list li > button, .list [aria-current="page"] { transition: none; } }
@@ -19,8 +19,8 @@
19
19
  /* Events: a hairline through the plot, a dot in the second chart series on top, and a label when there is room for it. */
20
20
  .eventRule { stroke: var(--border-strong); stroke-width: 1; shape-rendering: crispEdges; transition: stroke var(--duration-fast) var(--ease-standard); }
21
21
  .eventRule[data-active] { stroke: var(--series-2); }
22
- .event { position: absolute; top: 6px; display: flex; box-sizing: border-box; min-height: 24px; align-items: center; gap: 6px; padding: 0 6px 0 0; margin-left: -12px; border: 0; background: transparent; color: var(--text-secondary); cursor: default; font: inherit; font-size: var(--text-xs); line-height: 1; white-space: nowrap; -webkit-tap-highlight-color: transparent; }
23
- .event[data-flip] { flex-direction: row-reverse; padding: 0 0 0 6px; margin-left: 0; translate: calc(-100% + 12px) 0; }
22
+ .event { position: absolute; top: var(--space-related); display: flex; box-sizing: border-box; min-height: 24px; align-items: center; gap: var(--space-related); padding: 0 var(--space-related) 0 0; margin-left: calc(var(--space-group) * -1); border: 0; background: transparent; color: var(--text-secondary); cursor: default; font: inherit; font-size: var(--text-xs); line-height: 1; white-space: nowrap; -webkit-tap-highlight-color: transparent; }
23
+ .event[data-flip] { flex-direction: row-reverse; padding: 0 0 0 var(--space-related); margin-left: 0; translate: calc(-100% + 12px) 0; }
24
24
  .eventDot { display: grid; width: 24px; height: 24px; flex: none; place-items: center; }
25
25
  .eventDot::before { width: 7px; height: 7px; border-radius: 50%; corner-shape: round; background: var(--series-2); box-shadow: 0 0 0 2px var(--surface); content: ""; transition: transform var(--duration-fast) var(--ease-standard); }
26
26
  .event[data-active] .eventDot::before { transform: scale(1.3); }
@@ -28,23 +28,23 @@
28
28
  .event[data-labelled] .eventLabel { opacity: 1; }
29
29
  .event[data-active] .eventLabel { color: var(--foreground); }
30
30
 
31
- .tooltip { position: absolute; top: 0; left: 0; z-index: 2; display: grid; min-width: 132px; max-width: min(220px, 100%); gap: 2px; padding: 10px 12px; border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: var(--surface-raised); box-shadow: var(--shadow-floating); opacity: 0; pointer-events: none; scale: .96; transition: opacity var(--duration-fast) var(--ease-standard), scale var(--duration-fast) var(--ease-standard); }
31
+ .tooltip { position: absolute; top: 0; left: 0; z-index: 2; display: grid; min-width: 132px; max-width: min(220px, 100%); gap: var(--space-hair); padding: var(--space-group) var(--space-group); border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: var(--surface-raised); box-shadow: var(--shadow-floating); opacity: 0; pointer-events: none; scale: .96; transition: opacity var(--duration-fast) var(--ease-standard), scale var(--duration-fast) var(--ease-standard); }
32
32
  .chart[data-active] .tooltip { opacity: 1; scale: 1; }
33
- .tipTitle { margin: 0 0 2px; color: var(--text-secondary); font-size: var(--text-xs); line-height: var(--leading-body); white-space: nowrap; font-variant-numeric: tabular-nums; }
33
+ .tipTitle { margin: 0 0 var(--space-hair); color: var(--text-secondary); font-size: var(--text-xs); line-height: var(--leading-body); white-space: nowrap; font-variant-numeric: tabular-nums; }
34
34
  .tipValue { margin: 0; color: var(--foreground); font-size: var(--text-base); font-weight: 500; line-height: var(--leading-body); font-variant-numeric: tabular-nums; }
35
35
  .tipNote { margin: 0; color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); font-variant-numeric: tabular-nums; }
36
- .tipEvent { display: flex; align-items: center; gap: 6px; margin: 4px 0 0; color: var(--foreground); font-size: var(--text-xs); line-height: var(--leading-body); }
36
+ .tipEvent { display: flex; align-items: center; gap: var(--space-related); margin: var(--space-tight) 0 0; color: var(--foreground); font-size: var(--text-xs); line-height: var(--leading-body); }
37
37
  .tipEventDot { width: 7px; height: 7px; flex: none; border-radius: 50%; corner-shape: round; background: var(--series-2); }
38
38
 
39
39
  .message { position: absolute; inset: 0; display: grid; place-items: center; margin: 0; color: var(--text-muted); font-size: var(--text-sm); pointer-events: none; }
40
40
 
41
41
  .gutter { position: relative; grid-area: 1 / 2; min-width: 0; }
42
- .tick { position: absolute; left: 10px; color: var(--text-muted); font-size: var(--text-xs); line-height: 1; white-space: nowrap; font-variant-numeric: tabular-nums; translate: 0 -50%; pointer-events: none; }
42
+ .tick { position: absolute; left: var(--space-group); color: var(--text-muted); font-size: var(--text-xs); line-height: 1; white-space: nowrap; font-variant-numeric: tabular-nums; translate: 0 -50%; pointer-events: none; }
43
43
  .axis { position: relative; grid-area: 2 / 1; min-width: 0; }
44
- .axisLabel { position: absolute; top: 9px; color: var(--text-muted); font-size: var(--text-xs); line-height: 1; white-space: nowrap; font-variant-numeric: tabular-nums; }
44
+ .axisLabel { position: absolute; top: var(--space-related); color: var(--text-muted); font-size: var(--text-xs); line-height: 1; white-space: nowrap; font-variant-numeric: tabular-nums; }
45
45
 
46
46
  /* Overview: the whole history, with everything outside the window stepped back. */
47
- .strip { --strip-radius: var(--radius-detail); position: relative; grid-area: 3 / 1; min-width: 0; margin-top: 8px; border-radius: var(--strip-radius); background: color-mix(in oklab, var(--foreground) 2.5%, transparent); cursor: crosshair; touch-action: pan-y; user-select: none; -webkit-user-select: none; -webkit-tap-highlight-color: transparent; }
47
+ .strip { --strip-radius: var(--radius-detail); position: relative; grid-area: 3 / 1; min-width: 0; margin-top: var(--space-related); border-radius: var(--strip-radius); background: color-mix(in oklab, var(--foreground) 2.5%, transparent); cursor: crosshair; touch-action: pan-y; user-select: none; -webkit-user-select: none; -webkit-tap-highlight-color: transparent; }
48
48
  /* The overview drawing is clipped to the strip's corners; the handles stay outside the clip so they can overhang the ends. */
49
49
  .strip > .svg { clip-path: inset(0 round var(--strip-radius)); }
50
50
  .overviewLine { fill: none; stroke: color-mix(in oklab, var(--foreground) 28%, var(--surface)); stroke-width: 1; stroke-linejoin: round; }
@@ -57,7 +57,7 @@
57
57
  .shade[data-edge="end"] { border-radius: 0 var(--strip-radius) var(--strip-radius) 0; }
58
58
  .window { position: absolute; top: 0; bottom: 0; border-radius: var(--strip-radius); box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--series-1) 45%, transparent); cursor: grab; outline: none; transition: box-shadow var(--duration-fast) var(--ease-standard); }
59
59
  .window:active { cursor: grabbing; }
60
- .handle { position: absolute; top: 50%; display: grid; width: 28px; height: 44px; place-items: center; margin-left: -14px; translate: 0 -50%; cursor: ew-resize; outline: none; touch-action: none; }
60
+ .handle { position: absolute; top: 50%; display: grid; width: 28px; height: 44px; place-items: center; margin-left: calc(var(--space-inset) * -1); translate: 0 -50%; cursor: ew-resize; outline: none; touch-action: none; }
61
61
  .grip { width: 6px; height: 22px; border-radius: var(--radius-full); corner-shape: round; background: var(--surface-raised); box-shadow: 0 0 0 1px color-mix(in oklab, var(--foreground) 26%, transparent), var(--shadow-resting); transition: transform var(--duration-instant) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard); }
62
62
  .handle:active .grip { transform: scaleY(1.12); }
63
63
  @media (hover: hover) and (pointer: fine) { .handle:hover .grip { transform: scaleY(1.12); } }
@@ -1,9 +1,14 @@
1
1
  import type { ButtonHTMLAttributes } from "react";
2
- export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger";
3
- export type ButtonSize = "sm" | "md" | "lg";
2
+ /** `soft` is filled and borderless, for controls in a bar (experimental). */
3
+ export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger" | "soft";
4
+ /** `xs` (24px) is for dense UI such as tree rows and toolbars, usually icon-only with an aria-label. */
5
+ export type ButtonSize = "xs" | "sm" | "md" | "lg";
4
6
  export interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onDrag" | "onDragEnd" | "onDragStart" | "onAnimationStart"> {
7
+ /** The look: `primary` (default) for the main action, `secondary` bordered, `ghost` quiet, `danger` destructive, `soft` filled for bars. */
5
8
  variant?: ButtonVariant;
9
+ /** Height: `xs` 24px, `sm` 36px, `md` 44px (default), `lg` 50px. */
6
10
  size?: ButtonSize;
11
+ /** Shows a spinner in place of the label and sets aria-busy, keeping the button's width. */
7
12
  loading?: boolean;
8
13
  }
9
14
  export declare const Button: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
@@ -91,7 +91,9 @@ export const Button = forwardRef(function Button({ className, variant = "primary
91
91
  const anchorsLayer = (popup !== undefined && popup !== false && popup !== "false") || props.role === "combobox" || props["data-state"] !== undefined;
92
92
  const inert = disabled || loading || props["aria-disabled"] === true || props["aria-disabled"] === "true";
93
93
  const classes = [styles.button, styles[variant], styles[size], className].filter(Boolean).join(" ");
94
- return (_jsxs(motion.button, { ref: setRefs, tabIndex: props.tabIndex ?? 0, className: classes, "data-variant": variant, disabled: disabled, "aria-busy": loading || undefined, custom: buttonRef, variants: pressVariants, whileTap: reduceMotion || anchorsLayer || inert ? undefined : "pressed", transition: motionTokens.spring.snappy, ...props, "aria-disabled": loading || props["aria-disabled"] || undefined, onClick: loading ? event => event.preventDefault() : onClick, children: [_jsx(AnimatePresence, { initial: false, children: loading ? _jsx(motion.span, { className: styles.loader, "aria-hidden": "true", initial: reduceMotion ? { opacity: 0 } : { opacity: 0, scale: .6 }, animate: { opacity: 1, scale: 1 }, exit: reduceMotion ? fadeOut : { ...iconOut, scale: .8 }, transition: reduceMotion ? { duration: motionTokens.duration.instant } : iconEnter, children: _jsx("span", { className: styles.spinner }) }, "loader") : null }), _jsx(motion.span, { className: [styles.labelSlot, loading ? styles.loadingLabel : ""].filter(Boolean).join(" "), style: { width }, children: _jsx("span", { ref: contentRef, className: styles.labelContent, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(LabelPhase, { icon: !/\S/.test(key.replace(/<[^>]*>/g, "")), reduced: reduceMotion, children: children }, key) }) }) })] }));
94
+ // An icon with no text: square at the compact size.
95
+ const iconOnly = !/\S/.test(key.replace(/<[^>]*>/g, ""));
96
+ return (_jsxs(motion.button, { ref: setRefs, tabIndex: props.tabIndex ?? 0, className: classes, "data-variant": variant, "data-icon-only": iconOnly || undefined, disabled: disabled, "aria-busy": loading || undefined, custom: buttonRef, variants: pressVariants, whileTap: reduceMotion || anchorsLayer || inert ? undefined : "pressed", transition: motionTokens.spring.snappy, ...props, "aria-disabled": loading || props["aria-disabled"] || undefined, onClick: loading ? event => event.preventDefault() : onClick, children: [_jsx(AnimatePresence, { initial: false, children: loading ? _jsx(motion.span, { className: styles.loader, "aria-hidden": "true", initial: reduceMotion ? { opacity: 0 } : { opacity: 0, scale: .6 }, animate: { opacity: 1, scale: 1 }, exit: reduceMotion ? fadeOut : { ...iconOut, scale: .8 }, transition: reduceMotion ? { duration: motionTokens.duration.instant } : iconEnter, children: _jsx("span", { className: styles.spinner }) }, "loader") : null }), _jsx(motion.span, { className: [styles.labelSlot, loading ? styles.loadingLabel : ""].filter(Boolean).join(" "), style: { width }, children: _jsx("span", { ref: contentRef, className: styles.labelContent, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: _jsx(LabelPhase, { icon: !/\S/.test(key.replace(/<[^>]*>/g, "")), reduced: reduceMotion, children: children }, key) }) }) })] }));
95
97
  });
96
98
  Button.displayName = "Button";
97
99
  export default Button;
@@ -15,20 +15,29 @@
15
15
  .secondary { color: var(--foreground); background: var(--surface); border-color: var(--border); }
16
16
  .ghost { color: var(--text-secondary); background: transparent; border-color: transparent; }
17
17
  .danger { color: var(--danger); background: var(--surface); border-color: var(--border); }
18
+ /* Filled and borderless, for controls that live in a bar (a search trigger, a toolbar, a switcher). Place it on --background or
19
+ --surface; on a --surface-muted panel it has no fill to show. */
20
+ .soft { color: var(--foreground); background: var(--surface-muted); border-color: transparent; }
18
21
  /* A disabled outlined button keeps its shape and fades only its label, so it still reads as a button that is off, not as plain text. */
19
22
  .secondary:disabled, .danger:disabled { opacity: 1; color: var(--text-muted); background: var(--surface-muted); border-color: var(--border); }
20
23
  .danger:disabled { color: color-mix(in oklab, var(--danger) 55%, var(--surface-muted)); }
21
- .ghost:disabled { opacity: 1; color: var(--text-muted); }
24
+ .ghost:disabled, .soft:disabled { opacity: 1; color: var(--text-muted); }
22
25
  @media (hover: hover) and (pointer: fine) {
23
26
  .primary:hover:not(:disabled) { opacity: .91; box-shadow: var(--shadow-resting); }
24
27
  .secondary:hover:not(:disabled) { background: var(--surface-muted); box-shadow: var(--shadow-resting); }
25
28
  .ghost:hover:not(:disabled) { color: var(--foreground); background: var(--surface-muted); }
26
29
  .danger:hover:not(:disabled) { border-color: var(--danger); background: var(--surface-muted); }
30
+ .soft:hover:not(:disabled) { background: var(--surface-pressed); }
27
31
  }
28
32
  /* Color feedback on press, so touch and menu triggers (which never scale) still answer the finger. */
29
33
  .primary:active:not(:disabled) { opacity: .84; }
30
34
  .secondary:active:not(:disabled), .ghost:active:not(:disabled) { color: var(--foreground); background: var(--surface-muted); }
31
35
  .danger:active:not(:disabled) { border-color: var(--danger); background: var(--surface-muted); }
36
+ .soft:active:not(:disabled) { background: var(--surface-pressed); }
37
+ .xs { min-height: var(--control-height-xs); gap: var(--space-tight); padding-inline: var(--space-related); font-size: var(--text-xs); }
38
+ /* A compact icon-only button is a square, with the icon at its usual 16px. */
39
+ .xs[data-icon-only] { min-width: var(--control-height-xs); padding-inline: 0; }
40
+ .xs svg { width: 16px; height: 16px; }
32
41
  .sm { min-height: var(--control-height-sm); padding-inline: var(--space-group); font-size: var(--text-sm); }
33
42
  .md { min-height: var(--control-height-md); }
34
43
  .lg { min-height: var(--control-height-lg); padding-inline: var(--space-comfort); font-size: var(--text-sm); }
@@ -12,7 +12,7 @@
12
12
 
13
13
  /* The travelling highlight. Motion sets its box to the active segment; the visible shape sits inset so the border stays crisp. */
14
14
  .highlight { position: absolute; z-index: 0; top: 0; left: 0; pointer-events: none; }
15
- .highlight::before { position: absolute; inset: var(--bg-inset); border-radius: calc(var(--bg-radius) - var(--bg-inset) - 1px); corner-shape: var(--corner-shape); background: var(--bg-highlight); content: ""; transition: background-color var(--duration-fast) var(--ease-standard); }
15
+ .highlight::before { position: absolute; inset: var(--bg-inset); border-radius: calc(var(--bg-radius) - var(--bg-inset) - var(--space-hair)); corner-shape: var(--corner-shape); background: var(--bg-highlight); content: ""; transition: background-color var(--duration-fast) var(--ease-standard); }
16
16
  .highlight[data-pressed]::before { background: var(--bg-highlight-pressed); transition-duration: var(--duration-instant); }
17
17
 
18
18
  .segment { position: relative; z-index: 1; display: inline-flex; flex: none; min-width: calc(var(--bg-h) - 2px); height: calc(var(--bg-h) - 2px); align-items: center; justify-content: center; margin: 0; padding: 0 var(--bg-pad-x); border: 0; border-radius: 0; corner-shape: round; background: transparent; color: inherit; font: inherit; font-size: var(--text-sm); font-weight: 500; letter-spacing: var(--tracking-body); line-height: 1; white-space: nowrap; text-decoration: none; cursor: pointer; user-select: none; -webkit-tap-highlight-color: transparent; }
@@ -29,8 +29,8 @@
29
29
  .compact .label[data-collapsible] { display: none; }
30
30
 
31
31
  /* Hairline dividers sit between segments and fade where the highlight arrives, so it never sits beside a line. */
32
- .segment + .segment::before { position: absolute; top: var(--bg-divider-inset); bottom: var(--bg-divider-inset); left: -.5px; width: 1px; background: var(--bg-divider); content: ""; pointer-events: none; transition: opacity var(--duration-fast) var(--ease-standard); }
33
- .vertical .segment + .segment::before { top: -.5px; right: var(--bg-divider-inset); bottom: auto; left: var(--bg-divider-inset); width: auto; height: 1px; }
32
+ .segment + .segment::before { position: absolute; top: var(--bg-divider-inset); bottom: var(--bg-divider-inset); left: calc(var(--space-hair) * -1); width: 1px; background: var(--bg-divider); content: ""; pointer-events: none; transition: opacity var(--duration-fast) var(--ease-standard); }
33
+ .vertical .segment + .segment::before { top: calc(var(--space-hair) * -1); right: var(--bg-divider-inset); bottom: auto; left: var(--bg-divider-inset); width: auto; height: 1px; }
34
34
  .segment[data-quiet]::before { opacity: 0; }
35
35
 
36
36
  /* The press answers inside the segment: content dips and springs back, the group and its neighbours stay still. */
@@ -51,7 +51,7 @@
51
51
  .phase { display: inline-flex; align-items: center; gap: var(--space-related); white-space: nowrap; }
52
52
  .sizer { visibility: hidden; pointer-events: none; }
53
53
  .iconBox { display: inline-block; width: 16px; height: 16px; flex: none; }
54
- .sm .phase { gap: 6px; }
54
+ .sm .phase { gap: var(--space-related); }
55
55
  .icon { display: inline-flex; flex: none; }
56
56
  .icon svg { width: 16px; height: 16px; stroke-width: 1.75; }
57
57
  .value { display: inline-flex; align-items: center; font-variant-numeric: tabular-nums; }
@@ -60,7 +60,7 @@
60
60
 
61
61
  /* The menu grows from the group's edge: offset toward it, scale from the Radix origin, and leave faster than it arrives.
62
62
  Transitions instead of keyframes, so a close that interrupts the open reverses from where the menu is. */
63
- .menu { --menu-x: 0px; --menu-y: -5px; z-index: 60; min-width: min(13rem, 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); corner-shape: var(--corner-shape); background: var(--surface-raised); color: var(--foreground); 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); }
63
+ .menu { --menu-x: 0px; --menu-y: -5px; z-index: var(--layer-dropdown); min-width: min(13rem, 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); corner-shape: var(--corner-shape); background: var(--surface-raised); color: var(--foreground); 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); }
64
64
  .menu[data-side="top"] { --menu-y: 5px; }
65
65
  .menu[data-side="left"] { --menu-x: 5px; --menu-y: 0px; }
66
66
  .menu[data-side="right"] { --menu-x: -5px; --menu-y: 0px; }
@@ -70,7 +70,7 @@
70
70
  }
71
71
  /* Radix unmounts when the exit animation ends, so a no-op keyframe times the exit while the transition does the visual work. */
72
72
  .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; }
73
- .item { display: flex; min-height: 36px; align-items: center; gap: 10px; padding: 0 11px; border-radius: calc(var(--radius-container) - 6px); corner-shape: var(--corner-shape); color: inherit; font-size: var(--text-sm); text-decoration: none; cursor: pointer; outline: none; transition: 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); }
73
+ .item { 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)); corner-shape: var(--corner-shape); color: inherit; font-size: var(--text-sm); text-decoration: none; cursor: pointer; outline: none; transition: 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); }
74
74
  .item[data-highlighted] { background: var(--surface-muted); }
75
75
  .item[data-disabled] { cursor: default; opacity: .45; }
76
76
  .itemIcon { display: inline-flex; flex: none; color: var(--text-secondary); }
@@ -1,6 +1,6 @@
1
1
  .calendar { width: min(100%, 328px); min-width: 0; color: var(--foreground); }
2
2
  /* The header is its own size container: below 260px the title takes a full row and the controls sit under it, decided by width alone so a long month name never reflows it. */
3
- .header { container-type: inline-size; display: grid; min-height: 34px; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 4px 12px; margin-bottom: 14px; }
3
+ .header { container-type: inline-size; display: grid; min-height: 34px; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: var(--space-tight) var(--space-group); margin-bottom: var(--space-inset); }
4
4
  @container (max-width: 259px) { .heading, .navigation { grid-column: 1 / -1; } .navigation { justify-self: end; } }
5
5
  .heading { min-width: 0; margin: 0; font-family: var(--font-body); font-size: var(--text-base); font-weight: 500; line-height: var(--leading-body); letter-spacing: var(--tracking-body); }
6
6
  /* The global heading rule forces a tight line-height on h2, so the title row sets its own to center with the controls. */
@@ -9,22 +9,22 @@
9
9
  .title > .titleRow { grid-area: 1 / 1; justify-self: start; }
10
10
  .titleRow { display: inline-block; white-space: nowrap; font-variant-numeric: tabular-nums; }
11
11
  .srOnly { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
12
- .navigation { display: inline-flex; flex: 0 0 auto; align-items: center; gap: 2px; margin-right: -6px; }
12
+ .navigation { display: inline-flex; flex: 0 0 auto; align-items: center; gap: var(--space-hair); margin-right: calc(var(--space-related) * -1); }
13
13
  /* Quiet header controls: a fill on hover, a short press, and a spring release. */
14
14
  .navButton, .todayButton { display: grid; height: 32px; place-items: center; border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; color: var(--text-secondary); font: inherit; cursor: pointer; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard), scale var(--duration-spring) var(--ease-spring); }
15
15
  .navButton { width: 32px; }
16
- .todayButton { padding: 0 11px; font-size: var(--text-sm); font-weight: 500; line-height: 1; }
16
+ .todayButton { padding: 0 var(--space-group); font-size: var(--text-sm); font-weight: 500; line-height: 1; }
17
17
  @media (hover: hover) and (pointer: fine) { .navButton:hover:not([aria-disabled]), .todayButton:hover:not([aria-disabled]) { background: var(--surface-muted); color: var(--foreground); } }
18
18
  .navButton:active:not([aria-disabled]), .todayButton:active:not([aria-disabled]) { scale: .96; transition-duration: var(--duration-fast), var(--duration-fast), var(--duration-fast), 100ms; transition-timing-function: var(--ease-standard); }
19
19
  .navButton[aria-disabled], .todayButton[aria-disabled] { cursor: default; opacity: .36; }
20
- .weekdays, .week { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); column-gap: 4px; }
21
- .weekdays { margin-bottom: 6px; color: var(--text-muted); font-size: var(--text-xs); font-weight: 500; line-height: 24px; text-align: center; }
20
+ .weekdays, .week { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); column-gap: var(--space-tight); }
21
+ .weekdays { margin-bottom: var(--space-related); color: var(--text-muted); font-size: var(--text-xs); font-weight: 500; line-height: 24px; text-align: center; }
22
22
  .weekdays span { min-width: 0; overflow: hidden; }
23
23
  /* Months sit on one strip inside a clipped window. Every pane shares one grid cell and has six weeks, so the height never changes. */
24
24
  .monthViewport { position: relative; display: grid; overflow: hidden; contain: paint; }
25
25
  .monthBody { position: relative; grid-area: 1 / 1; min-width: 0; will-change: transform; }
26
26
  .monthBody[inert] { pointer-events: none; }
27
- .grid { display: grid; row-gap: 4px; }
27
+ .grid { display: grid; row-gap: var(--space-tight); }
28
28
  .placeholderDay { aspect-ratio: 1; }
29
29
  .day { position: relative; display: grid; width: 100%; aspect-ratio: 1; min-width: 0; place-items: center; border: 0; border-radius: 50%; corner-shape: round; padding: 0; background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-sm); font-weight: 400; line-height: 1; font-variant-numeric: tabular-nums; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: color var(--duration-fast) var(--ease-standard) var(--number-delay, 0ms); }
30
30
  /* The inner circle carries hover and press; the grid cell itself never moves, so the hit area stays put. */
@@ -39,7 +39,7 @@
39
39
  .day.outside { color: var(--text-muted); }
40
40
  .day.outside:not(.selected) .dayNumber { opacity: .8; }
41
41
  /* Today is a small dot under the number. It takes the number's color, so it flips with it when the disc arrives. */
42
- .day.today .dayNumber::after { content: ""; position: absolute; top: calc(100% + 4px); left: 50%; width: 4px; height: 4px; margin-left: -2px; border-radius: 50%; corner-shape: round; background: currentColor; }
42
+ .day.today .dayNumber::after { content: ""; position: absolute; top: calc(100% + var(--space-tight)); left: 50%; width: 4px; height: 4px; margin-left: calc(var(--space-hair) * -1); border-radius: 50%; corner-shape: round; background: currentColor; }
43
43
  .day.selected, .day.today { font-weight: 500; }
44
44
  .day.selected { color: var(--accent-foreground); }
45
45
  .day:disabled { cursor: default; color: var(--text-muted); }
@@ -29,9 +29,9 @@
29
29
  .srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
30
30
  .action { position: relative; z-index: 1; display: flex; flex: none; flex-wrap: wrap; gap: var(--space-group); }
31
31
 
32
- .overlay { position: fixed; inset: 0; z-index: 50; background: oklch(10% 0 0 / .46); backdrop-filter: blur(7px); }
32
+ .overlay { position: fixed; inset: 0; z-index: var(--layer-overlay); background: var(--scrim); backdrop-filter: blur(7px); }
33
33
  /* Centered with auto margins, so transform stays free for the morph. */
34
- .panel { position: fixed; inset: 0; z-index: 51; width: min(calc(100vw - var(--space-block)), 30rem); height: fit-content; max-height: calc(100dvh - var(--space-block)); margin: auto; overflow: hidden auto; overscroll-behavior: contain; border: 1px solid var(--border); border-radius: var(--radius-overlay); corner-shape: var(--corner-shape); background: var(--surface-raised); color: var(--foreground); box-shadow: var(--shadow-floating); transition: background-color var(--duration-standard) var(--ease-standard), box-shadow var(--duration-standard) var(--ease-standard); }
34
+ .panel { position: fixed; inset: 0; z-index: var(--layer-dialog); width: min(calc(100vw - var(--space-block)), 30rem); height: fit-content; max-height: calc(100dvh - var(--space-block)); margin: auto; overflow: hidden auto; overscroll-behavior: contain; border: 1px solid var(--border); border-radius: var(--radius-overlay); corner-shape: var(--corner-shape); background: var(--surface-raised); color: var(--foreground); box-shadow: var(--shadow-floating); transition: background-color var(--duration-standard) var(--ease-standard), box-shadow var(--duration-standard) var(--ease-standard); }
35
35
  .panel:focus { outline: none; }
36
36
  /* Closing: the panel takes the card's box and look, so it lands exactly where the card sits and hands back without a seam. */
37
37
  .panel[data-returning] { inset: auto; top: var(--landing-top); left: var(--landing-left); width: var(--landing-width); height: var(--landing-height); max-height: none; margin: 0; overflow: hidden; border-color: var(--border); background: var(--surface); box-shadow: none; pointer-events: none; }
@@ -12,10 +12,9 @@
12
12
  .card::before { position: absolute; inset: 0; border-radius: inherit; corner-shape: inherit; box-shadow: var(--shadow-floating); content: ""; opacity: 0; pointer-events: none; transition: opacity var(--duration-fast) var(--ease-standard); }
13
13
  .card[data-dragging]::before, .card[data-flying]::before { opacity: 1; }
14
14
  .face { position: relative; height: 100%; overflow: hidden; border: 1px solid var(--border); border-radius: inherit; corner-shape: inherit; background: var(--surface); color: var(--foreground); }
15
- .shade { position: absolute; inset: 0; background: oklch(0% 0 0 / .035); pointer-events: none; }
16
- :global(:root[data-theme="dark"]) .shade { background: oklch(0% 0 0 / .22); }
15
+ .shade { position: absolute; inset: 0; background: var(--shade); pointer-events: none; }
17
16
 
18
- .stamp { position: absolute; top: var(--space-inset); display: inline-flex; height: 2rem; align-items: center; gap: 6px; padding: 0 var(--space-group) 0 10px; border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface-raised); color: var(--foreground); font-size: var(--text-sm); font-weight: 500; white-space: nowrap; pointer-events: none; }
17
+ .stamp { position: absolute; top: var(--space-inset); display: inline-flex; height: 2rem; align-items: center; gap: var(--space-related); padding: 0 var(--space-group) 0 var(--space-group); border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface-raised); color: var(--foreground); font-size: var(--text-sm); font-weight: 500; white-space: nowrap; pointer-events: none; }
19
18
  .stamp[data-side="right"] { left: var(--space-inset); border-color: color-mix(in oklab, var(--success) 32%, var(--border)); background: color-mix(in oklab, var(--success) 9%, var(--surface-raised)); }
20
19
  .stamp[data-side="right"] svg { color: var(--success); }
21
20
  .stamp[data-side="left"] { right: var(--space-inset); }
@@ -13,7 +13,7 @@
13
13
  /* Controls line up with the edges of the centered slide. */
14
14
  .controls { display: flex; min-width: 0; align-items: center; gap: var(--space-related); padding-inline: calc((100cqw - var(--carousel-slide)) / 2); }
15
15
  .tabs { display: flex; min-width: 0; align-items: center; }
16
- .tab { display: grid; height: var(--control-height-sm); flex: none; place-items: center; border: 0; border-radius: var(--radius-full); corner-shape: round; padding: 0 5px; background: transparent; cursor: pointer; -webkit-tap-highlight-color: transparent; }
16
+ .tab { display: grid; height: var(--control-height-sm); flex: none; place-items: center; border: 0; border-radius: var(--radius-full); corner-shape: round; padding: 0 var(--space-tight); background: transparent; cursor: pointer; -webkit-tap-highlight-color: transparent; }
17
17
  /* Resting dots use the strong border color; the fill fades in as the pill forms, and the drain covers it from the right while rotation runs. */
18
18
  .dot { position: relative; display: block; width: 6px; height: 6px; overflow: hidden; border-radius: var(--radius-full); corner-shape: round; background: var(--border-strong); transition: background-color var(--duration-fast) var(--ease-standard); }
19
19
  .dotFill, .dotDrain { position: absolute; inset: 0; }
@@ -33,7 +33,7 @@
33
33
  @container (max-width: 340px) {
34
34
  .controls { gap: var(--space-tight); padding-inline: max(0px, calc((100cqw - var(--carousel-slide)) / 2 - var(--space-related))); }
35
35
  .arrows { gap: var(--space-tight); }
36
- .tab { padding: 0 4px; }
36
+ .tab { padding: 0 var(--space-tight); }
37
37
  }
38
38
  .visuallyHidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
39
39
 
@@ -14,32 +14,32 @@
14
14
  .scroller { min-height: 0; flex: 1; overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-gutter: stable; }
15
15
  /* The content springs up from where it was when the thread grows; the clip keeps that travel out of the scroll range. */
16
16
  .track { overflow: clip; }
17
- .content { padding: 12px 16px 14px; will-change: transform; }
17
+ .content { padding: var(--space-group) var(--space-inset) var(--space-inset); will-change: transform; }
18
18
 
19
19
  .list { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; }
20
20
 
21
- .day { display: flex; align-items: center; gap: 12px; padding: 20px 4px 4px; color: var(--text-muted); font-size: var(--text-xs); font-weight: 500; }
21
+ .day { display: flex; align-items: center; gap: var(--space-group); padding: var(--space-comfort) var(--space-tight) var(--space-tight); color: var(--text-muted); font-size: var(--text-xs); font-weight: 500; }
22
22
  .day::before, .day::after { height: 1px; flex: 1; background: var(--border-subtle); content: ""; }
23
- .day:first-child { padding-top: 4px; }
23
+ .day:first-child { padding-top: var(--space-tight); }
24
24
  .day > span:empty::before { content: "\00a0"; }
25
25
 
26
- .row { position: relative; display: flex; align-items: flex-end; gap: 8px; margin-top: 2px; }
27
- .row[data-first] { margin-top: 14px; }
28
- .day + .row { margin-top: 10px; }
26
+ .row { position: relative; display: flex; align-items: flex-end; gap: var(--space-related); margin-top: var(--space-hair); }
27
+ .row[data-first] { margin-top: var(--space-inset); }
28
+ .day + .row { margin-top: var(--space-group); }
29
29
  .row[data-mine] { justify-content: flex-end; }
30
30
 
31
31
  .gutter { width: 28px; flex: none; align-self: flex-end; }
32
- .stack { display: flex; min-width: 0; max-width: min(78%, 34rem); flex-direction: column; align-items: flex-start; gap: 4px; }
32
+ .stack { display: flex; min-width: 0; max-width: min(78%, 34rem); flex-direction: column; align-items: flex-start; gap: var(--space-tight); }
33
33
  .row[data-mine] .stack { align-items: flex-end; }
34
34
 
35
- .meta { display: flex; align-items: baseline; gap: 6px; margin: 0 0 2px; padding: 0 12px; color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
35
+ .meta { display: flex; align-items: baseline; gap: var(--space-related); margin: 0 0 var(--space-hair); padding: 0 var(--space-group); color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
36
36
  .author { color: var(--text-secondary); font-weight: 500; }
37
37
 
38
- .line { position: relative; display: flex; max-width: 100%; align-items: center; gap: 6px; }
38
+ .line { position: relative; display: flex; max-width: 100%; align-items: center; gap: var(--space-related); }
39
39
  /* The author's face sits in the gutter, level with the bottom of their last bubble rather than the reactions under it. */
40
- .face { position: absolute; bottom: 0; left: -36px; display: flex; }
40
+ .face { position: absolute; bottom: 0; left: calc(var(--space-block) * -1); display: flex; }
41
41
  .row[data-mine] .line { flex-direction: row-reverse; }
42
- .parts { display: flex; min-width: 0; flex-direction: column; align-items: flex-start; gap: 4px; }
42
+ .parts { display: flex; min-width: 0; flex-direction: column; align-items: flex-start; gap: var(--space-tight); }
43
43
  .row[data-mine] .parts { align-items: flex-end; }
44
44
 
45
45
  /* Bubbles in a group share a soft edge on the author's side, so a run of messages reads as one voice. */
@@ -47,7 +47,7 @@
47
47
  --r: 18px;
48
48
  --tight: 6px;
49
49
  max-width: 100%;
50
- padding: 8px 13px 9px;
50
+ padding: var(--space-related) var(--space-group) var(--space-related);
51
51
  border-radius: var(--r);
52
52
  background: var(--surface-muted);
53
53
  color: var(--foreground);
@@ -63,7 +63,7 @@
63
63
  .row[data-mine]:not([data-last]) .bubble { border-bottom-right-radius: var(--tight); }
64
64
  .row[data-failed] .bubble { opacity: .6; }
65
65
 
66
- .gallery { display: grid; width: min(240px, 100%); gap: 3px; grid-template-columns: 1fr; }
66
+ .gallery { display: grid; width: min(240px, 100%); gap: var(--space-tight); grid-template-columns: 1fr; }
67
67
  .gallery[data-count="2"],
68
68
  .gallery[data-count="3"],
69
69
  .gallery[data-count="4"] { grid-template-columns: 1fr 1fr; }
@@ -73,10 +73,10 @@
73
73
  .gallery:not([data-count="1"]) .image { border-radius: var(--radius-detail); corner-shape: var(--corner-shape); }
74
74
  .gallery:not([data-count="1"]) { overflow: hidden; border-radius: var(--radius-control); corner-shape: var(--corner-shape); }
75
75
  .image img { display: block; width: 100%; height: 100%; object-fit: cover; }
76
- .more { position: absolute; inset: 0; display: grid; place-items: center; background: oklch(0% 0 0 / .45); color: #fff; font-size: var(--text-lg); font-weight: 500; }
76
+ .more { position: absolute; inset: 0; display: grid; place-items: center; background: var(--media-scrim); color: var(--on-media); font-size: var(--text-lg); font-weight: 500; }
77
77
 
78
78
  .file {
79
- display: flex; max-width: 100%; align-items: center; gap: 10px; padding: 9px 14px 9px 11px;
79
+ display: flex; max-width: 100%; align-items: center; gap: var(--space-group); padding: var(--space-related) var(--space-inset) var(--space-related) var(--space-group);
80
80
  border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: var(--surface); color: var(--foreground); text-decoration: none;
81
81
  transition: background-color var(--duration-fast) var(--ease-standard);
82
82
  }
@@ -103,17 +103,17 @@
103
103
  @media (hover: none) { .reactButton { opacity: .7; } }
104
104
 
105
105
  .picker {
106
- position: absolute; z-index: 5; bottom: calc(100% + 6px); right: -4px;
107
- display: flex; gap: 2px; padding: 4px;
106
+ position: absolute; z-index: 5; bottom: calc(100% + var(--space-related)); right: calc(var(--space-tight) * -1);
107
+ display: flex; gap: var(--space-hair); padding: var(--space-tight);
108
108
  border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface-raised); box-shadow: var(--shadow-floating);
109
109
  transform-origin: calc(100% - 18px) 100%;
110
110
  }
111
- .picker[data-mine] { right: auto; left: -4px; transform-origin: 18px 100%; }
112
- .pickerWrap[data-below] .picker { top: calc(100% + 6px); bottom: auto; transform-origin: calc(100% - 18px) 0; }
111
+ .picker[data-mine] { right: auto; left: calc(var(--space-tight) * -1); transform-origin: 18px 100%; }
112
+ .pickerWrap[data-below] .picker { top: calc(100% + var(--space-related)); bottom: auto; transform-origin: calc(100% - 18px) 0; }
113
113
  .pickerWrap[data-below] .picker[data-mine] { transform-origin: 18px 0; }
114
114
  .emoji {
115
115
  display: grid; width: 34px; height: 34px; place-items: center; padding: 0;
116
- border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; cursor: pointer; font-size: 19px; line-height: 1;
116
+ border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; cursor: pointer; font-size: var(--text-lg); line-height: 1;
117
117
  -webkit-tap-highlight-color: transparent;
118
118
  transition: background-color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard);
119
119
  }
@@ -121,31 +121,31 @@
121
121
  .emoji:active { transform: scale(.9); }
122
122
  @media (hover: hover) and (pointer: fine) { .emoji:hover { background: var(--surface-muted); transform: scale(1.12); } }
123
123
 
124
- .reactionClip { overflow: hidden; margin: -2px -4px; padding: 2px 4px 2px 8px; }
125
- .row[data-mine] .reactionClip { padding: 2px 8px 2px 4px; }
126
- .reactions { display: flex; flex-wrap: wrap; gap: 4px; margin: 0; padding: 0; list-style: none; }
124
+ .reactionClip { overflow: hidden; margin: calc(var(--space-hair) * -1) calc(var(--space-tight) * -1); padding: var(--space-hair) var(--space-tight) var(--space-hair) var(--space-related); }
125
+ .row[data-mine] .reactionClip { padding: var(--space-hair) var(--space-related) var(--space-hair) var(--space-tight); }
126
+ .reactions { display: flex; flex-wrap: wrap; gap: var(--space-tight); margin: 0; padding: 0; list-style: none; }
127
127
  .row[data-mine] .reactions { justify-content: flex-end; }
128
128
  .reaction {
129
- display: inline-flex; height: 26px; align-items: center; gap: 5px; padding: 0 9px 0 7px;
129
+ display: inline-flex; height: 26px; align-items: center; gap: var(--space-tight); padding: 0 var(--space-related) 0 var(--space-related);
130
130
  border: 1px solid var(--border-subtle); border-radius: var(--radius-full); corner-shape: round; background: var(--surface); color: var(--text-secondary); cursor: pointer;
131
131
  font: inherit; font-size: var(--text-xs); font-weight: 500; line-height: 1; font-variant-numeric: tabular-nums;
132
132
  -webkit-tap-highlight-color: transparent;
133
133
  transition: background-color var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard);
134
134
  }
135
- .reaction > span:first-child { font-size: 14px; }
135
+ .reaction > span:first-child { font-size: var(--text-sm); }
136
136
  .reaction[aria-pressed="true"] { border-color: color-mix(in oklab, var(--accent) 28%, transparent); background: color-mix(in oklab, var(--accent) 8%, var(--surface)); color: var(--foreground); }
137
137
  @media (hover: hover) and (pointer: fine) { .reaction:hover { border-color: var(--border-strong); } }
138
138
  .reaction:active { transform: scale(.95); }
139
139
  .countRoll { display: inline-grid; overflow: hidden; font-variant-numeric: tabular-nums; }
140
140
  .countRoll > span { grid-area: 1 / 1; }
141
141
 
142
- .receipts { display: flex; min-height: 16px; align-items: center; gap: 6px; padding: 1px 6px 0; color: var(--text-muted); font-size: var(--text-xs); }
142
+ .receipts { display: flex; min-height: 16px; align-items: center; gap: var(--space-related); padding: 1px var(--space-related) 0; color: var(--text-muted); font-size: var(--text-xs); }
143
143
  .readers { display: inline-flex; }
144
144
  .reader { display: inline-flex; border-radius: 50%; corner-shape: round; box-shadow: 0 0 0 2px var(--surface); }
145
- .reader + .reader { margin-left: -4px; }
146
- .status[data-status="failed"] { display: inline-flex; align-items: center; gap: 8px; color: var(--danger); }
145
+ .reader + .reader { margin-left: calc(var(--space-tight) * -1); }
146
+ .status[data-status="failed"] { display: inline-flex; align-items: center; gap: var(--space-related); color: var(--danger); }
147
147
  .retry {
148
- display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px;
148
+ display: inline-flex; align-items: center; gap: var(--space-tight); padding: var(--space-hair) var(--space-related);
149
149
  border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface); color: var(--foreground); cursor: pointer;
150
150
  font: inherit; font-weight: 500; line-height: 16px;
151
151
  -webkit-tap-highlight-color: transparent;
@@ -157,8 +157,8 @@
157
157
  .avatar { display: inline-grid; flex: none; overflow: hidden; place-items: center; border-radius: 50%; corner-shape: round; background: var(--surface-muted); color: var(--text-secondary); font-weight: 500; }
158
158
  .avatar img { width: 100%; height: 100%; object-fit: cover; }
159
159
 
160
- .typing { display: grid; height: 36px; box-sizing: border-box; place-items: center; padding: 0 14px; }
161
- .dots { display: inline-flex; gap: 4px; }
160
+ .typing { display: grid; height: 36px; box-sizing: border-box; place-items: center; padding: 0 var(--space-inset); }
161
+ .dots { display: inline-flex; gap: var(--space-tight); }
162
162
  .dots i { width: 6px; height: 6px; border-radius: 50%; corner-shape: round; background: var(--text-muted); animation: breathe 1.2s var(--ease-in-out) infinite; }
163
163
  .dots i:nth-child(2) { animation-delay: .16s; }
164
164
  .dots i:nth-child(3) { animation-delay: .32s; }
@@ -169,8 +169,8 @@
169
169
  @media (prefers-reduced-motion: reduce) { .dots i { animation: none; opacity: .6; } }
170
170
 
171
171
  .pill {
172
- position: absolute; z-index: 6; bottom: 12px; left: 50%; translate: -50% 0;
173
- display: inline-flex; height: 32px; align-items: center; gap: 6px; padding: 0 14px 0 11px;
172
+ position: absolute; z-index: 6; bottom: var(--space-group); left: 50%; translate: -50% 0;
173
+ display: inline-flex; height: 32px; align-items: center; gap: var(--space-related); padding: 0 var(--space-inset) 0 var(--space-group);
174
174
  border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface-raised); box-shadow: var(--shadow-floating); color: var(--foreground); cursor: pointer;
175
175
  font: inherit; font-size: var(--text-xs); font-weight: 500; white-space: nowrap;
176
176
  -webkit-tap-highlight-color: transparent;
@@ -180,22 +180,22 @@
180
180
  .pill:active { scale: .97; }
181
181
 
182
182
  /* Composer. */
183
- .composer { display: grid; padding: 10px 12px 12px; border-top: 1px solid var(--border-subtle); transition: background-color var(--duration-fast) var(--ease-standard); }
183
+ .composer { display: grid; padding: var(--space-group) var(--space-group) var(--space-group); border-top: 1px solid var(--border-subtle); transition: background-color var(--duration-fast) var(--ease-standard); }
184
184
  .composer[data-dragging] { background: var(--accent-subtle); }
185
185
  .trayClip { overflow: hidden; }
186
- .tray { display: flex; gap: 8px; margin: 0; padding: 8px 8px 10px 4px; overflow-x: auto; list-style: none; scrollbar-width: none; }
186
+ .tray { display: flex; gap: var(--space-related); margin: 0; padding: var(--space-related) var(--space-related) var(--space-group) var(--space-tight); overflow-x: auto; list-style: none; scrollbar-width: none; }
187
187
  .pending { position: relative; flex: none; }
188
188
  .pendingImage { display: block; width: 56px; height: 56px; border-radius: var(--radius-control); corner-shape: var(--corner-shape); object-fit: cover; }
189
- .pendingFile { display: grid; width: 168px; height: 56px; box-sizing: border-box; align-content: center; column-gap: 8px; padding: 0 12px; border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: var(--surface); grid-template-columns: auto minmax(0, 1fr); }
189
+ .pendingFile { display: grid; width: 168px; height: 56px; box-sizing: border-box; align-content: center; column-gap: var(--space-related); padding: 0 var(--space-group); border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: var(--surface); grid-template-columns: auto minmax(0, 1fr); }
190
190
  .pendingFile > svg { grid-row: span 2; align-self: center; color: var(--text-secondary); }
191
191
  .pendingName { overflow: hidden; font-size: var(--text-xs); font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
192
192
  .pendingSize { color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
193
193
  .remove {
194
- position: absolute; top: -6px; right: -6px; display: grid; width: 20px; height: 20px; place-items: center; padding: 0;
194
+ position: absolute; top: calc(var(--space-related) * -1); right: calc(var(--space-related) * -1); display: grid; width: 20px; height: 20px; place-items: center; padding: 0;
195
195
  border: 2px solid var(--surface); border-radius: 50%; corner-shape: round; background: var(--foreground); color: var(--background); cursor: pointer;
196
196
  }
197
197
 
198
- .inputRow { display: flex; align-items: flex-end; gap: 6px; }
198
+ .inputRow { display: flex; align-items: flex-end; gap: var(--space-related); }
199
199
  .attach {
200
200
  display: grid; width: 40px; height: 40px; flex: none; place-items: center; margin-bottom: 1px; padding: 0;
201
201
  border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; color: var(--text-secondary); cursor: pointer;
@@ -216,7 +216,7 @@
216
216
  .field { position: relative; min-width: 0; flex: 1; overflow: hidden; border-radius: var(--radius-control); corner-shape: var(--corner-shape); }
217
217
  .mirror,
218
218
  .textarea {
219
- box-sizing: border-box; width: 100%; margin: 0; padding: 9px 6px 9px 16px; border: 0;
219
+ box-sizing: border-box; width: 100%; margin: 0; padding: var(--space-related) var(--space-related) var(--space-related) var(--space-inset); border: 0;
220
220
  font: inherit; font-size: var(--text-base); letter-spacing: var(--tracking-body); line-height: 22px;
221
221
  overflow-wrap: break-word; white-space: pre-wrap; word-break: break-word;
222
222
  }
@@ -225,7 +225,7 @@
225
225
  .textarea::placeholder { color: var(--text-muted); }
226
226
 
227
227
  .send {
228
- position: relative; display: grid; width: 32px; height: 32px; flex: none; place-items: center; margin: 0 4px 4px 0; overflow: hidden; padding: 0;
228
+ position: relative; display: grid; width: 32px; height: 32px; flex: none; place-items: center; margin: 0 var(--space-tight) var(--space-tight) 0; overflow: hidden; padding: 0;
229
229
  border: 0; border-radius: 50%; corner-shape: round; background: transparent; color: var(--text-muted); cursor: default;
230
230
  -webkit-tap-highlight-color: transparent;
231
231
  transition: background-color var(--duration-standard) var(--ease-standard), color var(--duration-standard) var(--ease-standard), transform var(--duration-instant) var(--ease-standard);
@@ -237,6 +237,6 @@
237
237
  .srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
238
238
 
239
239
  @media (max-width: 420px) {
240
- .content { padding-inline: 12px; }
240
+ .content { padding-inline: var(--space-group); }
241
241
  .stack { max-width: 84%; }
242
242
  }
@@ -9,7 +9,7 @@
9
9
  .box:active:not(:disabled) .visual { transform: scale(.95); transition: border-color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard); }
10
10
  .box:disabled { opacity: .5; cursor: not-allowed; }
11
11
  /* The label's first line centers on the square: (44px box - 19.6px line) / 2. */
12
- .copy { display: grid; gap: 2px; min-width: 0; margin-left: 1px; padding-top: calc((var(--control-height-md) - var(--text-sm) * 1.4) / 2); }
12
+ .copy { display: grid; gap: var(--space-hair); min-width: 0; margin-left: 1px; padding-top: calc((var(--control-height-md) - var(--text-sm) * 1.4) / 2); }
13
13
  .copy label { color: var(--foreground); font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); cursor: pointer; }
14
14
  .copy span { color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); }
15
15
  @media (prefers-reduced-motion: reduce) { .visual, .box:active:not(:disabled) .visual { transition: none; transform: none; } }
@@ -6,11 +6,11 @@
6
6
  /* The button is only the hit area and the layout box. What you see is the surface, which trails the box on a spring while the width morphs. */
7
7
  .chip { position: relative; display: inline-flex; max-width: 100%; padding: 0; border: 0; border-radius: var(--radius-full); corner-shape: round; background: none; color: var(--text-secondary); font: inherit; font-size: var(--text-sm); font-weight: 500; letter-spacing: var(--tracking-body); line-height: 20px; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: color var(--duration-fast) var(--ease-standard); }
8
8
  /* Press answers on the body with the individual scale property, so it never fights the layout transform on the button. */
9
- .body { position: relative; isolation: isolate; display: inline-flex; min-width: 0; height: var(--control-height-sm); align-items: center; padding: 0 14px; transition: scale var(--duration-spring) var(--ease-spring); }
9
+ .body { position: relative; isolation: isolate; display: inline-flex; min-width: 0; height: var(--control-height-sm); align-items: center; padding: 0 var(--space-inset); transition: scale var(--duration-spring) var(--ease-spring); }
10
10
  .chip:active .body { scale: .97; transition-duration: var(--duration-instant); transition-timing-function: var(--ease-standard); }
11
11
  .surface { position: absolute; z-index: -1; top: 0; bottom: 0; left: 0; border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface); transition: background-color var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard); }
12
12
  /* The check grows from its left edge where the label started; the slot hands its width to the label in one layout step, and the label's offset springs it over. */
13
- .check { position: absolute; top: calc(50% - 7px); left: 12px; display: grid; width: 14px; height: 14px; place-items: center; color: var(--accent-strong); transform-origin: 0 50%; }
13
+ .check { position: absolute; top: calc(50% - var(--space-related)); left: var(--space-group); display: grid; width: 14px; height: 14px; place-items: center; color: var(--accent-strong); transform-origin: 0 50%; }
14
14
  .check svg { display: block; overflow: visible; }
15
15
  .slot { width: 0; flex: none; }
16
16
  .body[data-selected="true"] .slot { width: 18px; }