@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
@@ -11,7 +11,7 @@
11
11
  line-height: var(--leading-body);
12
12
  }
13
13
  .root { z-index: 0; transition: z-index 0s linear .5s; }
14
- .root[data-open] { z-index: 30; transition-delay: 0s; }
14
+ .root[data-open] { z-index: var(--layer-popover); transition-delay: 0s; }
15
15
  .root:not([data-open]) .surface { z-index: 0; }
16
16
  .root *, .root *::before, .root *::after { box-sizing: border-box; }
17
17
 
@@ -38,8 +38,8 @@
38
38
  width: max-content;
39
39
  height: var(--control-height-md);
40
40
  align-items: center;
41
- gap: 10px;
42
- padding: 0 12px 0 14px;
41
+ gap: var(--space-group);
42
+ padding: 0 var(--space-group) 0 var(--space-inset);
43
43
  border: 0;
44
44
  background: transparent;
45
45
  color: var(--foreground);
@@ -62,10 +62,10 @@
62
62
  .wordInner { display: inline-block; }
63
63
 
64
64
  /* Panel */
65
- .panel { position: absolute; top: 0; left: 0; width: max-content; padding: 10px; --cell: 36px; }
66
- .body { display: flex; gap: 10px; }
65
+ .panel { position: absolute; top: 0; left: 0; width: max-content; padding: var(--space-group); --cell: 36px; }
66
+ .body { display: flex; gap: var(--space-group); }
67
67
 
68
- .rail { position: relative; display: flex; width: 9.25rem; flex: none; flex-direction: column; gap: 2px; padding-right: 10px; border-right: 1px solid var(--border-subtle); }
68
+ .rail { position: relative; display: flex; width: 9.25rem; flex: none; flex-direction: column; gap: var(--space-hair); padding-right: var(--space-group); border-right: 1px solid var(--border-subtle); }
69
69
  .railHighlight { position: absolute; top: 0; left: 0; border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: color-mix(in oklab, var(--foreground) 6.5%, transparent); pointer-events: none; }
70
70
  .preset {
71
71
  position: relative;
@@ -73,7 +73,7 @@
73
73
  height: 32px;
74
74
  flex: none;
75
75
  align-items: center;
76
- padding: 0 12px;
76
+ padding: 0 var(--space-group);
77
77
  border: 0;
78
78
  border-radius: var(--radius-control); corner-shape: var(--corner-shape);
79
79
  background: transparent;
@@ -87,7 +87,7 @@
87
87
  }
88
88
  .preset[aria-pressed="true"] { color: var(--foreground); font-weight: 500; }
89
89
 
90
- .calendars { position: relative; padding: 0 2px; }
90
+ .calendars { position: relative; padding: 0 var(--space-hair); }
91
91
  .navButton {
92
92
  position: absolute;
93
93
  z-index: 2;
@@ -105,14 +105,14 @@
105
105
  -webkit-tap-highlight-color: transparent;
106
106
  transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard);
107
107
  }
108
- .navButton[data-side="prev"] { left: 2px; }
109
- .navButton[data-side="next"] { right: 2px; }
108
+ .navButton[data-side="prev"] { left: var(--space-hair); }
109
+ .navButton[data-side="next"] { right: var(--space-hair); }
110
110
  .navButton:active:not(:disabled) { transform: scale(.92); }
111
111
  .navButton:disabled { opacity: .35; cursor: default; }
112
112
 
113
113
  /* Months slide horizontally inside a clipped window that keeps a little room for the blur. */
114
114
  .viewport { position: relative; overflow: clip; overflow-clip-margin: 2px; }
115
- .months { display: flex; gap: 24px; }
115
+ .months { display: flex; gap: var(--space-section); }
116
116
  .month { width: calc(var(--cell) * 7); }
117
117
  .monthTitle { height: 32px; margin: 0; color: var(--foreground); font-weight: 500; line-height: 32px; text-align: center; white-space: nowrap; }
118
118
  .grid { display: grid; }
@@ -125,7 +125,7 @@
125
125
  .layer { position: absolute; inset: 0; pointer-events: none; }
126
126
  .bar { position: absolute; height: calc(var(--cell) - 6px); border-radius: var(--radius-full); corner-shape: round; background: var(--accent-subtle); }
127
127
  .thumb { position: absolute; top: 0; left: 0; width: var(--cell); height: var(--cell); }
128
- .thumb::before { content: ""; position: absolute; inset: 3px; border-radius: 50%; corner-shape: round; background: var(--accent); }
128
+ .thumb::before { content: ""; position: absolute; inset: var(--space-tight); border-radius: 50%; corner-shape: round; background: var(--accent); }
129
129
 
130
130
  .day {
131
131
  position: relative;
@@ -148,23 +148,23 @@
148
148
  .day > span { position: relative; display: grid; width: calc(100% - 6px); height: calc(100% - 6px); place-items: center; border-radius: 50%; corner-shape: round; transition: background-color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard); }
149
149
  .day:active:not(:disabled) > span { transform: scale(.9); }
150
150
  .day[data-edge] { color: var(--accent-foreground); font-weight: 500; transition-delay: 40ms; }
151
- .day[data-today]::after { content: ""; position: absolute; bottom: 5px; left: 50%; width: 4px; height: 4px; margin-left: -2px; border-radius: 50%; corner-shape: round; background: var(--accent); }
151
+ .day[data-today]::after { content: ""; position: absolute; bottom: var(--space-tight); left: 50%; width: 4px; height: 4px; margin-left: calc(var(--space-hair) * -1); border-radius: 50%; corner-shape: round; background: var(--accent); }
152
152
  .day[data-today][data-edge]::after { background: var(--accent-foreground); }
153
153
  .day:disabled { color: var(--text-muted); cursor: default; opacity: .5; }
154
154
  .day:focus-visible:not([data-edge]) > span { background: color-mix(in oklab, var(--foreground) 7%, transparent); }
155
155
 
156
156
  /* Footer: the formatted range and the two decisions. */
157
- .footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 10px; padding: 10px 2px 0 12px; border-top: 1px solid var(--border-subtle); }
157
+ .footer { display: flex; align-items: center; justify-content: space-between; gap: var(--space-group); margin-top: var(--space-group); padding: var(--space-group) var(--space-hair) 0 var(--space-group); border-top: 1px solid var(--border-subtle); }
158
158
  .summary { display: grid; min-width: 0; gap: 1px; }
159
159
  .summaryValue { display: inline-flex; width: max-content; font-weight: 500; font-variant-numeric: tabular-nums; }
160
160
  .summaryCount { display: inline-flex; min-height: 1.4em; color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
161
- .actions { display: flex; flex: none; align-items: center; gap: 6px; }
161
+ .actions { display: flex; flex: none; align-items: center; gap: var(--space-related); }
162
162
  .ghost, .primary {
163
163
  display: inline-flex;
164
164
  height: var(--control-height-sm);
165
165
  align-items: center;
166
166
  justify-content: center;
167
- padding: 0 14px;
167
+ padding: 0 var(--space-inset);
168
168
  border: 0;
169
169
  border-radius: var(--radius-control); corner-shape: var(--corner-shape);
170
170
  cursor: pointer;
@@ -184,13 +184,13 @@
184
184
  .srOnly { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
185
185
 
186
186
  /* Compact: one month under a scrolling row of presets. */
187
- .panel[data-compact] { padding: 8px; }
188
- .panel[data-compact] .body { flex-direction: column; gap: 8px; }
189
- .panel[data-compact] .rail { width: auto; flex-direction: row; gap: 2px; margin: 0 -8px; padding: 0 8px 8px; overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--border-subtle); overscroll-behavior-x: contain; scrollbar-width: none; }
187
+ .panel[data-compact] { padding: var(--space-related); }
188
+ .panel[data-compact] .body { flex-direction: column; gap: var(--space-related); }
189
+ .panel[data-compact] .rail { width: auto; flex-direction: row; gap: var(--space-hair); margin: 0 calc(var(--space-related) * -1); padding: 0 var(--space-related) var(--space-related); overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--border-subtle); overscroll-behavior-x: contain; scrollbar-width: none; }
190
190
  .panel[data-compact] .rail::-webkit-scrollbar { display: none; }
191
- .panel[data-compact] .preset { padding: 0 11px; }
191
+ .panel[data-compact] .preset { padding: 0 var(--space-group); }
192
192
  .panel[data-compact] .calendars { align-self: center; }
193
- .panel[data-compact] .footer { flex-wrap: wrap; margin-top: 6px; padding-left: 11px; }
193
+ .panel[data-compact] .footer { flex-wrap: wrap; margin-top: var(--space-related); padding-left: var(--space-group); }
194
194
  .panel[data-compact] .actions { margin-left: auto; }
195
195
  .panel[data-compact] .summary { flex: 1 1 8rem; }
196
196
 
@@ -1,6 +1,6 @@
1
- .overlay { position: fixed; inset: 0; z-index: 50; background: oklch(10% 0 0 / .46); backdrop-filter: blur(7px); }
1
+ .overlay { position: fixed; inset: 0; z-index: var(--layer-overlay); background: var(--scrim); backdrop-filter: blur(7px); }
2
2
  /* Centered with auto margins (like a native modal dialog) so transform stays free for the entrance spring. */
3
- .content { position: fixed; z-index: 51; inset: 0; width: min(calc(100vw - var(--space-block)), 440px); height: fit-content; max-height: calc(100vh - var(--space-block)); max-height: calc(100dvh - var(--space-block)); margin: auto; overflow: auto; border: 1px solid var(--border); border-radius: var(--radius-overlay); background: var(--surface-raised); color: var(--foreground); box-shadow: var(--shadow-floating); }
3
+ .content { position: fixed; z-index: var(--layer-dialog); inset: 0; width: min(calc(100vw - var(--space-block)), 440px); height: fit-content; max-height: calc(100vh - var(--space-block)); max-height: calc(100dvh - var(--space-block)); margin: auto; overflow: auto; border: 1px solid var(--border); border-radius: var(--radius-overlay); background: var(--surface-raised); color: var(--foreground); box-shadow: var(--shadow-floating); }
4
4
  /* Motion animates both layers. Keyframes only run when DialogContent sits under a bare Radix root. */
5
5
  .keyframes.overlay[data-state="open"] { animation: overlay-in var(--duration-standard) var(--ease-enter) both; }
6
6
  .keyframes.overlay[data-state="closed"] { animation: overlay-out var(--duration-fast) var(--ease-standard) both; }
@@ -18,7 +18,7 @@
18
18
 
19
19
  /* The centre reads the active segment, or the total at rest. Old and new readouts share one grid cell, so a swap never moves or resizes anything. */
20
20
  .center { position: absolute; top: 50%; left: 50%; display: grid; width: calc(var(--inner) - 16px); transform: translate(-50%, -50%); pointer-events: none; }
21
- .readout { grid-area: 1 / 1; display: grid; min-width: 0; justify-items: center; gap: 2px; text-align: center; }
21
+ .readout { grid-area: 1 / 1; display: grid; min-width: 0; justify-items: center; gap: var(--space-hair); text-align: center; }
22
22
  .centerLabel, .centerMeta { display: block; width: 100%; min-height: calc(1em * var(--leading-body)); overflow: hidden; font-size: var(--text-xs); line-height: var(--leading-body); text-overflow: ellipsis; white-space: nowrap; }
23
23
  .centerLabel { color: var(--text-secondary); }
24
24
  .centerMeta { color: var(--text-muted); font-variant-numeric: tabular-nums; }
@@ -26,10 +26,10 @@
26
26
 
27
27
  /* Legend rows mirror the segments: hovering either lights both. */
28
28
  /* Rows carry their own 2px spacing so a row closing its height takes its gap with it; the list's negative margin drops the last one. */
29
- .legend { display: grid; margin: 0 0 -2px; padding: 0; list-style: none; }
29
+ .legend { display: grid; margin: 0 0 calc(var(--space-hair) * -1); padding: 0; list-style: none; }
30
30
  .item { overflow: hidden; }
31
- .item > .row { margin-bottom: 2px; }
32
- .row { display: grid; width: 100%; min-height: 40px; grid-template-columns: 10px minmax(0, 1fr) auto 3.25em; align-items: center; column-gap: 10px; padding: 6px 10px; border: 0; border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: transparent; color: var(--foreground); cursor: pointer; font: inherit; font-size: var(--text-sm); line-height: var(--leading-body); text-align: left; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard); }
31
+ .item > .row { margin-bottom: var(--space-hair); }
32
+ .row { display: grid; width: 100%; min-height: 40px; grid-template-columns: 10px minmax(0, 1fr) auto 3.25em; align-items: center; column-gap: var(--space-group); padding: var(--space-related) var(--space-group); border: 0; border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: transparent; color: var(--foreground); cursor: pointer; font: inherit; font-size: var(--text-sm); line-height: var(--leading-body); text-align: left; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard); }
33
33
  .row:active { transform: scale(.985); }
34
34
  .row[data-active], .row[data-pinned] { background: color-mix(in oklab, var(--foreground) 5%, transparent); }
35
35
  .dot { width: 10px; height: 10px; border-radius: 50%; corner-shape: round; background: var(--slice); box-shadow: inset 0 0 0 1.5px var(--slice); transition: background-color var(--duration-standard) var(--ease-standard), box-shadow var(--duration-standard) var(--ease-standard); }
@@ -1,8 +1,8 @@
1
1
  .overlay {
2
2
  position: fixed;
3
3
  inset: 0;
4
- z-index: 50;
5
- background: oklch(10% 0 0 / .38);
4
+ z-index: var(--layer-overlay);
5
+ background: var(--scrim);
6
6
  backdrop-filter: blur(4px);
7
7
  }
8
8
 
@@ -12,7 +12,7 @@
12
12
  --ease-drawer: cubic-bezier(.32,.72,0,1);
13
13
  --drawer-from: translateX(100%);
14
14
  position: fixed;
15
- z-index: 51;
15
+ z-index: var(--layer-dialog);
16
16
  display: flex;
17
17
  flex-direction: column;
18
18
  width: var(--drawer-size);
@@ -32,7 +32,7 @@
32
32
  .content[data-side="left"] { inset: 0 auto 0 0; border-width: 1px 1px 1px 0; border-radius: 0 var(--radius-container) var(--radius-container) 0; corner-shape: var(--corner-shape); }
33
33
  .content[data-side="top"] { inset: 0 0 auto; width: 100%; max-width: none; max-height: min(32rem, 100dvh); border-width: 0 0 1px; border-radius: 0 0 var(--radius-container) var(--radius-container); corner-shape: var(--corner-shape); }
34
34
  .content[data-side="bottom"] { inset: auto 0 0; width: 100%; max-width: none; max-height: min(32rem, 100dvh); border-width: 1px 0 0; border-radius: var(--radius-container) var(--radius-container) 0 0; corner-shape: var(--corner-shape); }
35
- .content[data-side="bottom"]::before { position: absolute; top: 9px; left: 50%; width: 2.25rem; height: 4px; border-radius: var(--radius-full); corner-shape: round; background: var(--border-strong); content: ""; opacity: .75; transform: translateX(-50%); }
35
+ .content[data-side="bottom"]::before { position: absolute; top: var(--space-related); left: 50%; width: 2.25rem; height: 4px; border-radius: var(--radius-full); corner-shape: round; background: var(--border-strong); content: ""; opacity: .75; transform: translateX(-50%); }
36
36
 
37
37
  /* Motion springs the panel from its edge. Keyframes only run when DrawerContent sits under a bare Radix root. */
38
38
  .content[data-side="left"] { --drawer-from: translateX(-100%); }
@@ -45,7 +45,7 @@
45
45
  .content:focus { outline: none; }
46
46
  /* Inside a container the layers anchor to that element instead of the viewport. */
47
47
  .overlay[data-contained], .content[data-contained] { position: absolute; }
48
- .overlay[data-contained] { z-index: 1; background: oklch(10% 0 0 / .16); backdrop-filter: none; }
48
+ .overlay[data-contained] { z-index: 1; background: var(--scrim-soft); backdrop-filter: none; }
49
49
  .content[data-contained] { --drawer-size: min(22rem, calc(100% - var(--space-block))); z-index: 2; max-height: 100%; box-shadow: none; }
50
50
  .content[data-contained][data-side="top"], .content[data-contained][data-side="bottom"] { max-height: min(32rem, calc(100% - var(--space-block))); }
51
51
  /* The container already draws the outer edges, so only the inner edge keeps a border. */
@@ -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; }