@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
@@ -3,7 +3,7 @@
3
3
  .label { justify-self: start; margin-bottom: var(--space-related); color: var(--foreground); font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); }
4
4
 
5
5
  /* One tall shell: the amount on the left, the currency on the right. Set --money-input-height to resize it. */
6
- .control { position: relative; display: flex; min-width: 0; height: var(--money-input-height, 76px); box-sizing: border-box; align-items: center; gap: var(--space-related); padding: 0 8px 0 18px; border: 1px solid var(--border-strong); border-radius: var(--radius-container); corner-shape: var(--corner-shape); background: var(--surface); transition: border-color var(--duration-fast) var(--ease-standard); }
6
+ .control { position: relative; display: flex; min-width: 0; height: var(--money-input-height, 76px); box-sizing: border-box; align-items: center; gap: var(--space-related); padding: 0 var(--space-related) 0 var(--space-comfort); border: 1px solid var(--border-strong); border-radius: var(--radius-container); corner-shape: var(--corner-shape); background: var(--surface); transition: border-color var(--duration-fast) var(--ease-standard); }
7
7
  @media (hover: hover) and (pointer: fine) { .control:hover:not([data-invalid]) { border-color: var(--foreground); } }
8
8
  .control[data-focused] { border-color: var(--foreground); }
9
9
  .control[data-invalid] { border-color: var(--danger); }
@@ -35,19 +35,19 @@
35
35
 
36
36
  /* Currency switch: plain text on the shell, a quiet fill on hover and while open. It answers a press with color, since it anchors a menu. */
37
37
  .menuAnchor { position: relative; flex: none; }
38
- .currencyButton { display: inline-flex; height: 36px; align-items: center; gap: 4px; padding: 0 8px 0 12px; border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; color: var(--foreground); cursor: pointer; font: inherit; font-size: var(--text-sm); font-weight: 500; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard); }
38
+ .currencyButton { display: inline-flex; height: 36px; align-items: center; gap: var(--space-tight); padding: 0 var(--space-related) 0 var(--space-group); border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; color: var(--foreground); cursor: pointer; font: inherit; font-size: var(--text-sm); font-weight: 500; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard); }
39
39
  @media (hover: hover) and (pointer: fine) { .currencyButton:hover:not(:disabled) { background: var(--surface-muted); } }
40
40
  .currencyButton:active:not(:disabled), .currencyButton[data-open] { background: var(--surface-muted); }
41
41
  .currencyButton:disabled { cursor: not-allowed; }
42
42
  .code { font-variant-numeric: tabular-nums; }
43
- .codeStatic { flex: none; padding: 0 10px; color: var(--text-secondary); font-size: var(--text-sm); font-weight: 500; }
43
+ .codeStatic { flex: none; padding: 0 var(--space-group); color: var(--text-secondary); font-size: var(--text-sm); font-weight: 500; }
44
44
  .chevron { display: grid; width: 16px; height: 16px; place-items: center; color: var(--text-muted); }
45
45
 
46
46
  /* The menu grows out of the currency button's corner and opens 8px below the shell, whatever its height. */
47
- .menu { --drop: calc((var(--money-input-height, 76px) - 36px) / 2 + var(--space-related)); position: absolute; z-index: 30; top: calc(100% + var(--drop)); right: -8px; width: min(256px, calc(100vw - 2rem)); box-sizing: border-box; padding: 6px; border: 1px solid var(--border); border-radius: var(--radius-container); corner-shape: var(--corner-shape); background: var(--surface-raised); box-shadow: var(--shadow-floating); transform-origin: calc(100% - 44px) calc(var(--drop) * -1 - 18px); }
48
- .list { position: relative; display: grid; max-height: 288px; gap: 2px; overflow-y: auto; overscroll-behavior: contain; outline: none; scrollbar-width: thin; }
47
+ .menu { --drop: calc((var(--money-input-height, 76px) - 36px) / 2 + var(--space-related)); position: absolute; z-index: var(--layer-dropdown); top: calc(100% + var(--drop)); right: calc(var(--space-related) * -1); width: min(256px, calc(100vw - 2rem)); box-sizing: border-box; padding: var(--space-related); border: 1px solid var(--border); border-radius: var(--radius-container); corner-shape: var(--corner-shape); background: var(--surface-raised); box-shadow: var(--shadow-floating); transform-origin: calc(100% - 44px) calc(var(--drop) * -1 - 18px); }
48
+ .list { position: relative; display: grid; max-height: 288px; gap: var(--space-hair); overflow-y: auto; overscroll-behavior: contain; outline: none; scrollbar-width: thin; }
49
49
  .highlight { position: absolute; top: 0; right: 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; }
50
- .option { position: relative; display: flex; min-height: 36px; box-sizing: border-box; align-items: center; gap: 10px; padding: 0 10px; border-radius: var(--radius-control); corner-shape: var(--corner-shape); color: var(--text-secondary); cursor: pointer; font-size: var(--text-sm); user-select: none; -webkit-user-select: none; transition: color var(--duration-fast) var(--ease-standard); }
50
+ .option { position: relative; display: flex; min-height: 36px; box-sizing: border-box; align-items: center; gap: var(--space-group); padding: 0 var(--space-group); border-radius: var(--radius-control); corner-shape: var(--corner-shape); color: var(--text-secondary); cursor: pointer; font-size: var(--text-sm); user-select: none; -webkit-user-select: none; transition: color var(--duration-fast) var(--ease-standard); }
51
51
  .option[data-active], .option[aria-selected="true"] { color: var(--foreground); }
52
52
  .optionSymbol { width: 22px; flex: none; color: var(--text-muted); text-align: center; }
53
53
  .optionCode { flex: none; font-weight: 500; font-variant-numeric: tabular-nums; }
@@ -57,8 +57,8 @@
57
57
 
58
58
  /* Quick add chips and the allowed range share one row under the shell. */
59
59
  .toolbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-related) var(--space-group); padding-top: var(--space-group); }
60
- .chips { display: flex; flex-wrap: wrap; gap: 6px; }
61
- .chip { height: 30px; padding: 0 12px; border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface); color: var(--text-secondary); cursor: pointer; font: inherit; font-size: var(--text-xs); font-weight: 500; font-variant-numeric: tabular-nums; -webkit-tap-highlight-color: transparent; touch-action: manipulation; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), transform var(--duration-spring) var(--ease-spring); }
60
+ .chips { display: flex; flex-wrap: wrap; gap: var(--space-related); }
61
+ .chip { height: 30px; padding: 0 var(--space-group); border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface); color: var(--text-secondary); cursor: pointer; font: inherit; font-size: var(--text-xs); font-weight: 500; font-variant-numeric: tabular-nums; -webkit-tap-highlight-color: transparent; touch-action: manipulation; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), transform var(--duration-spring) var(--ease-spring); }
62
62
  @media (hover: hover) and (pointer: fine) { .chip:hover:not(:disabled) { background: var(--surface-muted); color: var(--foreground); } }
63
63
  .chip:active:not(:disabled) { transform: scale(.96); transition-duration: var(--duration-fast), var(--duration-fast), var(--duration-fast), 100ms; }
64
64
  .chip[aria-disabled="true"] { opacity: .45; }
@@ -73,5 +73,5 @@
73
73
 
74
74
  .srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
75
75
 
76
- @media (max-width: 420px) { .control { padding-left: 14px; } .group { font-size: var(--text-2xl); } }
76
+ @media (max-width: 420px) { .control { padding-left: var(--space-inset); } .group { font-size: var(--text-2xl); } }
77
77
  @media (prefers-reduced-motion: reduce) { .control, .column, .check, .chip, .range { transition-duration: 0ms; } .chip:active:not(:disabled) { transform: none; } }
@@ -1,7 +1,7 @@
1
1
  /* The root lays out the label and the anchor. The anchor holds the closed trigger's place; the shape floats over it and grows past it when open. */
2
2
  .root { --ms-h: var(--control-height-md); --ms-trigger-w: 0px; position: relative; display: inline-grid; justify-items: start; gap: var(--space-related); min-width: 0; color: var(--foreground); font-family: var(--font-body); font-size: var(--text-sm); letter-spacing: var(--tracking-body); line-height: var(--leading-body); }
3
3
  .root[data-align="end"] { justify-items: end; }
4
- .root[data-open] { z-index: 30; }
4
+ .root[data-open] { z-index: var(--layer-dropdown); }
5
5
  .label { color: var(--text-secondary); font-size: var(--text-sm); font-weight: 500; }
6
6
 
7
7
  .anchor { position: relative; height: var(--ms-h); max-width: 100%; }
@@ -22,7 +22,7 @@
22
22
  /* The lid is the trigger row. It stays at the top of the surface in both states and sits above the list, so a lifted label passes over it. */
23
23
  .lid { position: absolute; z-index: 1; top: 0; right: 0; left: 0; height: var(--ms-h); }
24
24
 
25
- .trigger { box-sizing: border-box; display: flex; height: var(--ms-h); align-items: center; gap: var(--space-related); padding: 0 36px 0 14px; font-size: var(--text-sm); font-weight: 500; white-space: nowrap; }
25
+ .trigger { box-sizing: border-box; display: flex; height: var(--ms-h); align-items: center; gap: var(--space-related); padding: 0 var(--space-block) 0 var(--space-inset); font-size: var(--text-sm); font-weight: 500; white-space: nowrap; }
26
26
  .lid .trigger { position: absolute; inset: 0; width: 100%; margin: 0; border: 0; background: transparent; color: inherit; cursor: pointer; font-family: inherit; letter-spacing: inherit; text-align: left; -webkit-tap-highlight-color: transparent; touch-action: manipulation; }
27
27
  .lid .trigger:disabled { cursor: not-allowed; }
28
28
  /* An invisible copy that says how wide the closed trigger wants to be. It sits in flow until the first measurement lands, so the server render already has the right size. */
@@ -39,10 +39,10 @@
39
39
  .icon > svg { width: 16px; height: 16px; }
40
40
  .icon > img { width: 20px; height: 20px; border-radius: 50%; corner-shape: round; object-fit: cover; }
41
41
 
42
- .chevron { position: absolute; top: 50%; right: 12px; display: grid; width: 16px; height: 16px; margin-top: -8px; place-items: center; color: var(--text-muted); pointer-events: none; }
42
+ .chevron { position: absolute; top: 50%; right: var(--space-group); display: grid; width: 16px; height: 16px; margin-top: calc(var(--space-related) * -1); place-items: center; color: var(--text-muted); pointer-events: none; }
43
43
 
44
44
  /* Search takes the lid's place when the list is long. */
45
- .searchRow { position: absolute; inset: 0; display: flex; align-items: center; gap: var(--space-related); padding: 0 2px 0 14px; }
45
+ .searchRow { position: absolute; inset: 0; display: flex; align-items: center; gap: var(--space-related); padding: 0 var(--space-hair) 0 var(--space-inset); }
46
46
  .searchRow[inert] { pointer-events: none; }
47
47
  .searchIcon { flex: none; color: var(--text-muted); }
48
48
  .input { min-width: 0; height: 100%; flex: 1; padding: 0; border: 0; background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-sm); letter-spacing: var(--tracking-body); outline: none; }
@@ -56,19 +56,19 @@
56
56
  }
57
57
 
58
58
  /* The list hangs under the lid at its own width, so it measures the same whatever size the surface is mid spring. */
59
- .listFace { position: absolute; top: var(--ms-h); left: 0; box-sizing: border-box; width: max(var(--ms-trigger-w), min(var(--ms-panel-w), calc(100vw - 2rem))); padding: 0 6px 6px; }
59
+ .listFace { position: absolute; top: var(--ms-h); left: 0; box-sizing: border-box; width: max(var(--ms-trigger-w), min(var(--ms-panel-w), calc(100vw - 2rem))); padding: 0 var(--space-related) var(--space-related); }
60
60
  .root[data-align="end"] .listFace { right: 0; left: auto; }
61
- .listFace::before { content: ""; position: absolute; top: 0; right: 14px; left: 14px; height: 1px; background: var(--border-subtle); }
61
+ .listFace::before { content: ""; position: absolute; top: 0; right: var(--space-inset); left: var(--space-inset); height: 1px; background: var(--border-subtle); }
62
62
  .listFace[inert] { pointer-events: none; }
63
- .scroll { max-height: var(--ms-list-max); overflow-y: auto; overscroll-behavior: contain; padding-top: 6px; scrollbar-width: thin; }
64
- .options { position: relative; display: grid; gap: 2px; }
63
+ .scroll { max-height: var(--ms-list-max); overflow-y: auto; overscroll-behavior: contain; padding-top: var(--space-related); scrollbar-width: thin; }
64
+ .options { position: relative; display: grid; gap: var(--space-hair); }
65
65
  .highlight { position: absolute; top: 0; right: 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; }
66
66
 
67
- .group { display: grid; gap: 2px; }
68
- .group + .group { margin-top: 4px; }
69
- .groupLabel { padding: 8px 10px 4px; color: var(--text-muted); font-size: var(--text-xs); font-weight: 400; }
67
+ .group { display: grid; gap: var(--space-hair); }
68
+ .group + .group { margin-top: var(--space-tight); }
69
+ .groupLabel { padding: var(--space-related) var(--space-group) var(--space-tight); color: var(--text-muted); font-size: var(--text-xs); font-weight: 400; }
70
70
 
71
- .option { position: relative; display: flex; min-height: 36px; box-sizing: border-box; align-items: center; gap: var(--space-related); padding: 0 10px 0 8px; border-radius: var(--radius-control); corner-shape: var(--corner-shape); color: var(--text-secondary); cursor: pointer; user-select: none; -webkit-user-select: none; transition: color var(--duration-fast) var(--ease-standard); }
71
+ .option { position: relative; display: flex; min-height: 36px; box-sizing: border-box; align-items: center; gap: var(--space-related); padding: 0 var(--space-group) 0 var(--space-related); border-radius: var(--radius-control); corner-shape: var(--corner-shape); color: var(--text-secondary); cursor: pointer; user-select: none; -webkit-user-select: none; transition: color var(--duration-fast) var(--ease-standard); }
72
72
  .option[data-active], .option[aria-selected="true"] { color: var(--foreground); }
73
73
  .option[aria-disabled="true"] { color: var(--text-muted); cursor: not-allowed; opacity: .6; }
74
74
  /* Lid padding minus list inset, so a lifted label starts exactly level with where it lands. */
@@ -76,7 +76,7 @@
76
76
  .meta { flex: none; color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
77
77
  .check { display: grid; width: 16px; height: 16px; flex: none; place-items: center; color: var(--foreground); opacity: 0; transform: scale(.6); transition: opacity var(--duration-fast) var(--ease-standard), transform var(--duration-spring) var(--ease-spring); }
78
78
  .check[data-on] { opacity: 1; transform: none; }
79
- .empty { margin: 0; padding: 12px 10px 14px; color: var(--text-muted); font-size: var(--text-sm); }
79
+ .empty { margin: 0; padding: var(--space-group) var(--space-group) var(--space-inset); color: var(--text-muted); font-size: var(--text-sm); }
80
80
 
81
81
  .srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
82
82
 
@@ -2,12 +2,12 @@
2
2
  .control { position: relative; min-width: 0; }
3
3
  .label { font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); }
4
4
  /* The trigger anchors the menu, so press feedback stays in color; it never scales. */
5
- .trigger { position: relative; display: flex; width: 100%; min-height: var(--control-height-md); align-items: center; gap: 8px; border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); padding: 5px 11px 5px 12px; background: var(--surface); color: var(--foreground); font: inherit; font-size: var(--text-sm); text-align: left; cursor: pointer; transition: border-color var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard); }
5
+ .trigger { position: relative; display: flex; width: 100%; min-height: var(--control-height-md); align-items: center; gap: var(--space-related); border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); padding: var(--space-tight) var(--space-group) var(--space-tight) var(--space-group); background: var(--surface); color: var(--foreground); font: inherit; font-size: var(--text-sm); text-align: left; cursor: pointer; transition: border-color var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard), box-shadow var(--duration-fast) var(--ease-standard); }
6
6
  @media (hover: hover) and (pointer: fine) { .trigger:hover:not(:disabled) { border-color: var(--border-strong); background: var(--surface-muted); } }
7
7
  .trigger:active:not(:disabled), .trigger[aria-expanded="true"] { border-color: var(--border-strong); background: var(--surface-muted); }
8
8
  .trigger:disabled { opacity: .5; cursor: not-allowed; }
9
9
  /* The chevron stays pinned to the trailing edge; a selection only reserves room for the clear button beside it, so nothing jumps. */
10
- .hasClear .chevron { margin-left: 26px; }
10
+ .hasClear .chevron { margin-left: var(--space-section); }
11
11
  .value { position: relative; display: flex; min-width: 0; min-height: 26px; flex: 1; align-items: center; overflow: hidden; }
12
12
  .placeholder, .description { color: var(--text-muted); }
13
13
  /* The placeholder sits outside the flow so the first chip can open its slot from the leading edge. */
@@ -15,21 +15,21 @@
15
15
  /* The slot clips while its width springs open or closed; the chip keeps its natural width so the label never re-truncates mid-flight. */
16
16
  .slot { display: flex; flex: 0 1 auto; min-width: 0; overflow: hidden; }
17
17
  .slot:has(.more) { flex-shrink: 0; }
18
- .chip, .more { box-sizing: border-box; flex: 0 0 auto; max-width: 9rem; overflow: hidden; margin-right: 5px; border-radius: var(--radius-full); corner-shape: round; padding: 4px 8px; background: var(--surface-muted); box-shadow: inset 0 0 0 1px var(--border-subtle); color: var(--foreground); text-overflow: ellipsis; white-space: nowrap; font-size: var(--text-xs); transition: background-color var(--duration-fast) var(--ease-standard); }
18
+ .chip, .more { box-sizing: border-box; flex: 0 0 auto; max-width: 9rem; overflow: hidden; margin-right: var(--space-tight); border-radius: var(--radius-full); corner-shape: round; padding: var(--space-tight) var(--space-related); background: var(--surface-muted); box-shadow: inset 0 0 0 1px var(--border-subtle); color: var(--foreground); text-overflow: ellipsis; white-space: nowrap; font-size: var(--text-xs); transition: background-color var(--duration-fast) var(--ease-standard); }
19
19
  .trigger[aria-expanded="true"] .chip, .trigger[aria-expanded="true"] .more { background: var(--surface); }
20
20
  @media (hover: hover) and (pointer: fine) { .trigger:hover:not(:disabled) .chip, .trigger:hover:not(:disabled) .more { background: var(--surface); } }
21
21
  .more { display: inline-flex; color: var(--text-secondary); font-variant-numeric: tabular-nums; }
22
22
  .count { display: inline-grid; }
23
23
  .count > span { grid-area: 1 / 1; }
24
24
  .srOnly { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
25
- .clear { position: absolute; top: 0; bottom: 0; right: 36px; display: grid; width: 22px; height: 22px; margin: auto 0; place-items: center; 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); }
25
+ .clear { position: absolute; top: 0; bottom: 0; right: var(--space-block); display: grid; width: 22px; height: 22px; margin: auto 0; place-items: center; 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); }
26
26
  @media (hover: hover) and (pointer: fine) { .clear:hover { background: var(--surface); color: var(--foreground); } }
27
27
  .chevron { flex: 0 0 auto; color: var(--text-muted); transition: transform var(--duration-spring) var(--ease-spring); }
28
28
  .trigger[aria-expanded="true"] .chevron { transform: rotate(180deg); }
29
29
  .description { font-size: var(--text-xs); }
30
- .menu { position: absolute; z-index: 80; top: calc(100% + 8px); right: 0; left: 0; overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius-container); corner-shape: var(--corner-shape); padding: 5px; background: var(--surface-raised); box-shadow: var(--shadow-floating); transform-origin: top center; will-change: transform, opacity; }
30
+ .menu { position: absolute; z-index: var(--layer-dropdown); top: calc(100% + var(--space-related)); right: 0; left: 0; overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius-container); corner-shape: var(--corner-shape); padding: var(--space-tight); background: var(--surface-raised); box-shadow: var(--shadow-floating); transform-origin: top center; will-change: transform, opacity; }
31
31
  /* Arrow keys move the highlight often, so it changes almost instantly. Selection is shown by the check, not by weight, so labels never reflow. */
32
- .option { display: flex; width: 100%; min-height: 38px; align-items: center; justify-content: space-between; gap: 12px; border: 0; border-radius: calc(var(--radius-container) - 5px); corner-shape: var(--corner-shape); padding: 0 11px; background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-sm); text-align: left; cursor: pointer; transition: background-color 80ms var(--ease-standard), color 80ms var(--ease-standard); }
32
+ .option { display: flex; width: 100%; min-height: 38px; align-items: center; justify-content: space-between; gap: var(--space-group); border: 0; border-radius: calc(var(--radius-container) - var(--space-tight)); corner-shape: var(--corner-shape); padding: 0 var(--space-group); background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-sm); text-align: left; cursor: pointer; transition: background-color 80ms var(--ease-standard), color 80ms var(--ease-standard); }
33
33
  .option > span:first-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
34
34
  .option[data-active="true"] { background: var(--surface-muted); }
35
35
  .option:disabled { opacity: .42; cursor: not-allowed; }
@@ -0,0 +1,64 @@
1
+ import type { ReactNode, Ref } from "react";
2
+ /** What NavList's onNavigate receives: the chosen row's href, or none for an action row. */
3
+ export interface NavChoice {
4
+ href?: string;
5
+ }
6
+ export interface NavListProps {
7
+ /** Names the navigation for screen readers, such as "Main" or "Settings". */
8
+ label: string;
9
+ /** NavItem and NavSection elements, in order. */
10
+ children: ReactNode;
11
+ /** Placement only (width, margins, grid area); the rows' look comes from the component. */
12
+ className?: string;
13
+ /** The `<nav>` element. */
14
+ ref?: Ref<HTMLElement>;
15
+ /**
16
+ * Called when any row is chosen (click, Enter or Space; link and action rows), before it navigates or acts. A notification,
17
+ * such as closing a phone sheet: it cannot cancel. Locked rows and modified clicks (a new tab or window) do not call it.
18
+ */
19
+ onNavigate?: (choice: NavChoice) => void;
20
+ }
21
+ /**
22
+ * A sidebar's list of destinations: a `<nav>` with a list of NavItem rows, optionally grouped by NavSection. One hover highlight
23
+ * glides between rows for the pointer, and the current page's marker glides when the page changes.
24
+ */
25
+ export declare function NavList({ label, children, className, ref, onNavigate }: NavListProps): import("react").JSX.Element;
26
+ export interface NavSectionProps {
27
+ /** The section's heading, such as "Workspace". */
28
+ label: string;
29
+ /** The section's NavItem rows. */
30
+ children: ReactNode;
31
+ }
32
+ /** A labelled group of NavItem rows inside a NavList. */
33
+ export declare function NavSection({ label, children }: NavSectionProps): import("react").JSX.Element;
34
+ interface NavItemBase {
35
+ /** The row's label. */
36
+ children: ReactNode;
37
+ /** A 16px icon before the label. */
38
+ icon?: ReactNode;
39
+ /** The current page: sets aria-current="page" and the active marker. */
40
+ active?: boolean;
41
+ /** Turns the row off. It stays focusable (aria-disabled) so keyboard users can reach it and hear why. */
42
+ disabled?: boolean;
43
+ /** Why the row is off, such as "Pro". Shown as a badge and read as the row's description. Only shown when disabled. */
44
+ reason?: string;
45
+ /** Content at the row's end, such as a count or an avatar. Hidden while a reason shows. */
46
+ trailing?: ReactNode;
47
+ /**
48
+ * Called each time the pointer enters the row or it receives focus, before it is chosen: start loading the destination
49
+ * (prefetch). It can fire more than once, so keep it idempotent. Not called while disabled.
50
+ */
51
+ onIntent?: () => void;
52
+ /** Placement only; the row's look comes from the component. */
53
+ className?: string;
54
+ }
55
+ export type NavItemProps = NavItemBase & ({
56
+ href: string;
57
+ onSelect?: never;
58
+ } | {
59
+ href?: never; /** An action, such as opening search. */
60
+ onSelect: () => void;
61
+ });
62
+ /** One row of a NavList: a link by `href`, or an action by `onSelect`. */
63
+ export declare function NavItem({ children, icon, active, disabled, reason, trailing, onIntent, className, href, onSelect }: NavItemProps): import("react").JSX.Element;
64
+ export default NavList;
@@ -0,0 +1,103 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { createContext, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
4
+ import { MenuHighlight } from "../../lib/menu-highlight.js";
5
+ import { SiliconLink } from "../../lib/silicon-provider.js";
6
+ import { Badge } from "../badge/badge.js";
7
+ import styles from "./nav-list.module.css";
8
+ const ITEM = "[data-nav-item]";
9
+ const boxOf = (item) => ({ top: item.offsetTop, height: item.offsetHeight, danger: false });
10
+ /** Lets NavItem reach its list without prop drilling: the active marker's position, and reporting a choice. */
11
+ const NavContext = createContext(null);
12
+ /**
13
+ * A sidebar's list of destinations: a `<nav>` with a list of NavItem rows, optionally grouped by NavSection. One hover highlight
14
+ * glides between rows for the pointer, and the current page's marker glides when the page changes.
15
+ */
16
+ export function NavList({ label, children, className, ref, onNavigate }) {
17
+ const root = useRef(null);
18
+ const [hover, setHover] = useState(null);
19
+ const [active, setActive] = useState(null);
20
+ const pointer = useRef(false);
21
+ // The latest handler, read at choice time, so a new function each render never changes the context.
22
+ const navigateHandler = useRef(onNavigate);
23
+ useEffect(() => { navigateHandler.current = onNavigate; });
24
+ const setRoot = useCallback((node) => {
25
+ root.current = node;
26
+ if (typeof ref === "function")
27
+ return ref(node);
28
+ if (ref)
29
+ ref.current = node;
30
+ }, [ref]);
31
+ // The active marker follows the item with aria-current. It glides only when the page changes, never on first paint.
32
+ const measureActive = useCallback(() => {
33
+ const item = root.current?.querySelector(`${ITEM}[aria-current="page"]`);
34
+ setActive(current => {
35
+ if (!item)
36
+ return null;
37
+ const next = boxOf(item);
38
+ return current && current.top === next.top && current.height === next.height ? current : { ...next, glide: current !== null };
39
+ });
40
+ }, []);
41
+ useLayoutEffect(() => {
42
+ measureActive();
43
+ const node = root.current;
44
+ if (!node || typeof ResizeObserver === "undefined")
45
+ return;
46
+ // A resize (a late font, a collapsing section) moves the marker at once; only a page change glides.
47
+ const observer = new ResizeObserver(() => {
48
+ const item = node.querySelector(`${ITEM}[aria-current="page"]`);
49
+ setActive(current => {
50
+ if (!item)
51
+ return null;
52
+ const next = boxOf(item);
53
+ return current && current.top === next.top && current.height === next.height ? current : { ...next, glide: false };
54
+ });
55
+ });
56
+ observer.observe(node);
57
+ return () => observer.disconnect();
58
+ }, [measureActive]);
59
+ function show(target, glide) {
60
+ const item = target instanceof HTMLElement ? target.closest(ITEM) : null;
61
+ if (!item || item.getAttribute("aria-disabled") === "true")
62
+ return setHover(null);
63
+ setHover(current => ({ ...boxOf(item), glide: glide && current !== null }));
64
+ }
65
+ const context = useMemo(() => ({ activeChanged: measureActive, navigate: (choice) => navigateHandler.current?.(choice) }), [measureActive]);
66
+ return (_jsx(NavContext.Provider, { value: context, children: _jsxs("nav", { ref: setRoot, "aria-label": label, className: [styles.list, className].filter(Boolean).join(" "), onPointerOver: (event) => { pointer.current = true; show(event.target, true); }, onPointerLeave: () => setHover(null), onKeyDownCapture: () => { pointer.current = false; }, onFocus: (event) => { if (!pointer.current && event.target.matches(":focus-visible"))
67
+ show(event.target, false); }, onBlur: (event) => { if (!pointer.current && !event.currentTarget.contains(event.relatedTarget))
68
+ setHover(null); }, children: [_jsx(MenuHighlight, { state: hover, className: styles.highlight }), _jsx(MenuHighlight, { state: active, className: styles.active }), _jsx("ul", { className: styles.items, children: children })] }) }));
69
+ }
70
+ /** A labelled group of NavItem rows inside a NavList. */
71
+ export function NavSection({ label, children }) {
72
+ const id = useId();
73
+ return (_jsxs("li", { className: styles.section, children: [_jsx("div", { id: id, className: styles.sectionLabel, children: label }), _jsx("ul", { className: styles.items, "aria-labelledby": id, children: children })] }));
74
+ }
75
+ /** One row of a NavList: a link by `href`, or an action by `onSelect`. */
76
+ export function NavItem({ children, icon, active = false, disabled = false, reason, trailing, onIntent, className, href, onSelect }) {
77
+ const nav = useContext(NavContext);
78
+ const reasonId = useId();
79
+ const showReason = disabled && reason;
80
+ useLayoutEffect(() => { nav?.activeChanged(); }, [active, nav]);
81
+ const content = _jsxs(_Fragment, { children: [icon ? _jsx("span", { className: styles.icon, "aria-hidden": "true", children: icon }) : null, _jsx("span", { className: styles.label, children: children }), showReason ? _jsx(Badge, { id: reasonId, size: "sm", className: styles.reason, "aria-hidden": "true", children: reason })
82
+ : trailing ? _jsx("span", { className: styles.trailing, children: trailing }) : null] });
83
+ const shared = {
84
+ className: [styles.item, className].filter(Boolean).join(" "),
85
+ "data-nav-item": "",
86
+ "aria-current": active ? "page" : undefined,
87
+ "aria-disabled": disabled || undefined,
88
+ "aria-describedby": showReason ? reasonId : undefined,
89
+ };
90
+ if (disabled) {
91
+ // No href and no action: it can be focused and read, but not followed.
92
+ return _jsx("li", { children: _jsx("span", { ...shared, role: href ? "link" : "button", tabIndex: 0, onClick: (event) => event.preventDefault(), children: content }) });
93
+ }
94
+ const intent = onIntent ? { onPointerEnter: onIntent, onFocus: onIntent } : {};
95
+ if (href !== undefined) {
96
+ // A modified click opens a new tab or window, so this page stays where it is: no choice to report.
97
+ const onClick = (event) => { if (event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey)
98
+ nav?.navigate({ href }); };
99
+ return _jsx("li", { children: _jsx(SiliconLink, { ...shared, ...intent, href: href, onClick: onClick, children: content }) });
100
+ }
101
+ return _jsx("li", { children: _jsx("button", { ...shared, ...intent, type: "button", onClick: () => { nav?.navigate({}); onSelect?.(); }, children: content }) });
102
+ }
103
+ export default NavList;
@@ -0,0 +1,25 @@
1
+ /* The list is the positioning box for both markers, so rows measure their offsetTop against it. Place it on --background or
2
+ --surface; on a --surface-muted panel the hover fill has nothing to show against. */
3
+ .list { position: relative; display: block; font-family: var(--font-body); letter-spacing: var(--tracking-body); }
4
+ .items { display: grid; gap: var(--space-hair); margin: 0; padding: 0; list-style: none; }
5
+ .section + .section, .items > li + .section { margin-top: var(--space-group); }
6
+ .sectionLabel { padding: var(--space-tight) var(--space-related); color: var(--text-muted); font-size: var(--text-xs); font-weight: 500; line-height: var(--leading-body); }
7
+
8
+ /* Two markers under the rows: the current page (stronger), and the pointer's hover, which glides between rows. */
9
+ .active, .highlight { position: absolute; top: 0; right: 0; left: 0; border-radius: var(--radius-control); corner-shape: var(--corner-shape); pointer-events: none; }
10
+ .active { background: var(--surface-pressed); }
11
+ .highlight { background: var(--surface-muted); }
12
+
13
+ .item { position: relative; display: flex; box-sizing: border-box; width: 100%; min-height: 2rem; align-items: center; gap: var(--space-related); margin: 0; padding: 0 var(--space-related); border: 0; border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: transparent; color: var(--text-secondary); font: inherit; font-size: var(--text-sm); line-height: var(--leading-body); text-align: left; text-decoration: none; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: color var(--duration-fast) var(--ease-standard); }
14
+ .item:hover, .item:focus-visible, .item[aria-current="page"] { color: var(--foreground); }
15
+ .item[aria-current="page"] { font-weight: 500; }
16
+ .item[aria-disabled="true"] { color: var(--text-muted); cursor: not-allowed; }
17
+ .icon { display: inline-flex; flex: none; color: var(--text-secondary); }
18
+ .icon svg { width: 16px; height: 16px; }
19
+ .item[aria-current="page"] .icon, .item:hover .icon { color: var(--foreground); }
20
+ .item[aria-disabled="true"] .icon { color: var(--text-muted); }
21
+ .label { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
22
+ .trailing { display: inline-flex; flex: none; align-items: center; color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
23
+ .reason { flex: none; }
24
+
25
+ @media (prefers-reduced-motion: reduce) { .item { transition: none; } }
@@ -2,8 +2,8 @@
2
2
  .trigger[data-state="open"], .trigger:active { border-color: var(--border-strong); background: var(--surface-muted); }
3
3
  @media (hover: hover) and (pointer: fine) { .trigger:hover { border-color: var(--border-strong); background: var(--surface-muted); } }
4
4
  .trigger > span:first-child { display: grid; place-items: center; }
5
- .badge { position: absolute; top: -6px; right: -6px; display: grid; min-width: 20px; height: 20px; padding: 0 5px; place-items: center; overflow: hidden; border: 2px solid var(--surface); border-radius: var(--radius-full); corner-shape: round; background: var(--foreground); color: var(--background); font-size: var(--text-xs); font-weight: 500; line-height: 1; font-variant-numeric: tabular-nums; }
6
- .panel { z-index: 90; display: flex; flex-direction: column; width: min(424px, calc(100vw - 24px)); max-height: min(590px, calc(100vh - 24px)); overflow: hidden; 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); font-family: var(--font-body); letter-spacing: var(--tracking-body); transform-origin: var(--radix-popover-content-transform-origin); }
5
+ .badge { position: absolute; top: calc(var(--space-related) * -1); right: calc(var(--space-related) * -1); display: grid; min-width: 20px; height: 20px; padding: 0 var(--space-tight); place-items: center; overflow: hidden; border: 2px solid var(--surface); border-radius: var(--radius-full); corner-shape: round; background: var(--foreground); color: var(--background); font-size: var(--text-xs); font-weight: 500; line-height: 1; font-variant-numeric: tabular-nums; }
6
+ .panel { z-index: var(--layer-popover); display: flex; flex-direction: column; width: min(424px, calc(100vw - 24px)); max-height: min(590px, calc(100vh - 24px)); overflow: hidden; 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); font-family: var(--font-body); letter-spacing: var(--tracking-body); transform-origin: var(--radix-popover-content-transform-origin); }
7
7
  /* The panel grows out of the bell: it starts a few pixels toward the trigger on whichever side Radix placed it, and leaves faster than it arrives. */
8
8
  .panel { --panel-x: 0px; --panel-y: -6px; }
9
9
  .panel[data-side="top"] { --panel-y: 6px; }
@@ -11,61 +11,61 @@
11
11
  .panel[data-side="right"] { --panel-x: -6px; --panel-y: 0px; }
12
12
  .panel[data-state="open"] { animation: panel-open var(--duration-standard) var(--ease-enter) both; }
13
13
  .panel[data-state="closed"] { animation: panel-close var(--duration-instant) var(--ease-standard) both; }
14
- .header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 23px 24px 18px; }
14
+ .header { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-group); padding: var(--space-section) var(--space-section) var(--space-comfort); }
15
15
  .heading { display: flex; align-items: center; gap: var(--space-related); }
16
16
  .heading h2 { margin: 0; font-size: var(--text-xl); font-weight: 500; letter-spacing: var(--tracking-body); line-height: 1.2; }
17
- .count { display: grid; min-width: 21px; height: 21px; padding: 0 5px; place-items: center; overflow: hidden; border-radius: var(--radius-detail); corner-shape: var(--corner-shape); background: var(--surface-muted); color: var(--text-secondary); font-size: var(--text-xs); line-height: 1; font-variant-numeric: tabular-nums; }
18
- .header p { position: relative; margin: 4px 0 0; color: var(--text-secondary); font-size: var(--text-xs); line-height: var(--leading-body); }
17
+ .count { display: grid; min-width: 21px; height: 21px; padding: 0 var(--space-tight); place-items: center; overflow: hidden; border-radius: var(--radius-detail); corner-shape: var(--corner-shape); background: var(--surface-muted); color: var(--text-secondary); font-size: var(--text-xs); line-height: 1; font-variant-numeric: tabular-nums; }
18
+ .header p { position: relative; margin: var(--space-tight) 0 0; color: var(--text-secondary); font-size: var(--text-xs); line-height: var(--leading-body); }
19
19
  .close { display: grid; width: 30px; height: 30px; flex: none; place-items: center; border: 0; border-radius: var(--radius-detail); corner-shape: var(--corner-shape); background: transparent; color: var(--text-muted); cursor: pointer; transition: transform var(--duration-spring) var(--ease-spring), color var(--duration-fast) var(--ease-standard), background var(--duration-fast) var(--ease-standard); }
20
20
  .close:active { transform: scale(.96); }
21
- .toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 52px; padding: 0 17px 13px 20px; border-bottom: 1px solid var(--border-subtle); }
22
- .viewSwitch { display: inline-flex; align-items: center; gap: 3px; }
23
- .viewSwitch button { position: relative; isolation: isolate; min-width: 61px; height: 31px; padding: 0 10px; border: 0; border-radius: var(--radius-detail); corner-shape: var(--corner-shape); background: transparent; color: var(--text-secondary); font: inherit; font-size: var(--text-xs); cursor: pointer; transition: color var(--duration-fast) var(--ease-standard); }
21
+ .toolbar { display: flex; align-items: center; justify-content: space-between; gap: var(--space-group); min-height: 52px; padding: 0 var(--space-inset) var(--space-group) var(--space-comfort); border-bottom: 1px solid var(--border-subtle); }
22
+ .viewSwitch { display: inline-flex; align-items: center; gap: var(--space-tight); }
23
+ .viewSwitch button { position: relative; isolation: isolate; min-width: 61px; height: 31px; padding: 0 var(--space-group); border: 0; border-radius: var(--radius-detail); corner-shape: var(--corner-shape); background: transparent; color: var(--text-secondary); font: inherit; font-size: var(--text-xs); cursor: pointer; transition: color var(--duration-fast) var(--ease-standard); }
24
24
  .viewSwitch .viewActive { color: var(--foreground); }
25
25
  .viewSwitch button > span:last-child { position: relative; z-index: 1; }
26
26
  .viewHighlight { position: absolute; z-index: -1; inset: 0; border-radius: var(--radius-detail); corner-shape: var(--corner-shape); background: var(--surface-muted); }
27
- .markAll { display: inline-flex; align-items: center; gap: 6px; padding: 6px 0; border: 0; background: transparent; color: var(--text-secondary); font: inherit; font-size: var(--text-xs); white-space: nowrap; cursor: pointer; transition: transform var(--duration-spring) var(--ease-spring), color var(--duration-fast) var(--ease-standard); }
27
+ .markAll { display: inline-flex; align-items: center; gap: var(--space-related); padding: var(--space-related) 0; border: 0; background: transparent; color: var(--text-secondary); font: inherit; font-size: var(--text-xs); white-space: nowrap; cursor: pointer; transition: transform var(--duration-spring) var(--ease-spring), color var(--duration-fast) var(--ease-standard); }
28
28
  .markAll:active { transform: scale(.97); }
29
- .list { min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 9px 11px 10px; scrollbar-width: none; }
29
+ .list { min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: var(--space-related) var(--space-group) var(--space-group); scrollbar-width: none; }
30
30
  .list::-webkit-scrollbar { display: none; }
31
31
  .row { overflow: hidden; }
32
32
  /* Rows settle in with a short cascade when the panel opens or a view adds them. */
33
- .item { position: relative; padding: 10px; border-radius: calc(var(--radius-container) - 12px); corner-shape: var(--corner-shape); transition: background var(--duration-fast) var(--ease-standard); animation: item-in var(--duration-standard) var(--ease-enter) calc(var(--index, 0) * 35ms) both; }
33
+ .item { position: relative; padding: var(--space-group); border-radius: calc(var(--radius-container) - var(--space-group)); corner-shape: var(--corner-shape); transition: background var(--duration-fast) var(--ease-standard); animation: item-in var(--duration-standard) var(--ease-enter) calc(var(--index, 0) * 35ms) both; }
34
34
  .itemExpanded { background: var(--surface-muted); }
35
35
  .itemRead .itemTitle strong { color: var(--text-secondary); }
36
- .itemMain { display: flex; align-items: flex-start; gap: 11px; min-width: 0; }
36
+ .itemMain { display: flex; align-items: flex-start; gap: var(--space-group); min-width: 0; }
37
37
  .eventIcon { display: grid; width: 36px; height: 36px; flex: none; place-items: center; color: var(--accent); }
38
38
  .eventIcon.success { color: var(--success); }
39
39
  .eventIcon.warning { color: var(--warning); }
40
- .itemToggle { display: grid; min-width: 0; flex: 1; gap: 4px; padding: 2px 0 0; border: 0; background: transparent; color: var(--foreground); text-align: left; font: inherit; cursor: pointer; }
41
- .itemTitle { display: flex; align-items: center; gap: 6px; min-width: 0; min-height: 20px; }
40
+ .itemToggle { display: grid; min-width: 0; flex: 1; gap: var(--space-tight); padding: var(--space-hair) 0 0; border: 0; background: transparent; color: var(--foreground); text-align: left; font: inherit; cursor: pointer; }
41
+ .itemTitle { display: flex; align-items: center; gap: var(--space-related); min-width: 0; min-height: 20px; }
42
42
  .itemTitle strong { overflow: hidden; font-size: var(--text-sm); font-weight: 500; line-height: 1.4; text-overflow: ellipsis; white-space: nowrap; }
43
43
  .unreadDot { display: block; width: 6px; height: 6px; flex: none; border-radius: 50%; corner-shape: round; background: var(--foreground); }
44
44
  .itemPreview { overflow: hidden; color: var(--text-secondary); font-size: var(--text-xs); line-height: var(--leading-body); text-overflow: ellipsis; white-space: nowrap; }
45
- .time { flex: none; padding-top: 4px; color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); font-variant-numeric: tabular-nums; white-space: nowrap; }
46
- .details { overflow: hidden; padding-left: 47px; }
47
- .details p { margin: 12px 0; color: var(--text-secondary); font-size: var(--text-xs); line-height: var(--leading-body); }
48
- .itemActions { display: flex; flex-wrap: wrap; gap: 6px; padding-bottom: 5px; }
49
- .itemActions button { display: inline-flex; align-items: center; gap: 5px; height: 30px; padding: 0 10px; overflow: clip; border: 1px solid var(--border); border-radius: var(--radius-detail); corner-shape: var(--corner-shape); background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-xs); line-height: 1; white-space: nowrap; cursor: pointer; transition: transform var(--duration-spring) var(--ease-spring), border-color var(--duration-fast) var(--ease-standard), background var(--duration-fast) var(--ease-standard); }
45
+ .time { flex: none; padding-top: var(--space-tight); color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); font-variant-numeric: tabular-nums; white-space: nowrap; }
46
+ .details { overflow: hidden; padding-left: var(--space-page); }
47
+ .details p { margin: var(--space-group) 0; color: var(--text-secondary); font-size: var(--text-xs); line-height: var(--leading-body); }
48
+ .itemActions { display: flex; flex-wrap: wrap; gap: var(--space-related); padding-bottom: var(--space-tight); }
49
+ .itemActions button { display: inline-flex; align-items: center; gap: var(--space-tight); height: 30px; padding: 0 var(--space-group); overflow: clip; border: 1px solid var(--border); border-radius: var(--radius-detail); corner-shape: var(--corner-shape); background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-xs); line-height: 1; white-space: nowrap; cursor: pointer; transition: transform var(--duration-spring) var(--ease-spring), border-color var(--duration-fast) var(--ease-standard), background var(--duration-fast) var(--ease-standard); }
50
50
  .itemActions button:active { transform: scale(.97); }
51
51
  /* Quick press, spring release: the resting transition above carries the spring back. */
52
52
  .close:active, .markAll:active, .itemActions button:active, .footer button:active { transition-duration: var(--duration-instant); transition-timing-function: var(--ease-standard); }
53
53
  .morph { display: inline-flex; align-items: center; }
54
- .morphContent { position: relative; display: inline-flex; width: max-content; align-items: center; gap: 5px; }
54
+ .morphContent { position: relative; display: inline-flex; width: max-content; align-items: center; gap: var(--space-tight); }
55
55
  .actionIcon, .actionGlyph { position: relative; display: grid; place-items: center; }
56
56
  .swap { position: relative; display: inline-block; max-width: 100%; vertical-align: top; }
57
57
  .swapLine { display: block; }
58
58
  .roll { position: relative; display: inline-grid; vertical-align: top; }
59
59
  .rollValue { display: block; }
60
60
  .emptyFrame { overflow: hidden; }
61
- .empty { display: grid; justify-items: center; align-content: center; min-height: 190px; padding: 22px 12px; text-align: center; }
62
- .empty > svg { margin-bottom: 13px; color: var(--text-muted); }
61
+ .empty { display: grid; justify-items: center; align-content: center; min-height: 190px; padding: var(--space-section) var(--space-group); text-align: center; }
62
+ .empty > svg { margin-bottom: var(--space-group); color: var(--text-muted); }
63
63
  .empty strong { font-size: var(--text-sm); font-weight: 500; }
64
- .empty p { margin: 4px 0 0; color: var(--text-secondary); font-size: var(--text-xs); }
65
- .empty button { margin-top: 16px; padding: 0; border: 0; background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-xs); text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
64
+ .empty p { margin: var(--space-tight) 0 0; color: var(--text-secondary); font-size: var(--text-xs); }
65
+ .empty button { margin-top: var(--space-inset); padding: 0; border: 0; background: transparent; color: var(--foreground); font: inherit; font-size: var(--text-xs); text-decoration: underline; text-underline-offset: 3px; cursor: pointer; }
66
66
  .footerFrame { flex: none; overflow: hidden; }
67
- .footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 46px; padding: 8px 22px; border-top: 1px solid var(--border-subtle); color: var(--text-muted); font-size: var(--text-xs); }
68
- .footer button { padding: 4px 0; border: 0; background: transparent; color: var(--text-secondary); font: inherit; font-size: var(--text-xs); cursor: pointer; transition: transform var(--duration-spring) var(--ease-spring), color var(--duration-fast) var(--ease-standard); }
67
+ .footer { display: flex; align-items: center; justify-content: space-between; gap: var(--space-group); min-height: 46px; padding: var(--space-related) var(--space-section); border-top: 1px solid var(--border-subtle); color: var(--text-muted); font-size: var(--text-xs); }
68
+ .footer button { padding: var(--space-tight) 0; border: 0; background: transparent; color: var(--text-secondary); font: inherit; font-size: var(--text-xs); cursor: pointer; transition: transform var(--duration-spring) var(--ease-spring), color var(--duration-fast) var(--ease-standard); }
69
69
  .footer button:active { transform: scale(.97); }
70
70
  @media (hover: hover) and (pointer: fine) {
71
71
  .close:hover, .item:hover { background: var(--surface-muted); }
@@ -75,5 +75,5 @@
75
75
  @keyframes panel-open { from { opacity: 0; transform: translate(var(--panel-x), var(--panel-y)) scale(.97); } }
76
76
  @keyframes panel-close { to { opacity: 0; transform: translate(calc(var(--panel-x) / 2), calc(var(--panel-y) / 2)) scale(.98); } }
77
77
  @keyframes item-in { from { opacity: 0; transform: translateY(4px); } }
78
- @media (max-width: 380px) { .header { padding: 20px 16px 16px; } .heading h2 { font-size: var(--text-lg); } .toolbar { align-items: flex-start; padding: 0 12px 12px; } .list { padding: 8px 5px; } .item { padding-inline: 7px; } .itemMain { gap: 7px; } .details { padding-left: 43px; } .itemActions { gap: 5px; } .itemActions button { padding-inline: 7px; } .markAll span { display: none; } .footer { padding-inline: 16px; } }
78
+ @media (max-width: 380px) { .header { padding: var(--space-comfort) var(--space-inset) var(--space-inset); } .heading h2 { font-size: var(--text-lg); } .toolbar { align-items: flex-start; padding: 0 var(--space-group) var(--space-group); } .list { padding: var(--space-related) var(--space-tight); } .item { padding-inline: var(--space-related); } .itemMain { gap: var(--space-related); } .details { padding-left: var(--space-page); } .itemActions { gap: var(--space-tight); } .itemActions button { padding-inline: var(--space-related); } .markAll span { display: none; } .footer { padding-inline: var(--space-inset); } }
79
79
  @media (prefers-reduced-motion: reduce) { .panel[data-state], .trigger, .close, .viewSwitch button, .markAll, .item, .itemActions button, .footer button { animation: none; transition-duration: 0ms; } .close:active, .markAll:active, .itemActions button:active, .footer button:active { transform: none; } }
@@ -12,7 +12,7 @@
12
12
  .label[data-scrub] { cursor: ew-resize; touch-action: pan-y; user-select: none; -webkit-user-select: none; }
13
13
  /* A hairline shell. Focus darkens the border only (the library draws no rings); a typed value past a limit tints it with the warning
14
14
  role instead, calm rather than alarming. Set --number-field-width to resize the shell. */
15
- .control { display: flex; align-items: center; width: min(100%, var(--nf-width)); min-height: var(--nf-height); gap: 2px; border: 1px solid var(--border); border-radius: var(--radius-control); padding: var(--nf-inset); background: var(--surface); transition: border-color var(--duration-fast) var(--ease-standard), background-color var(--duration-standard) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard); }
15
+ .control { display: flex; align-items: center; width: min(100%, var(--nf-width)); min-height: var(--nf-height); gap: var(--space-hair); border: 1px solid var(--border); border-radius: var(--radius-control); padding: var(--nf-inset); background: var(--surface); transition: border-color var(--duration-fast) var(--ease-standard), background-color var(--duration-standard) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard); }
16
16
  @media (hover: hover) and (pointer: fine) { .control:hover, .field:has(.label[data-scrub]:hover) .control { border-color: var(--border-strong); } }
17
17
  .control[data-scrubbing] { border-color: var(--foreground); }
18
18
  .control:has(input:focus) { border-color: var(--foreground); }
@@ -20,7 +20,7 @@
20
20
  .control[data-warn]:has(input:focus) { border-color: color-mix(in oklab, var(--warning) 80%, var(--border)); }
21
21
  .control[data-disabled] { opacity: .5; background: var(--surface-muted); }
22
22
  /* Radius steps in by the shell's inset, so the pressed fill sits concentric with the shell. */
23
- .step { display: grid; width: var(--nf-step); height: var(--nf-step); flex: 0 0 var(--nf-step); place-items: center; border: 0; border-radius: calc(var(--radius-control) - var(--nf-inset) - 1px); padding: 0; background: transparent; color: var(--text-secondary); cursor: pointer; touch-action: manipulation; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), opacity var(--duration-standard) var(--ease-standard); }
23
+ .step { display: grid; width: var(--nf-step); height: var(--nf-step); flex: 0 0 var(--nf-step); place-items: center; border: 0; border-radius: calc(var(--radius-control) - var(--nf-inset) - var(--space-hair)); padding: 0; background: transparent; color: var(--text-secondary); cursor: pointer; touch-action: manipulation; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), opacity var(--duration-standard) var(--ease-standard); }
24
24
  .icon { display: grid; place-items: center; }
25
25
  .icon svg { transition: transform var(--duration-spring) var(--ease-spring); }
26
26
  @media (hover: hover) and (pointer: fine) { .step:hover:not(:disabled, [aria-disabled="true"]) { background: var(--surface-muted); color: var(--foreground); } }
@@ -1,7 +1,7 @@
1
1
  /* No row gap: grid tracks clamp a negative margin at zero, so a closed message row would still pay the gap. Each row carries its own space instead. */
2
2
  .field { display: grid; min-width: 0; }
3
- .label { margin-bottom: 8px; color: var(--foreground); font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); }
4
- .inputs { position: relative; display: flex; width: fit-content; max-width: 100%; gap: 8px; }
3
+ .label { margin-bottom: var(--space-related); color: var(--foreground); font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); }
4
+ .inputs { position: relative; display: flex; width: fit-content; max-width: 100%; gap: var(--space-related); }
5
5
  .slot { position: relative; display: grid; width: 42px; height: 48px; min-width: 0; flex: 0 1 42px; }
6
6
  /* The native text is transparent (caret stays visible); .glyph draws the digit so it can rise in. */
7
7
  .input { width: 100%; height: 100%; min-width: 0; border: 1px solid var(--border); border-radius: var(--radius-control); background: var(--surface); color: transparent; caret-color: var(--accent); font: inherit; font-size: var(--text-lg); font-variant-numeric: tabular-nums; text-align: center; outline: none; transition: border-color var(--duration-fast) var(--ease-standard), background-color var(--duration-fast) var(--ease-standard); }
@@ -17,12 +17,12 @@
17
17
  .inputs[data-invalid="true"] .ring { border-color: var(--danger); }
18
18
  /* The row's space is padding inside the clipped slot, so the height animates from a true zero. */
19
19
  .messageSlot { display: block; overflow: hidden; }
20
- .description, .error { display: block; padding-top: 8px; font-size: var(--text-xs); line-height: var(--leading-body); }
20
+ .description, .error { display: block; padding-top: var(--space-related); font-size: var(--text-xs); line-height: var(--leading-body); }
21
21
  .description { color: var(--text-muted); }.error { color: var(--danger); }
22
22
  /* Words are measured against this box when they pop out to leave, so it must be the positioned parent. */
23
23
  .words { position: relative; display: block; }
24
24
  .word { display: inline-block; white-space: pre; }
25
25
  .srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
26
- @media (max-width: 360px) { .inputs { gap: 6px; }.slot { height: 44px; } }
26
+ @media (max-width: 360px) { .inputs { gap: var(--space-related); }.slot { height: 44px; } }
27
27
  @media (forced-colors: active) { .ring { display: none; } .input:focus-visible { outline: 2px solid CanvasText; outline-offset: 2px; } .input { color: CanvasText; } .glyph { display: none; } }
28
28
  @media (prefers-reduced-motion: reduce) { .input, .ring { transition: none; } }
@@ -2,7 +2,7 @@
2
2
  .field { display: grid; min-width: 0; }
3
3
  .field label { margin-bottom: var(--space-related); font-size: var(--text-sm); font-weight: 500; }
4
4
  /* Focus shows as a darker border only; the shell never changes size. The toggle sits with an even inset on top, bottom and right. */
5
- .shell { display: flex; min-height: var(--control-height-md); align-items: center; border: 1px solid var(--border-strong); border-radius: var(--radius-control); padding: 0 calc((var(--control-height-md) - 2px - 30px) / 2) 0 var(--space-group); background: var(--surface); transition: border-color var(--duration-fast) var(--ease-standard); }
5
+ .shell { display: flex; min-height: var(--control-height-md); align-items: center; border: 1px solid var(--border-strong); border-radius: var(--radius-control); padding: 0 calc((var(--control-height-md) - var(--space-hair) - var(--space-block)) / 2) 0 var(--space-group); background: var(--surface); transition: border-color var(--duration-fast) var(--ease-standard); }
6
6
  @media (hover: hover) and (pointer: fine) { .shell:hover:not(:focus-within) { border-color: var(--foreground); } }
7
7
  .shell:focus-within { border-color: var(--foreground); }
8
8
  .shell:has(.input[aria-invalid="true"]) { border-color: var(--danger); }