@yewdiel21/motion-kit 0.17.0 → 0.19.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 +240 -199
- package/dist/auto/index.d.ts +19 -3
- package/dist/auto/index.js +35 -3
- package/dist/auto/index.js.map +1 -1
- package/dist/{chunk-QFBOHPWZ.js → chunk-CQP66AZK.js} +2 -2
- package/dist/{chunk-QP4QJOEN.js → chunk-H4STLMCM.js} +2 -2
- package/dist/{chunk-BZLMCHUD.js → chunk-KDIJX6G4.js} +3 -3
- package/dist/{chunk-DOOMULVV.js → chunk-OLT3CWDJ.js} +4 -2
- package/dist/chunk-OLT3CWDJ.js.map +1 -0
- package/dist/{chunk-VJFPTLAK.js → chunk-PEDYJ2TV.js} +8 -22
- package/dist/chunk-PEDYJ2TV.js.map +1 -0
- package/dist/{chunk-7IQTMF7Y.js → chunk-Y4GIBTRG.js} +5130 -2153
- package/dist/chunk-Y4GIBTRG.js.map +1 -0
- package/dist/css/components/badge-button.css +16 -0
- package/dist/css/components/bento-grid.css +13 -0
- package/dist/css/components/code-block.css +267 -0
- package/dist/css/components/command-search.css +274 -2
- package/dist/css/components/dialog.css +264 -0
- package/dist/css/components/drilldown-menu.css +11 -7
- package/dist/css/components/equalizer-slider.css +26 -0
- package/dist/css/components/handwriting-text.css +15 -0
- package/dist/css/components/heatmap-chart.css +267 -1
- package/dist/css/components/magnetize-button.css +18 -0
- package/dist/css/components/morphing-page-dots.css +13 -0
- package/dist/css/components/not-found.css +22 -0
- package/dist/css/components/notification-stack.css +42 -0
- package/dist/css/components/particle-button.css +14 -0
- package/dist/css/components/radio-group.css +29 -0
- package/dist/css/components/rating-scale.css +23 -0
- package/dist/css/components/scroll-area.css +261 -0
- package/dist/css/components/scroll-preview-dock.css +18 -0
- package/dist/css/components/select.css +300 -4
- package/dist/css/components/signature-pad.css +130 -103
- package/dist/css/components/stepper.css +36 -0
- package/dist/css/components/sticky-header.css +10 -0
- package/dist/css/components/temperature-slider.css +29 -0
- package/dist/css/components/voice-message-bubble.css +62 -45
- package/dist/css/components/waveform-player.css +49 -34
- package/dist/css/components/wheel-picker.css +16 -15
- package/dist/css/tokens.css +4 -0
- package/dist/elements/index.d.ts +217 -4
- package/dist/elements/index.js +614 -9
- package/dist/elements/index.js.map +1 -1
- package/dist/{engine-Bn6hC87X.d.ts → engine-D6XV-WNt.d.ts} +390 -1
- package/dist/gl/index.js +2 -2
- package/dist/index.css +886 -177
- package/dist/index.d.ts +23 -23
- package/dist/index.js +37 -5
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +126 -7
- package/dist/react/index.js +722 -7
- package/dist/react/index.js.map +1 -1
- package/dist/{reduced-YS6TZjpa.d.ts → reduced-4croIXl-.d.ts} +1 -1
- package/dist/{store-CZ6vdtIE.d.ts → store-CAHWkurn.d.ts} +1 -1
- package/package.json +110 -110
- package/registry.json +1357 -1160
- package/src/auto/auto-init.ts +341 -308
- package/src/components/badge-button/README.md +28 -0
- package/src/components/badge-button/badge-button.css +15 -0
- package/src/components/badge-button/element.ts +16 -0
- package/src/components/badge-button/engine.ts +85 -0
- package/src/components/badge-button/react.tsx +31 -0
- package/src/components/bento-grid/README.md +26 -0
- package/src/components/bento-grid/bento-grid.css +12 -0
- package/src/components/bento-grid/element.ts +22 -0
- package/src/components/bento-grid/engine.ts +153 -0
- package/src/components/bento-grid/react.tsx +25 -0
- package/src/components/code-block/README.md +1 -1
- package/src/components/code-block/code-block.css +5 -0
- package/src/components/code-block/engine.ts +5 -0
- package/src/components/command-search/README.md +1 -1
- package/src/components/command-search/command-search.css +12 -2
- package/src/components/command-search/engine.ts +6 -0
- package/src/components/dialog/README.md +104 -103
- package/src/components/dialog/dialog.css +2 -0
- package/src/components/dialog/engine.ts +465 -449
- package/src/components/drilldown-menu/drilldown-menu.css +11 -7
- package/src/components/drilldown-menu/engine.ts +44 -4
- package/src/components/equalizer-slider/README.md +43 -0
- package/src/components/equalizer-slider/element.ts +41 -0
- package/src/components/equalizer-slider/engine.ts +32 -0
- package/src/components/equalizer-slider/equalizer-slider.css +26 -0
- package/src/components/equalizer-slider/react.tsx +60 -0
- package/src/components/equalizer-slider/shared.ts +161 -0
- package/src/components/handwriting-text/README.md +29 -0
- package/src/components/handwriting-text/element.ts +14 -0
- package/src/components/handwriting-text/engine.ts +108 -0
- package/src/components/handwriting-text/glyphs.ts +71 -0
- package/src/components/handwriting-text/handwriting-text.css +14 -0
- package/src/components/handwriting-text/react.tsx +23 -0
- package/src/components/heatmap-chart/README.md +48 -48
- package/src/components/heatmap-chart/engine.ts +6 -1
- package/src/components/heatmap-chart/heatmap-chart.css +5 -1
- package/src/components/icons.ts +1 -0
- package/src/components/magnetize-button/README.md +26 -0
- package/src/components/magnetize-button/element.ts +13 -0
- package/src/components/magnetize-button/engine.ts +84 -0
- package/src/components/magnetize-button/magnetize-button.css +17 -0
- package/src/components/magnetize-button/react.tsx +22 -0
- package/src/components/morphing-page-dots/README.md +28 -0
- package/src/components/morphing-page-dots/element.ts +16 -0
- package/src/components/morphing-page-dots/engine.ts +89 -0
- package/src/components/morphing-page-dots/morphing-page-dots.css +12 -0
- package/src/components/morphing-page-dots/react.tsx +32 -0
- package/src/components/not-found/README.md +26 -0
- package/src/components/not-found/element.ts +15 -0
- package/src/components/not-found/engine.ts +57 -0
- package/src/components/not-found/not-found.css +21 -0
- package/src/components/not-found/react.tsx +23 -0
- package/src/components/notification-stack/README.md +24 -0
- package/src/components/notification-stack/element.ts +26 -0
- package/src/components/notification-stack/engine.ts +329 -0
- package/src/components/notification-stack/notification-stack.css +41 -0
- package/src/components/notification-stack/react.tsx +28 -0
- package/src/components/particle-button/README.md +26 -0
- package/src/components/particle-button/element.ts +13 -0
- package/src/components/particle-button/engine.ts +52 -0
- package/src/components/particle-button/particle-button.css +13 -0
- package/src/components/particle-button/react.tsx +22 -0
- package/src/components/radio-group/README.md +43 -0
- package/src/components/radio-group/element.ts +41 -0
- package/src/components/radio-group/engine.ts +11 -0
- package/src/components/radio-group/radio-group.css +28 -0
- package/src/components/radio-group/react.tsx +60 -0
- package/src/components/radio-group/shared.ts +118 -0
- package/src/components/rating-scale/README.md +43 -0
- package/src/components/rating-scale/element.ts +41 -0
- package/src/components/rating-scale/engine.ts +63 -0
- package/src/components/rating-scale/rating-scale.css +22 -0
- package/src/components/rating-scale/react.tsx +60 -0
- package/src/components/scroll-area/README.md +89 -0
- package/src/components/scroll-area/element.ts +35 -0
- package/src/components/scroll-area/engine.ts +528 -0
- package/src/components/scroll-area/react.tsx +67 -0
- package/src/components/scroll-area/scroll-area.css +260 -0
- package/src/components/scroll-preview-dock/README.md +24 -0
- package/src/components/scroll-preview-dock/element.ts +20 -0
- package/src/components/scroll-preview-dock/engine.ts +228 -0
- package/src/components/scroll-preview-dock/react.tsx +26 -0
- package/src/components/scroll-preview-dock/scroll-preview-dock.css +17 -0
- package/src/components/select/README.md +23 -9
- package/src/components/select/engine.ts +16 -4
- package/src/components/select/select.css +38 -4
- package/src/components/signature-pad/engine.ts +550 -437
- package/src/components/signature-pad/signature-pad.css +130 -103
- package/src/components/stepper/README.md +46 -0
- package/src/components/stepper/element.ts +42 -0
- package/src/components/stepper/engine.ts +103 -0
- package/src/components/stepper/react.tsx +60 -0
- package/src/components/stepper/stepper.css +35 -0
- package/src/components/sticky-header/README.md +30 -0
- package/src/components/sticky-header/element.ts +19 -0
- package/src/components/sticky-header/engine.ts +95 -0
- package/src/components/sticky-header/react.tsx +25 -0
- package/src/components/sticky-header/sticky-header.css +9 -0
- package/src/components/temperature-slider/README.md +42 -0
- package/src/components/temperature-slider/element.ts +53 -0
- package/src/components/temperature-slider/engine.ts +44 -0
- package/src/components/temperature-slider/react.tsx +66 -0
- package/src/components/temperature-slider/temperature-slider.css +29 -0
- package/src/components/voice-message-bubble/engine.ts +11 -0
- package/src/components/voice-message-bubble/voice-message-bubble.css +14 -12
- package/src/components/waveform-player/README.md +1 -1
- package/src/components/waveform-player/engine.ts +344 -261
- package/src/components/waveform-player/waveform-player.css +50 -35
- package/src/components/wheel-picker/README.md +3 -1
- package/src/components/wheel-picker/element.ts +12 -5
- package/src/components/wheel-picker/engine.ts +37 -8
- package/src/components/wheel-picker/wheel-picker.css +17 -16
- package/src/core/timing.ts +2 -0
- package/src/css/tokens.css +4 -0
- package/src/elements/index.ts +321 -273
- package/src/index.ts +312 -295
- package/src/motion/morph.ts +5 -2
- package/src/react/index.ts +17 -0
- package/dist/chunk-7IQTMF7Y.js.map +0 -1
- package/dist/chunk-DOOMULVV.js.map +0 -1
- package/dist/chunk-VJFPTLAK.js.map +0 -1
- /package/dist/{chunk-QFBOHPWZ.js.map → chunk-CQP66AZK.js.map} +0 -0
- /package/dist/{chunk-QP4QJOEN.js.map → chunk-H4STLMCM.js.map} +0 -0
- /package/dist/{chunk-BZLMCHUD.js.map → chunk-KDIJX6G4.js.map} +0 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/* scroll-area — the scroller keeps native scrolling with its system bar hidden; the kit's bars ride on top: a 6 px pill
|
|
2
|
+
(text at 32 %) that shows while hovered or scrolling, swells to 10 px over a tonal rail on the swing curve and turns the
|
|
3
|
+
accent while dragged. Edges with more content fade out; an optional tonal pill takes the view back to the top. */
|
|
4
|
+
@property --mk-scroll-thickness {
|
|
5
|
+
syntax: '<length>';
|
|
6
|
+
inherits: true;
|
|
7
|
+
initial-value: 6px;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.mk-scroll-area,
|
|
11
|
+
mk-scroll-area {
|
|
12
|
+
--mk-scroll-thumb: color-mix(in srgb, var(--mk-text) 32%, transparent);
|
|
13
|
+
--mk-scroll-thumb-hover: color-mix(in srgb, var(--mk-text) 48%, transparent);
|
|
14
|
+
--mk-scroll-rail: color-mix(in srgb, var(--mk-text) 6%, transparent);
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
min-width: 0;
|
|
18
|
+
min-height: 0;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Its own viewport fills the area (a height or a max-height on the area both work). */
|
|
23
|
+
:is(.mk-scroll-area, mk-scroll-area) > [data-mk-scroll-viewport] {
|
|
24
|
+
flex: 1 1 auto;
|
|
25
|
+
min-width: 0;
|
|
26
|
+
min-height: 0;
|
|
27
|
+
overflow: auto;
|
|
28
|
+
overscroll-behavior: contain;
|
|
29
|
+
border-radius: inherit;
|
|
30
|
+
outline: none;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
:is(.mk-scroll-area, mk-scroll-area)[data-mk-scroll-axis='y'] > [data-mk-scroll-viewport] {
|
|
34
|
+
overflow-x: hidden;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
:is(.mk-scroll-area, mk-scroll-area)[data-mk-scroll-axis='x'] > [data-mk-scroll-viewport] {
|
|
38
|
+
overflow-y: hidden;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
[data-mk-scroll-anchor] {
|
|
42
|
+
position: relative;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* The kit's bars replace the system's wherever the scroller's own styles asked for a thin one. */
|
|
46
|
+
[data-mk-scroll-viewport] {
|
|
47
|
+
scrollbar-width: none !important;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
[data-mk-scroll-viewport]::-webkit-scrollbar {
|
|
51
|
+
display: none !important;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
[data-mk-scroll-area]:has(> [data-mk-scroll-viewport]:focus-visible) {
|
|
55
|
+
outline: 2px solid var(--mk-accent);
|
|
56
|
+
outline-offset: 2px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Edge fades: as deep as what is hidden past each edge, up to 24 px (written by the engine). */
|
|
60
|
+
[data-mk-fade] > [data-mk-scroll-viewport] {
|
|
61
|
+
--mk-scroll-fade-top: 0px;
|
|
62
|
+
--mk-scroll-fade-right: 0px;
|
|
63
|
+
--mk-scroll-fade-bottom: 0px;
|
|
64
|
+
--mk-scroll-fade-left: 0px;
|
|
65
|
+
-webkit-mask-image:
|
|
66
|
+
linear-gradient(to bottom, transparent, #000 var(--mk-scroll-fade-top), #000 calc(100% - var(--mk-scroll-fade-bottom)), transparent),
|
|
67
|
+
linear-gradient(to right, transparent, #000 var(--mk-scroll-fade-left), #000 calc(100% - var(--mk-scroll-fade-right)), transparent);
|
|
68
|
+
-webkit-mask-composite: source-in;
|
|
69
|
+
mask-image:
|
|
70
|
+
linear-gradient(to bottom, transparent, #000 var(--mk-scroll-fade-top), #000 calc(100% - var(--mk-scroll-fade-bottom)), transparent),
|
|
71
|
+
linear-gradient(to right, transparent, #000 var(--mk-scroll-fade-left), #000 calc(100% - var(--mk-scroll-fade-right)), transparent);
|
|
72
|
+
mask-composite: intersect;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* Rails: a 14 px grip along the edge; its tonal rail and the pill inside are drawn smaller. */
|
|
76
|
+
[data-mk-scroll-rail] {
|
|
77
|
+
--mk-scroll-thickness: 6px;
|
|
78
|
+
position: absolute;
|
|
79
|
+
z-index: 2;
|
|
80
|
+
opacity: 0;
|
|
81
|
+
touch-action: none;
|
|
82
|
+
-webkit-user-select: none;
|
|
83
|
+
user-select: none;
|
|
84
|
+
transition:
|
|
85
|
+
opacity 0.2s ease,
|
|
86
|
+
--mk-scroll-thickness 0.35s var(--mk-ease-swatch);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
[data-mk-scroll-rail][hidden] {
|
|
90
|
+
display: none;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* The rails run along the viewport's box inside the root (--mk-scroll-view-*, written by the engine);
|
|
94
|
+
--mk-scroll-inset keeps their ends clear of rounded corners. */
|
|
95
|
+
[data-mk-scroll-rail][data-mk-axis='y'] {
|
|
96
|
+
top: calc(var(--mk-scroll-view-top, 0px) + var(--mk-scroll-inset, 6px));
|
|
97
|
+
right: var(--mk-scroll-view-right, 0px);
|
|
98
|
+
bottom: calc(var(--mk-scroll-view-bottom, 0px) + var(--mk-scroll-inset, 6px));
|
|
99
|
+
width: 14px;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
[data-mk-scroll-rail][data-mk-axis='x'] {
|
|
103
|
+
right: calc(var(--mk-scroll-view-right, 0px) + var(--mk-scroll-inset, 6px));
|
|
104
|
+
bottom: var(--mk-scroll-view-bottom, 0px);
|
|
105
|
+
left: calc(var(--mk-scroll-view-left, 0px) + var(--mk-scroll-inset, 6px));
|
|
106
|
+
height: 14px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Both bars: each stops short of the other, no corner square. */
|
|
110
|
+
[data-mk-overflow-x] > [data-mk-scroll-rail][data-mk-axis='y'] {
|
|
111
|
+
bottom: calc(var(--mk-scroll-view-bottom, 0px) + 14px);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
[data-mk-overflow-y] > [data-mk-scroll-rail][data-mk-axis='x'] {
|
|
115
|
+
right: calc(var(--mk-scroll-view-right, 0px) + 14px);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
[data-mk-active] > [data-mk-scroll-rail],
|
|
119
|
+
[data-mk-scroll-rail]:hover,
|
|
120
|
+
[data-mk-scroll-rail][data-mk-dragging] {
|
|
121
|
+
opacity: 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
[data-mk-scroll-rail]:hover,
|
|
125
|
+
[data-mk-scroll-rail][data-mk-dragging] {
|
|
126
|
+
--mk-scroll-thickness: 10px;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
[data-mk-scroll-rail]::before {
|
|
130
|
+
content: '';
|
|
131
|
+
position: absolute;
|
|
132
|
+
border-radius: 999px;
|
|
133
|
+
background: var(--mk-scroll-rail, color-mix(in srgb, var(--mk-text) 6%, transparent));
|
|
134
|
+
opacity: 0;
|
|
135
|
+
transition: opacity 0.2s ease;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
[data-mk-scroll-rail][data-mk-axis='y']::before {
|
|
139
|
+
top: 0;
|
|
140
|
+
right: 2px;
|
|
141
|
+
bottom: 0;
|
|
142
|
+
width: 10px;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
[data-mk-scroll-rail][data-mk-axis='x']::before {
|
|
146
|
+
right: 0;
|
|
147
|
+
bottom: 2px;
|
|
148
|
+
left: 0;
|
|
149
|
+
height: 10px;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
[data-mk-scroll-rail]:hover::before,
|
|
153
|
+
[data-mk-scroll-rail][data-mk-dragging]::before {
|
|
154
|
+
opacity: 1;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
[data-mk-scroll-thumb] {
|
|
158
|
+
position: absolute;
|
|
159
|
+
top: 0;
|
|
160
|
+
left: 0;
|
|
161
|
+
cursor: grab;
|
|
162
|
+
will-change: transform;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
[data-mk-scroll-rail][data-mk-axis='y'] [data-mk-scroll-thumb] {
|
|
166
|
+
width: 100%;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
[data-mk-scroll-rail][data-mk-axis='x'] [data-mk-scroll-thumb] {
|
|
170
|
+
height: 100%;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
[data-mk-scroll-rail][data-mk-dragging] [data-mk-scroll-thumb] {
|
|
174
|
+
cursor: grabbing;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* The pill stays centred on the 10 px rail while it swells. */
|
|
178
|
+
[data-mk-scroll-pill] {
|
|
179
|
+
position: absolute;
|
|
180
|
+
border-radius: 999px;
|
|
181
|
+
background: var(--mk-scroll-thumb, color-mix(in srgb, var(--mk-text) 32%, transparent));
|
|
182
|
+
transition: background-color 0.2s ease;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
[data-mk-scroll-rail][data-mk-axis='y'] [data-mk-scroll-pill] {
|
|
186
|
+
top: 0;
|
|
187
|
+
right: calc(2px + (10px - var(--mk-scroll-thickness)) / 2);
|
|
188
|
+
bottom: 0;
|
|
189
|
+
width: var(--mk-scroll-thickness);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
[data-mk-scroll-rail][data-mk-axis='x'] [data-mk-scroll-pill] {
|
|
193
|
+
right: 0;
|
|
194
|
+
bottom: calc(2px + (10px - var(--mk-scroll-thickness)) / 2);
|
|
195
|
+
left: 0;
|
|
196
|
+
height: var(--mk-scroll-thickness);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
[data-mk-scroll-rail]:hover [data-mk-scroll-pill] {
|
|
200
|
+
background: var(--mk-scroll-thumb-hover, color-mix(in srgb, var(--mk-text) 48%, transparent));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
[data-mk-scroll-rail][data-mk-dragging] [data-mk-scroll-pill] {
|
|
204
|
+
background: var(--mk-scroll-thumb-active, var(--mk-accent));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
[data-mk-dragging] [data-mk-scroll-viewport] {
|
|
208
|
+
-webkit-user-select: none;
|
|
209
|
+
user-select: none;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* Back to top: a tonal pill in the corner, clear of the bars. */
|
|
213
|
+
[data-mk-scroll-top] {
|
|
214
|
+
position: absolute;
|
|
215
|
+
z-index: 3;
|
|
216
|
+
right: calc(var(--mk-scroll-view-right, 0px) + 20px);
|
|
217
|
+
bottom: calc(var(--mk-scroll-view-bottom, 0px) + 16px);
|
|
218
|
+
display: inline-grid;
|
|
219
|
+
place-items: center;
|
|
220
|
+
width: 40px;
|
|
221
|
+
height: 40px;
|
|
222
|
+
padding: 0;
|
|
223
|
+
border: 0;
|
|
224
|
+
border-radius: 999px;
|
|
225
|
+
background: var(--mk-surface-4);
|
|
226
|
+
box-shadow: 0 8px 24px -8px color-mix(in srgb, var(--mk-shadow-color, #000) 45%, transparent);
|
|
227
|
+
color: var(--mk-text);
|
|
228
|
+
cursor: pointer;
|
|
229
|
+
transition:
|
|
230
|
+
background-color 0.15s,
|
|
231
|
+
box-shadow 0.2s;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
[data-mk-overflow-x] > [data-mk-scroll-top] {
|
|
235
|
+
bottom: calc(var(--mk-scroll-view-bottom, 0px) + 22px);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
[data-mk-scroll-top][hidden] {
|
|
239
|
+
display: none;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
[data-mk-scroll-top]:hover {
|
|
243
|
+
background: color-mix(in srgb, var(--mk-surface-4) 80%, var(--mk-text) 10%);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
[data-mk-scroll-top]:focus-visible {
|
|
247
|
+
outline: none;
|
|
248
|
+
box-shadow:
|
|
249
|
+
0 0 0 3px color-mix(in srgb, var(--mk-accent) 45%, transparent),
|
|
250
|
+
0 8px 24px -8px color-mix(in srgb, var(--mk-shadow-color, #000) 45%, transparent);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
@media (prefers-reduced-motion: reduce) {
|
|
254
|
+
[data-mk-scroll-rail],
|
|
255
|
+
[data-mk-scroll-rail]::before,
|
|
256
|
+
[data-mk-scroll-pill],
|
|
257
|
+
[data-mk-scroll-top] {
|
|
258
|
+
transition: none;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Scroll preview dock
|
|
2
|
+
|
|
3
|
+
Section links with thumbnails, a travelling selected pill and an adjacent hover/focus preview. Selection follows the actual scroll position in the supplied scroller.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
const dock = createScrollPreviewDock(document.querySelector('#dock'), {
|
|
7
|
+
scroller: document.querySelector('#story'),
|
|
8
|
+
offset: 12,
|
|
9
|
+
items: [
|
|
10
|
+
{ id: 'idea', label: 'La idea', description: 'Un punto de partida', thumbnail: '/idea.webp' },
|
|
11
|
+
{ id: 'shape', label: 'La forma', thumbnail: '/shape.webp' },
|
|
12
|
+
],
|
|
13
|
+
});
|
|
14
|
+
dock.select('shape');
|
|
15
|
+
dock.destroy();
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Exports: `createScrollPreviewDock`, `ScrollPreviewDockItem`, `ScrollPreviewDockOptions`, `ScrollPreviewDockInstance`; React `ScrollPreviewDock`, `ScrollPreviewDockProps`; elements `MkScrollPreviewDockElement`, `defineScrollPreviewDock` (`mk-scroll-preview-dock`). CSS: `scroll-preview-dock.css` plus kit tokens/theme/fonts.
|
|
19
|
+
|
|
20
|
+
Options: `items`, `scroller` (element/selector, otherwise window), `offset` (pixels), `label`, `disabled`, `onChange(id)`. Each item has `id`, `label`, optional `description`, `thumbnail`, `section` (element/selector, defaults to the element with that ID), `disabled`. Give each target section a real `id` to preserve native fragment-link behavior. Missing sections leave a working native link and do not initiate scripted scrolling.
|
|
21
|
+
|
|
22
|
+
Instance: `select(id)`, `update()`, `setOptions(partial)`, readonly `value`, `destroy()`. `mk:change` emits `{value}` for the tracked section. Hover/focus changes only the preview. ArrowLeft/ArrowRight and Home/End move focus among enabled links; normal Tab/Enter and modified link clicks retain native semantics. Wheel/touch interrupts scripted scrolling. Reduced motion jumps immediately.
|
|
23
|
+
|
|
24
|
+
React owns an empty host and accepts data through `items`, with optional `instanceRef`; children/HTML are ignored at runtime. An element accepts JSON `items`, scalar attributes and `setOptions`, `items`, `select`/`update` methods. Updates retain the engine and restore focus to a surviving link. Destroy removes listeners, pending animation frames and scroll tweens, restoring authored content.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { MkElement, defineElement, type OptionType } from '../../elements/base';
|
|
2
|
+
import { createScrollPreviewDock, type ScrollPreviewDockInstance, type ScrollPreviewDockOptions, type ScrollPreviewDockItem } from './engine';
|
|
3
|
+
|
|
4
|
+
export class MkScrollPreviewDockElement extends MkElement<ScrollPreviewDockInstance> {
|
|
5
|
+
static options: Record<string, OptionType> = {"items":"json","scroller":"string","offset":"number","label":"string","disabled":"boolean"};
|
|
6
|
+
protected create(options: Record<string, unknown>) { return createScrollPreviewDock(this, options); }
|
|
7
|
+
setOptions(options: Partial<ScrollPreviewDockOptions>) { this.instance?.setOptions(options); }
|
|
8
|
+
|
|
9
|
+
select(id: string) { this.instance?.select(id); }
|
|
10
|
+
update() { this.instance?.update(); }
|
|
11
|
+
get value() { return this.instance?.value ?? ''; }
|
|
12
|
+
set items(value: ScrollPreviewDockItem[]) { this.instance?.setOptions({ items: value }); }
|
|
13
|
+
attributeChangedCallback(name: string, previous: string | null, next: string | null) {
|
|
14
|
+
if (previous === next || !this.isConnected || !this.instance) return;
|
|
15
|
+
const option = name.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
|
|
16
|
+
const defaults: Record<string, unknown> = {"items":[],"scroller":null,"offset":0,"label":"Explorar secciones","disabled":false};
|
|
17
|
+
this.instance.setOptions({ [option]: this.options[option] ?? defaults[option] });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export const defineScrollPreviewDock = () => defineElement('mk-scroll-preview-dock', MkScrollPreviewDockElement);
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { gsap, EASE, registerMotion } from '../../core/gsap';
|
|
2
|
+
import { createScope } from '../../core/scope';
|
|
3
|
+
import { reduced } from '../../core/reduced';
|
|
4
|
+
import { inert, type Instance } from '../../core/types';
|
|
5
|
+
import { blurIn, press } from '../../motion/micro';
|
|
6
|
+
|
|
7
|
+
export interface ScrollPreviewDockItem {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
thumbnail?: string;
|
|
12
|
+
/** A section element or selector. Defaults to the element with this item's id. */
|
|
13
|
+
section?: HTMLElement | string;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface ScrollPreviewDockOptions {
|
|
17
|
+
items?: ScrollPreviewDockItem[];
|
|
18
|
+
scroller?: HTMLElement | string | null;
|
|
19
|
+
offset?: number;
|
|
20
|
+
label?: string;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
onChange?: (id: string) => void;
|
|
23
|
+
}
|
|
24
|
+
export interface ScrollPreviewDockInstance extends Instance {
|
|
25
|
+
select(id: string): void;
|
|
26
|
+
update(): void;
|
|
27
|
+
setOptions(options: Partial<ScrollPreviewDockOptions>): void;
|
|
28
|
+
readonly value: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Native section links, a travelling selection pill, and a focus/hover preview. */
|
|
32
|
+
export function createScrollPreviewDock(root: HTMLElement | null, options: ScrollPreviewDockOptions = {}): ScrollPreviewDockInstance {
|
|
33
|
+
if (!root) return inert({ select() {}, update() {}, setOptions() {}, value: '' });
|
|
34
|
+
registerMotion();
|
|
35
|
+
const scope = createScope();
|
|
36
|
+
let opts = { ...options };
|
|
37
|
+
let current = '';
|
|
38
|
+
let previewed = '';
|
|
39
|
+
let frame = 0;
|
|
40
|
+
let scrolling: gsap.core.Tween | null = null;
|
|
41
|
+
let unbindScroller = () => {};
|
|
42
|
+
let scroller: HTMLElement | null = null;
|
|
43
|
+
const original = [...root.childNodes];
|
|
44
|
+
const animations = new Set<gsap.core.Animation>();
|
|
45
|
+
const track = (animation: gsap.core.Animation | null) => { if (animation) animations.add(animation); };
|
|
46
|
+
const panel = document.createElement('div');
|
|
47
|
+
panel.className = 'mk-scroll-preview-dock__preview';
|
|
48
|
+
panel.setAttribute('aria-hidden', 'true');
|
|
49
|
+
const image = document.createElement('span');
|
|
50
|
+
image.className = 'mk-scroll-preview-dock__image';
|
|
51
|
+
const copy = document.createElement('span');
|
|
52
|
+
const title = document.createElement('strong');
|
|
53
|
+
const description = document.createElement('span');
|
|
54
|
+
description.className = 'mk-scroll-preview-dock__description';
|
|
55
|
+
copy.append(title, description);
|
|
56
|
+
panel.append(image, copy);
|
|
57
|
+
const nav = document.createElement('nav');
|
|
58
|
+
nav.className = 'mk-scroll-preview-dock__nav';
|
|
59
|
+
const pill = document.createElement('span');
|
|
60
|
+
pill.className = 'mk-scroll-preview-dock__pill';
|
|
61
|
+
pill.setAttribute('aria-hidden', 'true');
|
|
62
|
+
const empty = document.createElement('p');
|
|
63
|
+
empty.className = 'mk-scroll-preview-dock__empty';
|
|
64
|
+
empty.textContent = 'No hay secciones para explorar.';
|
|
65
|
+
root.replaceChildren(panel, nav, empty);
|
|
66
|
+
scope.attr(root, 'class', `${root.className} mk-scroll-preview-dock`.trim());
|
|
67
|
+
scope.attr(root, 'data-mk-ready', '');
|
|
68
|
+
const items = () => opts.items ?? [];
|
|
69
|
+
const enabled = (item: ScrollPreviewDockItem) => !opts.disabled && !item.disabled;
|
|
70
|
+
const section = (item: ScrollPreviewDockItem) => {
|
|
71
|
+
if (item.section instanceof HTMLElement) return item.section;
|
|
72
|
+
if (!item.section) return document.getElementById(item.id);
|
|
73
|
+
try { return (scroller ?? document).querySelector<HTMLElement>(item.section); } catch { return null; }
|
|
74
|
+
};
|
|
75
|
+
const links = () => [...nav.querySelectorAll<HTMLAnchorElement>('a')];
|
|
76
|
+
const thumb = (node: HTMLElement, item: ScrollPreviewDockItem) => {
|
|
77
|
+
node.replaceChildren();
|
|
78
|
+
if (item.thumbnail) { const img = document.createElement('img'); img.src = item.thumbnail; img.alt = ''; img.draggable = false; node.append(img); }
|
|
79
|
+
else { const letter = document.createElement('span'); letter.textContent = item.label.slice(0, 2); node.append(letter); }
|
|
80
|
+
};
|
|
81
|
+
const preview = (id: string, animate = true) => {
|
|
82
|
+
const item = items().find((item) => item.id === id);
|
|
83
|
+
if (!item || previewed === id) return;
|
|
84
|
+
previewed = id;
|
|
85
|
+
thumb(image, item);
|
|
86
|
+
title.textContent = item.label;
|
|
87
|
+
description.textContent = item.description ?? 'Explorar sección';
|
|
88
|
+
for (const link of links()) link.dataset.mkPreview = String(link.dataset.mkSection === id);
|
|
89
|
+
if (animate) { gsap.killTweensOf(panel); track(blurIn(panel, { from: 'soft', duration: .28 })); }
|
|
90
|
+
};
|
|
91
|
+
const place = (animate = true) => {
|
|
92
|
+
const active = links().find((link) => link.dataset.mkSection === current);
|
|
93
|
+
pill.hidden = !active;
|
|
94
|
+
if (!active) return;
|
|
95
|
+
track(gsap.to(pill, { x: active.offsetLeft, y: active.offsetTop, width: active.offsetWidth, height: active.offsetHeight, duration: reduced() || !animate ? 0 : .45, ease: EASE.swatch, overwrite: 'auto' }));
|
|
96
|
+
};
|
|
97
|
+
const activate = (id: string, notify = true) => {
|
|
98
|
+
if (scope.disposed || current === id) return;
|
|
99
|
+
current = id;
|
|
100
|
+
for (const link of links()) {
|
|
101
|
+
if (link.dataset.mkSection === id) link.setAttribute('aria-current', 'location');
|
|
102
|
+
else link.removeAttribute('aria-current');
|
|
103
|
+
}
|
|
104
|
+
place();
|
|
105
|
+
if (!root.contains(document.activeElement) && !root.matches(':hover')) preview(id);
|
|
106
|
+
else if (!previewed) preview(id, false);
|
|
107
|
+
if (notify) {
|
|
108
|
+
opts.onChange?.(id);
|
|
109
|
+
root.dispatchEvent(new CustomEvent('mk:change', { bubbles: true, detail: { value: id } }));
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const update = () => {
|
|
113
|
+
frame = 0;
|
|
114
|
+
if (scope.disposed) return;
|
|
115
|
+
const top = scroller ? scroller.getBoundingClientRect().top + scroller.clientTop : 0;
|
|
116
|
+
const height = scroller ? scroller.clientHeight : window.innerHeight;
|
|
117
|
+
const line = top + Math.min(120, height * .3) + (opts.offset ?? 0);
|
|
118
|
+
const targets = items().filter(enabled).map((item) => ({ item, node: section(item) })).filter((entry): entry is { item: ScrollPreviewDockItem; node: HTMLElement } => !!entry.node);
|
|
119
|
+
let target: (typeof targets)[number] | undefined = targets[0];
|
|
120
|
+
for (const entry of targets) if (entry.node.getBoundingClientRect().top <= line) target = entry;
|
|
121
|
+
const atEnd = scroller ? scroller.scrollHeight > scroller.clientHeight && scroller.scrollTop + scroller.clientHeight >= scroller.scrollHeight - 2 : document.documentElement.scrollHeight > window.innerHeight && window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 2;
|
|
122
|
+
if (atEnd) target = targets.at(-1);
|
|
123
|
+
if (target) activate(target.item.id);
|
|
124
|
+
place(false);
|
|
125
|
+
};
|
|
126
|
+
const queue = () => { if (!frame) frame = requestAnimationFrame(update); };
|
|
127
|
+
const bindScroller = () => {
|
|
128
|
+
unbindScroller();
|
|
129
|
+
scroller = typeof opts.scroller === 'string' ? (() => { try { return root.closest<HTMLElement>(opts.scroller) ?? document.querySelector<HTMLElement>(opts.scroller); } catch { return null; } })() : opts.scroller ?? null;
|
|
130
|
+
const target = scroller ?? window;
|
|
131
|
+
target.addEventListener('scroll', queue, { passive: true });
|
|
132
|
+
target.addEventListener('wheel', cancelScroll, { passive: true });
|
|
133
|
+
target.addEventListener('touchstart', cancelScroll, { passive: true });
|
|
134
|
+
unbindScroller = () => { target.removeEventListener('scroll', queue); target.removeEventListener('wheel', cancelScroll); target.removeEventListener('touchstart', cancelScroll); };
|
|
135
|
+
};
|
|
136
|
+
function cancelScroll() { scrolling?.kill(); scrolling = null; }
|
|
137
|
+
const select = (id: string) => {
|
|
138
|
+
if (scope.disposed) return;
|
|
139
|
+
const item = items().find((entry) => entry.id === id);
|
|
140
|
+
if (!item || !enabled(item)) return;
|
|
141
|
+
const target = section(item);
|
|
142
|
+
if (!target) return;
|
|
143
|
+
cancelScroll();
|
|
144
|
+
const top = Math.max(0, (scroller ? scroller.scrollTop + target.getBoundingClientRect().top - scroller.getBoundingClientRect().top - scroller.clientTop : window.scrollY + target.getBoundingClientRect().top) - (opts.offset ?? 0));
|
|
145
|
+
activate(id);
|
|
146
|
+
preview(id);
|
|
147
|
+
if (scroller) {
|
|
148
|
+
if (reduced()) scroller.scrollTop = top;
|
|
149
|
+
else scrolling = gsap.to(scroller, { scrollTop: top, duration: .65, ease: EASE.signature, overwrite: 'auto', onUpdate: queue });
|
|
150
|
+
} else if (reduced()) window.scrollTo({ top, behavior: 'instant' });
|
|
151
|
+
else {
|
|
152
|
+
const position = { top: window.scrollY };
|
|
153
|
+
scrolling = gsap.to(position, { top, duration: .65, ease: EASE.signature, onUpdate: () => { window.scrollTo(0, position.top); queue(); } });
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const render = () => {
|
|
157
|
+
gsap.killTweensOf(pill);
|
|
158
|
+
nav.replaceChildren(pill);
|
|
159
|
+
nav.setAttribute('aria-label', opts.label ?? 'Explorar secciones');
|
|
160
|
+
const unique = new Set<string>();
|
|
161
|
+
for (const item of items()) {
|
|
162
|
+
if (!item.id || unique.has(item.id)) continue;
|
|
163
|
+
unique.add(item.id);
|
|
164
|
+
const link = document.createElement('a');
|
|
165
|
+
link.href = `#${encodeURIComponent(section(item)?.id || item.id)}`;
|
|
166
|
+
link.dataset.mkSection = item.id;
|
|
167
|
+
link.className = 'mk-scroll-preview-dock__item';
|
|
168
|
+
const cover = document.createElement('span');
|
|
169
|
+
cover.className = 'mk-scroll-preview-dock__thumb';
|
|
170
|
+
cover.setAttribute('aria-hidden', 'true');
|
|
171
|
+
thumb(cover, item);
|
|
172
|
+
const label = document.createElement('span');
|
|
173
|
+
label.textContent = item.label;
|
|
174
|
+
link.append(cover, label);
|
|
175
|
+
if (!enabled(item)) { link.setAttribute('aria-disabled', 'true'); link.tabIndex = -1; }
|
|
176
|
+
nav.append(link);
|
|
177
|
+
}
|
|
178
|
+
empty.hidden = unique.size > 0;
|
|
179
|
+
panel.hidden = unique.size === 0;
|
|
180
|
+
nav.hidden = unique.size === 0;
|
|
181
|
+
scope.attr(root, 'data-mk-disabled', opts.disabled ? '' : null);
|
|
182
|
+
previewed = '';
|
|
183
|
+
const next = items().find((item) => item.id === current && enabled(item)) ?? items().find(enabled);
|
|
184
|
+
current = '';
|
|
185
|
+
if (next) { activate(next.id, false); preview(next.id, false); }
|
|
186
|
+
else pill.hidden = true;
|
|
187
|
+
};
|
|
188
|
+
scope.on(nav, 'click', (event) => {
|
|
189
|
+
const link = (event.target as Element).closest<HTMLAnchorElement>('a[data-mk-section]');
|
|
190
|
+
if (!link) return;
|
|
191
|
+
if (link.getAttribute('aria-disabled') === 'true') { event.preventDefault(); return; }
|
|
192
|
+
if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.button !== 0) return;
|
|
193
|
+
const item = items().find((item) => item.id === link.dataset.mkSection);
|
|
194
|
+
if (!item || !section(item)) return;
|
|
195
|
+
event.preventDefault(); track(press(link)); select(item.id);
|
|
196
|
+
});
|
|
197
|
+
scope.on(nav, 'pointerover', (event) => { const link = (event.target as Element).closest<HTMLElement>('a[data-mk-section]'); if (link && link.getAttribute('aria-disabled') !== 'true') preview(link.dataset.mkSection!); });
|
|
198
|
+
scope.on(nav, 'focusin', (event) => { const link = (event.target as Element).closest<HTMLElement>('a[data-mk-section]'); if (link) preview(link.dataset.mkSection!); });
|
|
199
|
+
scope.on(root, 'pointerleave', () => { if (!root.contains(document.activeElement)) preview(current); });
|
|
200
|
+
scope.on(root, 'focusout', (event) => { if (!root.contains(event.relatedTarget as Node | null)) preview(current); });
|
|
201
|
+
scope.on(nav, 'keydown', (event) => {
|
|
202
|
+
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) { if (['PageUp', 'PageDown', 'ArrowUp', 'ArrowDown', ' '].includes(event.key)) cancelScroll(); return; }
|
|
203
|
+
const available = links().filter((link) => link.getAttribute('aria-disabled') !== 'true');
|
|
204
|
+
const index = available.indexOf(document.activeElement as HTMLAnchorElement);
|
|
205
|
+
if (index < 0 || !available.length) return;
|
|
206
|
+
event.preventDefault();
|
|
207
|
+
available[event.key === 'Home' ? 0 : event.key === 'End' ? available.length - 1 : (index + (event.key === 'ArrowRight' ? 1 : -1) + available.length) % available.length]?.focus();
|
|
208
|
+
});
|
|
209
|
+
scope.on(window, 'resize', queue, { passive: true });
|
|
210
|
+
if (typeof ResizeObserver === 'function') { const resize = scope.observe(new ResizeObserver(queue)); resize.observe(nav); }
|
|
211
|
+
bindScroller(); render(); update();
|
|
212
|
+
return {
|
|
213
|
+
select, update,
|
|
214
|
+
get value() { return current; },
|
|
215
|
+
setOptions(next) {
|
|
216
|
+
if (scope.disposed) return;
|
|
217
|
+
const focusedId = root.contains(document.activeElement)
|
|
218
|
+
? (document.activeElement as HTMLElement | null)?.closest<HTMLAnchorElement>('a[data-mk-section]')?.dataset.mkSection
|
|
219
|
+
: undefined;
|
|
220
|
+
opts = { ...opts, ...next };
|
|
221
|
+
if ('scroller' in next) { cancelScroll(); bindScroller(); }
|
|
222
|
+
render();
|
|
223
|
+
if (focusedId) { const target = links().find((link) => link.dataset.mkSection === focusedId && link.getAttribute('aria-disabled') !== 'true') ?? links().find((link) => link.getAttribute('aria-disabled') !== 'true'); if (target) target.focus(); else { scope.attr(root, 'tabindex', root.getAttribute('tabindex')); root.tabIndex = -1; root.focus(); } }
|
|
224
|
+
update();
|
|
225
|
+
},
|
|
226
|
+
destroy() { if (scope.disposed) return; cancelAnimationFrame(frame); cancelScroll(); unbindScroller(); animations.forEach((animation) => animation.kill()); gsap.killTweensOf([...root.querySelectorAll('*')]); scope.dispose(); root.replaceChildren(...original); },
|
|
227
|
+
};
|
|
228
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useRef, type HTMLAttributes, type MutableRefObject } from 'react';
|
|
3
|
+
import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
|
|
4
|
+
import { createScrollPreviewDock, type ScrollPreviewDockOptions, type ScrollPreviewDockInstance } from './engine';
|
|
5
|
+
|
|
6
|
+
export interface ScrollPreviewDockProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange' | 'children' | 'dangerouslySetInnerHTML'>, ScrollPreviewDockOptions {
|
|
7
|
+
instanceRef?: MutableRefObject<ScrollPreviewDockInstance | null>;
|
|
8
|
+
children?: never;
|
|
9
|
+
dangerouslySetInnerHTML?: never;
|
|
10
|
+
}
|
|
11
|
+
export function ScrollPreviewDock({ items, scroller, offset, label, disabled, onChange, instanceRef, className, children, dangerouslySetInnerHTML: _html, ...props }: ScrollPreviewDockProps) {
|
|
12
|
+
const root = useRef<HTMLDivElement>(null);
|
|
13
|
+
const instance = useRef<ScrollPreviewDockInstance | null>(null);
|
|
14
|
+
const onChangeRef = useLatest(onChange);
|
|
15
|
+
useIsomorphicLayoutEffect(() => {
|
|
16
|
+
const created = createScrollPreviewDock(root.current, { items, scroller, offset, label, disabled, onChange: (...args) => onChangeRef.current?.(...args) });
|
|
17
|
+
instance.current = created;
|
|
18
|
+
return () => { created.destroy(); instance.current = null; };
|
|
19
|
+
// Options update the existing engine so timers, selection and scroll state survive.
|
|
20
|
+
}, []);
|
|
21
|
+
useIsomorphicLayoutEffect(() => {
|
|
22
|
+
instance.current?.setOptions({ items, scroller, offset, label, disabled });
|
|
23
|
+
}, [items, scroller, offset, label, disabled]);
|
|
24
|
+
useIsomorphicLayoutEffect(() => { if (instanceRef) instanceRef.current = instance.current; return () => { if (instanceRef) instanceRef.current = null; }; }, [instanceRef]);
|
|
25
|
+
return <div {...props} ref={root} className={['mk-scroll-preview-dock', className].filter(Boolean).join(' ')}></div>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
.mk-scroll-preview-dock, mk-scroll-preview-dock { box-sizing: border-box; display: grid; gap: 10px; width: min(100%, 400px); color: var(--mk-text); font-family: var(--mk-font); }
|
|
2
|
+
.mk-scroll-preview-dock__preview { display: flex; align-items: center; gap: 12px; min-height: 60px; box-sizing: border-box; padding: 12px 16px; border-radius: 24px; background: var(--mk-surface-2); box-shadow: 0 1px 0 var(--mk-outline-soft) inset; }
|
|
3
|
+
.mk-scroll-preview-dock__image { display: grid; place-items: center; flex: 0 0 44px; width: 44px; height: 44px; border-radius: 12px; background: var(--mk-surface-3, var(--mk-surface)); overflow: hidden; font-weight: 600; font-size: 14px; }
|
|
4
|
+
.mk-scroll-preview-dock__image img, .mk-scroll-preview-dock__thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
|
5
|
+
.mk-scroll-preview-dock__preview strong { display: block; font-size: 13px; font-weight: 600; line-height: 1.4; }
|
|
6
|
+
.mk-scroll-preview-dock__description { display: block; color: var(--mk-muted); font-size: 11px; line-height: 1.5; }
|
|
7
|
+
.mk-scroll-preview-dock__nav { position: relative; box-sizing: border-box; display: flex; align-items: stretch; justify-content: space-between; gap: 4px; width: 100%; padding: 6px; border-radius: 32px; background: var(--mk-surface); }
|
|
8
|
+
.mk-scroll-preview-dock__pill { position: absolute; top: 0; left: 0; border-radius: 24px; background: var(--mk-surface-3, var(--mk-surface-2)); pointer-events: none; box-shadow: 0 1px 0 var(--mk-outline-soft) inset; }
|
|
9
|
+
.mk-scroll-preview-dock__item { position: relative; z-index: 1; display: flex; flex: 1 1 0; align-items: center; flex-direction: column; justify-content: center; gap: 6px; min-width: 0; min-height: 68px; padding: 6px; border-radius: 24px; text-align: center; color: var(--mk-muted); text-decoration: none; font-size: 10px; line-height: 1.3; -webkit-tap-highlight-color: transparent; transition: color .2s; }
|
|
10
|
+
.mk-scroll-preview-dock__thumb { display: grid; place-items: center; width: 32px; height: 32px; border-radius: 12px; background: var(--mk-surface-2); overflow: hidden; color: var(--mk-text); font-size: 10px; font-weight: 600; transition: transform .4s var(--mk-ease-swatch), border-radius .4s var(--mk-ease-swatch); }
|
|
11
|
+
.mk-scroll-preview-dock__item[aria-current] { color: var(--mk-text); }
|
|
12
|
+
.mk-scroll-preview-dock__item[data-mk-preview=true] .mk-scroll-preview-dock__thumb { transform: translateY(-3px) rotate(-4deg) scale(1.08); border-radius: 8px; }
|
|
13
|
+
.mk-scroll-preview-dock__item:focus-visible { outline: 2px solid var(--mk-accent); outline-offset: 2px; }
|
|
14
|
+
.mk-scroll-preview-dock__item[aria-disabled=true] { opacity: .4; cursor: not-allowed; }
|
|
15
|
+
.mk-scroll-preview-dock [hidden] { display: none !important; }
|
|
16
|
+
.mk-scroll-preview-dock__empty { margin: 0; padding: 20px; text-align: center; background: var(--mk-surface); border-radius: 24px; font-size: 12px; color: var(--mk-muted); }
|
|
17
|
+
@media (prefers-reduced-motion: reduce) { .mk-scroll-preview-dock * { transition: none !important; } }
|
|
@@ -1,34 +1,48 @@
|
|
|
1
1
|
# select
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Origen: archivo del kit y parámetros (tiempos, curvas).
|
|
3
|
+
Una elección de una lista que se abre como el `menu` del kit (crece desde su botón y las opciones entran escalonadas) en lugar de la lista del sistema. Mejora un `<select>` escrito a mano, o crea uno desde `options`, y lo mantiene sincronizado y oculto: los formularios lo envían y sus escuchas de `change` siguen funcionando. El valor elegido entra con un blur breve y el chevron gira. Una lista larga se desplaza con la barra de [`scroll-area`](../scroll-area/README.md), no con la del sistema; respeta las esquinas redondeadas del menú y funde los bordes.
|
|
6
4
|
|
|
7
5
|
## HTML / Astro
|
|
8
6
|
|
|
9
7
|
```html
|
|
10
|
-
<mk-select
|
|
8
|
+
<mk-select class="mk-select" label="Responsable">
|
|
9
|
+
<select name="owner">
|
|
10
|
+
<option value="">Elige a alguien</option>
|
|
11
|
+
<optgroup label="Diseño"><option value="ana">Ana Gómez</option></optgroup>
|
|
12
|
+
<optgroup label="Código"><option value="leo">Leo Ruiz</option></optgroup>
|
|
13
|
+
</select>
|
|
14
|
+
</mk-select>
|
|
11
15
|
<script>
|
|
12
16
|
import { defineElements } from '@yewdiel21/motion-kit/elements';
|
|
13
17
|
defineElements(['mk-select']);
|
|
14
18
|
</script>
|
|
15
19
|
```
|
|
16
20
|
|
|
21
|
+
Dentro de un `.mk-field` el botón toma la altura, el radio y el relleno de los demás campos. Para cambiar el valor desde fuera usa el atributo `value` (o `setValue` en la instancia); el `<select>` y el botón se actualizan juntos.
|
|
22
|
+
|
|
17
23
|
## React
|
|
18
24
|
|
|
19
25
|
```tsx
|
|
20
|
-
<Select
|
|
26
|
+
<Select label="Prioridad" name="priority" value={priority} onChange={setPriority}
|
|
27
|
+
options={[{ value: 'low', label: 'Baja' }, { value: 'normal', label: 'Normal' }, { value: 'high', label: 'Alta' }]} />
|
|
21
28
|
```
|
|
22
29
|
|
|
30
|
+
`options` acepta `{ value, label, disabled?, group? }`; `group` crea encabezados como `<optgroup>`.
|
|
31
|
+
|
|
23
32
|
## Opciones
|
|
24
33
|
|
|
25
34
|
| Opción | Atributo | Por defecto | Qué hace |
|
|
26
35
|
|---|---|---|---|
|
|
36
|
+
| `options` | — | las del `<select>` | Opciones; sin ellas se leen del `<select>` interior. |
|
|
37
|
+
| `value` | `value` | el del `<select>` | Valor elegido. |
|
|
38
|
+
| `name` | `name` | — | Nombre del `<select>` que crea (uno escrito conserva el suyo). |
|
|
39
|
+
| `label` | `label` | `'Opciones'` | Nombre accesible; el botón se lee «label: elegido». |
|
|
40
|
+
| `placeholder` | `placeholder` | texto de la opción vacía o `'Elige…'` | Texto mientras no hay elección. |
|
|
41
|
+
| `disabled` | `disabled` | `false` | Desactiva el botón. |
|
|
42
|
+
| `onChange(value)` | evento `change` del `<select>` / `mk:change` | — | Avisa al elegir. |
|
|
27
43
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
`--mk-select-radius`
|
|
44
|
+
Instancia: `setValue(value, notify?)`, `setOptions(options)`, `refresh()`, `open()`, `close()`, `value`, `destroy()`.
|
|
31
45
|
|
|
32
46
|
## Accesibilidad
|
|
33
47
|
|
|
34
|
-
|
|
48
|
+
El botón tiene `aria-haspopup="menu"` y `aria-expanded`; el menú usa `menuitemradio` con `aria-checked` en la opción elegida. ↓ ↑ Intro y Espacio abren el menú; con el botón cerrado, escribir una letra elige la siguiente opción que empieza por ella. Funciona dentro de diálogos, paneles y popovers sin cerrarlos. Con movimiento reducido el menú aparece sin escalonado y el valor cambia sin blur.
|