@yewdiel21/motion-kit 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +174 -155
  2. package/bin/motion-kit.mjs +3 -3
  3. package/dist/auto/index.d.ts +6 -3
  4. package/dist/auto/index.js +8 -2
  5. package/dist/auto/index.js.map +1 -1
  6. package/dist/{chunk-EY4BJKC4.js → chunk-6IRJYXJU.js} +2 -2
  7. package/dist/{chunk-CU6SXW3E.js → chunk-A6QQCF6V.js} +1309 -310
  8. package/dist/chunk-A6QQCF6V.js.map +1 -0
  9. package/dist/chunk-JXRS3OEA.js +3638 -0
  10. package/dist/chunk-JXRS3OEA.js.map +1 -0
  11. package/dist/css/components/add-to-cart.css +72 -0
  12. package/dist/css/components/bidirectional-slider.css +168 -0
  13. package/dist/css/components/data-table.css +121 -26
  14. package/dist/css/components/filter-builder.css +10 -15
  15. package/dist/css/components/heatmap-chart.css +391 -0
  16. package/dist/css/components/hover-card.css +8 -2
  17. package/dist/css/components/image-preview.css +90 -2
  18. package/dist/css/components/json-viewer.css +33 -1
  19. package/dist/css/components/nav.css +102 -102
  20. package/dist/css/components/orbit-menu.css +130 -0
  21. package/dist/css/components/pagination.css +10 -5
  22. package/dist/css/components/pie-chart.css +390 -0
  23. package/dist/css/components/pill-button.css +135 -0
  24. package/dist/css/components/radar-chart.css +370 -0
  25. package/dist/css/components/radial-chart.css +398 -0
  26. package/dist/css/components/sankey-chart.css +358 -0
  27. package/dist/css/components/scroll-island.css +111 -0
  28. package/dist/css/components/select.css +136 -0
  29. package/dist/css/components/toolbar.css +122 -0
  30. package/dist/css/components/tree-view.css +79 -0
  31. package/dist/css/components/voice-dots.css +70 -0
  32. package/dist/elements/index.d.ts +180 -7
  33. package/dist/elements/index.js +287 -6
  34. package/dist/elements/index.js.map +1 -1
  35. package/dist/{engine-Cn6IhE3z.d.ts → engine-5zRIssTO.d.ts} +11 -1
  36. package/dist/index.css +1832 -168
  37. package/dist/index.d.ts +4 -4
  38. package/dist/index.js +33 -3
  39. package/dist/index.js.map +1 -1
  40. package/dist/react/index.d.ts +117 -11
  41. package/dist/react/index.js +314 -13
  42. package/dist/react/index.js.map +1 -1
  43. package/dist/{reduced-BBUusld6.d.ts → reduced-6X_bns60.d.ts} +1 -1
  44. package/dist/{reveal-DEvp6U6T.d.ts → reveal-BfyKKsw4.d.ts} +97 -4
  45. package/dist/store-CA2Qcun_.d.ts +453 -0
  46. package/package.json +9 -16
  47. package/registry.json +1096 -948
  48. package/src/auto/auto-init.ts +6 -0
  49. package/src/components/add-to-cart/README.md +34 -0
  50. package/src/components/add-to-cart/add-to-cart.css +71 -0
  51. package/src/components/add-to-cart/element.ts +20 -0
  52. package/src/components/add-to-cart/engine.ts +206 -0
  53. package/src/components/add-to-cart/react.tsx +29 -0
  54. package/src/components/bidirectional-slider/README.md +46 -0
  55. package/src/components/bidirectional-slider/bidirectional-slider.css +167 -0
  56. package/src/components/bidirectional-slider/element.ts +42 -0
  57. package/src/components/bidirectional-slider/engine.ts +289 -0
  58. package/src/components/bidirectional-slider/react.tsx +45 -0
  59. package/src/components/chart/README.md +10 -0
  60. package/src/components/chart/element.ts +2 -0
  61. package/src/components/chart/engine.ts +50 -23
  62. package/src/components/chart/parts.ts +237 -0
  63. package/src/components/chart/react.tsx +2 -2
  64. package/src/components/chart/use-chart.ts +55 -0
  65. package/src/components/data-table/README.md +3 -3
  66. package/src/components/data-table/data-table.css +121 -26
  67. package/src/components/data-table/engine.ts +81 -29
  68. package/src/components/editable-table/engine.ts +67 -11
  69. package/src/components/filter-builder/engine.ts +33 -2
  70. package/src/components/filter-builder/filter-builder.css +10 -15
  71. package/src/components/heatmap-chart/README.md +48 -0
  72. package/src/components/heatmap-chart/element.ts +21 -0
  73. package/src/components/heatmap-chart/engine.ts +281 -0
  74. package/src/components/heatmap-chart/heatmap-chart.css +93 -0
  75. package/src/components/heatmap-chart/react.tsx +14 -0
  76. package/src/components/hover-card/engine.ts +30 -1
  77. package/src/components/hover-card/hover-card.css +8 -2
  78. package/src/components/image-preview/element.ts +1 -1
  79. package/src/components/image-preview/engine.ts +95 -15
  80. package/src/components/image-preview/image-preview.css +90 -2
  81. package/src/components/image-preview/react.tsx +3 -3
  82. package/src/components/json-viewer/engine.ts +11 -2
  83. package/src/components/json-viewer/json-viewer.css +33 -1
  84. package/src/components/nav/README.md +12 -10
  85. package/src/components/nav/element.ts +5 -5
  86. package/src/components/nav/engine.ts +21 -14
  87. package/src/components/nav/nav.css +102 -102
  88. package/src/components/nav/react.tsx +4 -4
  89. package/src/components/orbit-menu/README.md +46 -0
  90. package/src/components/orbit-menu/element.ts +27 -0
  91. package/src/components/orbit-menu/engine.ts +179 -0
  92. package/src/components/orbit-menu/orbit-menu.css +129 -0
  93. package/src/components/orbit-menu/react.tsx +44 -0
  94. package/src/components/pagination/README.md +1 -1
  95. package/src/components/pagination/engine.ts +50 -4
  96. package/src/components/pagination/pagination.css +10 -5
  97. package/src/components/pie-chart/README.md +48 -0
  98. package/src/components/pie-chart/element.ts +30 -0
  99. package/src/components/pie-chart/engine.ts +283 -0
  100. package/src/components/pie-chart/pie-chart.css +92 -0
  101. package/src/components/pie-chart/react.tsx +14 -0
  102. package/src/components/pill-button/README.md +46 -0
  103. package/src/components/pill-button/element.ts +16 -0
  104. package/src/components/pill-button/engine.ts +77 -0
  105. package/src/components/pill-button/pill-button.css +134 -0
  106. package/src/components/pill-button/react.tsx +32 -0
  107. package/src/components/radar-chart/README.md +48 -0
  108. package/src/components/radar-chart/element.ts +30 -0
  109. package/src/components/radar-chart/engine.ts +327 -0
  110. package/src/components/radar-chart/radar-chart.css +72 -0
  111. package/src/components/radar-chart/react.tsx +14 -0
  112. package/src/components/radial-chart/README.md +48 -0
  113. package/src/components/radial-chart/element.ts +30 -0
  114. package/src/components/radial-chart/engine.ts +290 -0
  115. package/src/components/radial-chart/radial-chart.css +100 -0
  116. package/src/components/radial-chart/react.tsx +14 -0
  117. package/src/components/sankey-chart/README.md +48 -0
  118. package/src/components/sankey-chart/element.ts +31 -0
  119. package/src/components/sankey-chart/engine.ts +401 -0
  120. package/src/components/sankey-chart/react.tsx +14 -0
  121. package/src/components/sankey-chart/sankey-chart.css +60 -0
  122. package/src/components/scroll-island/README.md +34 -0
  123. package/src/components/scroll-island/element.ts +16 -0
  124. package/src/components/scroll-island/engine.ts +252 -0
  125. package/src/components/scroll-island/react.tsx +26 -0
  126. package/src/components/scroll-island/scroll-island.css +110 -0
  127. package/src/components/select/README.md +34 -0
  128. package/src/components/select/element.ts +26 -0
  129. package/src/components/select/engine.ts +267 -0
  130. package/src/components/select/react.tsx +33 -0
  131. package/src/components/select/select.css +135 -0
  132. package/src/components/toolbar/README.md +46 -0
  133. package/src/components/toolbar/element.ts +30 -0
  134. package/src/components/toolbar/engine.ts +230 -0
  135. package/src/components/toolbar/react.tsx +34 -0
  136. package/src/components/toolbar/toolbar.css +121 -0
  137. package/src/components/tree-view/element.ts +1 -1
  138. package/src/components/tree-view/engine.ts +174 -0
  139. package/src/components/tree-view/react.tsx +7 -3
  140. package/src/components/tree-view/tree-view.css +79 -0
  141. package/src/components/voice-dots/README.md +48 -0
  142. package/src/components/voice-dots/element.ts +45 -0
  143. package/src/components/voice-dots/engine.ts +243 -0
  144. package/src/components/voice-dots/react.tsx +52 -0
  145. package/src/components/voice-dots/voice-dots.css +69 -0
  146. package/src/elements/index.ts +39 -0
  147. package/src/index.ts +16 -0
  148. package/src/motion/liquid.ts +14 -4
  149. package/src/react/index.ts +14 -1
  150. package/dist/chunk-6P4P7NU6.js +0 -1401
  151. package/dist/chunk-6P4P7NU6.js.map +0 -1
  152. package/dist/chunk-CU6SXW3E.js.map +0 -1
  153. package/dist/store-DjkTBX7f.d.ts +0 -195
  154. /package/dist/{chunk-EY4BJKC4.js.map → chunk-6IRJYXJU.js.map} +0 -0
@@ -8,14 +8,19 @@ import { rollDigits } from '../../motion/micro';
8
8
  * Image preview: thumbnails ([data-mk-image]) open a modal <dialog> where the
9
9
  * picture grows out of its thumbnail (0.7 s on the signature curve, the
10
10
  * thumbnail's radius easing to 24 px) over a blurred backdrop. Previous/next
11
- * (buttons, arrow keys, a swipe) slide the next picture in out of a blur; the
11
+ * (buttons, arrow keys, a swipe, the strip of thumbnails) move both pictures at
12
+ * once: the old one leaves as a copy that slides away, shrinking into a blur,
13
+ * while the new one (decoded first) comes in from the side it goes to; a ring
14
+ * glides along the strip to the current thumbnail; the
12
15
  * counter rolls. A double click or tap zooms ×2 where you point and a drag pans
13
16
  * it. Dragging down, Escape or the close button send the picture back to its
14
17
  * thumbnail, and focus returns to it.
15
18
  */
16
19
 
17
20
  export interface ImagePreviewOptions {
18
- labels?: { close?: string; prev?: string; next?: string; dialog?: string };
21
+ labels?: { close?: string; prev?: string; next?: string; dialog?: string; strip?: string; picture?: string };
22
+ /** A strip of thumbnails under the picture (default true; hidden on phones and with one picture). */
23
+ strip?: boolean;
19
24
  /** Loop from the last picture to the first (default true). */
20
25
  loop?: boolean;
21
26
  onChange?: (index: number) => void;
@@ -52,7 +57,7 @@ export function createImagePreview(root: HTMLElement | null, options: ImagePrevi
52
57
  registerMotion();
53
58
  const scope = createScope();
54
59
  const loop = options.loop ?? true;
55
- const labels = { close: 'Cerrar', prev: 'Imagen anterior', next: 'Imagen siguiente', dialog: 'Vista de imagen', ...options.labels };
60
+ const labels = { close: 'Cerrar', prev: 'Imagen anterior', next: 'Imagen siguiente', dialog: 'Vista de imagen', strip: 'Miniaturas', picture: 'Imagen', ...options.labels };
56
61
 
57
62
  const pictures = (): Picture[] =>
58
63
  [...root.querySelectorAll<HTMLElement>('[data-mk-image]')].map((thumb) => {
@@ -84,7 +89,15 @@ export function createImagePreview(root: HTMLElement | null, options: ImagePrevi
84
89
  const counter = document.createElement('span');
85
90
  counter.dataset.mkLightbox = 'count';
86
91
  counter.setAttribute('aria-live', 'polite');
87
- dialog.append(backdrop, stage, prev, next, close, counter);
92
+ const strip = document.createElement('div');
93
+ strip.dataset.mkLightbox = 'strip';
94
+ strip.setAttribute('role', 'group');
95
+ strip.setAttribute('aria-label', labels.strip);
96
+ strip.hidden = true;
97
+ const ring = document.createElement('span');
98
+ ring.dataset.mkLightboxRing = '';
99
+ ring.setAttribute('aria-hidden', 'true');
100
+ dialog.append(backdrop, stage, prev, next, close, counter, strip);
88
101
  root.append(dialog);
89
102
  scope.add(() => dialog.remove());
90
103
  scope.attr(root, 'data-mk-ready', '');
@@ -128,6 +141,44 @@ export function createImagePreview(root: HTMLElement | null, options: ImagePrevi
128
141
  prev.hidden = next.hidden = list.length < 2;
129
142
  };
130
143
 
144
+ let ringShown = false;
145
+ const buildStrip = () => {
146
+ const show = options.strip !== false && list.length > 1;
147
+ strip.hidden = !show;
148
+ dialog.toggleAttribute('data-mk-strip', show);
149
+ ringShown = false;
150
+ if (!show) return strip.replaceChildren();
151
+ strip.replaceChildren(
152
+ ring,
153
+ ...list.map((picture, i) => {
154
+ const thumb = document.createElement('button');
155
+ thumb.type = 'button';
156
+ thumb.dataset.mkLightboxThumb = String(i);
157
+ thumb.setAttribute('aria-label', `${labels.picture} ${i + 1} de ${list.length}`);
158
+ const small = thumbImg(picture);
159
+ const img = document.createElement('img');
160
+ img.src = (small instanceof HTMLImageElement ? small.getAttribute('src') || small.currentSrc : '') || picture.src;
161
+ img.alt = '';
162
+ img.draggable = false;
163
+ thumb.append(img);
164
+ return thumb;
165
+ }),
166
+ );
167
+ };
168
+ const paintStrip = () => {
169
+ if (strip.hidden) return;
170
+ const thumbs = [...strip.querySelectorAll<HTMLElement>('[data-mk-lightbox-thumb]')];
171
+ thumbs.forEach((thumb, i) => (i === index ? thumb.setAttribute('aria-current', 'true') : thumb.removeAttribute('aria-current')));
172
+ const active = thumbs[index];
173
+ if (!active) return;
174
+ const box = { x: active.offsetLeft, y: active.offsetTop, width: active.offsetWidth, height: active.offsetHeight };
175
+ if (!ringShown || reduced()) gsap.set(ring, box);
176
+ else keep(gsap.to(ring, { ...box, duration: 0.35, ease: EASE.swatch }));
177
+ ringShown = true;
178
+ // A long strip keeps the current thumbnail in its middle (the page behind never scrolls).
179
+ if (typeof strip.scrollTo === 'function') strip.scrollTo({ left: active.offsetLeft + active.offsetWidth / 2 - strip.clientWidth / 2, behavior: reduced() ? 'auto' : 'smooth' });
180
+ };
181
+
131
182
  /** Transform that puts the big image over the thumbnail. */
132
183
  const fromThumb = (picture: Picture) => {
133
184
  const small = thumbImg(picture)!.getBoundingClientRect();
@@ -158,16 +209,18 @@ export function createImagePreview(root: HTMLElement | null, options: ImagePrevi
158
209
  isOpen = true;
159
210
  index = target;
160
211
  counter.textContent = '';
212
+ buildStrip();
161
213
  write(target);
162
214
  if (typeof dialog.showModal === 'function') dialog.showModal();
163
215
  else dialog.setAttribute('open', '');
216
+ paintStrip();
164
217
  close.focus({ preventScroll: true });
165
218
  const picture = list[target]!;
166
219
  options.onOpenChange?.(true);
167
220
  root.dispatchEvent(new CustomEvent('mk:open', { bubbles: true, detail: { index } }));
168
221
  if (reduced()) return;
169
222
  keep(gsap.fromTo(backdrop, { opacity: 0 }, { opacity: 1, duration: 0.4, ease: 'power2.out', clearProps: 'opacity' }));
170
- keep(gsap.fromTo([prev, next, close, counter, caption], { opacity: 0 }, { opacity: 1, duration: 0.3, delay: 0.3, clearProps: 'opacity' }));
223
+ keep(gsap.fromTo([prev, next, close, counter, caption, strip], { opacity: 0 }, { opacity: 1, duration: 0.3, delay: 0.3, clearProps: 'opacity' }));
171
224
  const grow = () => {
172
225
  const start = fromThumb(picture);
173
226
  if (!start) return void keep(gsap.fromTo(image, { opacity: 0, scale: 0.92 }, { opacity: 1, scale: 1, duration: 0.35, ease: EASE.pop, clearProps: 'opacity,scale,transform' }));
@@ -201,11 +254,11 @@ export function createImagePreview(root: HTMLElement | null, options: ImagePrevi
201
254
  gsap.set(image, { scale: 1, x: 0, y: 0 });
202
255
  const end = fromThumb(picture);
203
256
  keep(gsap.to(backdrop, { opacity: 0, duration: 0.35, ease: 'power2.in' }));
204
- keep(gsap.to([prev, next, close, counter, caption], { opacity: 0, duration: 0.15 }));
257
+ keep(gsap.to([prev, next, close, counter, caption, strip], { opacity: 0, duration: 0.15 }));
205
258
  if (!end) return void keep(gsap.to(image, { opacity: 0, scale: 0.92, duration: 0.25, ease: 'power2.in', onComplete: finish }));
206
259
  thumbImg(picture)!.style.visibility = 'hidden';
207
260
  keep(gsap.to(image, { ...end, duration: 0.5, ease: EASE.signature, onComplete: () => {
208
- gsap.set([prev, next, close, counter, caption], { clearProps: 'opacity' });
261
+ gsap.set([prev, next, close, counter, caption, strip], { clearProps: 'opacity' });
209
262
  finish();
210
263
  } }));
211
264
  };
@@ -222,16 +275,43 @@ export function createImagePreview(root: HTMLElement | null, options: ImagePrevi
222
275
  index = target;
223
276
  options.onChange?.(index);
224
277
  root!.dispatchEvent(new CustomEvent('mk:change', { bubbles: true, detail: { index, previous: before } }));
225
- if (reduced()) return write(target);
278
+ if (reduced()) {
279
+ write(target);
280
+ paintStrip();
281
+ return;
282
+ }
226
283
  gsap.killTweensOf(image);
227
- keep(
228
- gsap
229
- .timeline()
230
- .to(image, { x: -60 * direction, opacity: 0, filter: 'blur(12px)', duration: 0.18, ease: 'power2.in' })
231
- .call(() => write(target))
232
- .fromTo(image, { x: 60 * direction, opacity: 0, filter: 'blur(12px)' }, { x: 0, opacity: 1, filter: 'blur(0px)', duration: 0.4, ease: EASE.signature, clearProps: 'transform,opacity,filter' }),
233
- );
284
+ ghost?.remove();
285
+ // The picture that leaves is a copy over the old one (where a swipe left it), so the new one can come in at once.
286
+ const leaving = image.cloneNode() as HTMLImageElement;
287
+ leaving.dataset.mkLightbox = 'ghost';
288
+ leaving.alt = '';
289
+ leaving.setAttribute('aria-hidden', 'true');
290
+ Object.assign(leaving.style, { position: 'absolute', left: `${image.offsetLeft}px`, top: `${image.offsetTop}px`, width: `${image.offsetWidth}px`, height: `${image.offsetHeight}px`, margin: '0' });
291
+ stage.append(leaving);
292
+ ghost = leaving;
293
+ write(target);
294
+ paintStrip();
295
+ gsap.set(image, { opacity: 0, x: 0, y: 0 });
296
+ keep(gsap.to(leaving, { xPercent: -28 * direction, scale: 0.86, opacity: 0, filter: 'blur(10px)', duration: 0.45, ease: EASE.signature, onComplete: () => {
297
+ leaving.remove();
298
+ if (ghost === leaving) ghost = null;
299
+ } }));
300
+ const enter = () => {
301
+ if (!isOpen || index !== target) return;
302
+ keep(gsap.fromTo(image, { xPercent: 24 * direction, scale: 1.04, opacity: 0, filter: 'blur(10px)' }, { xPercent: 0, scale: 1, opacity: 1, filter: 'blur(0px)', duration: 0.5, ease: EASE.signature, clearProps: 'transform,opacity,filter' }));
303
+ };
304
+ // The new picture enters decoded (never blank), waiting at most 250 ms for it.
305
+ const decoded = typeof image.decode === 'function' ? image.decode().catch(() => undefined) : Promise.resolve();
306
+ if (image.complete && image.naturalWidth) enter();
307
+ else void Promise.race([decoded, new Promise((done) => setTimeout(done, 250))]).then(enter);
234
308
  }
309
+ let ghost: HTMLImageElement | null = null;
310
+ scope.add(() => ghost?.remove());
311
+ scope.on(strip, 'click', (event) => {
312
+ const thumb = (event.target as Element | null)?.closest<HTMLElement>('[data-mk-lightbox-thumb]');
313
+ if (thumb) go(Number(thumb.dataset.mkLightboxThumb));
314
+ });
235
315
 
236
316
  scope.on(root, 'click', (event) => {
237
317
  const thumb = (event.target as Element | null)?.closest<HTMLElement>('[data-mk-image]');
@@ -1,5 +1,6 @@
1
1
  /* image-preview — thumbnails in Melonmind cards (16 px radius) and a full-screen dialog: a blurred dark backdrop, the
2
- picture at 16 px radius, round glass buttons and a counter pill. */
2
+ picture at 16 px radius, round glass buttons, a counter pill and a glass strip of 8 px thumbnails with a ring that
3
+ glides to the current one. */
3
4
  .mk-image-preview,
4
5
  mk-image-preview {
5
6
  display: grid;
@@ -88,6 +89,83 @@ mk-image-preview {
88
89
  user-select: none;
89
90
  }
90
91
 
92
+ /* The picture that leaves while the next one comes in (the engine places it over the old one). */
93
+ [data-mk-lightbox='ghost'] {
94
+ border-radius: 16px;
95
+ object-fit: contain;
96
+ pointer-events: none;
97
+ }
98
+
99
+ [data-mk-lightbox-dialog][data-mk-strip] [data-mk-lightbox='image'] {
100
+ max-height: min(78dvh, calc(100dvh - 11rem));
101
+ }
102
+
103
+ [data-mk-lightbox='strip'] {
104
+ position: absolute;
105
+ bottom: 1rem;
106
+ left: 50%;
107
+ display: flex;
108
+ gap: 0.375rem;
109
+ max-width: calc(100vw - 2rem);
110
+ padding: 0.375rem;
111
+ overflow-x: auto;
112
+ border-radius: 14px;
113
+ background: rgb(255 255 255 / 0.08);
114
+ box-shadow:
115
+ inset 1px 1px 1px rgb(255 255 255 / 0.12),
116
+ inset -1px -1px 1px rgb(255 255 255 / 0.05);
117
+ backdrop-filter: blur(16px);
118
+ scrollbar-width: none;
119
+ translate: -50% 0;
120
+ }
121
+
122
+ [data-mk-lightbox='strip'][hidden] {
123
+ display: none;
124
+ }
125
+
126
+ [data-mk-lightbox-thumb] {
127
+ position: relative;
128
+ z-index: 1;
129
+ flex: none;
130
+ width: 2.75rem;
131
+ height: 2.75rem;
132
+ padding: 0;
133
+ overflow: hidden;
134
+ border: 0;
135
+ border-radius: 8px;
136
+ background: rgb(255 255 255 / 0.06);
137
+ opacity: 0.5;
138
+ cursor: pointer;
139
+ transition: opacity 0.2s ease;
140
+ }
141
+
142
+ [data-mk-lightbox-thumb]:hover,
143
+ [data-mk-lightbox-thumb][aria-current='true'] {
144
+ opacity: 1;
145
+ }
146
+
147
+ [data-mk-lightbox-thumb]:focus-visible {
148
+ outline: 2px solid #fff;
149
+ outline-offset: 2px;
150
+ }
151
+
152
+ [data-mk-lightbox-thumb] img {
153
+ display: block;
154
+ width: 100%;
155
+ height: 100%;
156
+ object-fit: cover;
157
+ }
158
+
159
+ [data-mk-lightbox-ring] {
160
+ position: absolute;
161
+ top: 0;
162
+ left: 0;
163
+ z-index: 2;
164
+ border-radius: 9px;
165
+ box-shadow: 0 0 0 2px #fff;
166
+ pointer-events: none;
167
+ }
168
+
91
169
  [data-mk-lightbox-dialog][data-mk-zoomed] [data-mk-lightbox='image'] {
92
170
  cursor: move;
93
171
  }
@@ -177,6 +255,15 @@ mk-image-preview {
177
255
  }
178
256
 
179
257
  @media (max-width: 680px) {
258
+ /* Phones swipe; the arrows take the bottom. */
259
+ [data-mk-lightbox='strip'] {
260
+ display: none;
261
+ }
262
+
263
+ [data-mk-lightbox-dialog][data-mk-strip] [data-mk-lightbox='image'] {
264
+ max-height: 78dvh;
265
+ }
266
+
180
267
  [data-mk-lightbox='prev'],
181
268
  [data-mk-lightbox='next'] {
182
269
  top: auto;
@@ -187,7 +274,8 @@ mk-image-preview {
187
274
 
188
275
  @media (prefers-reduced-motion: reduce) {
189
276
  :is(.mk-image-preview, mk-image-preview) [data-mk-image],
190
- [data-mk-lightbox-dialog] button[data-mk-lightbox] {
277
+ [data-mk-lightbox-dialog] button[data-mk-lightbox],
278
+ [data-mk-lightbox-thumb] {
191
279
  transition: none;
192
280
  scale: none !important;
193
281
  }
@@ -17,20 +17,20 @@ export interface ImagePreviewProps extends Omit<HTMLAttributes<HTMLDivElement>,
17
17
  }
18
18
 
19
19
  /** <ImagePreview images={[{ src, thumb, alt, caption }]} />: a grid of thumbnails that open a viewer. */
20
- export function ImagePreview({ images, labels, loop, onChange, onOpenChange, instanceRef, className, ...props }: ImagePreviewProps) {
20
+ export function ImagePreview({ images, labels, loop, strip, onChange, onOpenChange, instanceRef, className, ...props }: ImagePreviewProps) {
21
21
  const ref = useRef<HTMLDivElement>(null);
22
22
  const change = useLatest(onChange);
23
23
  const openChange = useLatest(onOpenChange);
24
24
  const key = JSON.stringify(labels ?? {});
25
25
  useIsomorphicLayoutEffect(() => {
26
- const created = createImagePreview(ref.current, { labels, loop, onChange: (i) => change.current?.(i), onOpenChange: (open) => openChange.current?.(open) });
26
+ const created = createImagePreview(ref.current, { labels, loop, strip, onChange: (i) => change.current?.(i), onOpenChange: (open) => openChange.current?.(open) });
27
27
  if (instanceRef) instanceRef.current = created;
28
28
  return () => {
29
29
  created.destroy();
30
30
  if (instanceRef) instanceRef.current = null;
31
31
  };
32
32
  // eslint-disable-next-line react-hooks/exhaustive-deps
33
- }, [key, loop]);
33
+ }, [key, loop, strip]);
34
34
  return (
35
35
  <div {...props} ref={ref} className={['mk-image-preview', className].filter(Boolean).join(' ')}>
36
36
  {images.map((image, i) => (
@@ -3,7 +3,7 @@ import { reduced } from '../../core/reduced';
3
3
  import { createScope } from '../../core/scope';
4
4
  import { inert, type Instance } from '../../core/types';
5
5
  import { walk, type TreeNode } from '../../core/tree';
6
- import { press } from '../../motion/micro';
6
+ import { blurIn, press } from '../../motion/micro';
7
7
  import { markText } from '../../core/mark';
8
8
  import { createTreeView } from '../tree-view/engine';
9
9
 
@@ -16,7 +16,10 @@ import { createTreeView } from '../tree-view/engine';
16
16
  * pop; Ctrl/⌘+C copies the focused value. The search marks keys and values and
17
17
  * opens what holds them. setData() keeps what was open and flashes the values
18
18
  * that changed (and the keys that are new) in the accent for a moment. The
19
- * selected row's path ($.user.roles[0]) shows below, ready to copy.
19
+ * selected row's path ($.user.roles[0]) shows below, ready to copy: it rises
20
+ * out of a blur the first time and slides in on each new choice. On hover the
21
+ * copy button slides in and the value sits on a faint pill of its colour; an
22
+ * open branch lets its summary ("4 claves") step back.
20
23
  */
21
24
 
22
25
  export type JsonType = 'object' | 'array' | 'string' | 'number' | 'boolean' | 'null';
@@ -336,8 +339,14 @@ export function createJsonViewer(root: HTMLElement | null, options: JsonViewerOp
336
339
  },
337
340
  onSelect: (node) => {
338
341
  const meta = node.data as JsonMeta;
342
+ const appearing = foot.hidden;
343
+ const moved = pathText.textContent !== meta.path;
339
344
  foot.hidden = false;
340
345
  pathText.textContent = meta.path;
346
+ if (!reduced()) {
347
+ if (appearing) keep(blurIn(foot, { from: 'rise', duration: 0.32 }));
348
+ else if (moved) keep(blurIn(pathText, { from: 'soft', duration: 0.28 }));
349
+ }
341
350
  options.onSelect?.(meta.path, meta.value);
342
351
  root.dispatchEvent(new CustomEvent('mk:select', { bubbles: true, detail: { path: meta.path, value: meta.value } }));
343
352
  },
@@ -79,7 +79,8 @@ mk-json-viewer {
79
79
  transition:
80
80
  background-color 0.15s,
81
81
  color 0.15s,
82
- opacity 0.15s;
82
+ opacity 0.15s,
83
+ translate 0.2s ease;
83
84
  }
84
85
 
85
86
  [data-mk-jv-tool]:hover,
@@ -189,6 +190,27 @@ mk-json-viewer {
189
190
  color: var(--mk-muted);
190
191
  font-size: 0.75rem;
191
192
  text-overflow: ellipsis;
193
+ transition:
194
+ opacity 0.25s ease,
195
+ translate 0.3s cubic-bezier(0.38, 0.49, 0, 1.5);
196
+ }
197
+
198
+ /* An open branch shows its content: the count steps back. */
199
+ [role='treeitem'][aria-expanded='true'] > [data-mk-tree-row] [data-mk-jv-summary] {
200
+ opacity: 0.45;
201
+ translate: -3px 0;
202
+ }
203
+
204
+ /* Hovered values sit on a faint pill of their own colour. */
205
+ [data-mk-jv-value] {
206
+ margin: 0 -0.2rem;
207
+ padding: 0 0.2rem;
208
+ border-radius: 4px;
209
+ transition: background-color 0.2s ease;
210
+ }
211
+
212
+ [data-mk-tree-row]:hover [data-mk-jv-value] {
213
+ background: color-mix(in srgb, currentColor 12%, transparent);
192
214
  }
193
215
 
194
216
  [data-mk-jv-copy] {
@@ -197,11 +219,14 @@ mk-json-viewer {
197
219
  margin-left: auto;
198
220
  border-radius: 6px;
199
221
  opacity: 0;
222
+ translate: 4px 0;
200
223
  }
201
224
 
225
+ /* The copy button slides in on hover (or focus). */
202
226
  [data-mk-tree-row]:hover [data-mk-jv-copy],
203
227
  [role='treeitem']:focus-visible > [data-mk-tree-row] [data-mk-jv-copy] {
204
228
  opacity: 1;
229
+ translate: 0 0;
205
230
  }
206
231
 
207
232
  @media (hover: none) {
@@ -268,4 +293,11 @@ mk-json-viewer {
268
293
  [data-mk-jv-added] {
269
294
  animation: none;
270
295
  }
296
+
297
+ [data-mk-jv-copy],
298
+ [data-mk-jv-summary],
299
+ [data-mk-jv-value] {
300
+ transition: none;
301
+ translate: none;
302
+ }
271
303
  }
@@ -4,13 +4,15 @@ Navegación con siete estilos de escritorio y una sola barra de cristal en el m
4
4
 
5
5
  | Estilo | Cómo se ve |
6
6
  |---|---|
7
- | `clasica` | Barra lateral con borde; la página actual en una píldora rellena |
8
- | `bonita` | Píldoras de texto centradas que se ensanchan al pasar y al estar activas (padding con resorte, 0.3 s `cubic(.38,.49,0,1.5)`) |
7
+ | `sidebar` | Barra lateral con borde; la página actual en una píldora rellena |
8
+ | `pills` | Píldoras de texto centradas que se ensanchan al pasar y al estar activas (padding con resorte, 0.3 s `cubic(.38,.49,0,1.5)`) |
9
9
  | `dock` | Barra flotante de cristal centrada abajo; el botón activo se ensancha a 80 px |
10
- | `guapa` | La vista es una tarjeta sobre la cáscara tintada; la página actual es un chip blanco |
11
- | `tasks` | Riel de botones redondos de 56 px; el activo se cuadra (radio 0.3 s `cubic(.38,.49,0,2)`) |
12
- | `melon` | Barra lateral suave con un lavado primario en la página actual |
13
- | `tunnel` | Palabras grandes; la que tiene el ratón y sus vecinas crecen como un dock (1.2 / 1.09 / 1.03) |
10
+ | `card` | La vista es una tarjeta sobre la cáscara tintada; la página actual es un chip blanco |
11
+ | `rail` | Riel de botones redondos de 56 px; el activo se cuadra (radio 0.3 s `cubic(.38,.49,0,2)`) |
12
+ | `wash` | Barra lateral suave con un lavado primario en la página actual |
13
+ | `magnify` | Palabras grandes; la que tiene el ratón y sus vecinas crecen como un dock (1.2 / 1.09 / 1.03) |
14
+
15
+ Los nombres anteriores a la 0.15 (`clasica`, `bonita`, `guapa`, `tasks`, `melon`, `tunnel`) siguen funcionando: el motor los convierte en `sidebar`, `pills`, `card`, `rail`, `wash` y `magnify` (`NAV_STYLE_ALIASES`, `toNavStyle()`); en el marcado escrito usa ya la clase nueva para que se vea bien antes del JS.
14
16
 
15
17
  `accent` pinta la página actual en primary-container. Al cambiar de página el icono se rellena (`mk-nav-icon`, 125 ms, `FILL 0 → 1`, para fuentes de iconos como Material Symbols). Con `reveal`, al arrancar la cáscara se descubre desde el primer botón (`revealFrom`).
16
18
 
@@ -22,7 +24,7 @@ Navegación con siete estilos de escritorio y una sola barra de cristal en el m
22
24
  <header class="mk-shell__header"><h1 class="mk-shell__title">Inicio</h1></header>
23
25
  …
24
26
  </main>
25
- <mk-nav class="mk-nav mk-nav--bonita" nav-style="bonita" storage-key="mi-app-nav">
27
+ <mk-nav class="mk-nav mk-nav--pills" nav-style="pills" storage-key="mi-app-nav">
26
28
  <div class="mk-nav__buttons">
27
29
  <a class="mk-nav__button" href="/" aria-current="page"><svg class="mk-nav__icon">…</svg><span class="mk-nav__name">Inicio</span></a>
28
30
  <a class="mk-nav__button" href="/movimientos"><svg class="mk-nav__icon">…</svg><span class="mk-nav__name">Movimientos</span></a>
@@ -42,7 +44,7 @@ Navegación con siete estilos de escritorio y una sola barra de cristal en el m
42
44
 
43
45
  Escribe la clase `mk-nav--<estilo>` en el marcado para que se vea bien antes de que cargue el JS. Sin custom element: `createNav(document.querySelector('.mk-nav'), opciones)` sobre un `<nav>`.
44
46
 
45
- Partes: `.mk-nav__button--first` (botón de arriba: cuenta, espacio de trabajo), `.mk-nav__buttons`, `.mk-nav__actions` (`.mk-nav__button--action`), `.mk-nav__options` + `.mk-nav__label` (bloque extra que solo muestran clásica, guapa, melon y tunnel), `.mk-nav__icon`, `.mk-nav__name`, `data-mk-hide-mobile` (se esconde en el móvil). Vista: `.mk-shell__view`, `.mk-shell__header`, `.mk-shell__title`, `.mk-shell__content` (cada estilo ajusta su cabecera).
47
+ Partes: `.mk-nav__button--first` (botón de arriba: cuenta, espacio de trabajo), `.mk-nav__buttons`, `.mk-nav__actions` (`.mk-nav__button--action`), `.mk-nav__options` + `.mk-nav__label` (bloque extra que solo muestran sidebar, card, wash y magnify), `.mk-nav__icon`, `.mk-nav__name`, `data-mk-hide-mobile` (se esconde en el móvil). Vista: `.mk-shell__view`, `.mk-shell__header`, `.mk-shell__title`, `.mk-shell__content` (cada estilo ajusta su cabecera).
46
48
 
47
49
  ## React
48
50
 
@@ -52,7 +54,7 @@ const [page, setPage] = useState('home');
52
54
  <div className="mk-shell" style={{ height: '100dvh' }}>
53
55
  <main className="mk-shell__view">…</main>
54
56
  <Nav
55
- navStyle="tasks"
57
+ navStyle="rail"
56
58
  items={[
57
59
  { value: 'home', label: 'Inicio', icon: <HomeIcon /> },
58
60
  { value: 'movements', label: 'Movimientos', icon: <RowsIcon /> },
@@ -76,7 +78,7 @@ const [page, setPage] = useState('home');
76
78
 
77
79
  ## Variables CSS
78
80
 
79
- `--mk-nav-icon-size` (24px; 16px en guapa y melon con acento). Colores de los roles Material (`surface-container-*`, `primary-container`, `tertiary-container`…).
81
+ `--mk-nav-icon-size` (24px; 16px en card y wash con acento). Colores de los roles Material (`surface-container-*`, `primary-container`, `tertiary-container`…).
80
82
 
81
83
  ## Accesibilidad
82
84
 
@@ -1,8 +1,8 @@
1
1
  import { MkElement, defineElement, type OptionType } from '../../elements/base';
2
- import { createNav, type NavInstance, type NavStyle } from './engine';
2
+ import { createNav, type NavInstance, type NavStyle, type NavStyleAlias } from './engine';
3
3
 
4
4
  /**
5
- * <mk-nav class="mk-nav mk-nav--bonita" nav-style="bonita" [accent] [reveal] [storage-key="mi-app-nav"]>
5
+ * <mk-nav class="mk-nav mk-nav--pills" nav-style="pills" [accent] [reveal] [storage-key="mi-app-nav"]>
6
6
  * <div class="mk-nav__buttons"><a class="mk-nav__button" href="/">…</a></div>
7
7
  * </mk-nav>
8
8
  * Emits `mk:navigate` (detail: index, value, item, originalEvent) on click.
@@ -12,7 +12,7 @@ export class MkNavElement extends MkElement<NavInstance> {
12
12
 
13
13
  protected create(options: Record<string, unknown>) {
14
14
  return createNav(this, {
15
- style: options.navStyle as NavStyle | undefined,
15
+ style: options.navStyle as NavStyle | NavStyleAlias | undefined,
16
16
  accent: options.accent as boolean | undefined,
17
17
  reveal: Boolean(options.reveal),
18
18
  storageKey: options.storageKey as string | undefined,
@@ -22,7 +22,7 @@ export class MkNavElement extends MkElement<NavInstance> {
22
22
  // Style and accent change in place: rebuilding would replay the reveal.
23
23
  attributeChangedCallback(name: string, previous: string | null, next: string | null) {
24
24
  if (previous === next || !this.instance) return;
25
- if (name === 'nav-style' && next) this.instance.setStyle(next as NavStyle);
25
+ if (name === 'nav-style' && next) this.instance.setStyle(next as NavStyle | NavStyleAlias);
26
26
  else if (name === 'accent') this.instance.setAccent(next !== null && next !== 'false');
27
27
  else super.attributeChangedCallback(name, previous, next);
28
28
  }
@@ -31,7 +31,7 @@ export class MkNavElement extends MkElement<NavInstance> {
31
31
  this.instance?.select(target);
32
32
  }
33
33
 
34
- setStyle(style: NavStyle) {
34
+ setStyle(style: NavStyle | NavStyleAlias) {
35
35
  this.instance?.setStyle(style);
36
36
  }
37
37
  }
@@ -4,20 +4,26 @@ import { inert, warnOnce, type Instance } from '../../core/types';
4
4
  import { revealFrom, type RevealHandle } from '../../motion/reveal';
5
5
 
6
6
  /*
7
- * navigation: seven desktop styles (Clásica, Bonita, Dock, Guapa,
8
- * Tasks, Melon, Tunnel) and one glass bar on phones, all in CSS (nav.css). The
7
+ * navigation: seven desktop styles named after how they look (sidebar, pills,
8
+ * dock, card, rail, wash, magnify) and one glass bar on phones, all in CSS (nav.css). The
9
9
  * engine marks the current page (aria-current="page"), switches styles (and can
10
10
  * remember the choice), and on start can reveal the
11
11
  * app shell from the first button.
12
12
  * Parts: .mk-nav__buttons .mk-nav__button (data-mk-value or href), .mk-nav__button--first.
13
13
  */
14
14
 
15
- export const NAV_STYLES = ['clasica', 'bonita', 'dock', 'guapa', 'tasks', 'melon', 'tunnel'] as const;
15
+ export const NAV_STYLES = ['sidebar', 'pills', 'dock', 'card', 'rail', 'wash', 'magnify'] as const;
16
16
  export type NavStyle = (typeof NAV_STYLES)[number];
17
+ /** The names before 0.15, still accepted: each turns into the style it always was. */
18
+ export const NAV_STYLE_ALIASES = { clasica: 'sidebar', bonita: 'pills', guapa: 'card', tasks: 'rail', melon: 'wash', tunnel: 'magnify' } as const satisfies Record<string, NavStyle>;
19
+ export type NavStyleAlias = keyof typeof NAV_STYLE_ALIASES;
20
+ /** A style name (new or old) as one of NAV_STYLES, or undefined. */
21
+ export const toNavStyle = (value: unknown): NavStyle | undefined =>
22
+ NAV_STYLES.includes(value as NavStyle) ? (value as NavStyle) : (NAV_STYLE_ALIASES as Record<string, NavStyle>)[value as string];
17
23
 
18
24
  export interface NavOptions {
19
- /** Default: the saved one (storageKey), else the mk-nav--<style> class in the markup, else 'bonita' (Money's default). */
20
- style?: NavStyle;
25
+ /** Default: the saved one (storageKey), else the mk-nav--<style> class in the markup, else 'pills'. Old names (clasica…) work too. */
26
+ style?: NavStyle | NavStyleAlias;
21
27
  /** The accent variant: the current page in primary-container. */
22
28
  accent?: boolean;
23
29
  /** Reveal the shell from the first button when the nav starts (off by default). */
@@ -31,7 +37,7 @@ export interface NavOptions {
31
37
  export interface NavInstance extends Instance {
32
38
  /** Mark a page as current, by index or value (silent: no event). */
33
39
  select(target: number | string): void;
34
- setStyle(style: NavStyle): void;
40
+ setStyle(style: NavStyle | NavStyleAlias): void;
35
41
  setAccent(on: boolean): void;
36
42
  readonly index: number;
37
43
  readonly style: NavStyle;
@@ -47,7 +53,6 @@ export interface NavigateDetail {
47
53
  }
48
54
 
49
55
  const ITEM = '.mk-nav__buttons .mk-nav__button';
50
- const isStyle = (value: unknown): value is NavStyle => NAV_STYLES.includes(value as NavStyle);
51
56
  const valueOf = (item: HTMLElement) => item.dataset.mkValue ?? item.getAttribute('href') ?? undefined;
52
57
 
53
58
  const read = (key: string) => {
@@ -77,17 +82,18 @@ export function revealShell(nav: HTMLElement): RevealHandle | null {
77
82
  export function createNav(root: HTMLElement | null, options: NavOptions = {}): NavInstance {
78
83
  if (!root) {
79
84
  warnOnce('nav: missing root');
80
- return inert({ select() {}, setStyle() {}, setAccent() {}, index: -1, style: 'bonita', items: [] });
85
+ return inert({ select() {}, setStyle() {}, setAccent() {}, index: -1, style: 'pills', items: [] });
81
86
  }
82
87
  const scope = createScope();
83
88
  const items = () => [...root.querySelectorAll<HTMLElement>(ITEM)];
84
- const fromClass = NAV_STYLES.find((name) => root.classList.contains(`mk-nav--${name}`));
89
+ const known = [...NAV_STYLES, ...Object.keys(NAV_STYLE_ALIASES)];
90
+ const fromClass = toNavStyle(known.find((name) => root.classList.contains(`mk-nav--${name}`)));
85
91
  const saved = options.storageKey ? read(options.storageKey) : null;
86
- let style: NavStyle = options.style ?? (isStyle(saved) ? saved : undefined) ?? fromClass ?? 'bonita';
92
+ let style: NavStyle = toNavStyle(options.style) ?? toNavStyle(saved) ?? fromClass ?? 'pills';
87
93
 
88
94
  const paint = (accent: boolean) => {
89
95
  const classes = new Set(root.classList);
90
- for (const name of NAV_STYLES) classes.delete(`mk-nav--${name}`);
96
+ for (const name of known) classes.delete(`mk-nav--${name}`);
91
97
  classes.add('mk-nav');
92
98
  classes.add(`mk-nav--${style}`);
93
99
  if (accent) classes.add('mk-nav--accent');
@@ -133,13 +139,14 @@ export function createNav(root: HTMLElement | null, options: NavOptions = {}): N
133
139
  else mark(item);
134
140
  },
135
141
  setStyle(next) {
136
- if (!isStyle(next)) {
142
+ const resolved = toNavStyle(next);
143
+ if (!resolved) {
137
144
  warnOnce(`nav: unknown style "${String(next)}" (use ${NAV_STYLES.join(', ')})`);
138
145
  return;
139
146
  }
140
- style = next;
147
+ style = resolved;
141
148
  paint(accent);
142
- if (options.storageKey) write(options.storageKey, next);
149
+ if (options.storageKey) write(options.storageKey, resolved);
143
150
  },
144
151
  setAccent(on) {
145
152
  accent = on;