@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.
Files changed (146) hide show
  1. package/README.md +23 -0
  2. package/dist/auto/index.d.ts +18 -3
  3. package/dist/auto/index.js +32 -2
  4. package/dist/auto/index.js.map +1 -1
  5. package/dist/{chunk-VJFPTLAK.js → chunk-AWDEKDEJ.js} +2 -2
  6. package/dist/{chunk-BZLMCHUD.js → chunk-EDMXWUW3.js} +2 -2
  7. package/dist/{chunk-7IQTMF7Y.js → chunk-Q7IW4FV3.js} +2580 -58
  8. package/dist/chunk-Q7IW4FV3.js.map +1 -0
  9. package/dist/css/components/badge-button.css +16 -0
  10. package/dist/css/components/bento-grid.css +13 -0
  11. package/dist/css/components/drilldown-menu.css +11 -7
  12. package/dist/css/components/equalizer-slider.css +26 -0
  13. package/dist/css/components/handwriting-text.css +15 -0
  14. package/dist/css/components/magnetize-button.css +18 -0
  15. package/dist/css/components/morphing-page-dots.css +13 -0
  16. package/dist/css/components/not-found.css +22 -0
  17. package/dist/css/components/notification-stack.css +42 -0
  18. package/dist/css/components/particle-button.css +14 -0
  19. package/dist/css/components/radio-group.css +29 -0
  20. package/dist/css/components/rating-scale.css +23 -0
  21. package/dist/css/components/scroll-preview-dock.css +18 -0
  22. package/dist/css/components/select.css +16 -0
  23. package/dist/css/components/signature-pad.css +130 -103
  24. package/dist/css/components/stepper.css +36 -0
  25. package/dist/css/components/sticky-header.css +10 -0
  26. package/dist/css/components/temperature-slider.css +29 -0
  27. package/dist/css/components/voice-message-bubble.css +62 -45
  28. package/dist/css/components/waveform-player.css +49 -34
  29. package/dist/css/components/wheel-picker.css +16 -15
  30. package/dist/css/tokens.css +4 -0
  31. package/dist/elements/index.d.ts +206 -4
  32. package/dist/elements/index.js +581 -6
  33. package/dist/elements/index.js.map +1 -1
  34. package/dist/{engine-Bn6hC87X.d.ts → engine-t8d7brjn.d.ts} +341 -1
  35. package/dist/index.css +578 -170
  36. package/dist/index.d.ts +23 -23
  37. package/dist/index.js +33 -3
  38. package/dist/index.js.map +1 -1
  39. package/dist/react/index.d.ts +113 -7
  40. package/dist/react/index.js +669 -3
  41. package/dist/react/index.js.map +1 -1
  42. package/dist/{reduced-YS6TZjpa.d.ts → reduced-dPsZYqtI.d.ts} +1 -1
  43. package/dist/{store-CZ6vdtIE.d.ts → store-6hZNKiee.d.ts} +1 -1
  44. package/package.json +1 -1
  45. package/registry.json +180 -0
  46. package/src/auto/auto-init.ts +31 -0
  47. package/src/components/badge-button/README.md +28 -0
  48. package/src/components/badge-button/badge-button.css +15 -0
  49. package/src/components/badge-button/element.ts +16 -0
  50. package/src/components/badge-button/engine.ts +85 -0
  51. package/src/components/badge-button/react.tsx +31 -0
  52. package/src/components/bento-grid/README.md +26 -0
  53. package/src/components/bento-grid/bento-grid.css +12 -0
  54. package/src/components/bento-grid/element.ts +22 -0
  55. package/src/components/bento-grid/engine.ts +153 -0
  56. package/src/components/bento-grid/react.tsx +25 -0
  57. package/src/components/drilldown-menu/drilldown-menu.css +11 -7
  58. package/src/components/drilldown-menu/engine.ts +44 -4
  59. package/src/components/equalizer-slider/README.md +43 -0
  60. package/src/components/equalizer-slider/element.ts +41 -0
  61. package/src/components/equalizer-slider/engine.ts +32 -0
  62. package/src/components/equalizer-slider/equalizer-slider.css +26 -0
  63. package/src/components/equalizer-slider/react.tsx +60 -0
  64. package/src/components/equalizer-slider/shared.ts +161 -0
  65. package/src/components/handwriting-text/README.md +29 -0
  66. package/src/components/handwriting-text/element.ts +14 -0
  67. package/src/components/handwriting-text/engine.ts +108 -0
  68. package/src/components/handwriting-text/glyphs.ts +71 -0
  69. package/src/components/handwriting-text/handwriting-text.css +14 -0
  70. package/src/components/handwriting-text/react.tsx +23 -0
  71. package/src/components/magnetize-button/README.md +26 -0
  72. package/src/components/magnetize-button/element.ts +13 -0
  73. package/src/components/magnetize-button/engine.ts +84 -0
  74. package/src/components/magnetize-button/magnetize-button.css +17 -0
  75. package/src/components/magnetize-button/react.tsx +22 -0
  76. package/src/components/morphing-page-dots/README.md +28 -0
  77. package/src/components/morphing-page-dots/element.ts +16 -0
  78. package/src/components/morphing-page-dots/engine.ts +89 -0
  79. package/src/components/morphing-page-dots/morphing-page-dots.css +12 -0
  80. package/src/components/morphing-page-dots/react.tsx +32 -0
  81. package/src/components/not-found/README.md +26 -0
  82. package/src/components/not-found/element.ts +15 -0
  83. package/src/components/not-found/engine.ts +57 -0
  84. package/src/components/not-found/not-found.css +21 -0
  85. package/src/components/not-found/react.tsx +23 -0
  86. package/src/components/notification-stack/README.md +24 -0
  87. package/src/components/notification-stack/element.ts +26 -0
  88. package/src/components/notification-stack/engine.ts +329 -0
  89. package/src/components/notification-stack/notification-stack.css +41 -0
  90. package/src/components/notification-stack/react.tsx +28 -0
  91. package/src/components/particle-button/README.md +26 -0
  92. package/src/components/particle-button/element.ts +13 -0
  93. package/src/components/particle-button/engine.ts +52 -0
  94. package/src/components/particle-button/particle-button.css +13 -0
  95. package/src/components/particle-button/react.tsx +22 -0
  96. package/src/components/radio-group/README.md +43 -0
  97. package/src/components/radio-group/element.ts +41 -0
  98. package/src/components/radio-group/engine.ts +11 -0
  99. package/src/components/radio-group/radio-group.css +28 -0
  100. package/src/components/radio-group/react.tsx +60 -0
  101. package/src/components/radio-group/shared.ts +118 -0
  102. package/src/components/rating-scale/README.md +43 -0
  103. package/src/components/rating-scale/element.ts +41 -0
  104. package/src/components/rating-scale/engine.ts +63 -0
  105. package/src/components/rating-scale/rating-scale.css +22 -0
  106. package/src/components/rating-scale/react.tsx +60 -0
  107. package/src/components/scroll-preview-dock/README.md +24 -0
  108. package/src/components/scroll-preview-dock/element.ts +20 -0
  109. package/src/components/scroll-preview-dock/engine.ts +228 -0
  110. package/src/components/scroll-preview-dock/react.tsx +26 -0
  111. package/src/components/scroll-preview-dock/scroll-preview-dock.css +17 -0
  112. package/src/components/select/README.md +23 -9
  113. package/src/components/select/select.css +16 -0
  114. package/src/components/signature-pad/engine.ts +550 -437
  115. package/src/components/signature-pad/signature-pad.css +130 -103
  116. package/src/components/stepper/README.md +46 -0
  117. package/src/components/stepper/element.ts +42 -0
  118. package/src/components/stepper/engine.ts +103 -0
  119. package/src/components/stepper/react.tsx +60 -0
  120. package/src/components/stepper/stepper.css +35 -0
  121. package/src/components/sticky-header/README.md +30 -0
  122. package/src/components/sticky-header/element.ts +19 -0
  123. package/src/components/sticky-header/engine.ts +95 -0
  124. package/src/components/sticky-header/react.tsx +25 -0
  125. package/src/components/sticky-header/sticky-header.css +9 -0
  126. package/src/components/temperature-slider/README.md +42 -0
  127. package/src/components/temperature-slider/element.ts +53 -0
  128. package/src/components/temperature-slider/engine.ts +44 -0
  129. package/src/components/temperature-slider/react.tsx +66 -0
  130. package/src/components/temperature-slider/temperature-slider.css +29 -0
  131. package/src/components/voice-message-bubble/engine.ts +11 -0
  132. package/src/components/voice-message-bubble/voice-message-bubble.css +14 -12
  133. package/src/components/waveform-player/README.md +1 -1
  134. package/src/components/waveform-player/engine.ts +344 -261
  135. package/src/components/waveform-player/waveform-player.css +50 -35
  136. package/src/components/wheel-picker/README.md +3 -1
  137. package/src/components/wheel-picker/element.ts +12 -5
  138. package/src/components/wheel-picker/engine.ts +37 -8
  139. package/src/components/wheel-picker/wheel-picker.css +17 -16
  140. package/src/css/tokens.css +4 -0
  141. package/src/elements/index.ts +45 -0
  142. package/src/index.ts +311 -295
  143. package/src/react/index.ts +16 -0
  144. package/dist/chunk-7IQTMF7Y.js.map +0 -1
  145. /package/dist/{chunk-VJFPTLAK.js.map → chunk-AWDEKDEJ.js.map} +0 -0
  146. /package/dist/{chunk-BZLMCHUD.js.map → chunk-EDMXWUW3.js.map} +0 -0
@@ -0,0 +1,16 @@
1
+ import { MkElement, defineElement, type OptionType } from '../../elements/base';
2
+ import { createMorphingPageDots, type MorphingPageDotsInstance } from './engine';
3
+
4
+ export class MkMorphingPageDotsElement extends MkElement<MorphingPageDotsInstance> {
5
+ static options: Record<string, OptionType> = { count: 'number', value: 'number', label: 'string', labels: 'json', pageIds: 'json', disabled: 'boolean' };
6
+ private retained: number | undefined;
7
+ protected create(options: Record<string, unknown>) { return createMorphingPageDots(this, { ...options, ...(this.retained === undefined ? {} : { value: this.retained }) }); }
8
+ disconnectedCallback() { this.retained = this.instance?.value ?? this.retained; super.disconnectedCallback(); }
9
+ attributeChangedCallback(name: string, previous: string | null, next: string | null) {
10
+ const focusIndex = [...this.querySelectorAll('button')].findIndex((button) => button === document.activeElement);
11
+ if (previous !== next) this.retained = name === 'value' ? undefined : this.instance?.value ?? this.retained;
12
+ super.attributeChangedCallback(name, previous, next);
13
+ if (focusIndex >= 0) { const buttons = this.querySelectorAll('button'); buttons[Math.min(focusIndex, buttons.length - 1)]?.focus({ preventScroll: true }); }
14
+ }
15
+ }
16
+ export const defineMorphingPageDots = () => defineElement('mk-morphing-page-dots', MkMorphingPageDotsElement);
@@ -0,0 +1,89 @@
1
+ import { gsap, EASE, registerMotion } from '../../core/gsap';
2
+ import { reduced } from '../../core/reduced';
3
+ import { createScope } from '../../core/scope';
4
+ import { inert, type Instance } from '../../core/types';
5
+ import { press } from '../../motion/micro';
6
+
7
+ export interface MorphingPageDotsOptions {
8
+ count?: number;
9
+ value?: number;
10
+ label?: string;
11
+ /** Accessible page names; by default "Página 1 de 5". */
12
+ labels?: string[];
13
+ /** Optional ids of the real carousel slides controlled by each dot. */
14
+ pageIds?: string[];
15
+ disabled?: boolean;
16
+ onChange?: (index: number) => void;
17
+ }
18
+ export interface MorphingPageDotsInstance extends Instance {
19
+ setValue(index: number, notify?: boolean): void;
20
+ readonly value: number;
21
+ }
22
+ export function createMorphingPageDots(root: HTMLElement | null, options: MorphingPageDotsOptions = {}): MorphingPageDotsInstance {
23
+ if (!root) return inert({ setValue() {}, value: 0 });
24
+ registerMotion();
25
+ const scope = createScope();
26
+ const count = Number.isFinite(options.count) ? Math.min(20, Math.max(1, Math.round(options.count!))) : 5;
27
+ const normalize = (index: number) => Math.min(count - 1, Math.max(0, Math.round(index)));
28
+ let value = normalize(Number.isFinite(options.value) ? options.value! : 0);
29
+ const authored = [...root.childNodes];
30
+ const rail = document.createElement('span'); rail.dataset.mkPageRail = '';
31
+ const blob = document.createElement('i'); blob.dataset.mkPageBlob = ''; blob.setAttribute('aria-hidden', 'true'); rail.append(blob);
32
+ const buttons: HTMLButtonElement[] = [];
33
+ for (let i = 0; i < count; i++) {
34
+ const button = document.createElement('button'); button.type = 'button'; button.dataset.mkPageDot = '';
35
+ button.setAttribute('aria-label', options.labels?.[i] ?? `Página ${i + 1} de ${count}`);
36
+ if (options.pageIds?.[i]) button.setAttribute('aria-controls', options.pageIds[i]!);
37
+ button.disabled = Boolean(options.disabled);
38
+ const dot = document.createElement('i'); dot.setAttribute('aria-hidden', 'true'); button.append(dot); rail.append(button); buttons.push(button);
39
+ }
40
+ root.replaceChildren(rail);
41
+ scope.add(() => { rail.remove(); root.append(...authored); });
42
+ scope.attr(root, 'role', 'group');
43
+ scope.attr(root, 'aria-label', options.label ?? 'Páginas');
44
+ scope.attr(root, 'data-mk-morphing-page-dots', '');
45
+ scope.attr(root, 'data-mk-ready', '');
46
+ let moving: gsap.core.Timeline | null = null;
47
+ let pressing: gsap.core.Animation | null = null;
48
+ const at = (index: number) => {
49
+ const button = buttons[index]!;
50
+ const size = button.offsetWidth || 44;
51
+ return (button.offsetLeft || index * size) + size / 2 - 12;
52
+ };
53
+ const place = () => { moving?.kill(); moving = null; gsap.set(blob, { left: at(value), width: 24, scaleY: 1 }); };
54
+ const paint = () => buttons.forEach((button, i) => { button.setAttribute('aria-current', i === value ? 'page' : 'false'); button.tabIndex = i === value ? 0 : -1; });
55
+ const setValue = (index: number, notify = false) => {
56
+ if (scope.disposed || !Number.isFinite(index)) return;
57
+ const next = normalize(index);
58
+ if (value === next) return;
59
+ const start = at(value); const end = at(next);
60
+ value = next;
61
+ paint();
62
+ moving?.kill();
63
+ if (reduced()) place();
64
+ else {
65
+ // Stretch the leading edge, then let the tail catch up on the shared swatch curve.
66
+ const current = Number(gsap.getProperty(blob, 'left')) || start;
67
+ moving = gsap.timeline()
68
+ .to(blob, { left: Math.min(current, end), width: Math.abs(end - current) + 24, scaleY: .68, duration: .18, ease: EASE.wipe })
69
+ .to(blob, { left: end, width: 24, scaleY: 1, duration: .48, ease: EASE.swatch }, .13);
70
+ }
71
+ if (notify) { options.onChange?.(value); root.dispatchEvent(new CustomEvent('mk:change', { bubbles: true, detail: { value } })); }
72
+ };
73
+ buttons.forEach((button, i) => {
74
+ scope.on(button, 'click', () => { if (button.disabled) return; pressing?.kill(); pressing = press(button); setValue(i, true); });
75
+ scope.on(button, 'keydown', (event) => {
76
+ if (button.disabled) return;
77
+ const key = event.key;
78
+ if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(key)) return;
79
+ event.preventDefault();
80
+ const rtl = getComputedStyle(root).direction === 'rtl';
81
+ let next = key === 'Home' ? 0 : key === 'End' ? count - 1 : (value + ((key === 'ArrowRight') !== rtl ? 1 : -1) + count) % count;
82
+ setValue(next, true); buttons[next]!.focus();
83
+ });
84
+ });
85
+ if (typeof ResizeObserver !== 'undefined') scope.observe(new ResizeObserver(place)).observe(rail);
86
+ paint(); place();
87
+ scope.add(() => { moving?.kill(); pressing?.kill(); });
88
+ return { setValue, get value() { return value; }, destroy: () => scope.dispose() };
89
+ }
@@ -0,0 +1,12 @@
1
+ .mk-morphing-page-dots,
2
+ mk-morphing-page-dots { display: inline-flex; max-width: 100%; color: var(--mk-text); font-family: var(--mk-font); }
3
+ [data-mk-page-rail] { position: relative; display: inline-flex; max-width: 100%; border-radius: 999px; background: var(--mk-surface-2); padding: 0; overflow-x: auto; }
4
+ [data-mk-page-dot] { position: relative; flex: 0 0 44px; width: 44px; height: 44px; display: grid; place-items: center; padding: 0; background: transparent; border: 0; border-radius: 999px; cursor: pointer; }
5
+ [data-mk-page-dot] > i { width: 8px; height: 8px; border-radius: 999px; background: var(--mk-muted); opacity: .45; transition: opacity 150ms; }
6
+ [data-mk-page-dot]:hover > i { opacity: .85; }
7
+ [data-mk-page-dot][aria-current='page'] > i { opacity: 0; }
8
+ [data-mk-page-dot]:focus-visible { outline: 2px solid var(--mk-accent); outline-offset: -3px; }
9
+ [data-mk-page-dot]:disabled { cursor: not-allowed; opacity: .4; }
10
+ [data-mk-page-blob] { position: absolute; top: 18px; left: 10px; width: 24px; height: 8px; background: var(--mk-text); border-radius: 999px; pointer-events: none; }
11
+ [data-mk-morphing-page-dots]:has(button:disabled) [data-mk-page-blob] { opacity: .4; }
12
+ @media (prefers-reduced-motion: reduce) { [data-mk-page-dot] > i { transition: none; } }
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+ import { useRef, type HTMLAttributes, type Ref } from 'react';
3
+ import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
4
+ import { createMorphingPageDots, type MorphingPageDotsInstance, type MorphingPageDotsOptions } from './engine';
5
+
6
+ export interface MorphingPageDotsProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'dangerouslySetInnerHTML' | 'onChange'>, MorphingPageDotsOptions {
7
+ defaultValue?: number;
8
+ instanceRef?: Ref<MorphingPageDotsInstance>;
9
+ }
10
+ export function MorphingPageDots({ count, value, defaultValue = 0, label, labels, pageIds, disabled, onChange, instanceRef, className, ...props }: MorphingPageDotsProps) {
11
+ const { children: _children, dangerouslySetInnerHTML: _html, ...safe } = props as HTMLAttributes<HTMLDivElement>;
12
+ const root = useRef<HTMLDivElement>(null);
13
+ const instance = useRef<MorphingPageDotsInstance | null>(null);
14
+ const current = useLatest({ value, onChange });
15
+ const retained = useRef(defaultValue);
16
+ const labelsKey = JSON.stringify(labels); const pagesKey = JSON.stringify(pageIds);
17
+ useIsomorphicLayoutEffect(() => {
18
+ let created: MorphingPageDotsInstance;
19
+ created = createMorphingPageDots(root.current, {
20
+ count, value: current.current.value ?? retained.current, label, labels, pageIds, disabled,
21
+ onChange: (next) => { current.current.onChange?.(next); if (current.current.value !== undefined) created.setValue(current.current.value); },
22
+ });
23
+ instance.current = created;
24
+ return () => { retained.current = created.value; created.destroy(); instance.current = null; };
25
+ }, [count, label, labelsKey, pagesKey, disabled]);
26
+ useIsomorphicLayoutEffect(() => { if (value !== undefined) instance.current?.setValue(value); }, [value]);
27
+ useIsomorphicLayoutEffect(() => {
28
+ if (typeof instanceRef === 'function') instanceRef(instance.current); else if (instanceRef) instanceRef.current = instance.current;
29
+ return () => { if (typeof instanceRef === 'function') instanceRef(null); else if (instanceRef) instanceRef.current = null; };
30
+ }, [instanceRef, count, label, labelsKey, pagesKey, disabled]);
31
+ return <div {...safe} ref={root} className={['mk-morphing-page-dots', className].filter(Boolean).join(' ')} />;
32
+ }
@@ -0,0 +1,26 @@
1
+ # Not found
2
+
3
+ Estado 404 con ilustración vectorial original: dos cuatros y un pequeño portal en lugar del cero. Su entrada combina blur escalonado y un asentamiento `swatch`; permanece quieto después. Usa superficies del tema y reserva el acento para la brújula.
4
+
5
+ ```ts
6
+ import { createNotFound } from '@yewdiel21/motion-kit';
7
+ import '@yewdiel21/motion-kit/css/not-found';
8
+ const missing = createNotFound(document.querySelector('#missing'), {
9
+ brand: 'Mi biblioteca', homeHref: '/library',
10
+ });
11
+ // missing.replay(); missing.destroy();
12
+ ```
13
+
14
+ `NotFoundOptions`: `brand` (Motion Kit), `title`, `description`, `homeHref` (/), `homeLabel`, `backLabel`, `showBack` (true), `onBack`. El enlace Home navega a su href real. Back llama `history.back()`; se desactiva cuando no hay entrada previa. `onBack` permite que un router posea la acción. `NotFoundInstance`: `replay()`, `destroy()`. Todo el contenido configurable se inserta como texto, no como HTML.
15
+
16
+ ```tsx
17
+ <NotFound brand="Mi biblioteca" homeHref="/library" instanceRef={missingRef} />
18
+ ```
19
+
20
+ `NotFoundProps` añade atributos de div e `instanceRef`; el motor posee todos los hijos, por lo que filtra `children` y `dangerouslySetInnerHTML` en ejecución. Cambiar copy o destinos actualiza la instancia. Movimiento reducido conserva toda la navegación y muestra el estado completo inmediatamente.
21
+
22
+ ```html
23
+ <mk-not-found brand="Mi biblioteca" home-href="/library" title="No encontramos esa lectura."></mk-not-found>
24
+ ```
25
+
26
+ `defineNotFound()` registra `MkNotFoundElement`; atributos `brand`, `title`, `description`, `home-href`, `home-label`, `back-label`, `show-back`. El contenido usa heading h2 para poder incrustarlo en un layout; la página anfitriona determina su h1 y su código HTTP. `destroy()` detiene la entrada y devuelve los nodos y atributos originales.
@@ -0,0 +1,15 @@
1
+ import { MkElement, defineElement, type OptionType } from '../../elements/base';
2
+ import { createNotFound, type NotFoundInstance } from './engine';
3
+
4
+ export class MkNotFoundElement extends MkElement<NotFoundInstance> {
5
+ static options: Record<string, OptionType> = { brand: 'string', title: 'string', description: 'string', homeHref: 'string', homeLabel: 'string', backLabel: 'string', showBack: 'boolean' };
6
+ protected create(options: Record<string, unknown>) { return createNotFound(this, options); }
7
+ attributeChangedCallback(name: string, previous: string | null, next: string | null) {
8
+ const active = document.activeElement;
9
+ const focused = this.contains(active);
10
+ const back = active?.tagName === 'BUTTON';
11
+ super.attributeChangedCallback(name, previous, next);
12
+ if (focused) (this.querySelector<HTMLElement>(back ? 'button:not(:disabled)' : 'a') ?? this.querySelector<HTMLElement>('a'))?.focus({ preventScroll: true });
13
+ }
14
+ }
15
+ export const defineNotFound = () => defineElement('mk-not-found', MkNotFoundElement);
@@ -0,0 +1,57 @@
1
+ import { gsap, EASE, registerMotion } from '../../core/gsap';
2
+ import { reduced } from '../../core/reduced';
3
+ import { createScope } from '../../core/scope';
4
+ import { inert, type Instance } from '../../core/types';
5
+ import { blurIn, press } from '../../motion/micro';
6
+
7
+ export interface NotFoundOptions {
8
+ brand?: string;
9
+ title?: string;
10
+ description?: string;
11
+ homeHref?: string;
12
+ homeLabel?: string;
13
+ backLabel?: string;
14
+ showBack?: boolean;
15
+ /** A router may own Back. Without this callback it calls history.back(). */
16
+ onBack?: () => void;
17
+ }
18
+ export interface NotFoundInstance extends Instance { replay(): void; }
19
+ const ILLUSTRATION = '<svg viewBox="0 0 320 140" fill="none" aria-hidden="true"><path data-mk-lost-track d="M20 115H100Q118 115 118 95V72Q118 53 138 53H185Q205 53 205 73V95Q205 115 224 115H300"/><g data-mk-lost-four><path d="M72 23L35 79H84M77 23V107"/><path d="M263 23L226 79H275M268 23V107"/></g><g data-mk-lost-portal><rect x="124" y="18" width="72" height="96" rx="32"/><ellipse cx="160" cy="66" rx="17" ry="29"/><path d="M145 66h30M160 51v30"/></g><path data-mk-lost-star d="M211 13v12m-6-6h12M105 118v8m-4-4h8"/></svg>';
20
+ export function createNotFound(root: HTMLElement | null, options: NotFoundOptions = {}): NotFoundInstance {
21
+ if (!root) return inert({ replay() {} });
22
+ registerMotion();
23
+ const scope = createScope();
24
+ const authored = [...root.childNodes];
25
+ const content = document.createElement('div'); content.dataset.mkNotFoundContent = '';
26
+ const art = document.createElement('div'); art.dataset.mkNotFoundArt = ''; art.innerHTML = ILLUSTRATION;
27
+ const eyebrow = document.createElement('p'); eyebrow.dataset.mkNotFoundCode = ''; eyebrow.textContent = `404 · ${options.brand ?? 'Motion Kit'}`;
28
+ const heading = document.createElement('h2'); heading.textContent = options.title ?? 'Este camino se perdió.';
29
+ const description = document.createElement('p'); description.dataset.mkNotFoundDescription = ''; description.textContent = options.description ?? 'La página que buscas no está aquí. Volvamos a un lugar conocido.';
30
+ const actions = document.createElement('div'); actions.dataset.mkNotFoundActions = '';
31
+ const home = document.createElement('a'); home.href = options.homeHref ?? '/'; home.textContent = options.homeLabel ?? 'Volver al inicio';
32
+ actions.append(home);
33
+ const back = document.createElement('button'); back.type = 'button'; back.textContent = options.backLabel ?? 'Ir atrás';
34
+ back.disabled = !options.onBack && history.length <= 1;
35
+ if (options.showBack !== false) actions.append(back);
36
+ content.append(art, eyebrow, heading, description, actions);
37
+ root.replaceChildren(content);
38
+ scope.add(() => { content.remove(); root.append(...authored); });
39
+ scope.attr(root, 'data-mk-not-found', '');
40
+ scope.attr(root, 'data-mk-ready', '');
41
+ let entering: gsap.core.Animation | null = null;
42
+ let drawing: gsap.core.Animation | null = null;
43
+ let pressing: gsap.core.Animation | null = null;
44
+ const portal = art.querySelector('[data-mk-lost-portal]');
45
+ const replay = () => {
46
+ if (scope.disposed) return;
47
+ entering?.kill(); drawing?.kill();
48
+ if (reduced()) { gsap.set([art, eyebrow, heading, description, actions], { clearProps: 'all' }); return; }
49
+ entering = blurIn([art, eyebrow, heading, description, actions], { from: 'soft', duration: .5, stagger: .055 });
50
+ drawing = gsap.fromTo(portal, { rotation: -12, scale: .82, transformOrigin: '50% 50%' }, { rotation: 0, scale: 1, duration: .8, ease: EASE.swatch });
51
+ };
52
+ scope.on(back, 'click', () => { if (!back.disabled) { pressing?.kill(); pressing = press(back); if (options.onBack) options.onBack(); else history.back(); } });
53
+ scope.on(home, 'click', () => { pressing?.kill(); pressing = press(home); });
54
+ replay();
55
+ scope.add(() => { entering?.kill(); drawing?.kill(); pressing?.kill(); });
56
+ return { replay, destroy: () => scope.dispose() };
57
+ }
@@ -0,0 +1,21 @@
1
+ .mk-not-found,
2
+ mk-not-found { display: block; max-width: 34rem; color: var(--mk-text); font-family: var(--mk-font); }
3
+ [data-mk-not-found-content] { display: grid; justify-items: center; text-align: center; gap: .75rem; padding: .75rem 1rem; }
4
+ [data-mk-not-found-art] { width: min(100%, 320px); }
5
+ [data-mk-not-found-art] svg { display: block; width: 100%; overflow: visible; }
6
+ [data-mk-lost-track] { stroke: var(--mk-outline); stroke-width: 1.5; stroke-dasharray: 3 5; }
7
+ [data-mk-lost-four] { stroke: var(--mk-text); stroke-width: 9; stroke-linecap: round; stroke-linejoin: round; }
8
+ [data-mk-lost-portal] rect { fill: var(--mk-surface-3); stroke: var(--mk-outline-soft); }
9
+ [data-mk-lost-portal] ellipse { fill: var(--mk-surface-1); stroke: var(--mk-outline); stroke-width: 1.5; }
10
+ [data-mk-lost-portal] path,
11
+ [data-mk-lost-star] { stroke: var(--mk-accent); stroke-width: 2; stroke-linecap: round; }
12
+ [data-mk-not-found-code] { margin: 0; color: var(--mk-muted); font-size: .6875rem; letter-spacing: .1em; text-transform: uppercase; }
13
+ [data-mk-not-found-content] h2 { margin: 0; font-size: clamp(1.5rem, 4vw, 2rem); font-weight: 550; line-height: 1.15; letter-spacing: -.045em; }
14
+ [data-mk-not-found-description] { max-width: 34ch; margin: 0; color: var(--mk-muted); font-size: .875rem; line-height: 1.55; }
15
+ [data-mk-not-found-actions] { display: flex; flex-wrap: wrap; justify-content: center; gap: .5rem; margin-top: .5rem; }
16
+ [data-mk-not-found-actions] :is(a, button) { display: inline-flex; align-items: center; justify-content: center; min-height: 44px; padding: .7rem 1rem; border: 0; border-radius: 16px; background: var(--mk-surface-3); color: var(--mk-text); font: 500 .875rem var(--mk-font); text-decoration: none; cursor: pointer; transition: background 160ms, border-radius 320ms var(--mk-ease-swatch); }
17
+ [data-mk-not-found-actions] a { background: var(--mk-text); color: var(--mk-bg); }
18
+ [data-mk-not-found-actions] :is(a, button):hover { border-radius: 24px; }
19
+ [data-mk-not-found-actions] :is(a, button):focus-visible { outline: 2px solid var(--mk-accent); outline-offset: 4px; }
20
+ [data-mk-not-found-actions] button:disabled { opacity: .4; cursor: not-allowed; }
21
+ @media (prefers-reduced-motion: reduce) { [data-mk-not-found-actions] :is(a, button) { transition: none; } }
@@ -0,0 +1,23 @@
1
+ 'use client';
2
+ import { useRef, type HTMLAttributes, type Ref } from 'react';
3
+ import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
4
+ import { createNotFound, type NotFoundInstance, type NotFoundOptions } from './engine';
5
+
6
+ export interface NotFoundProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'dangerouslySetInnerHTML' | 'title'>, NotFoundOptions { instanceRef?: Ref<NotFoundInstance>; }
7
+ export function NotFound({ brand, title, description, homeHref, homeLabel, backLabel, showBack, onBack, instanceRef, className, ...props }: NotFoundProps) {
8
+ const { children: _children, dangerouslySetInnerHTML: _html, ...safe } = props as HTMLAttributes<HTMLDivElement>;
9
+ const root = useRef<HTMLDivElement>(null);
10
+ const instance = useRef<NotFoundInstance | null>(null);
11
+ const current = useLatest(onBack);
12
+ const hasBack = Boolean(onBack);
13
+ useIsomorphicLayoutEffect(() => {
14
+ const created = createNotFound(root.current, { brand, title, description, homeHref, homeLabel, backLabel, showBack, onBack: hasBack ? () => current.current?.() : undefined });
15
+ instance.current = created;
16
+ return () => { created.destroy(); instance.current = null; };
17
+ }, [brand, title, description, homeHref, homeLabel, backLabel, showBack, hasBack]);
18
+ useIsomorphicLayoutEffect(() => {
19
+ if (typeof instanceRef === 'function') instanceRef(instance.current); else if (instanceRef) instanceRef.current = instance.current;
20
+ return () => { if (typeof instanceRef === 'function') instanceRef(null); else if (instanceRef) instanceRef.current = null; };
21
+ }, [instanceRef, brand, title, description, homeHref, homeLabel, backLabel, showBack, hasBack]);
22
+ return <div {...safe} ref={root} className={['mk-not-found', className].filter(Boolean).join(' ')} />;
23
+ }
@@ -0,0 +1,24 @@
1
+ # Notification stack
2
+
3
+ A local notification deck with overlapping tonal cards, explicit expand/collapse, actions, dismissal and pausable durations. It is independent of the global `toast` store, so several decks can coexist.
4
+
5
+ ```ts
6
+ import { createNotificationStack } from '@yewdiel21/motion-kit';
7
+ const stack = createNotificationStack(document.querySelector('#notifications'), {
8
+ items: [{ id: 'saved', title: 'Cambios guardados', tone: 'success', duration: 0 }],
9
+ });
10
+ stack.push({ id: 'review', title: 'Versión lista', duration: 6000, action: { label: 'Revisar', onClick: openReview } });
11
+ stack.expand();
12
+ // On unmount:
13
+ stack.destroy();
14
+ ```
15
+
16
+ Exports: `createNotificationStack`, `NotificationStackItem`, `NotificationStackOptions`, `NotificationStackInstance`; React `NotificationStack`, `NotificationStackProps`; elements `MkNotificationStackElement`, `defineNotificationStack` (`mk-notification-stack`). CSS: `notification-stack.css` plus the kit tokens/theme/fonts.
17
+
18
+ Options: `items`, `expanded`, `direction` (`down` default, `up` or `side`), `duration` (milliseconds, default 6000, zero persists), `disabled`, `label`, `labels` (`expand`, `collapse`, `dismiss`, `empty`), `onDismiss(id)`, `onAction(id)`, `onExpandedChange(expanded)`. Items have `id`, `title`, optional `description`, `tone`, `duration`, `dismissible`, `action: {label,onClick?}`. IDs are unique; pushing an existing ID is ignored. `items` lists oldest first. `setOptions({items})` replaces data while preserving the remaining lifetime of matching IDs.
19
+
20
+ `direction: 'up'` rests the cards on the deck's floor and grows above the front card (anchor its container to the bottom, for example with `flex-direction: column-reverse`); `'side'` peeks at the right edge and opens into a strip that scrolls sideways. A click on a closed deck opens it, and hovering spreads its peeks to invite that click; the toggle remains the keyboard path. Opening fans the cards out one by one as their blur clears.
21
+
22
+ Instance: `push(item)`, `dismiss(id)`, `clear()`, `expand()`, `collapse()`, `toggle()`, `setOptions(partial)`, readonly `items`/`expanded`, `destroy()`. Events: `mk:dismiss` `{id}` and `mk:change` `{expanded}`. Hover, focus, document visibility and disabled state pause timers. Covered cards are inert until expanded. Dismissal moves focus to a surviving control or the region. Reduced motion applies the same state immediately.
23
+
24
+ React owns an empty host; pass `items`, never children or HTML. `instanceRef` exposes the engine for `push`. An element accepts JSON `items`/`labels` and the other scalar options as attributes; `push`, `items` and `setOptions` also work as properties/methods. Attribute updates retain the live engine.
@@ -0,0 +1,26 @@
1
+ import { MkElement, defineElement, type OptionType } from '../../elements/base';
2
+ import { createNotificationStack, type NotificationStackInstance, type NotificationStackOptions, type NotificationStackItem } from './engine';
3
+
4
+ export class MkNotificationStackElement extends MkElement<NotificationStackInstance> {
5
+ static options: Record<string, OptionType> = {"items":"json","expanded":"boolean","direction":"string","duration":"number","disabled":"boolean","label":"string","labels":"json"};
6
+ protected create(options: Record<string, unknown>) { return createNotificationStack(this, options); }
7
+ setOptions(options: Partial<NotificationStackOptions>) { this.instance?.setOptions(options); }
8
+
9
+ push(item: NotificationStackItem) { this.instance?.push(item); }
10
+ dismiss(id: string) { this.instance?.dismiss(id); }
11
+ clear() { this.instance?.clear(); }
12
+ expand() { this.instance?.expand(); }
13
+ collapse() { this.instance?.collapse(); }
14
+ toggle() { this.instance?.toggle(); }
15
+ get expanded() { return this.instance?.expanded ?? false; }
16
+ set expanded(value: boolean) { this.instance?.setOptions({ expanded: value }); }
17
+ get items() { return [...(this.instance?.items ?? [])]; }
18
+ set items(value: NotificationStackItem[]) { this.instance?.setOptions({ items: value }); }
19
+ attributeChangedCallback(name: string, previous: string | null, next: string | null) {
20
+ if (previous === next || !this.isConnected || !this.instance) return;
21
+ const option = name.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
22
+ const defaults: Record<string, unknown> = {"items":[],"expanded":false,"direction":"down","duration":6000,"disabled":false,"label":"Notificaciones","labels":{}};
23
+ this.instance.setOptions({ [option]: this.options[option] ?? defaults[option] });
24
+ }
25
+ }
26
+ export const defineNotificationStack = () => defineElement('mk-notification-stack', MkNotificationStackElement);