@yewdiel21/motion-kit 0.17.0 → 0.18.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 +23 -0
- package/dist/auto/index.d.ts +18 -3
- package/dist/auto/index.js +32 -2
- package/dist/auto/index.js.map +1 -1
- package/dist/{chunk-VJFPTLAK.js → chunk-AWDEKDEJ.js} +2 -2
- package/dist/{chunk-BZLMCHUD.js → chunk-EDMXWUW3.js} +2 -2
- package/dist/{chunk-7IQTMF7Y.js → chunk-Q7IW4FV3.js} +2580 -58
- package/dist/chunk-Q7IW4FV3.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/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/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-preview-dock.css +18 -0
- package/dist/css/components/select.css +16 -0
- 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 +206 -4
- package/dist/elements/index.js +581 -6
- package/dist/elements/index.js.map +1 -1
- package/dist/{engine-Bn6hC87X.d.ts → engine-t8d7brjn.d.ts} +341 -1
- package/dist/index.css +578 -170
- package/dist/index.d.ts +23 -23
- package/dist/index.js +33 -3
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +113 -7
- package/dist/react/index.js +669 -3
- package/dist/react/index.js.map +1 -1
- package/dist/{reduced-YS6TZjpa.d.ts → reduced-dPsZYqtI.d.ts} +1 -1
- package/dist/{store-CZ6vdtIE.d.ts → store-6hZNKiee.d.ts} +1 -1
- package/package.json +1 -1
- package/registry.json +180 -0
- package/src/auto/auto-init.ts +31 -0
- 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/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/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-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/select.css +16 -0
- 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/css/tokens.css +4 -0
- package/src/elements/index.ts +45 -0
- package/src/index.ts +311 -295
- package/src/react/index.ts +16 -0
- package/dist/chunk-7IQTMF7Y.js.map +0 -1
- /package/dist/{chunk-VJFPTLAK.js.map → chunk-AWDEKDEJ.js.map} +0 -0
- /package/dist/{chunk-BZLMCHUD.js.map → chunk-EDMXWUW3.js.map} +0 -0
|
@@ -1,103 +1,130 @@
|
|
|
1
|
-
/* Normalized SVG ink survives layout changes. Only the drawing area captures touch. */
|
|
2
|
-
.mk-signature-pad,
|
|
3
|
-
mk-signature-pad {
|
|
4
|
-
--mk-signature-height: clamp(12rem, 32vw, 17rem);
|
|
5
|
-
display: block;
|
|
6
|
-
min-width: 0;
|
|
7
|
-
overflow: hidden;
|
|
8
|
-
border: 1px solid var(--mk-outline-soft);
|
|
9
|
-
border-radius: 24px;
|
|
10
|
-
background: var(--mk-surface-1);
|
|
11
|
-
color: var(--mk-text);
|
|
12
|
-
font-family: var(--mk-font);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
:is(.mk-signature-pad, mk-signature-pad) [data-mk-signature-
|
|
38
|
-
position: absolute;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
:is(.mk-signature-pad, mk-signature-pad) [data-mk-signature-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
:is(.mk-signature-pad, mk-signature-pad) [data-mk-signature-action]
|
|
91
|
-
|
|
92
|
-
:
|
|
93
|
-
:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
:
|
|
100
|
-
:
|
|
101
|
-
:
|
|
102
|
-
|
|
103
|
-
|
|
1
|
+
/* Normalized SVG ink survives layout changes. Only the drawing area captures touch. */
|
|
2
|
+
.mk-signature-pad,
|
|
3
|
+
mk-signature-pad {
|
|
4
|
+
--mk-signature-height: clamp(12rem, 32vw, 17rem);
|
|
5
|
+
display: block;
|
|
6
|
+
min-width: 0;
|
|
7
|
+
overflow: hidden;
|
|
8
|
+
border: 1px solid var(--mk-outline-soft);
|
|
9
|
+
border-radius: 24px;
|
|
10
|
+
background: var(--mk-surface-1);
|
|
11
|
+
color: var(--mk-text);
|
|
12
|
+
font-family: var(--mk-font);
|
|
13
|
+
}
|
|
14
|
+
[data-mk-signature-header] { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: .5rem; padding: 1rem 1.25rem; font-size: .8125rem; font-weight: 500; }
|
|
15
|
+
[data-mk-signature-header] [data-mk-signature-status] { display: inline-flex; gap: .4rem; align-items: center; }
|
|
16
|
+
[data-mk-signature-status]::before { content: ''; width: 6px; height: 6px; flex: none; border-radius: 50%; background: var(--mk-muted); transition: background .2s; }
|
|
17
|
+
:is(.mk-signature-pad, mk-signature-pad):not([data-mk-empty]) [data-mk-signature-status]::before { background: var(--mk-accent); animation: mk-signature-dot .45s var(--mk-ease-swatch); }
|
|
18
|
+
@keyframes mk-signature-dot { from { scale: .2; } }
|
|
19
|
+
|
|
20
|
+
:is(.mk-signature-pad, mk-signature-pad) [data-mk-signature-surface] {
|
|
21
|
+
position: relative;
|
|
22
|
+
height: var(--mk-signature-height);
|
|
23
|
+
min-height: 8rem;
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
background: var(--mk-signature-paper, var(--mk-surface-2));
|
|
26
|
+
margin-inline: 8px;
|
|
27
|
+
border-radius: var(--mk-radius-md);
|
|
28
|
+
cursor: crosshair;
|
|
29
|
+
touch-action: none;
|
|
30
|
+
user-select: none;
|
|
31
|
+
-webkit-user-select: none;
|
|
32
|
+
}
|
|
33
|
+
[data-mk-signature-cursor] { position: absolute; top: 0; left: 0; width: 16px; height: 16px; border: 1px solid currentColor; border-radius: 50%; opacity: 0; transform: translate(-50%, -50%) scale(.5); pointer-events: none; transition: opacity .12s, width .3s var(--mk-ease-click), height .3s var(--mk-ease-click), transform .3s var(--mk-ease-click); }
|
|
34
|
+
[data-mk-pen-visible]:not([data-mk-disabled]):not([data-mk-readonly]) [data-mk-signature-cursor] { opacity: .45; transform: translate(-50%, -50%) scale(1); }
|
|
35
|
+
[data-mk-drawing] [data-mk-signature-cursor] { width: 8px; height: 8px; background: currentColor; }
|
|
36
|
+
|
|
37
|
+
:is(.mk-signature-pad, mk-signature-pad) [data-mk-signature-surface]::after {
|
|
38
|
+
position: absolute;
|
|
39
|
+
right: 1.5rem;
|
|
40
|
+
bottom: 2rem;
|
|
41
|
+
left: 1.5rem;
|
|
42
|
+
border-bottom: 1px dashed var(--mk-outline-soft);
|
|
43
|
+
content: '';
|
|
44
|
+
pointer-events: none;
|
|
45
|
+
transition: border-color .25s;
|
|
46
|
+
}
|
|
47
|
+
/* The line you sign on wakes while the pen is down. */
|
|
48
|
+
:is(.mk-signature-pad, mk-signature-pad)[data-mk-drawing] [data-mk-signature-surface]::after { border-bottom-color: color-mix(in srgb, var(--mk-text) 40%, transparent); }
|
|
49
|
+
|
|
50
|
+
:is(.mk-signature-pad, mk-signature-pad) :is([data-mk-signature-drawing], [data-mk-signature-ghost]) {
|
|
51
|
+
position: absolute;
|
|
52
|
+
inset: 0;
|
|
53
|
+
display: block;
|
|
54
|
+
width: 100%;
|
|
55
|
+
height: 100%;
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
:is(.mk-signature-pad, mk-signature-pad) [data-mk-signature-placeholder] {
|
|
60
|
+
position: absolute;
|
|
61
|
+
inset: 0;
|
|
62
|
+
display: grid;
|
|
63
|
+
place-items: center;
|
|
64
|
+
color: var(--mk-muted);
|
|
65
|
+
font-size: 1.5rem;
|
|
66
|
+
font-style: italic;
|
|
67
|
+
font-weight: 400;
|
|
68
|
+
pointer-events: none;
|
|
69
|
+
/* Returning waits for leaving ink, so the prompt never sits on top of it. */
|
|
70
|
+
transition: opacity .3s ease .3s, filter .3s ease .3s, scale .45s var(--mk-ease-swatch) .3s;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
:is(.mk-signature-pad, mk-signature-pad) [data-mk-signature-placeholder][hidden] { display: grid; opacity: 0; filter: blur(6px); scale: .96; transition-duration: .16s; transition-delay: 0s; }
|
|
74
|
+
|
|
75
|
+
:is(.mk-signature-pad, mk-signature-pad) [data-mk-signature-footer] {
|
|
76
|
+
display: flex;
|
|
77
|
+
flex-wrap: wrap;
|
|
78
|
+
align-items: center;
|
|
79
|
+
justify-content: space-between;
|
|
80
|
+
gap: 0.75rem;
|
|
81
|
+
padding: 0.875rem 1rem 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
:is(.mk-signature-pad, mk-signature-pad) [data-mk-signature-toolbar] {
|
|
85
|
+
display: flex;
|
|
86
|
+
flex-wrap: wrap;
|
|
87
|
+
gap: 0.375rem;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
:is(.mk-signature-pad, mk-signature-pad) [data-mk-signature-action] {
|
|
91
|
+
display: inline-flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
justify-content: center;
|
|
94
|
+
gap: .4rem;
|
|
95
|
+
min-height: 2.75rem;
|
|
96
|
+
padding: 0.5rem 0.875rem;
|
|
97
|
+
border: 0;
|
|
98
|
+
border-radius: var(--mk-radius-sm);
|
|
99
|
+
background: var(--mk-surface-3);
|
|
100
|
+
color: var(--mk-text);
|
|
101
|
+
font: inherit;
|
|
102
|
+
font-size: 0.8125rem;
|
|
103
|
+
font-weight: 500;
|
|
104
|
+
cursor: pointer;
|
|
105
|
+
transition: background .15s, border-radius .3s var(--mk-ease-playful), opacity .2s;
|
|
106
|
+
}
|
|
107
|
+
[data-mk-signature-action] svg { width: 17px; height: 17px; }
|
|
108
|
+
|
|
109
|
+
:is(.mk-signature-pad, mk-signature-pad) [data-mk-signature-action]:hover:enabled { background: var(--mk-surface-4); border-radius: var(--mk-radius-full); }
|
|
110
|
+
:is(.mk-signature-pad, mk-signature-pad) [data-mk-signature-action]:focus-visible { outline: 2px solid var(--mk-accent); outline-offset: 2px; }
|
|
111
|
+
:is(.mk-signature-pad, mk-signature-pad) [data-mk-signature-action]:disabled { opacity: 0.4; cursor: default; }
|
|
112
|
+
|
|
113
|
+
:is(.mk-signature-pad, mk-signature-pad) [data-mk-signature-status],
|
|
114
|
+
:is(.mk-signature-pad, mk-signature-pad) [data-mk-signature-hint] {
|
|
115
|
+
color: var(--mk-muted);
|
|
116
|
+
font-size: 0.75rem;
|
|
117
|
+
line-height: 1.5;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
:is(.mk-signature-pad, mk-signature-pad) [data-mk-signature-status] { font-variant-numeric: tabular-nums; }
|
|
121
|
+
:is(.mk-signature-pad, mk-signature-pad) [data-mk-signature-hint] { margin: 0; padding: 0.75rem 1rem 1rem; }
|
|
122
|
+
:is(.mk-signature-pad, mk-signature-pad)[data-mk-readonly] [data-mk-signature-surface],
|
|
123
|
+
:is(.mk-signature-pad, mk-signature-pad)[data-mk-disabled] [data-mk-signature-surface] { cursor: default; touch-action: auto; }
|
|
124
|
+
:is(.mk-signature-pad, mk-signature-pad)[data-mk-disabled] [data-mk-signature-surface] { opacity: 0.55; }
|
|
125
|
+
@media (prefers-reduced-motion: reduce) {
|
|
126
|
+
[data-mk-signature-cursor], [data-mk-signature-action], [data-mk-signature-placeholder], [data-mk-signature-surface]::after { transition: none !important; }
|
|
127
|
+
[data-mk-signature-status]::before { animation: none !important; }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
:is(.mk-signature-pad, mk-signature-pad) [data-mk-signature-ghost] { pointer-events: none; }
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Stepper
|
|
2
|
+
|
|
3
|
+
Navegación de progreso para workflows, con marcadores horizontales, verticales, compactos o puntos. El paso actual cambia su radio y la marca entra con pop; las filas llegan con blur escalonado. El contenido de cada paso pertenece a la aplicación, distinto del motor de paneles steps.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { createStepper } from '@yewdiel21/motion-kit';
|
|
7
|
+
import '@yewdiel21/motion-kit/css/stepper';
|
|
8
|
+
|
|
9
|
+
const control = createStepper(document.querySelector('.mk-stepper'), {
|
|
10
|
+
items: [{label: 'Cuenta'}, {label: 'Perfil'}, {label: 'Listo'}],
|
|
11
|
+
value: 0, label: 'Alta', onChange: renderCurrentStep,
|
|
12
|
+
});
|
|
13
|
+
control.next();
|
|
14
|
+
control.previous();
|
|
15
|
+
control.setValue(2);
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Opciones y contrato
|
|
19
|
+
|
|
20
|
+
`items: { label, description?, disabled?, completed?, error? }[]`, `variant: horizontal | vertical | compact | dots` (horizontal), `value` / `defaultValue` (índice desde 0), `linear`, `disabled`, `label`, `name`, `onChange(index)`.
|
|
21
|
+
|
|
22
|
+
Los botones tienen type=button y solo uno lleva aria-current=step. linear permite entrar al siguiente paso habilitado y volver a los visitados. next/previous omiten pasos disabled. setValue permite a la app navegar de forma explícita; las opciones disabled bloquean los gestos del usuario, no las actualizaciones imperativas. name añade un input oculto con el índice. La lista vacía indica que no hay pasos.
|
|
23
|
+
|
|
24
|
+
La instancia expone `value: number`, `setValue(value, notify = false)`, `reset(notify = false)` y `destroy()`. Las escrituras imperativas actualizan el control incluso estando disabled. `setValue` no cambia el valor inicial al que vuelve reset. Las notificaciones usan callbacks y eventos `mk:change` con `detail: { value }`.
|
|
25
|
+
|
|
26
|
+
## React y HTML
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import { Stepper } from '@yewdiel21/motion-kit/react';
|
|
30
|
+
|
|
31
|
+
<Stepper items={steps} value={step} onChange={setStep} variant="compact" />
|
|
32
|
+
<section>{panels[step]}</section>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`value` controla la instancia; `defaultValue` inicia una instancia que conserva sus propios cambios. Las opciones pueden cambiar sin perder la selección no controlada. `instanceRef` recibe la instancia y admite cambiar de objeto ref. Los children y dangerouslySetInnerHTML no se pasan al host porque el motor posee ese DOM.
|
|
36
|
+
|
|
37
|
+
```html
|
|
38
|
+
<mk-stepper class="mk-stepper" items='[{"label":"Cuenta"},{"label":"Perfil"},{"label":"Listo"}]' value="0" variant="horizontal"></mk-stepper>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Registra con `defineStepper()` desde `@yewdiel21/motion-kit/elements` o utiliza el auto registro. Los atributos de opciones se pueden cambiar en vivo; el valor imperativo se conserva al cambiar otra opción. `disabled="false"` se lee como false. El elemento expone `value`, `setValue`, `reset`, `next` y `previous`.
|
|
42
|
+
|
|
43
|
+
## Accesibilidad y lifecycle
|
|
44
|
+
|
|
45
|
+
Los botones mantienen Tab, Enter y Espacio; cada estado se describe en el nombre accesible. El reset de formulario es cancelable; el control espera al reset nativo y verifica defaultPrevented. Movimiento reducido conserva toda la funcionalidad. destroy es idempotente y libera animaciones, eventos y timers, elimina solo las partes creadas y restaura los atributos del host. Los temas utilizan DM Sans y tokens --mk-*; el CSS de este componente es independiente de los otros estilos.
|
|
46
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { MkElement, defineElement, type OptionType } from '../../elements/base';
|
|
2
|
+
import { createStepper, type StepperInstance } from './engine';
|
|
3
|
+
|
|
4
|
+
/** Light DOM; attribute changes keep the current imperative value and reset baseline. */
|
|
5
|
+
export class MkStepperElement extends MkElement<StepperInstance> {
|
|
6
|
+
static options: Record<string, OptionType> = {"items":"json","value":"number","label":"string","name":"string","variant":"string","linear":"boolean","disabled":"boolean"};
|
|
7
|
+
private baseline: number | undefined;
|
|
8
|
+
private saved: number | undefined;
|
|
9
|
+
protected create(options: Record<string, unknown>) {
|
|
10
|
+
const created = createStepper(this, { ...options, value: this.saved !== undefined ? this.saved : options.value as number | undefined, defaultValue: this.baseline });
|
|
11
|
+
if (this.baseline === undefined) this.baseline = created.value;
|
|
12
|
+
this.saved = undefined;
|
|
13
|
+
return created;
|
|
14
|
+
}
|
|
15
|
+
disconnectedCallback() {
|
|
16
|
+
this.saved = this.instance?.value;
|
|
17
|
+
super.disconnectedCallback();
|
|
18
|
+
}
|
|
19
|
+
attributeChangedCallback(name: string, previous: string | null, next: string | null) {
|
|
20
|
+
if (previous === next) return;
|
|
21
|
+
if (name === 'value' && (!this.instance || !this.isConnected)) {
|
|
22
|
+
this.saved = next === null ? this.baseline ?? 0 : Number.isFinite(Number(next)) ? Number(next) : this.baseline ?? 0;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (!this.instance || !this.isConnected) return;
|
|
26
|
+
if (name === 'value') {
|
|
27
|
+
const parsed = next === null ? this.baseline ?? 0 : Number(next);
|
|
28
|
+
this.instance.setValue(parsed, false);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
this.saved = this.instance.value;
|
|
32
|
+
super.attributeChangedCallback(name, previous, next);
|
|
33
|
+
}
|
|
34
|
+
get value(): number { return this.instance?.value ?? this.saved ?? 0; }
|
|
35
|
+
set value(next: number) { this.setValue(next); }
|
|
36
|
+
setValue(value: number, notify = false) { if (this.instance) this.instance.setValue(value, notify); else this.saved = value; }
|
|
37
|
+
reset(notify = false) { this.instance?.reset(notify); }
|
|
38
|
+
next(notify = true) { this.instance?.next(notify); }
|
|
39
|
+
previous(notify = true) { this.instance?.previous(notify); }
|
|
40
|
+
}
|
|
41
|
+
export const defineStepper = () => defineElement('mk-stepper', MkStepperElement);
|
|
42
|
+
|
|
@@ -0,0 +1,103 @@
|
|
|
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, rollDigits } from '../../motion/micro';
|
|
6
|
+
|
|
7
|
+
export interface StepperItem { label: string; description?: string; disabled?: boolean; completed?: boolean; error?: boolean }
|
|
8
|
+
export interface StepperOptions {
|
|
9
|
+
items?: StepperItem[]; value?: number; defaultValue?: number; label?: string; name?: string;
|
|
10
|
+
variant?: 'horizontal' | 'vertical' | 'compact' | 'dots';
|
|
11
|
+
/** Only the next unfinished step can be entered with the navigation buttons. */
|
|
12
|
+
linear?: boolean; disabled?: boolean;
|
|
13
|
+
onChange?: (value: number) => void;
|
|
14
|
+
}
|
|
15
|
+
export interface StepperInstance extends Instance {
|
|
16
|
+
readonly value: number;
|
|
17
|
+
setValue(value: number, notify?: boolean): void;
|
|
18
|
+
next(notify?: boolean): void; previous(notify?: boolean): void; reset(notify?: boolean): void;
|
|
19
|
+
}
|
|
20
|
+
export function createStepper(root: HTMLElement | null, options: StepperOptions = {}): StepperInstance {
|
|
21
|
+
if (!root) return inert({ value: 0, setValue() {}, next() {}, previous() {}, reset() {} });
|
|
22
|
+
registerMotion();
|
|
23
|
+
const scope = createScope();
|
|
24
|
+
const effects = new Set<gsap.core.Animation>();
|
|
25
|
+
const keep = (effect: gsap.core.Animation | null) => {
|
|
26
|
+
for (const prior of effects) if (prior.totalProgress() === 1) effects.delete(prior);
|
|
27
|
+
if (effect) effects.add(effect);
|
|
28
|
+
};
|
|
29
|
+
scope.add(() => { effects.forEach((effect) => effect.kill()); effects.clear(); });
|
|
30
|
+
const items = (Array.isArray(options.items) ? options.items : []).filter((item) => item && typeof item.label === 'string');
|
|
31
|
+
const clamp = (n: number) => Math.max(0, Math.min(Math.max(0, items.length - 1), Number.isFinite(n) ? Math.round(n) : 0));
|
|
32
|
+
const initial = clamp(options.defaultValue ?? options.value ?? 0);
|
|
33
|
+
let value = clamp(options.value ?? initial); let furthest = value;
|
|
34
|
+
let pending: { value: number; furthest: number } | null = null;
|
|
35
|
+
const nav = document.createElement('nav'); nav.dataset.mkStepperBody = ''; nav.setAttribute('aria-label', options.label ?? 'Progreso');
|
|
36
|
+
const list = document.createElement('ol'); list.dataset.mkStepperList = ''; nav.append(list);
|
|
37
|
+
const caption = document.createElement('span'); caption.dataset.mkStepperCaption = ''; caption.setAttribute('aria-live', 'polite'); nav.append(caption);
|
|
38
|
+
root.append(nav); scope.add(() => nav.remove());
|
|
39
|
+
const buttons: HTMLButtonElement[] = []; const rows: HTMLLIElement[] = []; const marks: HTMLElement[] = [];
|
|
40
|
+
items.forEach((item, index) => {
|
|
41
|
+
const row = document.createElement('li'); row.dataset.mkStepperItem = '';
|
|
42
|
+
const button = document.createElement('button'); button.type = 'button'; button.dataset.mkStepperStep = String(index);
|
|
43
|
+
const marker = document.createElement('span'); marker.dataset.mkStepperMarker = ''; marker.setAttribute('aria-hidden', 'true');
|
|
44
|
+
const copy = document.createElement('span'); copy.dataset.mkStepperCopy = '';
|
|
45
|
+
const title = document.createElement('span'); title.dataset.mkStepperLabel = ''; title.textContent = item.label; copy.append(title);
|
|
46
|
+
if (item.description) { const description = document.createElement('span'); description.dataset.mkStepperDescription = ''; description.textContent = item.description; copy.append(description); }
|
|
47
|
+
button.append(marker, copy); row.append(button); list.append(row); buttons.push(button); rows.push(row); marks.push(marker);
|
|
48
|
+
scope.on(button, 'click', () => { if (!button.disabled && !button.matches(':disabled')) choose(index, true); });
|
|
49
|
+
});
|
|
50
|
+
const hidden = options.name ? document.createElement('input') : null;
|
|
51
|
+
if (hidden) { hidden.type = 'hidden'; hidden.name = options.name!; hidden.disabled = Boolean(options.disabled); nav.append(hidden); }
|
|
52
|
+
scope.attr(root, 'data-mk-ready', ''); scope.attr(root, 'data-mk-variant', options.variant ?? 'horizontal');
|
|
53
|
+
scope.attr(root, 'data-mk-disabled', options.disabled ? '' : null);
|
|
54
|
+
const paint = (animate: boolean, before: number) => {
|
|
55
|
+
const nextEnabled = items.findIndex((item, index) => index > furthest && !item.disabled);
|
|
56
|
+
const availableThrough = Math.max(furthest, nextEnabled);
|
|
57
|
+
items.forEach((item, index) => {
|
|
58
|
+
const selected = index === value; const complete = item.completed || index < value;
|
|
59
|
+
rows[index]!.toggleAttribute('data-mk-current', selected);
|
|
60
|
+
rows[index]!.toggleAttribute('data-mk-complete', Boolean(complete));
|
|
61
|
+
rows[index]!.toggleAttribute('data-mk-error', Boolean(item.error));
|
|
62
|
+
const button = buttons[index]!;
|
|
63
|
+
button.disabled = Boolean(options.disabled || item.disabled || options.linear && index > availableThrough);
|
|
64
|
+
if (selected) button.setAttribute('aria-current', 'step'); else button.removeAttribute('aria-current');
|
|
65
|
+
button.setAttribute('aria-label', `${index + 1}. ${item.label}${item.error ? ', requiere atención' : complete ? ', completado' : ''}`);
|
|
66
|
+
marks[index]!.textContent = item.error ? '!' : complete ? '✓' : String(index + 1);
|
|
67
|
+
});
|
|
68
|
+
if (hidden) hidden.value = String(value);
|
|
69
|
+
const text = items.length ? `${value + 1} / ${items.length} · ${items[value]?.label ?? ''}` : 'No hay pasos';
|
|
70
|
+
if (animate && !reduced()) keep(rollDigits(caption, text, value >= before ? 1 : -1, 0.3)); else caption.textContent = text;
|
|
71
|
+
};
|
|
72
|
+
const choose = (next: number, notify = false) => {
|
|
73
|
+
if (scope.disposed) return;
|
|
74
|
+
if (notify) pending = { value, furthest };
|
|
75
|
+
const before = value; value = clamp(next); furthest = Math.max(furthest, value); paint(before !== value, before);
|
|
76
|
+
if (before === value) return;
|
|
77
|
+
if (!reduced() && buttons[value]) {
|
|
78
|
+
keep(press(buttons[value]));
|
|
79
|
+
keep(gsap.fromTo(marks[value]!, { scale: 0.7, filter: 'blur(4px)' }, { scale: 1, filter: 'blur(0px)', duration: 0.4, ease: EASE.pop, overwrite: 'auto', clearProps: 'transform,filter' }));
|
|
80
|
+
}
|
|
81
|
+
if (notify) { const emitted = value; options.onChange?.(emitted); if (!scope.disposed) root.dispatchEvent(new CustomEvent('mk:change', { bubbles: true, detail: { value: emitted } })); }
|
|
82
|
+
};
|
|
83
|
+
const move = (direction: number, notify = true) => {
|
|
84
|
+
if (options.disabled) return;
|
|
85
|
+
for (let i = value + direction; i >= 0 && i < items.length; i += direction) if (!items[i]?.disabled) { choose(i, notify); break; }
|
|
86
|
+
};
|
|
87
|
+
const form = root.closest('form');
|
|
88
|
+
if (form) scope.on(form, 'reset', (event) => scope.timeout(() => { if (!scope.disposed && !event.defaultPrevented) { furthest = initial; choose(initial); } }, 0));
|
|
89
|
+
paint(false, value); keep(blurIn(rows, { from: 'soft', stagger: 0.04, duration: 0.35 }));
|
|
90
|
+
return {
|
|
91
|
+
get value() { return value; },
|
|
92
|
+
setValue(next, notify = false) {
|
|
93
|
+
const target = clamp(next);
|
|
94
|
+
// A consumer can veto a notified navigation by restoring its prior value.
|
|
95
|
+
// Roll the visit history back with it, before unlocking any later step.
|
|
96
|
+
if (!notify && pending && target === pending.value) furthest = pending.furthest;
|
|
97
|
+
pending = null;
|
|
98
|
+
choose(target, notify);
|
|
99
|
+
},
|
|
100
|
+
next: (notify = true) => move(1, notify), previous: (notify = true) => move(-1, notify),
|
|
101
|
+
reset: (notify = false) => { pending = null; furthest = initial; choose(initial, notify); }, destroy: () => scope.dispose(),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useRef, type HTMLAttributes, type MutableRefObject } from 'react';
|
|
3
|
+
import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
|
|
4
|
+
import { createStepper, type StepperOptions, type StepperInstance } from './engine';
|
|
5
|
+
|
|
6
|
+
export interface StepperProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange' | 'onInput' | 'defaultValue' | 'children' | 'dangerouslySetInnerHTML'>, Omit<StepperOptions, 'value' | 'defaultValue'> {
|
|
7
|
+
value?: number;
|
|
8
|
+
defaultValue?: number;
|
|
9
|
+
instanceRef?: MutableRefObject<StepperInstance | null>;
|
|
10
|
+
/** The engine owns this component's DOM. */
|
|
11
|
+
children?: never;
|
|
12
|
+
dangerouslySetInnerHTML?: never;
|
|
13
|
+
}
|
|
14
|
+
export function Stepper({ value, defaultValue, items, label, name, variant, linear, disabled, onChange, instanceRef, className, children: _children, dangerouslySetInnerHTML: _html, ...props }: StepperProps) {
|
|
15
|
+
const root = useRef<HTMLDivElement>(null);
|
|
16
|
+
const engine = useRef<StepperInstance | null>(null);
|
|
17
|
+
const latestValue = useLatest(value);
|
|
18
|
+
const change = useLatest(onChange);
|
|
19
|
+
|
|
20
|
+
const baseline = useRef(value !== undefined ? value : defaultValue);
|
|
21
|
+
const current = useRef(baseline.current);
|
|
22
|
+
const reconcile = () => queueMicrotask(() => {
|
|
23
|
+
if (latestValue.current !== undefined) engine.current?.setValue(latestValue.current, false);
|
|
24
|
+
});
|
|
25
|
+
useIsomorphicLayoutEffect(() => {
|
|
26
|
+
const created = createStepper(root.current, {
|
|
27
|
+
items, label, name, variant, linear, disabled, value: latestValue.current !== undefined ? latestValue.current : current.current, defaultValue: baseline.current,
|
|
28
|
+
onChange: (next) => { change.current?.(next); reconcile(); },
|
|
29
|
+
|
|
30
|
+
});
|
|
31
|
+
engine.current = created;
|
|
32
|
+
const form = root.current?.closest('form');
|
|
33
|
+
const pending = new Set<ReturnType<typeof setTimeout>>();
|
|
34
|
+
const reset = (event: Event) => {
|
|
35
|
+
const timer = setTimeout(() => {
|
|
36
|
+
pending.delete(timer);
|
|
37
|
+
if (!event.defaultPrevented && latestValue.current !== undefined) created.setValue(latestValue.current, false);
|
|
38
|
+
}, 0);
|
|
39
|
+
pending.add(timer);
|
|
40
|
+
};
|
|
41
|
+
form?.addEventListener('reset', reset);
|
|
42
|
+
return () => {
|
|
43
|
+
form?.removeEventListener('reset', reset);
|
|
44
|
+
pending.forEach((timer) => clearTimeout(timer));
|
|
45
|
+
current.current = created.value; created.destroy(); engine.current = null;
|
|
46
|
+
};
|
|
47
|
+
// Callbacks and controlled value are read through refs.
|
|
48
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
49
|
+
}, [items, label, name, variant, linear, disabled]);
|
|
50
|
+
useIsomorphicLayoutEffect(() => {
|
|
51
|
+
if (value !== undefined) engine.current?.setValue(value, false);
|
|
52
|
+
});
|
|
53
|
+
useIsomorphicLayoutEffect(() => {
|
|
54
|
+
if (!instanceRef) return;
|
|
55
|
+
instanceRef.current = engine.current;
|
|
56
|
+
return () => { instanceRef.current = null; };
|
|
57
|
+
}, [instanceRef, items, label, name, variant, linear, disabled]);
|
|
58
|
+
return <div {...props} ref={root} className={['mk-stepper', className].filter(Boolean).join(' ')} />;
|
|
59
|
+
}
|
|
60
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
:is(.mk-stepper, mk-stepper) { display: block; min-width: 0; color: var(--mk-text); font: 500 .875rem/1.35 var(--mk-font); }
|
|
2
|
+
[data-mk-stepper-body] { display: grid; gap: 1rem; }
|
|
3
|
+
[data-mk-stepper-list] { display: flex; align-items: stretch; gap: .5rem; list-style: none; margin: 0; padding: 0; }
|
|
4
|
+
[data-mk-stepper-item] { position: relative; flex: 1; min-width: 0; }
|
|
5
|
+
[data-mk-stepper-step] { position: relative; display: flex; flex-direction: column; align-items: flex-start; gap: .75rem; width: 100%; min-height: 44px; padding: .75rem; border: 0; border-radius: 16px; background: transparent; color: var(--mk-muted); font: inherit; text-align: start; cursor: pointer; transition: background .25s, border-radius .4s var(--mk-ease-swatch), color .2s; -webkit-tap-highlight-color: transparent; }
|
|
6
|
+
[data-mk-stepper-marker] { display: grid; place-items: center; flex: none; box-sizing: border-box; width: 36px; height: 36px; border-radius: 12px; background: var(--mk-surface-2); color: var(--mk-muted); font-size: .8125rem; transition: background .25s, color .25s, width .4s var(--mk-ease-swatch), border-radius .4s var(--mk-ease-swatch); }
|
|
7
|
+
[data-mk-stepper-copy] { display: grid; gap: .25rem; min-width: 0; }
|
|
8
|
+
[data-mk-stepper-description] { font-size: .75rem; color: var(--mk-muted); font-weight: 400; }
|
|
9
|
+
[data-mk-stepper-item][data-mk-current] [data-mk-stepper-step] { background: var(--mk-surface-2); color: var(--mk-text); border-radius: 24px; }
|
|
10
|
+
[data-mk-stepper-item][data-mk-current] [data-mk-stepper-marker] { background: var(--mk-text); color: var(--mk-bg); border-radius: 64px; }
|
|
11
|
+
[data-mk-stepper-item][data-mk-complete] [data-mk-stepper-marker] { background: var(--mk-surface-3); color: var(--mk-text); border-radius: 64px; }
|
|
12
|
+
[data-mk-stepper-item][data-mk-error] [data-mk-stepper-marker] { background: var(--mk-error-soft); color: var(--mk-error); }
|
|
13
|
+
[data-mk-stepper-step]:focus-visible { outline: 2px solid var(--mk-accent); outline-offset: 3px; }
|
|
14
|
+
[data-mk-stepper-step]:disabled { opacity: .4; cursor: not-allowed; }
|
|
15
|
+
[data-mk-stepper-caption] { min-height: 1.3em; color: var(--mk-muted); font-size: .75rem; text-align: center; font-variant-numeric: tabular-nums; }
|
|
16
|
+
:is(.mk-stepper, mk-stepper)[data-mk-variant='vertical'] [data-mk-stepper-list] { flex-direction: column; gap: .375rem; }
|
|
17
|
+
:is(.mk-stepper, mk-stepper)[data-mk-variant='vertical'] [data-mk-stepper-step] { flex-direction: row; align-items: center; }
|
|
18
|
+
:is(.mk-stepper, mk-stepper)[data-mk-variant='vertical'] [data-mk-stepper-caption] { text-align: start; padding-inline: .75rem; }
|
|
19
|
+
:is(.mk-stepper, mk-stepper)[data-mk-variant='compact'] [data-mk-stepper-step] { flex-direction: row; align-items: center; padding: .375rem; border-radius: 64px; }
|
|
20
|
+
:is(.mk-stepper, mk-stepper)[data-mk-variant='compact'] [data-mk-stepper-item] { flex: 0 1 auto; }
|
|
21
|
+
:is(.mk-stepper, mk-stepper)[data-mk-variant='compact'] [data-mk-stepper-item][data-mk-current] { flex: 1; }
|
|
22
|
+
:is(.mk-stepper, mk-stepper)[data-mk-variant='compact'] [data-mk-stepper-marker] { border-radius: 64px; }
|
|
23
|
+
:is(.mk-stepper, mk-stepper)[data-mk-variant='compact'] [data-mk-stepper-copy] { display: none; }
|
|
24
|
+
:is(.mk-stepper, mk-stepper)[data-mk-variant='compact'] [data-mk-current] [data-mk-stepper-copy] { display: grid; padding-inline-end: .75rem; }
|
|
25
|
+
:is(.mk-stepper, mk-stepper)[data-mk-variant='compact'] [data-mk-stepper-description] { display: none; }
|
|
26
|
+
:is(.mk-stepper, mk-stepper)[data-mk-variant='dots'] [data-mk-stepper-list] { justify-content: center; gap: .25rem; }
|
|
27
|
+
:is(.mk-stepper, mk-stepper)[data-mk-variant='dots'] [data-mk-stepper-item] { flex: none; }
|
|
28
|
+
:is(.mk-stepper, mk-stepper)[data-mk-variant='dots'] [data-mk-stepper-step] { width: 44px; height: 44px; padding: 0; display: grid; place-items: center; background: transparent; }
|
|
29
|
+
:is(.mk-stepper, mk-stepper)[data-mk-variant='dots'] [data-mk-stepper-marker] { width: 8px; height: 8px; border-radius: 64px; font-size: 0; }
|
|
30
|
+
:is(.mk-stepper, mk-stepper)[data-mk-variant='dots'] [data-mk-current] [data-mk-stepper-marker] { width: 28px; }
|
|
31
|
+
:is(.mk-stepper, mk-stepper)[data-mk-variant='dots'] [data-mk-stepper-copy] { display: none; }
|
|
32
|
+
@media (hover: hover) { [data-mk-stepper-step]:not(:disabled):hover { background: var(--mk-surface-2); border-radius: 24px; color: var(--mk-text); } }
|
|
33
|
+
@media (max-width: 420px) { :is(.mk-stepper, mk-stepper)[data-mk-variant='horizontal'] [data-mk-stepper-step] { padding: .5rem; font-size: .75rem; } :is(.mk-stepper, mk-stepper)[data-mk-variant='horizontal'] [data-mk-stepper-description] { display: none; } }
|
|
34
|
+
@media (prefers-reduced-motion: reduce) { [data-mk-stepper-step], [data-mk-stepper-marker] { transition: none; } }
|
|
35
|
+
@media (forced-colors: active) { [data-mk-current] [data-mk-stepper-marker] { outline: 2px solid Highlight; } }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Sticky header
|
|
2
|
+
|
|
3
|
+
An authored sticky surface continuously compacts inside a stable expanded-height slot. The reserved slot prevents content from jumping when its geometry changes.
|
|
4
|
+
|
|
5
|
+
```html
|
|
6
|
+
<div id="scroller" style="height:320px;overflow:auto">
|
|
7
|
+
<header id="header">
|
|
8
|
+
<div data-mk-sticky-surface>
|
|
9
|
+
<div><h2 data-mk-sticky-title>Mi espacio</h2><p data-mk-sticky-subtitle>Todo lo que estás creando.</p></div>
|
|
10
|
+
<button type="button">Nuevo</button>
|
|
11
|
+
</div>
|
|
12
|
+
</header>
|
|
13
|
+
…content…
|
|
14
|
+
</div>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
const header = createStickyHeader(document.querySelector('#header'), {
|
|
19
|
+
scroller: document.querySelector('#scroller'), expandedHeight: 104, compactHeight: 64,
|
|
20
|
+
});
|
|
21
|
+
header.destroy();
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Exports: `createStickyHeader`, `StickyHeaderOptions`, `StickyHeaderInstance`; React `StickyHeader`, `StickyHeaderProps`; elements `MkStickyHeaderElement`, `defineStickyHeader` (`mk-sticky-header`). CSS: `sticky-header.css` plus kit tokens/theme/fonts.
|
|
25
|
+
|
|
26
|
+
Options: `scroller` (element/selector, otherwise window), `expandedHeight` (104px), `compactHeight` (64px), `threshold` (8px), `range` (96px of scroll), `top` (sticky inset, 0px), `disabled` (holds expanded geometry; native child controls remain usable), `onChange(compact)`. Geometry follows scroll progress through the supplied range; `compact` changes at half range. The title shrinks and subtitle folds into a blur as radius, padding, height and tonal surface change.
|
|
27
|
+
|
|
28
|
+
Instance: `update()`, `setOptions(partial)`, readonly `compact`/`progress`, `destroy()`. `mk:change` carries `{compact,progress}` at compact-state crossings. Native links/buttons and focus order stay authored. Reduced motion applies geometry immediately. Destroy restores exact authored styles/attributes and removes listeners, animation frames, observers and animations.
|
|
29
|
+
|
|
30
|
+
React intentionally accepts children and renders the required surface wrapper; mark title/subtitle parts inside them. In Vanilla/elements, author `[data-mk-sticky-surface]` explicitly. The element accepts scalar option attributes and exposes `setOptions`, `update`, `compact` and `progress`; changing attributes preserves its instance.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { MkElement, defineElement, type OptionType } from '../../elements/base';
|
|
2
|
+
import { createStickyHeader, type StickyHeaderInstance, type StickyHeaderOptions } from './engine';
|
|
3
|
+
|
|
4
|
+
export class MkStickyHeaderElement extends MkElement<StickyHeaderInstance> {
|
|
5
|
+
static options: Record<string, OptionType> = {"scroller":"string","expandedHeight":"number","compactHeight":"number","threshold":"number","range":"number","top":"number","disabled":"boolean"};
|
|
6
|
+
protected create(options: Record<string, unknown>) { return createStickyHeader(this, options); }
|
|
7
|
+
setOptions(options: Partial<StickyHeaderOptions>) { this.instance?.setOptions(options); }
|
|
8
|
+
|
|
9
|
+
update() { this.instance?.update(); }
|
|
10
|
+
get compact() { return this.instance?.compact ?? false; }
|
|
11
|
+
get progress() { return this.instance?.progress ?? 0; }
|
|
12
|
+
attributeChangedCallback(name: string, previous: string | null, next: string | null) {
|
|
13
|
+
if (previous === next || !this.isConnected || !this.instance) return;
|
|
14
|
+
const option = name.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
|
|
15
|
+
const defaults: Record<string, unknown> = {"scroller":null,"expandedHeight":104,"compactHeight":64,"threshold":8,"range":96,"top":0,"disabled":false};
|
|
16
|
+
this.instance.setOptions({ [option]: this.options[option] ?? defaults[option] });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export const defineStickyHeader = () => defineElement('mk-sticky-header', MkStickyHeaderElement);
|