@teamofsilicons/silicon-ui 0.2.0 → 0.4.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.
- package/README.md +4 -3
- package/check.mjs +14 -5
- package/components/activity-heatmap/activity-heatmap.module.css +4 -4
- package/components/alert/alert.module.css +1 -1
- package/components/animated-counter/animated-counter.js +1 -1
- package/components/announcement-bar/announcement-bar.module.css +6 -6
- package/components/avatar/avatar.d.ts +2 -2
- package/components/avatar/avatar.js +1 -1
- package/components/avatar/avatar.module.css +5 -2
- package/components/avatar-group/avatar-group.module.css +4 -4
- package/components/badge/badge.module.css +4 -4
- package/components/bar-chart/bar-chart.js +1 -1
- package/components/bar-chart/bar-chart.module.css +4 -4
- package/components/billing-toggle/billing-toggle.js +2 -2
- package/components/billing-toggle/billing-toggle.module.css +10 -10
- package/components/bottom-sheet/bottom-sheet.js +1 -1
- package/components/bottom-sheet/bottom-sheet.module.css +4 -5
- package/components/breadcrumb/breadcrumb.d.ts +8 -2
- package/components/breadcrumb/breadcrumb.js +21 -9
- package/components/breadcrumb/breadcrumb.module.css +7 -3
- package/components/brush-chart/brush-chart.module.css +9 -9
- package/components/button/button.d.ts +7 -2
- package/components/button/button.js +3 -1
- package/components/button/button.module.css +10 -1
- package/components/button-group/button-group.module.css +7 -7
- package/components/calendar/calendar.js +15 -5
- package/components/calendar/calendar.module.css +13 -10
- package/components/card/card.js +4 -4
- package/components/card/card.module.css +2 -2
- package/components/card-stack/card-stack.js +2 -2
- package/components/card-stack/card-stack.module.css +2 -3
- package/components/carousel/carousel.module.css +3 -3
- package/components/chat-thread/chat-thread.module.css +41 -41
- package/components/checkbox/checkbox.module.css +1 -1
- package/components/chip-group/chip-group.js +1 -1
- package/components/chip-group/chip-group.module.css +2 -2
- package/components/code-block/code-block.module.css +7 -14
- package/components/color-picker/color-picker.module.css +20 -21
- package/components/combobox/combobox.js +5 -1
- package/components/combobox/combobox.module.css +2 -2
- package/components/command-palette/command-palette.js +18 -11
- package/components/command-palette/command-palette.module.css +18 -18
- package/components/comment-thread/comment-thread.module.css +35 -35
- package/components/confirm-morph/confirm-morph.js +5 -5
- package/components/confirm-morph/confirm-morph.module.css +9 -9
- package/components/context-menu/context-menu.js +10 -28
- package/components/context-menu/context-menu.module.css +3 -3
- package/components/copy-button/copy-button.js +4 -4
- package/components/countdown/countdown.module.css +4 -4
- package/components/date-picker/date-picker.js +3 -3
- package/components/date-picker/date-picker.module.css +7 -7
- package/components/date-range-picker/date-range-picker.js +13 -10
- package/components/date-range-picker/date-range-picker.module.css +23 -22
- package/components/dialog/dialog.module.css +2 -2
- package/components/donut-chart/donut-chart.module.css +5 -5
- package/components/drawer/drawer.module.css +7 -7
- package/components/dropdown-menu/dropdown-menu.d.ts +71 -1
- package/components/dropdown-menu/dropdown-menu.js +100 -6
- package/components/dropdown-menu/dropdown-menu.module.css +15 -5
- package/components/elastic-slider/elastic-slider.js +2 -2
- package/components/elastic-slider/elastic-slider.module.css +4 -4
- package/components/expandable-card/expandable-card.js +1 -1
- package/components/expandable-card/expandable-card.module.css +1 -1
- package/components/expanding-button-group/expanding-button-group.module.css +3 -3
- package/components/expanding-search/expanding-search.js +3 -3
- package/components/expanding-search/expanding-search.module.css +9 -9
- package/components/file-dropzone/file-dropzone.js +3 -3
- package/components/file-dropzone/file-dropzone.module.css +21 -21
- package/components/file-upload/file-upload.module.css +7 -7
- package/components/filter-toolbar/filter-toolbar.js +7 -3
- package/components/filter-toolbar/filter-toolbar.module.css +9 -9
- package/components/floating-button-group/floating-button-group.module.css +7 -7
- package/components/gauge/gauge.js +1 -1
- package/components/gauge/gauge.module.css +2 -2
- package/components/hover-card/hover-card.js +2 -2
- package/components/hover-card/hover-card.module.css +2 -2
- package/components/image-compare/image-compare.module.css +4 -4
- package/components/inline-edit/inline-edit.module.css +12 -12
- package/components/json-viewer/json-viewer.module.css +12 -12
- package/components/line-chart/line-chart.js +1 -1
- package/components/line-chart/line-chart.module.css +8 -8
- package/components/mention-input/mention-input.js +4 -2
- package/components/mention-input/mention-input.module.css +8 -8
- package/components/metric-card/metric-card.module.css +1 -1
- package/components/money-input/money-input.js +6 -3
- package/components/money-input/money-input.module.css +10 -10
- package/components/morph-select/morph-select.js +7 -5
- package/components/morph-select/morph-select.module.css +13 -13
- package/components/multi-select/multi-select.js +4 -2
- package/components/multi-select/multi-select.module.css +6 -6
- package/components/nav-list/nav-list.d.ts +64 -0
- package/components/nav-list/nav-list.js +96 -0
- package/components/nav-list/nav-list.module.css +25 -0
- package/components/notification-center/notification-center.module.css +27 -27
- package/components/number-field/number-field.js +3 -3
- package/components/number-field/number-field.module.css +3 -3
- package/components/otp-input/otp-input.module.css +4 -4
- package/components/pagination/pagination.module.css +1 -1
- package/components/password-field/password-field.module.css +2 -2
- package/components/password-strength/password-strength.module.css +8 -8
- package/components/phone-input/phone-input.js +9 -7
- package/components/phone-input/phone-input.module.css +19 -19
- package/components/popover/popover.module.css +2 -2
- package/components/progress/progress.module.css +1 -1
- package/components/radio-cards/radio-cards.module.css +8 -8
- package/components/radio-group/radio-group.js +4 -1
- package/components/radio-group/radio-group.module.css +4 -3
- package/components/resizable-panels/resizable-panels.module.css +5 -5
- package/components/rich-text-editor/rich-text-editor.js +3 -3
- package/components/rich-text-editor/rich-text-editor.module.css +12 -12
- package/components/ridgeline/ridgeline.module.css +7 -7
- package/components/scroll-area/scroll-area.module.css +8 -8
- package/components/segmented-control/segmented-control.d.ts +3 -1
- package/components/segmented-control/segmented-control.js +5 -2
- package/components/segmented-control/segmented-control.module.css +9 -4
- package/components/select/select.module.css +5 -5
- package/components/shortcut-recorder/shortcut-recorder.js +5 -5
- package/components/shortcut-recorder/shortcut-recorder.module.css +16 -16
- package/components/signature-pad/signature-pad.module.css +11 -11
- package/components/slider/slider.module.css +4 -4
- package/components/slope-chart/slope-chart.module.css +7 -7
- package/components/slot-text/slot-text.js +2 -1
- package/components/sortable-data-table/sortable-data-table.module.css +4 -4
- package/components/sparkline/sparkline.js +1 -1
- package/components/split-button/split-button.module.css +6 -6
- package/components/stepper/stepper.module.css +3 -3
- package/components/streamgraph/streamgraph.module.css +7 -7
- package/components/swipe-actions/swipe-actions.js +2 -2
- package/components/swipe-actions/swipe-actions.module.css +5 -5
- package/components/switch/switch.js +2 -2
- package/components/switch/switch.module.css +1 -1
- package/components/tabs/tabs.d.ts +9 -2
- package/components/tabs/tabs.js +11 -5
- package/components/tabs/tabs.module.css +10 -3
- package/components/tag-input/tag-input.module.css +4 -4
- package/components/text-reveal/text-reveal.js +1 -1
- package/components/time-picker/time-picker.js +4 -2
- package/components/time-picker/time-picker.module.css +3 -3
- package/components/timeline/timeline.js +4 -4
- package/components/timeline/timeline.module.css +3 -3
- package/components/toast/toast.js +1 -1
- package/components/toast/toast.module.css +5 -5
- package/components/toast-stack/toast-stack.js +1 -1
- package/components/toast-stack/toast-stack.module.css +5 -5
- package/components/tooltip/tooltip.module.css +1 -1
- package/components/tree-view/tree-view.js +16 -12
- package/components/tree-view/tree-view.module.css +10 -8
- package/components/treemap/treemap.module.css +13 -13
- package/components/usage-meter/usage-meter.js +1 -1
- package/components/usage-meter/usage-meter.module.css +5 -5
- package/components/user-menu/user-menu.js +16 -14
- package/components/user-menu/user-menu.module.css +22 -22
- package/components/waffle-chart/waffle-chart.module.css +7 -7
- package/components.json +416 -1
- package/eslint.js +35 -3
- package/lib/fluid-hover.d.ts +83 -0
- package/lib/fluid-hover.js +184 -0
- package/lib/menu-highlight.d.ts +24 -11
- package/lib/menu-highlight.js +29 -28
- package/lib/motion-tokens.d.ts +76 -18
- package/lib/motion-tokens.js +45 -10
- package/package.json +1 -1
- package/styles.css +50 -7
|
@@ -14,32 +14,32 @@
|
|
|
14
14
|
.scroller { min-height: 0; flex: 1; overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-gutter: stable; }
|
|
15
15
|
/* The content springs up from where it was when the thread grows; the clip keeps that travel out of the scroll range. */
|
|
16
16
|
.track { overflow: clip; }
|
|
17
|
-
.content { padding:
|
|
17
|
+
.content { padding: var(--space-group) var(--space-inset) var(--space-inset); will-change: transform; }
|
|
18
18
|
|
|
19
19
|
.list { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; }
|
|
20
20
|
|
|
21
|
-
.day { display: flex; align-items: center; gap:
|
|
21
|
+
.day { display: flex; align-items: center; gap: var(--space-group); padding: var(--space-comfort) var(--space-tight) var(--space-tight); color: var(--text-muted); font-size: var(--text-xs); font-weight: 500; }
|
|
22
22
|
.day::before, .day::after { height: 1px; flex: 1; background: var(--border-subtle); content: ""; }
|
|
23
|
-
.day:first-child { padding-top:
|
|
23
|
+
.day:first-child { padding-top: var(--space-tight); }
|
|
24
24
|
.day > span:empty::before { content: "\00a0"; }
|
|
25
25
|
|
|
26
|
-
.row { position: relative; display: flex; align-items: flex-end; gap:
|
|
27
|
-
.row[data-first] { margin-top:
|
|
28
|
-
.day + .row { margin-top:
|
|
26
|
+
.row { position: relative; display: flex; align-items: flex-end; gap: var(--space-related); margin-top: var(--space-hair); }
|
|
27
|
+
.row[data-first] { margin-top: var(--space-inset); }
|
|
28
|
+
.day + .row { margin-top: var(--space-group); }
|
|
29
29
|
.row[data-mine] { justify-content: flex-end; }
|
|
30
30
|
|
|
31
31
|
.gutter { width: 28px; flex: none; align-self: flex-end; }
|
|
32
|
-
.stack { display: flex; min-width: 0; max-width: min(78%, 34rem); flex-direction: column; align-items: flex-start; gap:
|
|
32
|
+
.stack { display: flex; min-width: 0; max-width: min(78%, 34rem); flex-direction: column; align-items: flex-start; gap: var(--space-tight); }
|
|
33
33
|
.row[data-mine] .stack { align-items: flex-end; }
|
|
34
34
|
|
|
35
|
-
.meta { display: flex; align-items: baseline; gap:
|
|
35
|
+
.meta { display: flex; align-items: baseline; gap: var(--space-related); margin: 0 0 var(--space-hair); padding: 0 var(--space-group); color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
|
|
36
36
|
.author { color: var(--text-secondary); font-weight: 500; }
|
|
37
37
|
|
|
38
|
-
.line { position: relative; display: flex; max-width: 100%; align-items: center; gap:
|
|
38
|
+
.line { position: relative; display: flex; max-width: 100%; align-items: center; gap: var(--space-related); }
|
|
39
39
|
/* The author's face sits in the gutter, level with the bottom of their last bubble rather than the reactions under it. */
|
|
40
|
-
.face { position: absolute; bottom: 0; left: -
|
|
40
|
+
.face { position: absolute; bottom: 0; left: calc(var(--space-block) * -1); display: flex; }
|
|
41
41
|
.row[data-mine] .line { flex-direction: row-reverse; }
|
|
42
|
-
.parts { display: flex; min-width: 0; flex-direction: column; align-items: flex-start; gap:
|
|
42
|
+
.parts { display: flex; min-width: 0; flex-direction: column; align-items: flex-start; gap: var(--space-tight); }
|
|
43
43
|
.row[data-mine] .parts { align-items: flex-end; }
|
|
44
44
|
|
|
45
45
|
/* Bubbles in a group share a soft edge on the author's side, so a run of messages reads as one voice. */
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
--r: 18px;
|
|
48
48
|
--tight: 6px;
|
|
49
49
|
max-width: 100%;
|
|
50
|
-
padding:
|
|
50
|
+
padding: var(--space-related) var(--space-group) var(--space-related);
|
|
51
51
|
border-radius: var(--r);
|
|
52
52
|
background: var(--surface-muted);
|
|
53
53
|
color: var(--foreground);
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
.row[data-mine]:not([data-last]) .bubble { border-bottom-right-radius: var(--tight); }
|
|
64
64
|
.row[data-failed] .bubble { opacity: .6; }
|
|
65
65
|
|
|
66
|
-
.gallery { display: grid; width: min(240px, 100%); gap:
|
|
66
|
+
.gallery { display: grid; width: min(240px, 100%); gap: var(--space-tight); grid-template-columns: 1fr; }
|
|
67
67
|
.gallery[data-count="2"],
|
|
68
68
|
.gallery[data-count="3"],
|
|
69
69
|
.gallery[data-count="4"] { grid-template-columns: 1fr 1fr; }
|
|
@@ -73,10 +73,10 @@
|
|
|
73
73
|
.gallery:not([data-count="1"]) .image { border-radius: var(--radius-detail); corner-shape: var(--corner-shape); }
|
|
74
74
|
.gallery:not([data-count="1"]) { overflow: hidden; border-radius: var(--radius-control); corner-shape: var(--corner-shape); }
|
|
75
75
|
.image img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
|
76
|
-
.more { position: absolute; inset: 0; display: grid; place-items: center; background:
|
|
76
|
+
.more { position: absolute; inset: 0; display: grid; place-items: center; background: var(--media-scrim); color: var(--on-media); font-size: var(--text-lg); font-weight: 500; }
|
|
77
77
|
|
|
78
78
|
.file {
|
|
79
|
-
display: flex; max-width: 100%; align-items: center; gap:
|
|
79
|
+
display: flex; max-width: 100%; align-items: center; gap: var(--space-group); padding: var(--space-related) var(--space-inset) var(--space-related) var(--space-group);
|
|
80
80
|
border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: var(--surface); color: var(--foreground); text-decoration: none;
|
|
81
81
|
transition: background-color var(--duration-fast) var(--ease-standard);
|
|
82
82
|
}
|
|
@@ -103,17 +103,17 @@
|
|
|
103
103
|
@media (hover: none) { .reactButton { opacity: .7; } }
|
|
104
104
|
|
|
105
105
|
.picker {
|
|
106
|
-
position: absolute; z-index: 5; bottom: calc(100% +
|
|
107
|
-
display: flex; gap:
|
|
106
|
+
position: absolute; z-index: 5; bottom: calc(100% + var(--space-related)); right: calc(var(--space-tight) * -1);
|
|
107
|
+
display: flex; gap: var(--space-hair); padding: var(--space-tight);
|
|
108
108
|
border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface-raised); box-shadow: var(--shadow-floating);
|
|
109
109
|
transform-origin: calc(100% - 18px) 100%;
|
|
110
110
|
}
|
|
111
|
-
.picker[data-mine] { right: auto; left: -
|
|
112
|
-
.pickerWrap[data-below] .picker { top: calc(100% +
|
|
111
|
+
.picker[data-mine] { right: auto; left: calc(var(--space-tight) * -1); transform-origin: 18px 100%; }
|
|
112
|
+
.pickerWrap[data-below] .picker { top: calc(100% + var(--space-related)); bottom: auto; transform-origin: calc(100% - 18px) 0; }
|
|
113
113
|
.pickerWrap[data-below] .picker[data-mine] { transform-origin: 18px 0; }
|
|
114
114
|
.emoji {
|
|
115
115
|
display: grid; width: 34px; height: 34px; place-items: center; padding: 0;
|
|
116
|
-
border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; cursor: pointer; font-size:
|
|
116
|
+
border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; cursor: pointer; font-size: var(--text-lg); line-height: 1;
|
|
117
117
|
-webkit-tap-highlight-color: transparent;
|
|
118
118
|
transition: background-color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard);
|
|
119
119
|
}
|
|
@@ -121,31 +121,31 @@
|
|
|
121
121
|
.emoji:active { transform: scale(.9); }
|
|
122
122
|
@media (hover: hover) and (pointer: fine) { .emoji:hover { background: var(--surface-muted); transform: scale(1.12); } }
|
|
123
123
|
|
|
124
|
-
.reactionClip { overflow: hidden; margin: -
|
|
125
|
-
.row[data-mine] .reactionClip { padding:
|
|
126
|
-
.reactions { display: flex; flex-wrap: wrap; gap:
|
|
124
|
+
.reactionClip { overflow: hidden; margin: calc(var(--space-hair) * -1) calc(var(--space-tight) * -1); padding: var(--space-hair) var(--space-tight) var(--space-hair) var(--space-related); }
|
|
125
|
+
.row[data-mine] .reactionClip { padding: var(--space-hair) var(--space-related) var(--space-hair) var(--space-tight); }
|
|
126
|
+
.reactions { display: flex; flex-wrap: wrap; gap: var(--space-tight); margin: 0; padding: 0; list-style: none; }
|
|
127
127
|
.row[data-mine] .reactions { justify-content: flex-end; }
|
|
128
128
|
.reaction {
|
|
129
|
-
display: inline-flex; height: 26px; align-items: center; gap:
|
|
129
|
+
display: inline-flex; height: 26px; align-items: center; gap: var(--space-tight); padding: 0 var(--space-related) 0 var(--space-related);
|
|
130
130
|
border: 1px solid var(--border-subtle); border-radius: var(--radius-full); corner-shape: round; background: var(--surface); color: var(--text-secondary); cursor: pointer;
|
|
131
131
|
font: inherit; font-size: var(--text-xs); font-weight: 500; line-height: 1; font-variant-numeric: tabular-nums;
|
|
132
132
|
-webkit-tap-highlight-color: transparent;
|
|
133
133
|
transition: background-color var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard);
|
|
134
134
|
}
|
|
135
|
-
.reaction > span:first-child { font-size:
|
|
135
|
+
.reaction > span:first-child { font-size: var(--text-sm); }
|
|
136
136
|
.reaction[aria-pressed="true"] { border-color: color-mix(in oklab, var(--accent) 28%, transparent); background: color-mix(in oklab, var(--accent) 8%, var(--surface)); color: var(--foreground); }
|
|
137
137
|
@media (hover: hover) and (pointer: fine) { .reaction:hover { border-color: var(--border-strong); } }
|
|
138
138
|
.reaction:active { transform: scale(.95); }
|
|
139
139
|
.countRoll { display: inline-grid; overflow: hidden; font-variant-numeric: tabular-nums; }
|
|
140
140
|
.countRoll > span { grid-area: 1 / 1; }
|
|
141
141
|
|
|
142
|
-
.receipts { display: flex; min-height: 16px; align-items: center; gap:
|
|
142
|
+
.receipts { display: flex; min-height: 16px; align-items: center; gap: var(--space-related); padding: 1px var(--space-related) 0; color: var(--text-muted); font-size: var(--text-xs); }
|
|
143
143
|
.readers { display: inline-flex; }
|
|
144
144
|
.reader { display: inline-flex; border-radius: 50%; corner-shape: round; box-shadow: 0 0 0 2px var(--surface); }
|
|
145
|
-
.reader + .reader { margin-left: -
|
|
146
|
-
.status[data-status="failed"] { display: inline-flex; align-items: center; gap:
|
|
145
|
+
.reader + .reader { margin-left: calc(var(--space-tight) * -1); }
|
|
146
|
+
.status[data-status="failed"] { display: inline-flex; align-items: center; gap: var(--space-related); color: var(--danger); }
|
|
147
147
|
.retry {
|
|
148
|
-
display: inline-flex; align-items: center; gap:
|
|
148
|
+
display: inline-flex; align-items: center; gap: var(--space-tight); padding: var(--space-hair) var(--space-related);
|
|
149
149
|
border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface); color: var(--foreground); cursor: pointer;
|
|
150
150
|
font: inherit; font-weight: 500; line-height: 16px;
|
|
151
151
|
-webkit-tap-highlight-color: transparent;
|
|
@@ -157,8 +157,8 @@
|
|
|
157
157
|
.avatar { display: inline-grid; flex: none; overflow: hidden; place-items: center; border-radius: 50%; corner-shape: round; background: var(--surface-muted); color: var(--text-secondary); font-weight: 500; }
|
|
158
158
|
.avatar img { width: 100%; height: 100%; object-fit: cover; }
|
|
159
159
|
|
|
160
|
-
.typing { display: grid; height: 36px; box-sizing: border-box; place-items: center; padding: 0
|
|
161
|
-
.dots { display: inline-flex; gap:
|
|
160
|
+
.typing { display: grid; height: 36px; box-sizing: border-box; place-items: center; padding: 0 var(--space-inset); }
|
|
161
|
+
.dots { display: inline-flex; gap: var(--space-tight); }
|
|
162
162
|
.dots i { width: 6px; height: 6px; border-radius: 50%; corner-shape: round; background: var(--text-muted); animation: breathe 1.2s var(--ease-in-out) infinite; }
|
|
163
163
|
.dots i:nth-child(2) { animation-delay: .16s; }
|
|
164
164
|
.dots i:nth-child(3) { animation-delay: .32s; }
|
|
@@ -169,8 +169,8 @@
|
|
|
169
169
|
@media (prefers-reduced-motion: reduce) { .dots i { animation: none; opacity: .6; } }
|
|
170
170
|
|
|
171
171
|
.pill {
|
|
172
|
-
position: absolute; z-index: 6; bottom:
|
|
173
|
-
display: inline-flex; height: 32px; align-items: center; gap:
|
|
172
|
+
position: absolute; z-index: 6; bottom: var(--space-group); left: 50%; translate: -50% 0;
|
|
173
|
+
display: inline-flex; height: 32px; align-items: center; gap: var(--space-related); padding: 0 var(--space-inset) 0 var(--space-group);
|
|
174
174
|
border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface-raised); box-shadow: var(--shadow-floating); color: var(--foreground); cursor: pointer;
|
|
175
175
|
font: inherit; font-size: var(--text-xs); font-weight: 500; white-space: nowrap;
|
|
176
176
|
-webkit-tap-highlight-color: transparent;
|
|
@@ -180,22 +180,22 @@
|
|
|
180
180
|
.pill:active { scale: .97; }
|
|
181
181
|
|
|
182
182
|
/* Composer. */
|
|
183
|
-
.composer { display: grid; padding:
|
|
183
|
+
.composer { display: grid; padding: var(--space-group) var(--space-group) var(--space-group); border-top: 1px solid var(--border-subtle); transition: background-color var(--duration-fast) var(--ease-standard); }
|
|
184
184
|
.composer[data-dragging] { background: var(--accent-subtle); }
|
|
185
185
|
.trayClip { overflow: hidden; }
|
|
186
|
-
.tray { display: flex; gap:
|
|
186
|
+
.tray { display: flex; gap: var(--space-related); margin: 0; padding: var(--space-related) var(--space-related) var(--space-group) var(--space-tight); overflow-x: auto; list-style: none; scrollbar-width: none; }
|
|
187
187
|
.pending { position: relative; flex: none; }
|
|
188
188
|
.pendingImage { display: block; width: 56px; height: 56px; border-radius: var(--radius-control); corner-shape: var(--corner-shape); object-fit: cover; }
|
|
189
|
-
.pendingFile { display: grid; width: 168px; height: 56px; box-sizing: border-box; align-content: center; column-gap:
|
|
189
|
+
.pendingFile { display: grid; width: 168px; height: 56px; box-sizing: border-box; align-content: center; column-gap: var(--space-related); padding: 0 var(--space-group); border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: var(--surface); grid-template-columns: auto minmax(0, 1fr); }
|
|
190
190
|
.pendingFile > svg { grid-row: span 2; align-self: center; color: var(--text-secondary); }
|
|
191
191
|
.pendingName { overflow: hidden; font-size: var(--text-xs); font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
|
|
192
192
|
.pendingSize { color: var(--text-muted); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
|
|
193
193
|
.remove {
|
|
194
|
-
position: absolute; top: -
|
|
194
|
+
position: absolute; top: calc(var(--space-related) * -1); right: calc(var(--space-related) * -1); display: grid; width: 20px; height: 20px; place-items: center; padding: 0;
|
|
195
195
|
border: 2px solid var(--surface); border-radius: 50%; corner-shape: round; background: var(--foreground); color: var(--background); cursor: pointer;
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
.inputRow { display: flex; align-items: flex-end; gap:
|
|
198
|
+
.inputRow { display: flex; align-items: flex-end; gap: var(--space-related); }
|
|
199
199
|
.attach {
|
|
200
200
|
display: grid; width: 40px; height: 40px; flex: none; place-items: center; margin-bottom: 1px; padding: 0;
|
|
201
201
|
border: 0; border-radius: var(--radius-full); corner-shape: round; background: transparent; color: var(--text-secondary); cursor: pointer;
|
|
@@ -216,7 +216,7 @@
|
|
|
216
216
|
.field { position: relative; min-width: 0; flex: 1; overflow: hidden; border-radius: var(--radius-control); corner-shape: var(--corner-shape); }
|
|
217
217
|
.mirror,
|
|
218
218
|
.textarea {
|
|
219
|
-
box-sizing: border-box; width: 100%; margin: 0; padding:
|
|
219
|
+
box-sizing: border-box; width: 100%; margin: 0; padding: var(--space-related) var(--space-related) var(--space-related) var(--space-inset); border: 0;
|
|
220
220
|
font: inherit; font-size: var(--text-base); letter-spacing: var(--tracking-body); line-height: 22px;
|
|
221
221
|
overflow-wrap: break-word; white-space: pre-wrap; word-break: break-word;
|
|
222
222
|
}
|
|
@@ -225,7 +225,7 @@
|
|
|
225
225
|
.textarea::placeholder { color: var(--text-muted); }
|
|
226
226
|
|
|
227
227
|
.send {
|
|
228
|
-
position: relative; display: grid; width: 32px; height: 32px; flex: none; place-items: center; margin: 0
|
|
228
|
+
position: relative; display: grid; width: 32px; height: 32px; flex: none; place-items: center; margin: 0 var(--space-tight) var(--space-tight) 0; overflow: hidden; padding: 0;
|
|
229
229
|
border: 0; border-radius: 50%; corner-shape: round; background: transparent; color: var(--text-muted); cursor: default;
|
|
230
230
|
-webkit-tap-highlight-color: transparent;
|
|
231
231
|
transition: background-color var(--duration-standard) var(--ease-standard), color var(--duration-standard) var(--ease-standard), transform var(--duration-instant) var(--ease-standard);
|
|
@@ -237,6 +237,6 @@
|
|
|
237
237
|
.srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
238
238
|
|
|
239
239
|
@media (max-width: 420px) {
|
|
240
|
-
.content { padding-inline:
|
|
240
|
+
.content { padding-inline: var(--space-group); }
|
|
241
241
|
.stack { max-width: 84%; }
|
|
242
242
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
.box:active:not(:disabled) .visual { transform: scale(.95); transition: border-color var(--duration-fast) var(--ease-standard), transform var(--duration-instant) var(--ease-standard); }
|
|
10
10
|
.box:disabled { opacity: .5; cursor: not-allowed; }
|
|
11
11
|
/* The label's first line centers on the square: (44px box - 19.6px line) / 2. */
|
|
12
|
-
.copy { display: grid; gap:
|
|
12
|
+
.copy { display: grid; gap: var(--space-hair); min-width: 0; margin-left: 1px; padding-top: calc((var(--control-height-md) - var(--text-sm) * 1.4) / 2); }
|
|
13
13
|
.copy label { color: var(--foreground); font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); cursor: pointer; }
|
|
14
14
|
.copy span { color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); }
|
|
15
15
|
@media (prefers-reduced-motion: reduce) { .visual, .box:active:not(:disabled) .visual { transition: none; transform: none; } }
|
|
@@ -9,7 +9,7 @@ const MORE = "\u0000more";
|
|
|
9
9
|
const standard = [...motionTokens.ease.standard];
|
|
10
10
|
const enterEase = [...motionTokens.ease.enter];
|
|
11
11
|
const fade = { duration: motionTokens.duration.fast, ease: standard };
|
|
12
|
-
const reducedFade = { duration: .
|
|
12
|
+
const reducedFade = { duration: motionTokens.duration.fast, ease: standard };
|
|
13
13
|
const exitFast = { duration: motionTokens.duration.fast, ease: standard };
|
|
14
14
|
const shown = { opacity: 1, y: "0em", scale: 1, filter: "blur(0px)" };
|
|
15
15
|
const textIn = { opacity: 0, y: "0.3em", filter: `blur(${motionTokens.blur.soft}px)` };
|
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
/* The button is only the hit area and the layout box. What you see is the surface, which trails the box on a spring while the width morphs. */
|
|
7
7
|
.chip { position: relative; display: inline-flex; max-width: 100%; padding: 0; border: 0; border-radius: var(--radius-full); corner-shape: round; background: none; color: var(--text-secondary); font: inherit; font-size: var(--text-sm); font-weight: 500; letter-spacing: var(--tracking-body); line-height: 20px; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: color var(--duration-fast) var(--ease-standard); }
|
|
8
8
|
/* Press answers on the body with the individual scale property, so it never fights the layout transform on the button. */
|
|
9
|
-
.body { position: relative; isolation: isolate; display: inline-flex; min-width: 0; height: var(--control-height-sm); align-items: center; padding: 0
|
|
9
|
+
.body { position: relative; isolation: isolate; display: inline-flex; min-width: 0; height: var(--control-height-sm); align-items: center; padding: 0 var(--space-inset); transition: scale var(--duration-spring) var(--ease-spring); }
|
|
10
10
|
.chip:active .body { scale: .97; transition-duration: var(--duration-instant); transition-timing-function: var(--ease-standard); }
|
|
11
11
|
.surface { position: absolute; z-index: -1; top: 0; bottom: 0; left: 0; border: 1px solid var(--border); border-radius: var(--radius-full); corner-shape: round; background: var(--surface); transition: background-color var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard); }
|
|
12
12
|
/* The check grows from its left edge where the label started; the slot hands its width to the label in one layout step, and the label's offset springs it over. */
|
|
13
|
-
.check { position: absolute; top: calc(50% -
|
|
13
|
+
.check { position: absolute; top: calc(50% - var(--space-related)); left: var(--space-group); display: grid; width: 14px; height: 14px; place-items: center; color: var(--accent-strong); transform-origin: 0 50%; }
|
|
14
14
|
.check svg { display: block; overflow: visible; }
|
|
15
15
|
.slot { width: 0; flex: none; }
|
|
16
16
|
.body[data-selected="true"] .slot { width: 18px; }
|
|
@@ -23,21 +23,14 @@
|
|
|
23
23
|
.chevron { display: grid; flex: none; place-items: center; color: var(--text-muted); }
|
|
24
24
|
/* Each role has contrast in both themes; color supports, but never replaces, source text. */
|
|
25
25
|
.comment { color: var(--text-muted); font-style: italic; }
|
|
26
|
-
.string { color:
|
|
27
|
-
.number { color:
|
|
28
|
-
.keyword { color:
|
|
29
|
-
.type { color:
|
|
30
|
-
.function { color:
|
|
31
|
-
.property { color:
|
|
32
|
-
.tag { color:
|
|
26
|
+
.string { color: var(--syntax-string); }
|
|
27
|
+
.number { color: var(--syntax-number); }
|
|
28
|
+
.keyword { color: var(--syntax-keyword); }
|
|
29
|
+
.type { color: var(--syntax-type); }
|
|
30
|
+
.function { color: var(--syntax-function); }
|
|
31
|
+
.property { color: var(--syntax-property); }
|
|
32
|
+
.tag { color: var(--syntax-type); }
|
|
33
33
|
.punctuation { color: var(--text-secondary); }
|
|
34
|
-
:global(:root[data-theme="dark"]) .string { color: oklch(77% .15 150); }
|
|
35
|
-
:global(:root[data-theme="dark"]) .number { color: oklch(80% .14 75); }
|
|
36
|
-
:global(:root[data-theme="dark"]) .keyword { color: oklch(78% .16 285); }
|
|
37
|
-
:global(:root[data-theme="dark"]) .type { color: oklch(79% .13 230); }
|
|
38
|
-
:global(:root[data-theme="dark"]) .function { color: oklch(80% .13 25); }
|
|
39
|
-
:global(:root[data-theme="dark"]) .property { color: oklch(79% .12 85); }
|
|
40
|
-
:global(:root[data-theme="dark"]) .tag { color: oklch(79% .13 230); }
|
|
41
34
|
@media (hover: hover) and (pointer: fine) { .expand:hover { background: var(--surface-muted); color: var(--foreground); } .expand:hover .chevron { color: var(--foreground); } }
|
|
42
35
|
@media (max-width: 420px) { .header { padding-left: var(--space-group); } .header :global(button) { width: var(--control-height-sm); padding-inline: 0; } .header :global(button) > span:last-of-type { display: none; } .viewport { --code-collapsed: calc(var(--code-lines, 0) * var(--text-xs) * 1.7 + var(--space-inset) * 2); } .pre { padding: var(--space-inset); font-size: var(--text-xs); } }
|
|
43
36
|
@media (prefers-reduced-motion: reduce) { .expand { transition: none; } }
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
}
|
|
12
12
|
:global(:root[data-theme="dark"]) .root { --picker-fill: var(--surface-raised); --checker: repeating-conic-gradient(oklch(42% 0 0) 0 25%, oklch(30% 0 0) 0 50%) 0 0 / 10px 10px; }
|
|
13
13
|
|
|
14
|
-
.trigger, .head { display: flex; height: 44px; align-items: center; gap:
|
|
14
|
+
.trigger, .head { display: flex; height: 44px; align-items: center; gap: var(--space-group); padding: var(--space-related) var(--space-inset) var(--space-related) var(--space-related); }
|
|
15
15
|
.trigger {
|
|
16
16
|
border: 1px solid var(--border); border-radius: var(--radius-container); corner-shape: var(--corner-shape); background: var(--picker-fill); box-shadow: var(--shadow-resting); color: inherit; font: inherit; cursor: pointer;
|
|
17
17
|
-webkit-tap-highlight-color: transparent; transition: border-color var(--duration-fast) var(--ease-standard);
|
|
@@ -22,76 +22,75 @@
|
|
|
22
22
|
|
|
23
23
|
/* A chip over a checkerboard, so opacity reads as opacity. */
|
|
24
24
|
.chip { position: relative; display: block; width: 32px; height: 32px; flex: none; overflow: hidden; border-radius: 50%; corner-shape: round; background: var(--checker); }
|
|
25
|
-
.chip > span { position: absolute; inset: 0; border-radius: inherit; corner-shape: inherit; box-shadow: inset
|
|
25
|
+
.chip > span { position: absolute; inset: 0; border-radius: inherit; corner-shape: inherit; box-shadow: var(--edge-inset); }
|
|
26
26
|
|
|
27
27
|
/* The floating layer. Its drop shadow follows the clipped shape, so the shadow grows with the panel. */
|
|
28
|
-
.float { position: absolute; z-index:
|
|
29
|
-
:global(:root[data-theme="dark"]) .float { filter: drop-shadow(0 20px 44px oklch(0% 0 0 / .5)); }
|
|
28
|
+
.float { position: absolute; z-index: var(--layer-popover); top: -1px; left: -1px; filter: var(--shadow-drop); pointer-events: none; }
|
|
30
29
|
.float[data-open] { pointer-events: auto; }
|
|
31
|
-
.panel { width: var(--picker-width); padding-bottom:
|
|
32
|
-
.head { gap:
|
|
30
|
+
.panel { width: var(--picker-width); padding-bottom: var(--space-inset); border: 1px solid var(--border); border-radius: var(--radius-container); corner-shape: var(--corner-shape); background: var(--picker-fill); will-change: clip-path; }
|
|
31
|
+
.head { gap: var(--space-group); padding-right: var(--space-related); }
|
|
33
32
|
.head .triggerText { flex: 1; }
|
|
34
|
-
.headActions { display: flex; gap:
|
|
33
|
+
.headActions { display: flex; gap: var(--space-hair); }
|
|
35
34
|
.iconButton {
|
|
36
35
|
display: grid; width: 32px; height: 32px; place-items: center; padding: 0; border: 0; border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: transparent; color: var(--text-secondary); cursor: pointer;
|
|
37
36
|
transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), transform var(--duration-fast) var(--ease-standard);
|
|
38
37
|
}
|
|
39
38
|
.iconButton:active { transform: scale(.94); }
|
|
40
39
|
/* One column locked to the panel: a wider grade label ("AA large") must never widen the area and sliders mid drag. */
|
|
41
|
-
.body { display: grid; grid-template-columns: minmax(0, 1fr); gap:
|
|
40
|
+
.body { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-inset); padding: var(--space-related) var(--space-inset) 0; }
|
|
42
41
|
|
|
43
42
|
/* Saturation runs left to right, brightness bottom to top, over the pure hue. */
|
|
44
43
|
.area {
|
|
45
44
|
position: relative; height: 164px; border-radius: var(--radius-control); corner-shape: var(--corner-shape); cursor: crosshair; touch-action: none;
|
|
46
|
-
background: linear-gradient(to top,
|
|
47
|
-
box-shadow: inset
|
|
45
|
+
background: linear-gradient(to top, var(--color-black), transparent), linear-gradient(to right, var(--color-white), transparent), var(--picker-hue);
|
|
46
|
+
box-shadow: var(--edge-inset);
|
|
48
47
|
}
|
|
49
48
|
.areaThumb, .sliderThumb {
|
|
50
49
|
position: absolute; width: 22px; height: 22px; border-radius: 50%; corner-shape: round; translate: -50% -50%; cursor: grab;
|
|
51
|
-
box-shadow: 0 0 0 3px
|
|
50
|
+
box-shadow: 0 0 0 3px var(--color-white), var(--shadow-knob), var(--edge-inset);
|
|
52
51
|
transition: scale var(--duration-spring) var(--ease-spring);
|
|
53
52
|
}
|
|
54
53
|
.areaThumb { background: var(--picker-opaque); }
|
|
55
54
|
.area[data-active] .areaThumb, .sliderThumb[data-active] { scale: 1.2; cursor: grabbing; }
|
|
56
55
|
|
|
57
|
-
.slider { position: relative; height: 14px; margin: 0
|
|
58
|
-
.slider::before { content: ""; position: absolute; inset: 0 -
|
|
56
|
+
.slider { position: relative; height: 14px; margin: 0 var(--space-group); border-radius: var(--radius-full); corner-shape: round; cursor: pointer; touch-action: none; }
|
|
57
|
+
.slider::before { content: ""; position: absolute; inset: 0 calc(var(--space-group) * -1); border-radius: inherit; corner-shape: inherit; background: var(--track); box-shadow: var(--edge-inset); }
|
|
59
58
|
.sliderThumb { top: 50%; background: var(--checker); }
|
|
60
59
|
.sliderThumb::after { content: ""; position: absolute; inset: 0; border-radius: inherit; corner-shape: inherit; background: var(--thumb-fill); }
|
|
61
60
|
/* The gradient stops sit at the thumb's travel ends, so the color under the thumb is the value it reports. */
|
|
62
61
|
.hue { --track: linear-gradient(to right, #f00 11px, #ff0 calc(11px + (100% - 22px) * .1666), #0f0 calc(11px + (100% - 22px) * .3333), #0ff 50%, #00f calc(11px + (100% - 22px) * .6666), #f0f calc(11px + (100% - 22px) * .8333), #f00 calc(100% - 11px)); }
|
|
63
62
|
.alpha { --track: linear-gradient(to right, transparent 11px, var(--picker-opaque) calc(100% - 11px)), var(--checker); }
|
|
64
63
|
|
|
65
|
-
.field { display: flex; height: 40px; align-items: stretch; overflow: hidden; margin-top:
|
|
64
|
+
.field { display: flex; height: 40px; align-items: stretch; overflow: hidden; margin-top: var(--space-hair); border: 1px solid var(--border); border-radius: var(--radius-control); corner-shape: var(--corner-shape); background: var(--surface-muted); transition: border-color var(--duration-fast) var(--ease-standard); }
|
|
66
65
|
.field[data-invalid] { border-color: var(--danger); }
|
|
67
66
|
.format {
|
|
68
|
-
display: flex; min-width: 56px; align-items: center; justify-content: center; padding: 0
|
|
67
|
+
display: flex; min-width: 56px; align-items: center; justify-content: center; padding: 0 var(--space-group); border: 0; border-right: 1px solid var(--border); background: transparent;
|
|
69
68
|
color: var(--foreground); font: inherit; font-size: var(--text-xs); font-weight: 500; cursor: pointer; transition: background-color var(--duration-fast) var(--ease-standard);
|
|
70
69
|
}
|
|
71
70
|
.inputWrap { position: relative; display: flex; min-width: 0; flex: 1; }
|
|
72
|
-
.input, .morph { padding: 0
|
|
71
|
+
.input, .morph { padding: 0 var(--space-group); font: inherit; font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
|
|
73
72
|
.input { width: 100%; min-width: 0; border: 0; background: transparent; color: var(--foreground); caret-color: var(--foreground); }
|
|
74
73
|
.input[data-morphing] { color: transparent; }
|
|
75
74
|
.morph { position: absolute; inset: 0; display: flex; align-items: center; overflow: hidden; color: var(--foreground); pointer-events: none; white-space: nowrap; }
|
|
76
|
-
.error { min-height: 0; margin: -
|
|
75
|
+
.error { min-height: 0; margin: calc(var(--space-group) * -1) var(--space-hair) calc(var(--space-tight) * -1); color: var(--danger); font-size: var(--text-xs); }
|
|
77
76
|
.error:empty { display: none; }
|
|
78
77
|
|
|
79
|
-
.contrast { display: flex; align-items: center; gap:
|
|
78
|
+
.contrast { display: flex; align-items: center; gap: var(--space-group); font-size: var(--text-sm); }
|
|
80
79
|
.sample { display: grid; width: 36px; height: 28px; flex: none; place-items: center; border: 1px solid var(--border); border-radius: var(--radius-detail); corner-shape: var(--corner-shape); background: var(--picker-bg); color: var(--picker-color); font-weight: 500; }
|
|
81
80
|
.ratio { font-variant-numeric: tabular-nums; font-weight: 500; min-width: 4.4ch; }
|
|
82
81
|
.against { flex: 1; min-width: 0; overflow: hidden; color: var(--text-secondary); font-size: var(--text-xs); text-overflow: ellipsis; white-space: nowrap; }
|
|
83
|
-
.grade { position: relative; display: inline-grid; height: 22px; align-items: center; padding: 0
|
|
82
|
+
.grade { position: relative; display: inline-grid; height: 22px; align-items: center; padding: 0 var(--space-related); border-radius: var(--radius-full); corner-shape: round; font-size: var(--text-xs); font-weight: 500; white-space: nowrap; transition: background-color var(--duration-standard) var(--ease-standard), color var(--duration-standard) var(--ease-standard); }
|
|
84
83
|
.grade[data-grade="pass"] { background: color-mix(in oklab, var(--success) 14%, transparent); color: var(--success); }
|
|
85
84
|
.grade[data-grade="large"] { background: color-mix(in oklab, var(--warning) 16%, transparent); color: var(--warning); }
|
|
86
85
|
.grade[data-grade="fail"] { background: color-mix(in oklab, var(--danger) 12%, transparent); color: var(--danger); }
|
|
87
86
|
|
|
88
|
-
.swatches { display: flex; align-items: center; gap:
|
|
87
|
+
.swatches { display: flex; align-items: center; gap: var(--space-related); min-height: 32px; }
|
|
89
88
|
.add {
|
|
90
89
|
display: grid; width: 30px; height: 30px; flex: none; place-items: center; padding: 0; border: 1px dashed var(--border-strong); border-radius: 50%; corner-shape: round; background: transparent; color: var(--text-secondary); cursor: pointer;
|
|
91
90
|
transition: color var(--duration-fast) var(--ease-standard), border-color var(--duration-fast) var(--ease-standard), transform var(--duration-fast) var(--ease-standard);
|
|
92
91
|
}
|
|
93
92
|
.add:active { transform: scale(.92); }
|
|
94
|
-
.swatchList { display: flex; min-width: 0; flex: 1; gap:
|
|
93
|
+
.swatchList { display: flex; min-width: 0; flex: 1; gap: var(--space-related); }
|
|
95
94
|
.swatchSlot { position: relative; flex: none; touch-action: pan-y; }
|
|
96
95
|
.swatch { display: grid; width: 30px; height: 30px; place-items: center; padding: 0; border: 0; border-radius: 50%; corner-shape: round; background: transparent; cursor: pointer; }
|
|
97
96
|
.swatchChip { width: 26px; height: 26px; transition: scale var(--duration-spring) var(--ease-spring); }
|
|
@@ -4,6 +4,7 @@ import { animate, AnimatePresence, motion, useReducedMotion } from "motion/react
|
|
|
4
4
|
import { Check, ChevronDown, Search, X } from "lucide-react";
|
|
5
5
|
import { forwardRef, useEffect, useId, useImperativeHandle, useMemo, useRef, useState, } from "react";
|
|
6
6
|
import { motionTokens } from "../../lib/motion-tokens.js";
|
|
7
|
+
import { useNearestItem } from "../../lib/fluid-hover.js";
|
|
7
8
|
import styles from "./combobox.module.css";
|
|
8
9
|
/** Follows the listbox height with a critically damped spring, so filtering never snaps the menu. */
|
|
9
10
|
function AutoHeight({ children, reduceMotion }) {
|
|
@@ -32,6 +33,9 @@ export const Combobox = forwardRef(function Combobox({ label, options, value: co
|
|
|
32
33
|
const [open, setOpen] = useState(false);
|
|
33
34
|
const [query, setQuery] = useState("");
|
|
34
35
|
const [activeIndex, setActiveIndex] = useState(-1);
|
|
36
|
+
// Fluid hover: the mouse makes the nearest option active, gaps and padding included, and a click there picks it.
|
|
37
|
+
// The listbox keeps focus in the input, so a press in its padding never closes the popover.
|
|
38
|
+
const nearest = useNearestItem({ items: '[role="option"]', onPick: (item) => setActiveIndex(Number(item.dataset.index)) });
|
|
35
39
|
const selectedValue = controlledValue ?? uncontrolledValue;
|
|
36
40
|
const selectedOption = options.find((option) => option.value === selectedValue);
|
|
37
41
|
useImperativeHandle(forwardedRef, () => inputRef.current);
|
|
@@ -162,6 +166,6 @@ export const Combobox = forwardRef(function Combobox({ label, options, value: co
|
|
|
162
166
|
setQuery(event.target.value);
|
|
163
167
|
setOpen(true);
|
|
164
168
|
setActiveIndex(-1);
|
|
165
|
-
}, onKeyDown: handleKeyDown }), _jsx(AnimatePresence, { initial: false, children: selectedOption && !disabled && (_jsx(motion.button, { type: "button", className: styles.clear, "aria-label": "Clear selection", onMouseDown: event => event.preventDefault(), onClick: clear, initial: reduceMotion ? { opacity: 0 } : { opacity: 0, scale: 0.6, filter: `blur(${motionTokens.blur.subtle}px)` }, animate: { opacity: 1, scale: 1, filter: "blur(0px)", transition: reduceMotion ? { duration: motionTokens.duration.instant } : { ...motionTokens.spring.snappy, opacity: { duration: motionTokens.duration.fast } } }, exit: { opacity: 0, ...(reduceMotion ? {} : { scale: 0.6, filter: `blur(${motionTokens.blur.subtle}px)` }), transition: { duration: motionTokens.duration.instant, ease: [...motionTokens.ease.standard] } }, whileTap: { scale: reduceMotion ? 1 : 0.96, transition: { duration: motionTokens.duration.instant, ease: [...motionTokens.ease.standard] } }, children: _jsx(X, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })) }), _jsx(ChevronDown, { className: styles.chevron, size: 16, strokeWidth: 1.75, "aria-hidden": "true" })] }), description && _jsx("span", { id: hintId, className: styles.hint, children: description }), _jsx(AnimatePresence, { initial: false, children: open && (_jsx(motion.div, { className: styles.popover, initial: reduceMotion ? { opacity: 0 } : { opacity: 0, y: -6, scale: 0.97 }, animate: { opacity: 1, y: 0, scale: 1, transition: reduceMotion ? { duration: motionTokens.duration.instant } : { ...motionTokens.spring.snappy, opacity: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.enter] } } }, exit: { opacity: 0, ...(reduceMotion ? {} : { y: -4, scale: 0.98 }), transition: { duration: motionTokens.duration.instant, ease: [...motionTokens.ease.standard] } }, role: "presentation", children: _jsx(AutoHeight, { reduceMotion: reduceMotion, children: _jsx("div", { id: listboxId, className: styles.listbox, role: "listbox", "aria-label": `${label} options`, tabIndex: -1, children: filteredOptions.length ? filteredOptions.map((option, index) => (_jsxs("div", { ref: (element) => { optionRefs.current[option.value] = element; }, id: `${controlId}-option-${option.value}`, className: styles.option, "data-active": index === activeIndex ? "true" : undefined, "data-disabled": option.disabled ? "true" : undefined, role: "option", "aria-selected": option.value === selectedValue, "aria-disabled": option.disabled || undefined, onMouseDown: (event) => event.preventDefault(),
|
|
169
|
+
}, onKeyDown: handleKeyDown }), _jsx(AnimatePresence, { initial: false, children: selectedOption && !disabled && (_jsx(motion.button, { type: "button", className: styles.clear, "aria-label": "Clear selection", onMouseDown: event => event.preventDefault(), onClick: clear, initial: reduceMotion ? { opacity: 0 } : { opacity: 0, scale: 0.6, filter: `blur(${motionTokens.blur.subtle}px)` }, animate: { opacity: 1, scale: 1, filter: "blur(0px)", transition: reduceMotion ? { duration: motionTokens.duration.instant } : { ...motionTokens.spring.snappy, opacity: { duration: motionTokens.duration.fast } } }, exit: { opacity: 0, ...(reduceMotion ? {} : { scale: 0.6, filter: `blur(${motionTokens.blur.subtle}px)` }), transition: { duration: motionTokens.duration.instant, ease: [...motionTokens.ease.standard] } }, whileTap: { scale: reduceMotion ? 1 : 0.96, transition: { duration: motionTokens.duration.instant, ease: [...motionTokens.ease.standard] } }, children: _jsx(X, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) })) }), _jsx(ChevronDown, { className: styles.chevron, size: 16, strokeWidth: 1.75, "aria-hidden": "true" })] }), description && _jsx("span", { id: hintId, className: styles.hint, children: description }), _jsx(AnimatePresence, { initial: false, children: open && (_jsx(motion.div, { className: styles.popover, initial: reduceMotion ? { opacity: 0 } : { opacity: 0, y: -6, scale: 0.97 }, animate: { opacity: 1, y: 0, scale: 1, transition: reduceMotion ? { duration: motionTokens.duration.instant } : { ...motionTokens.spring.snappy, opacity: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.enter] } } }, exit: { opacity: 0, ...(reduceMotion ? {} : { y: -4, scale: 0.98 }), transition: { duration: motionTokens.duration.instant, ease: [...motionTokens.ease.standard] } }, role: "presentation", children: _jsx(AutoHeight, { reduceMotion: reduceMotion, children: _jsx("div", { id: listboxId, className: styles.listbox, role: "listbox", "aria-label": `${label} options`, tabIndex: -1, onMouseDown: (event) => event.preventDefault(), ...nearest, children: filteredOptions.length ? filteredOptions.map((option, index) => (_jsxs("div", { ref: (element) => { optionRefs.current[option.value] = element; }, id: `${controlId}-option-${option.value}`, className: styles.option, "data-active": index === activeIndex ? "true" : undefined, "data-disabled": option.disabled ? "true" : undefined, role: "option", "aria-selected": option.value === selectedValue, "aria-disabled": option.disabled || undefined, onMouseDown: (event) => event.preventDefault(), "data-index": index, onClick: () => choose(option), children: [_jsx("span", { children: option.label }), option.value === selectedValue && _jsx(Check, { className: styles.check, size: 16, strokeWidth: 1.75, "aria-hidden": "true" })] }, option.value))) : _jsx(motion.div, { className: styles.empty, role: "status", initial: reduceMotion ? false : { opacity: 0, y: 4, filter: `blur(${motionTokens.blur.soft}px)` }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, transition: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] }, children: emptyMessage }) }) }) })) })] }));
|
|
166
170
|
});
|
|
167
171
|
Combobox.displayName = "Combobox";
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
@media (hover: hover) and (pointer: fine) { .clear:hover { background: var(--surface-muted); color: var(--foreground); } }
|
|
16
16
|
.clear:focus-visible { --focus-outline-offset: 1px; }
|
|
17
17
|
.hint { color: var(--text-muted); font-size: var(--text-xs); }
|
|
18
|
-
.popover { position: absolute; z-index:
|
|
18
|
+
.popover { position: absolute; z-index: var(--layer-dropdown); top: calc(100% + var(--space-related)); right: 0; left: 0; overflow: hidden; transform-origin: top center; border: 1px solid var(--border); border-radius: var(--radius-container); padding: var(--space-tight); background: var(--surface-raised); color: var(--foreground); box-shadow: var(--shadow-floating); will-change: transform, opacity; }
|
|
19
19
|
.autoHeight { overflow: hidden; }
|
|
20
20
|
.listbox { max-height: min(300px, 40vh); overflow-y: auto; overscroll-behavior: contain; }
|
|
21
21
|
/* Arrow keys move the highlight often, so it changes almost instantly. */
|
|
22
|
-
.option { display: flex; min-height: 36px; align-items: center; justify-content: space-between; gap: var(--space-group); border-radius: calc(var(--radius-container) -
|
|
22
|
+
.option { display: flex; min-height: 36px; align-items: center; justify-content: space-between; gap: var(--space-group); border-radius: calc(var(--radius-container) - var(--space-tight)); padding: 0 var(--space-group); color: var(--foreground); font-size: var(--text-sm); cursor: pointer; outline: none; user-select: none; transition: background-color 80ms var(--ease-standard), color 80ms var(--ease-standard); }
|
|
23
23
|
.option > span:first-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
24
24
|
.option[data-active="true"] { background: var(--surface-muted); }
|
|
25
25
|
.option[data-disabled="true"] { opacity: .42; cursor: not-allowed; }
|
|
@@ -4,6 +4,7 @@ import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "re
|
|
|
4
4
|
import { AnimatePresence, animate, motion, useMotionValue, useReducedMotion } from "motion/react";
|
|
5
5
|
import { Command as CommandIcon, Search, X } from "lucide-react";
|
|
6
6
|
import { motionTokens } from "../../lib/motion-tokens.js";
|
|
7
|
+
import { useNearestItem } from "../../lib/fluid-hover.js";
|
|
7
8
|
import styles from "./command-palette.module.css";
|
|
8
9
|
const enter = { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] };
|
|
9
10
|
const leave = { duration: motionTokens.duration.instant, ease: [...motionTokens.ease.standard] };
|
|
@@ -74,6 +75,21 @@ export function CommandPalette({ items, placeholder = "Search commands", onSelec
|
|
|
74
75
|
observer.observe(list);
|
|
75
76
|
return () => observer.disconnect();
|
|
76
77
|
}, []);
|
|
78
|
+
// Fluid hover: the mouse makes the nearest result active, group headings and padding included, and a click there runs it.
|
|
79
|
+
const nearest = useNearestItem({ items: '[role="option"]', onPick: item => {
|
|
80
|
+
const index = Number(item.dataset.index);
|
|
81
|
+
if (index !== safeActiveIndex) {
|
|
82
|
+
pointer.current = true;
|
|
83
|
+
setActiveIndex(index);
|
|
84
|
+
}
|
|
85
|
+
} });
|
|
86
|
+
function onListPointerMove(event) {
|
|
87
|
+
// Ignore the synthetic moves browsers send while the list scrolls under a still pointer.
|
|
88
|
+
if (event.clientX === lastPointer.current.x && event.clientY === lastPointer.current.y)
|
|
89
|
+
return;
|
|
90
|
+
lastPointer.current = { x: event.clientX, y: event.clientY };
|
|
91
|
+
nearest.onPointerMove(event);
|
|
92
|
+
}
|
|
77
93
|
// One highlight follows the active result: a spring for the pointer, a 70ms slide for arrow keys, a fade when it first appears.
|
|
78
94
|
useLayoutEffect(() => {
|
|
79
95
|
const node = activeId ? document.getElementById(`${inputId}-${activeId}`) : null;
|
|
@@ -82,7 +98,7 @@ export function CommandPalette({ items, placeholder = "Search commands", onSelec
|
|
|
82
98
|
animate(highlightOpacity, 0, { duration: reduced ? 0 : .1 });
|
|
83
99
|
return;
|
|
84
100
|
}
|
|
85
|
-
const move = !highlightShown.current || reduced ? { duration: 0 } : pointer.current ? motionTokens.spring.snappy : { duration: .
|
|
101
|
+
const move = !highlightShown.current || reduced ? { duration: 0 } : pointer.current ? motionTokens.spring.snappy : { duration: motionTokens.duration.instant, ease: [...motionTokens.ease.enter] };
|
|
86
102
|
// Rows sit inside positioned groups, so their offset is summed up to the list (layout transforms stay out of it).
|
|
87
103
|
let top = 0;
|
|
88
104
|
for (let element = node; element && element !== listRef.current; element = element.offsetParent)
|
|
@@ -137,15 +153,6 @@ export function CommandPalette({ items, placeholder = "Search commands", onSelec
|
|
|
137
153
|
onClose?.();
|
|
138
154
|
}
|
|
139
155
|
}
|
|
140
|
-
return _jsxs(motion.div, { className: styles.palette, initial: reduced ? false : { opacity: 0, y: 6, scale: .98 }, animate: { opacity: 1, y: 0, scale: 1 }, transition: reduced ? { duration: 0 } : { default: motionTokens.spring.smooth, opacity: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.enter] } }, children: [_jsxs("div", { className: styles.searchRow, children: [_jsx("span", { className: styles.searchIcon, children: _jsx(Search, { width: 18, height: 18, strokeWidth: 1.75, "aria-hidden": "true" }) }), _jsx("label", { className: styles.visuallyHidden, htmlFor: inputId, children: label }), _jsx("input", { ref: inputRef, id: inputId, role: "combobox", "aria-autocomplete": "list", "aria-expanded": "true", "aria-controls": `${inputId}-results`, "aria-activedescendant": activeId ? `${inputId}-${activeId}` : undefined, value: query, onChange: event => { setQuery(event.target.value); setActiveIndex(null); }, onKeyDown: handleKeyDown, placeholder: placeholder, autoComplete: "off" }), _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: query ? _jsx(motion.button, { className: styles.clearButton, type: "button", "aria-label": "Clear search", onClick: () => { setQuery(""); setActiveIndex(null); inputRef.current?.focus(); }, initial: reduced ? false : { opacity: 0, scale: .6, filter: `blur(${motionTokens.blur.subtle}px)` }, animate: { opacity: 1, scale: 1, filter: "blur(0px)" }, exit: reduced ? { opacity: 0, transition: { duration: 0 } } : { opacity: 0, scale: .6, filter: `blur(${motionTokens.blur.subtle}px)`, transition: leave }, transition: reduced ? { duration: 0 } : { default: motionTokens.spring.snappy, opacity: { duration: motionTokens.duration.instant } }, children: _jsx(X, { width: 15, height: 15, "aria-hidden": "true" }) }, "clear") : null }), onClose ? _jsx("button", { className: styles.closeButton, type: "button", "aria-label": "Close command palette", onClick: onClose, children: "Esc" }) : null, _jsx("kbd", { className: styles.commandKey, children: "\u2318 K" })] }), _jsx(motion.div, { className: styles.resultsFrame, initial: false, animate: { height: listHeight }, transition: reduced ? { duration: 0 } : motionTokens.spring.smooth, children: _jsxs(motion.div, { ref: listRef, layoutScroll: true, id: `${inputId}-results`, className: styles.results, role: "listbox", "aria-label": "Command results", children: [_jsx(motion.span, { className: styles.highlight, style: { y: highlightY, height: highlightHeight, opacity: highlightOpacity }, "aria-hidden": "true" }), _jsx(AnimatePresence, { mode: "wait", initial: false, children: filtered.length ? _jsx(motion.div, { initial: reduced ? false : { opacity: 0 }, animate: { opacity: 1 }, exit: reduced ? { opacity: 0, transition: { duration: 0 } } : { opacity: 0, transition: leave }, transition: enter, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, custom: glide, children: groupedItems.map(([group, entries]) => _jsxs(motion.div, { layout: glide ? "position" : false, className: styles.group, role: "group", "aria-label": group, variants: rowExit, initial: reduced ? false : { opacity: 0 }, animate: { opacity: 1 }, exit: "exit", transition: reduced ? { duration: 0 } : { default: enter, layout: motionTokens.spring.smooth }, children: [_jsx("span", { className: styles.groupHeading, "aria-hidden": "true", children: group }), _jsx(AnimatePresence, { mode: "popLayout", initial: false, custom: glide, children: entries.map(({ item, index }) => _jsxs(motion.button, { layout: glide ? "position" : false, id: `${inputId}-${item.id}`, className: `${styles.result} ${index === safeActiveIndex ? styles.activeResult : ""}`, type: "button", role: "option", "aria-selected": index === safeActiveIndex, onClick: () => choose(item),
|
|
141
|
-
// Ignore the synthetic moves browsers send while the list scrolls under a still pointer.
|
|
142
|
-
if (event.clientX === lastPointer.current.x && event.clientY === lastPointer.current.y)
|
|
143
|
-
return;
|
|
144
|
-
lastPointer.current = { x: event.clientX, y: event.clientY };
|
|
145
|
-
if (index !== safeActiveIndex) {
|
|
146
|
-
pointer.current = true;
|
|
147
|
-
setActiveIndex(index);
|
|
148
|
-
}
|
|
149
|
-
}, variants: rowExit, initial: reduced ? false : { opacity: 0 }, animate: { opacity: 1 }, exit: "exit", transition: reduced ? { duration: 0 } : { default: enter, layout: motionTokens.spring.smooth }, children: [_jsx("span", { className: styles.itemIcon, "aria-hidden": "true", children: item.icon ?? _jsx(CommandIcon, { width: 16, height: 16 }) }), _jsxs("span", { className: styles.resultCopy, children: [_jsx("strong", { children: item.label }), item.description && _jsx("small", { children: item.description })] }), item.shortcut && _jsx("kbd", { className: styles.shortcut, children: item.shortcut })] }, item.id)) })] }, group)) }) }, "results") : _jsxs(motion.div, { className: styles.empty, initial: reduced ? false : { opacity: 0, y: 6, filter: `blur(${motionTokens.blur.subtle}px)` }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, exit: reduced ? { opacity: 0, transition: { duration: 0 } } : { opacity: 0, y: -4, transition: leave }, transition: enter, children: [_jsx("span", { children: _jsx(Search, { width: 20, height: 20, "aria-hidden": "true" }) }), _jsx("strong", { children: "No matching actions" }), _jsx("small", { children: "Try a different word or clear the search." })] }, "empty") })] }) })] });
|
|
156
|
+
return _jsxs(motion.div, { className: styles.palette, initial: reduced ? false : { opacity: 0, y: 6, scale: .98 }, animate: { opacity: 1, y: 0, scale: 1 }, transition: reduced ? { duration: 0 } : { default: motionTokens.spring.smooth, opacity: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.enter] } }, children: [_jsxs("div", { className: styles.searchRow, children: [_jsx("span", { className: styles.searchIcon, children: _jsx(Search, { width: 18, height: 18, strokeWidth: 1.75, "aria-hidden": "true" }) }), _jsx("label", { className: styles.visuallyHidden, htmlFor: inputId, children: label }), _jsx("input", { ref: inputRef, id: inputId, role: "combobox", "aria-autocomplete": "list", "aria-expanded": "true", "aria-controls": `${inputId}-results`, "aria-activedescendant": activeId ? `${inputId}-${activeId}` : undefined, value: query, onChange: event => { setQuery(event.target.value); setActiveIndex(null); }, onKeyDown: handleKeyDown, placeholder: placeholder, autoComplete: "off" }), _jsx(AnimatePresence, { mode: "popLayout", initial: false, children: query ? _jsx(motion.button, { className: styles.clearButton, type: "button", "aria-label": "Clear search", onClick: () => { setQuery(""); setActiveIndex(null); inputRef.current?.focus(); }, initial: reduced ? false : { opacity: 0, scale: .6, filter: `blur(${motionTokens.blur.subtle}px)` }, animate: { opacity: 1, scale: 1, filter: "blur(0px)" }, exit: reduced ? { opacity: 0, transition: { duration: 0 } } : { opacity: 0, scale: .6, filter: `blur(${motionTokens.blur.subtle}px)`, transition: leave }, transition: reduced ? { duration: 0 } : { default: motionTokens.spring.snappy, opacity: { duration: motionTokens.duration.instant } }, children: _jsx(X, { width: 15, height: 15, "aria-hidden": "true" }) }, "clear") : null }), onClose ? _jsx("button", { className: styles.closeButton, type: "button", "aria-label": "Close command palette", onClick: onClose, children: "Esc" }) : null, _jsx("kbd", { className: styles.commandKey, children: "\u2318 K" })] }), _jsx(motion.div, { className: styles.resultsFrame, initial: false, animate: { height: listHeight }, transition: reduced ? { duration: 0 } : motionTokens.spring.smooth, children: _jsxs(motion.div, { ref: listRef, layoutScroll: true, id: `${inputId}-results`, className: styles.results, role: "listbox", "aria-label": "Command results", onPointerMove: onListPointerMove, onClick: nearest.onClick, children: [_jsx(motion.span, { className: styles.highlight, style: { y: highlightY, height: highlightHeight, opacity: highlightOpacity }, "aria-hidden": "true" }), _jsx(AnimatePresence, { mode: "wait", initial: false, children: filtered.length ? _jsx(motion.div, { initial: reduced ? false : { opacity: 0 }, animate: { opacity: 1 }, exit: reduced ? { opacity: 0, transition: { duration: 0 } } : { opacity: 0, transition: leave }, transition: enter, children: _jsx(AnimatePresence, { mode: "popLayout", initial: false, custom: glide, children: groupedItems.map(([group, entries]) => _jsxs(motion.div, { layout: glide ? "position" : false, className: styles.group, role: "group", "aria-label": group, variants: rowExit, initial: reduced ? false : { opacity: 0 }, animate: { opacity: 1 }, exit: "exit", transition: reduced ? { duration: 0 } : { default: enter, layout: motionTokens.spring.smooth }, children: [_jsx("span", { className: styles.groupHeading, "aria-hidden": "true", children: group }), _jsx(AnimatePresence, { mode: "popLayout", initial: false, custom: glide, children: entries.map(({ item, index }) => _jsxs(motion.button, { layout: glide ? "position" : false, id: `${inputId}-${item.id}`, className: `${styles.result} ${index === safeActiveIndex ? styles.activeResult : ""}`, type: "button", role: "option", "aria-selected": index === safeActiveIndex, onClick: () => choose(item), "data-index": index, variants: rowExit, initial: reduced ? false : { opacity: 0 }, animate: { opacity: 1 }, exit: "exit", transition: reduced ? { duration: 0 } : { default: enter, layout: motionTokens.spring.smooth }, children: [_jsx("span", { className: styles.itemIcon, "aria-hidden": "true", children: item.icon ?? _jsx(CommandIcon, { width: 16, height: 16 }) }), _jsxs("span", { className: styles.resultCopy, children: [_jsx("strong", { children: item.label }), item.description && _jsx("small", { children: item.description })] }), item.shortcut && _jsx("kbd", { className: styles.shortcut, children: item.shortcut })] }, item.id)) })] }, group)) }) }, "results") : _jsxs(motion.div, { className: styles.empty, initial: reduced ? false : { opacity: 0, y: 6, filter: `blur(${motionTokens.blur.subtle}px)` }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, exit: reduced ? { opacity: 0, transition: { duration: 0 } } : { opacity: 0, y: -4, transition: leave }, transition: enter, children: [_jsx("span", { children: _jsx(Search, { width: 20, height: 20, "aria-hidden": "true" }) }), _jsx("strong", { children: "No matching actions" }), _jsx("small", { children: "Try a different word or clear the search." })] }, "empty") })] }) })] });
|
|
150
157
|
}
|
|
151
158
|
export default CommandPalette;
|