@yewdiel21/motion-kit 0.16.0 → 0.17.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 (60) hide show
  1. package/README.md +21 -0
  2. package/dist/auto/index.d.ts +8 -3
  3. package/dist/auto/index.js +13 -3
  4. package/dist/auto/index.js.map +1 -1
  5. package/dist/{chunk-SDQ776LG.js → chunk-7IQTMF7Y.js} +1414 -38
  6. package/dist/chunk-7IQTMF7Y.js.map +1 -0
  7. package/dist/{chunk-NFZIX3IA.js → chunk-BZLMCHUD.js} +2 -2
  8. package/dist/{chunk-RBEKR5UZ.js → chunk-VJFPTLAK.js} +2 -2
  9. package/dist/css/components/drilldown-menu.css +130 -0
  10. package/dist/css/components/signature-pad.css +104 -0
  11. package/dist/css/components/voice-message-bubble.css +50 -0
  12. package/dist/css/components/waveform-player.css +36 -0
  13. package/dist/css/components/wheel-picker.css +17 -0
  14. package/dist/elements/index.d.ts +90 -4
  15. package/dist/elements/index.js +281 -4
  16. package/dist/elements/index.js.map +1 -1
  17. package/dist/{reveal-DRpNO4KP.d.ts → engine-Bn6hC87X.d.ts} +194 -1
  18. package/dist/index.css +305 -0
  19. package/dist/index.d.ts +3 -3
  20. package/dist/index.js +13 -3
  21. package/dist/index.js.map +1 -1
  22. package/dist/react/index.d.ts +45 -7
  23. package/dist/react/index.js +227 -3
  24. package/dist/react/index.js.map +1 -1
  25. package/dist/{reduced-DQwVXQYb.d.ts → reduced-YS6TZjpa.d.ts} +1 -1
  26. package/dist/{store-Da3zSh8Z.d.ts → store-CZ6vdtIE.d.ts} +1 -1
  27. package/package.json +110 -110
  28. package/registry.json +64 -0
  29. package/src/auto/auto-init.ts +308 -293
  30. package/src/components/drilldown-menu/README.md +163 -0
  31. package/src/components/drilldown-menu/drilldown-menu.css +129 -0
  32. package/src/components/drilldown-menu/element.ts +59 -0
  33. package/src/components/drilldown-menu/engine.ts +338 -0
  34. package/src/components/drilldown-menu/react.tsx +76 -0
  35. package/src/components/signature-pad/README.md +97 -0
  36. package/src/components/signature-pad/element.ts +57 -0
  37. package/src/components/signature-pad/engine.ts +437 -0
  38. package/src/components/signature-pad/react.tsx +56 -0
  39. package/src/components/signature-pad/signature-pad.css +103 -0
  40. package/src/components/voice-message-bubble/README.md +31 -0
  41. package/src/components/voice-message-bubble/element.ts +20 -0
  42. package/src/components/voice-message-bubble/engine.ts +68 -0
  43. package/src/components/voice-message-bubble/react.tsx +27 -0
  44. package/src/components/voice-message-bubble/voice-message-bubble.css +12 -0
  45. package/src/components/waveform-player/README.md +30 -0
  46. package/src/components/waveform-player/element.ts +21 -0
  47. package/src/components/waveform-player/engine.ts +261 -0
  48. package/src/components/waveform-player/react.tsx +27 -0
  49. package/src/components/waveform-player/waveform-player.css +35 -0
  50. package/src/components/wheel-picker/README.md +33 -0
  51. package/src/components/wheel-picker/element.ts +18 -0
  52. package/src/components/wheel-picker/engine.ts +210 -0
  53. package/src/components/wheel-picker/react.tsx +32 -0
  54. package/src/components/wheel-picker/wheel-picker.css +16 -0
  55. package/src/elements/index.ts +273 -258
  56. package/src/index.ts +295 -290
  57. package/src/react/index.ts +176 -171
  58. package/dist/chunk-SDQ776LG.js.map +0 -1
  59. /package/dist/{chunk-NFZIX3IA.js.map → chunk-BZLMCHUD.js.map} +0 -0
  60. /package/dist/{chunk-RBEKR5UZ.js.map → chunk-VJFPTLAK.js.map} +0 -0
@@ -0,0 +1,163 @@
1
+ # Drilldown Menu
2
+
3
+ A hierarchical action menu that shows one level at a time. Opening a branch slides the new level in from the right through a 6 px blur; going back reverses the direction. Motion uses the kit's signature curve over 360 ms. The engine keeps one live menu, cancels interrupted motion, and honors reduced motion.
4
+
5
+ ## Vanilla
6
+
7
+ ```html
8
+ <div id="actions" class="mk-drilldown-menu"></div>
9
+ ```
10
+
11
+ ```ts
12
+ import { createDrilldownMenu, type DrilldownMenuItem } from '@yewdiel21/motion-kit';
13
+ import '@yewdiel21/motion-kit/css';
14
+
15
+ const items: DrilldownMenuItem[] = [
16
+ {
17
+ id: 'project', label: 'Project', description: 'Manage your workspace',
18
+ children: [{
19
+ id: 'share', label: 'Share',
20
+ children: [
21
+ { id: 'link', label: 'Copy link', description: 'Anyone with the link can view' },
22
+ { id: 'invite', label: 'Invite people', disabled: true },
23
+ ],
24
+ }],
25
+ },
26
+ { id: 'help', label: 'Help' },
27
+ ];
28
+
29
+ const menu = createDrilldownMenu(document.querySelector('#actions'), {
30
+ items,
31
+ label: 'Workspace actions',
32
+ onSelect: (item, path) => console.log(item.label, path),
33
+ onPathChange: (path) => console.log('Level', path),
34
+ });
35
+
36
+ menu.open('project');
37
+ menu.open('share');
38
+ menu.back();
39
+ menu.root(); // reset() also returns to the root
40
+ menu.setPath(['project', 'share']);
41
+ menu.setItems(items);
42
+ menu.update({ label: 'Quick actions', emptyText: 'No available actions' });
43
+ // When the component is removed:
44
+ menu.destroy();
45
+ ```
46
+
47
+ Use `@yewdiel21/motion-kit/css/components/drilldown-menu` for just this component's styles; theme tokens must already be present. The root needs `mk-drilldown-menu`; the custom element receives the same styles directly.
48
+
49
+ ## React
50
+
51
+ ```tsx
52
+ import { useState } from 'react';
53
+ import { DrilldownMenu } from '@yewdiel21/motion-kit/react';
54
+ import type { DrilldownMenuItem } from '@yewdiel21/motion-kit';
55
+
56
+ function Actions({ items }: { items: DrilldownMenuItem[] }) {
57
+ const [path, setPath] = useState<string[]>([]);
58
+ const [selection, setSelection] = useState('Choose an action');
59
+ return <>
60
+ <DrilldownMenu
61
+ items={items}
62
+ label="Workspace actions"
63
+ path={path}
64
+ onPathChange={setPath}
65
+ onSelect={(item) => setSelection(item.label)}
66
+ />
67
+ <p role="status">{selection}</p>
68
+ </>;
69
+ }
70
+ ```
71
+
72
+ Omit `path` for internal navigation; use `defaultPath` to choose its initial level. Passing `path` makes navigation controlled: `onPathChange` requests the next branch path, and the parent accepts it by updating the prop. A parent that keeps the previous path declines the request. Invalid paths are safely truncated to their valid prefix. Updating `items` retains a valid path and focused action; removing or disabling an ancestor returns to the nearest valid parent and reports the repaired path.
73
+
74
+ `DrilldownMenuProps` also accepts normal `<div>` attributes, excluding children and the native `onSelect`. A forwarded `DrilldownMenuHandle` exposes `open`, `back`, `root`, `reset`, `setPath`, and `focusItem`. In controlled mode the `path` prop remains authoritative over ref-driven navigation.
75
+
76
+ ## Custom element
77
+
78
+ ```ts
79
+ import { defineDrilldownMenu, type MkDrilldownMenuElement } from '@yewdiel21/motion-kit/elements';
80
+ defineDrilldownMenu(); // safe to call more than once
81
+
82
+ const element = document.querySelector<MkDrilldownMenuElement>('mk-drilldown-menu')!;
83
+ element.items = items; // may also be assigned before connection
84
+ element.path = ['project'];
85
+ element.addEventListener('mk:select', (event) => {
86
+ console.log((event as CustomEvent).detail); // { item, path }
87
+ });
88
+ ```
89
+
90
+ ```html
91
+ <mk-drilldown-menu
92
+ label="Workspace actions"
93
+ back-label="Previous level"
94
+ empty-text="No available actions"
95
+ items='[{"id":"project","label":"Project","children":[{"id":"share","label":"Share","children":[{"id":"link","label":"Copy link"}]}]}]'
96
+ path='["project"]'
97
+ ></mk-drilldown-menu>
98
+ ```
99
+
100
+ `items` and `path` attributes accept JSON arrays and update the live instance; malformed JSON leaves the last valid value intact. `items`/`path` properties and `setItems`/`setPath` methods accept arrays directly. `open`, `back`, `root`, `reset`, and `focusItem` forward to the live engine. The `instance` property is null while disconnected. Reconnecting creates a fresh instance from the current attributes/properties. The element uses light DOM, so inherited themes apply.
101
+
102
+ ## Data and API
103
+
104
+ ```ts
105
+ interface DrilldownMenuItem {
106
+ id: string;
107
+ label: string;
108
+ description?: string;
109
+ disabled?: boolean;
110
+ children?: DrilldownMenuItem[];
111
+ }
112
+ ```
113
+
114
+ IDs must be unique among siblings. The same ID may appear in unrelated levels. A `children` array marks a branch; `children: []` opens an empty level. An item without children is an action. Labels/descriptions are inserted as text; they never execute HTML. Input data and returned item arrays are copied, so the engine does not mutate application data. Supply finite, acyclic data.
115
+
116
+ | Option | Default | Meaning |
117
+ | --- | --- | --- |
118
+ | `items` | `[]` | Root actions and branches |
119
+ | `path` | `[]` | Initial branch IDs (engine); controlled branch IDs (React) |
120
+ | `label` | `Actions` | Root heading and accessible menu name |
121
+ | `backLabel` | `Back` | Accessible name of the back button |
122
+ | `emptyText` | `No actions available` | Empty-level status |
123
+ | `onSelect(item, path)` | — | Leaf action and full ID path, including the leaf |
124
+ | `onPathChange(path)` | — | User/programmatic navigation or a repaired data path |
125
+
126
+ `DrilldownMenuInstance` exposes:
127
+
128
+ | Member | Behavior |
129
+ | --- | --- |
130
+ | `open(id): boolean` | Opens a current-level enabled branch and focuses its first action |
131
+ | `back(): boolean` | Returns one level and restores focus to its parent branch |
132
+ | `root()` / `reset()` | Returns to the root; keeps focus inside if it was inside |
133
+ | `setPath(path, notify = false)` | Navigates to a valid prefix; optional path callback/event |
134
+ | `setItems(items)` | Replaces root data, repairs invalid paths and preserves focus |
135
+ | `update(options)` | Updates data, path, text and callbacks together |
136
+ | `focusItem(id)` | Focuses an enabled current-level item |
137
+ | `path` | Copy of the current branch ID path |
138
+ | `items` | Copy of the current level's items |
139
+ | `element` | Original root element |
140
+ | `destroy()` | Cancels motion, removes listeners, restores original child nodes/attributes; idempotent |
141
+
142
+ `open`, `back`, `root`, and `reset` notify on a changed path. Explicit `setPath` and `update({ path })` are silent by default. `setItems` reports a path repaired by removed/disabled/non-branch ancestors. `mk:path-change` bubbles with `{ path }`; `mk:select` bubbles with `{ item, path }`. Callbacks receive the same information. Choosing an action leaves the menu at its current level.
143
+
144
+ ## Keyboard and accessibility
145
+
146
+ The current level has `role="menu"`, named by its visible header. Each action is a button with `role="menuitem"`; its label provides its accessible name and its description is referenced separately. Branches expose `aria-haspopup="menu"`. Disabled actions use both native `disabled` and `aria-disabled` and are skipped by focus navigation. Header/back and ancestor breadcrumbs sit outside the menu. The current breadcrumb uses `aria-current="location"`.
147
+
148
+ | Key | Action |
149
+ | --- | --- |
150
+ | Up / Down | Previous / next enabled item, wrapping |
151
+ | Home / End | First / last enabled item |
152
+ | Enter / Space | Open branch or choose leaf action |
153
+ | Right | Open a focused branch |
154
+ | Left / Escape | Return one level and focus the branch you came from |
155
+ | Tab / Shift+Tab | Normal page navigation through header/crumb controls and one roving menu stop |
156
+
157
+ At the root, Left/Escape are left available to a containing surface. Empty and all-disabled levels keep the menu itself focusable so the back keys work. The initial render does not take focus; opening a branch intentionally focuses the new level. Updates preserve focus only when focus was already inside. Reduced-motion users receive immediate navigation without slide, blur or animated height.
158
+
159
+ ## Styling
160
+
161
+ The component inherits `--mk-text`, `--mk-muted`, `--mk-surface-1`, `--mk-surface-3`, `--mk-outline-soft`, and `--mk-accent`. Override `--mk-drilldown-radius` (28 px) and `--mk-drilldown-item-radius` (18 px) for corner shapes. Width is fluid up to 26 rem; override `max-width` on the root for wider panels. Labels wrap, breadcrumbs wrap, and descriptions remain readable in narrow surfaces.
162
+
163
+ This is an inline hierarchical action menu. Place it in an existing dialog/popover if you need an anchored or dismissible surface. It does not route to URLs, fetch children, virtualize data, or provide typeahead.
@@ -0,0 +1,129 @@
1
+ /* Drilldown menu: a single live level travels in the navigation direction. */
2
+ .mk-drilldown-menu,
3
+ mk-drilldown-menu {
4
+ --mk-drilldown-radius: 28px;
5
+ --mk-drilldown-item-radius: 18px;
6
+ display: block;
7
+ box-sizing: border-box;
8
+ width: 100%;
9
+ max-width: 26rem;
10
+ min-width: 0;
11
+ padding: 10px;
12
+ border: 1px solid var(--mk-outline-soft);
13
+ border-radius: var(--mk-drilldown-radius);
14
+ background: var(--mk-surface-1);
15
+ color: var(--mk-text);
16
+ font-family: var(--md-sys-typescale-body-large-font, inherit);
17
+ box-shadow: 0 12px 36px -20px rgb(0 0 0 / 0.25);
18
+ }
19
+
20
+ [data-mk-drilldown-header] {
21
+ display: flex;
22
+ align-items: center;
23
+ gap: 8px;
24
+ min-height: 48px;
25
+ padding: 4px 10px;
26
+ }
27
+
28
+ [data-mk-drilldown-heading] {
29
+ min-width: 0;
30
+ overflow-wrap: anywhere;
31
+ font-size: 1.1rem;
32
+ font-weight: 650;
33
+ letter-spacing: -0.025em;
34
+ }
35
+
36
+ [data-mk-drilldown-back] {
37
+ display: inline-flex;
38
+ flex: none;
39
+ align-items: center;
40
+ justify-content: center;
41
+ width: 36px;
42
+ height: 36px;
43
+ padding: 0;
44
+ border: 0;
45
+ border-radius: 50%;
46
+ background: var(--mk-surface-3);
47
+ color: var(--mk-text);
48
+ cursor: pointer;
49
+ }
50
+
51
+ [data-mk-drilldown-back] > svg { transform: rotate(180deg); }
52
+ [data-mk-drilldown-back][hidden],
53
+ [data-mk-drilldown-empty][hidden] { display: none; }
54
+
55
+ [data-mk-drilldown-breadcrumb] ol {
56
+ display: flex;
57
+ flex-wrap: wrap;
58
+ align-items: center;
59
+ gap: 4px;
60
+ margin: 0;
61
+ padding: 0 12px 12px;
62
+ list-style: none;
63
+ color: var(--mk-muted);
64
+ font-size: 0.75rem;
65
+ line-height: 1.5;
66
+ }
67
+
68
+ [data-mk-drilldown-breadcrumb] li {
69
+ display: inline-flex;
70
+ align-items: center;
71
+ gap: 4px;
72
+ min-width: 0;
73
+ overflow-wrap: anywhere;
74
+ }
75
+
76
+ [data-mk-drilldown-breadcrumb] li + li::before { content: '/'; opacity: 0.5; }
77
+ [data-mk-drilldown-breadcrumb] [aria-current] { color: var(--mk-text); }
78
+ [data-mk-drilldown-crumb] {
79
+ padding: 2px;
80
+ border: 0;
81
+ border-radius: 4px;
82
+ background: transparent;
83
+ color: inherit;
84
+ font: inherit;
85
+ cursor: pointer;
86
+ }
87
+
88
+ [data-mk-drilldown-viewport] { overflow: hidden; border-radius: var(--mk-drilldown-item-radius); }
89
+ [data-mk-drilldown-level] { display: grid; gap: 3px; }
90
+ [data-mk-drilldown-item] {
91
+ display: flex;
92
+ align-items: center;
93
+ justify-content: space-between;
94
+ gap: 18px;
95
+ width: 100%;
96
+ min-height: 58px;
97
+ padding: 13px 15px;
98
+ border: 0;
99
+ border-radius: var(--mk-drilldown-item-radius);
100
+ background: transparent;
101
+ color: var(--mk-text);
102
+ font: inherit;
103
+ font-size: 0.9rem;
104
+ line-height: 1.4;
105
+ text-align: start;
106
+ cursor: pointer;
107
+ transition: background-color 0.16s ease;
108
+ }
109
+
110
+ [data-mk-drilldown-item]:enabled:hover,
111
+ [data-mk-drilldown-item]:focus-visible { background: var(--mk-surface-3); }
112
+ [data-mk-drilldown-item]:disabled { opacity: 0.42; cursor: default; }
113
+ [data-mk-drilldown-text] { display: grid; gap: 3px; min-width: 0; overflow-wrap: anywhere; }
114
+ [data-mk-drilldown-label] { font-weight: 550; }
115
+ [data-mk-drilldown-description] { color: var(--mk-muted); font-size: 0.8rem; }
116
+ [data-mk-drilldown-arrow] { display: inline-flex; flex: none; color: var(--mk-muted); }
117
+ [data-mk-drilldown-empty] { margin: 0; padding: 24px 16px; color: var(--mk-muted); font-size: 0.875rem; }
118
+
119
+ [data-mk-drilldown-item]:focus-visible,
120
+ [data-mk-drilldown-back]:focus-visible,
121
+ [data-mk-drilldown-crumb]:focus-visible,
122
+ [data-mk-drilldown-level]:focus-visible {
123
+ outline: 2px solid var(--mk-accent);
124
+ outline-offset: -2px;
125
+ }
126
+
127
+ @media (prefers-reduced-motion: reduce) {
128
+ [data-mk-drilldown-item] { transition: none; }
129
+ }
@@ -0,0 +1,59 @@
1
+ import { MkElement, defineElement, type OptionType } from '../../elements/base';
2
+ import { createDrilldownMenu, type DrilldownMenuInstance, type DrilldownMenuItem, type DrilldownMenuOptions } from './engine';
3
+
4
+ /** Light DOM action menu; items/path accept JSON. Emits mk:select and mk:path-change. */
5
+ export class MkDrilldownMenuElement extends MkElement<DrilldownMenuInstance> {
6
+ static options: Record<string, OptionType> = { items: 'json', path: 'json', label: 'string', backLabel: 'string', emptyText: 'string' };
7
+ private pendingItems: DrilldownMenuItem[] | undefined;
8
+ private pendingPath: string[] | undefined;
9
+
10
+ protected create(options: Record<string, unknown>) {
11
+ return createDrilldownMenu(this, {
12
+ ...options,
13
+ ...(this.pendingItems !== undefined ? { items: this.pendingItems } : {}),
14
+ ...(this.pendingPath !== undefined ? { path: this.pendingPath } : {}),
15
+ } as DrilldownMenuOptions);
16
+ }
17
+
18
+ attributeChangedCallback(name: string, previous: string | null, next: string | null) {
19
+ if (previous === next) return;
20
+ if (name === 'items' || name === 'path') {
21
+ try {
22
+ const value: unknown = JSON.parse(next ?? '[]');
23
+ if (!Array.isArray(value)) return;
24
+ if (name === 'items') {
25
+ this.pendingItems = value as DrilldownMenuItem[];
26
+ this.instance?.setItems(this.pendingItems);
27
+ } else {
28
+ this.pendingPath = value as string[];
29
+ this.instance?.setPath(this.pendingPath);
30
+ }
31
+ } catch {
32
+ // Invalid JSON leaves the last valid data and path intact.
33
+ }
34
+ } else if (this.instance) {
35
+ const { label, backLabel, emptyText } = this.options;
36
+ this.instance.update({ label, backLabel, emptyText } as DrilldownMenuOptions);
37
+ }
38
+ }
39
+
40
+ get items() { return this.pendingItems ?? (this.options.items as DrilldownMenuItem[] | undefined) ?? []; }
41
+ set items(items: DrilldownMenuItem[]) {
42
+ this.pendingItems = items;
43
+ this.instance?.setItems(items);
44
+ }
45
+ get path() { return this.instance?.path ?? this.pendingPath ?? (this.options.path as string[] | undefined) ?? []; }
46
+ set path(path: string[]) {
47
+ this.pendingPath = path;
48
+ this.instance?.setPath(path);
49
+ }
50
+ open(id: string) { return this.instance?.open(id) ?? false; }
51
+ back() { return this.instance?.back() ?? false; }
52
+ root() { this.instance?.root(); }
53
+ reset() { this.instance?.reset(); }
54
+ setItems(items: DrilldownMenuItem[]) { this.items = items; }
55
+ setPath(path: string[]) { this.path = path; }
56
+ focusItem(id: string) { this.instance?.focusItem(id); }
57
+ }
58
+
59
+ export const defineDrilldownMenu = () => defineElement('mk-drilldown-menu', MkDrilldownMenuElement);