@yewdiel21/motion-kit 0.14.0 → 0.16.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 (156) hide show
  1. package/README.md +178 -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-NFZIX3IA.js} +2 -2
  7. package/dist/chunk-RBEKR5UZ.js +3660 -0
  8. package/dist/chunk-RBEKR5UZ.js.map +1 -0
  9. package/dist/{chunk-CU6SXW3E.js → chunk-SDQ776LG.js} +1596 -485
  10. package/dist/chunk-SDQ776LG.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/chart.css +7 -0
  14. package/dist/css/components/data-table.css +121 -26
  15. package/dist/css/components/filter-builder.css +10 -15
  16. package/dist/css/components/heatmap-chart.css +398 -0
  17. package/dist/css/components/hover-card.css +8 -2
  18. package/dist/css/components/image-preview.css +90 -2
  19. package/dist/css/components/json-viewer.css +33 -1
  20. package/dist/css/components/nav.css +102 -102
  21. package/dist/css/components/orbit-menu.css +130 -0
  22. package/dist/css/components/pagination.css +10 -5
  23. package/dist/css/components/pie-chart.css +397 -0
  24. package/dist/css/components/pill-button.css +135 -0
  25. package/dist/css/components/radar-chart.css +377 -0
  26. package/dist/css/components/radial-chart.css +412 -0
  27. package/dist/css/components/sankey-chart.css +365 -0
  28. package/dist/css/components/scroll-island.css +114 -0
  29. package/dist/css/components/select.css +136 -0
  30. package/dist/css/components/toolbar.css +122 -0
  31. package/dist/css/components/tree-view.css +79 -0
  32. package/dist/css/components/voice-dots.css +70 -0
  33. package/dist/elements/index.d.ts +180 -7
  34. package/dist/elements/index.js +293 -7
  35. package/dist/elements/index.js.map +1 -1
  36. package/dist/{engine-Cn6IhE3z.d.ts → engine-5zRIssTO.d.ts} +11 -1
  37. package/dist/index.css +1849 -168
  38. package/dist/index.d.ts +4 -4
  39. package/dist/index.js +33 -3
  40. package/dist/index.js.map +1 -1
  41. package/dist/react/index.d.ts +117 -11
  42. package/dist/react/index.js +320 -13
  43. package/dist/react/index.js.map +1 -1
  44. package/dist/{reduced-BBUusld6.d.ts → reduced-DQwVXQYb.d.ts} +1 -1
  45. package/dist/{reveal-DEvp6U6T.d.ts → reveal-DRpNO4KP.d.ts} +109 -4
  46. package/dist/store-Da3zSh8Z.d.ts +457 -0
  47. package/package.json +9 -16
  48. package/registry.json +1096 -948
  49. package/src/auto/auto-init.ts +6 -0
  50. package/src/components/add-to-cart/README.md +49 -0
  51. package/src/components/add-to-cart/add-to-cart.css +71 -0
  52. package/src/components/add-to-cart/element.ts +20 -0
  53. package/src/components/add-to-cart/engine.ts +206 -0
  54. package/src/components/add-to-cart/react.tsx +33 -0
  55. package/src/components/bidirectional-slider/README.md +46 -0
  56. package/src/components/bidirectional-slider/bidirectional-slider.css +167 -0
  57. package/src/components/bidirectional-slider/element.ts +42 -0
  58. package/src/components/bidirectional-slider/engine.ts +289 -0
  59. package/src/components/bidirectional-slider/react.tsx +45 -0
  60. package/src/components/chart/README.md +49 -24
  61. package/src/components/chart/chart.css +7 -0
  62. package/src/components/chart/element.ts +4 -1
  63. package/src/components/chart/engine.ts +153 -34
  64. package/src/components/chart/parts.ts +237 -0
  65. package/src/components/chart/react.tsx +2 -2
  66. package/src/components/chart/use-chart.ts +55 -0
  67. package/src/components/data-table/README.md +3 -3
  68. package/src/components/data-table/data-table.css +121 -26
  69. package/src/components/data-table/engine.ts +81 -29
  70. package/src/components/editable-table/engine.ts +67 -11
  71. package/src/components/filter-builder/engine.ts +33 -2
  72. package/src/components/filter-builder/filter-builder.css +10 -15
  73. package/src/components/heatmap-chart/README.md +48 -0
  74. package/src/components/heatmap-chart/element.ts +21 -0
  75. package/src/components/heatmap-chart/engine.ts +281 -0
  76. package/src/components/heatmap-chart/heatmap-chart.css +93 -0
  77. package/src/components/heatmap-chart/react.tsx +14 -0
  78. package/src/components/hover-card/engine.ts +30 -1
  79. package/src/components/hover-card/hover-card.css +8 -2
  80. package/src/components/image-preview/element.ts +1 -1
  81. package/src/components/image-preview/engine.ts +95 -15
  82. package/src/components/image-preview/image-preview.css +90 -2
  83. package/src/components/image-preview/react.tsx +3 -3
  84. package/src/components/json-viewer/engine.ts +11 -2
  85. package/src/components/json-viewer/json-viewer.css +33 -1
  86. package/src/components/nav/README.md +12 -10
  87. package/src/components/nav/element.ts +5 -5
  88. package/src/components/nav/engine.ts +21 -14
  89. package/src/components/nav/nav.css +102 -102
  90. package/src/components/nav/react.tsx +4 -4
  91. package/src/components/orbit-menu/README.md +46 -0
  92. package/src/components/orbit-menu/element.ts +27 -0
  93. package/src/components/orbit-menu/engine.ts +179 -0
  94. package/src/components/orbit-menu/orbit-menu.css +129 -0
  95. package/src/components/orbit-menu/react.tsx +44 -0
  96. package/src/components/pagination/README.md +1 -1
  97. package/src/components/pagination/engine.ts +50 -4
  98. package/src/components/pagination/pagination.css +10 -5
  99. package/src/components/pie-chart/README.md +48 -0
  100. package/src/components/pie-chart/element.ts +30 -0
  101. package/src/components/pie-chart/engine.ts +283 -0
  102. package/src/components/pie-chart/pie-chart.css +92 -0
  103. package/src/components/pie-chart/react.tsx +14 -0
  104. package/src/components/pill-button/README.md +46 -0
  105. package/src/components/pill-button/element.ts +16 -0
  106. package/src/components/pill-button/engine.ts +77 -0
  107. package/src/components/pill-button/pill-button.css +134 -0
  108. package/src/components/pill-button/react.tsx +32 -0
  109. package/src/components/radar-chart/README.md +48 -0
  110. package/src/components/radar-chart/element.ts +30 -0
  111. package/src/components/radar-chart/engine.ts +327 -0
  112. package/src/components/radar-chart/radar-chart.css +72 -0
  113. package/src/components/radar-chart/react.tsx +14 -0
  114. package/src/components/radial-chart/README.md +58 -0
  115. package/src/components/radial-chart/element.ts +30 -0
  116. package/src/components/radial-chart/engine.ts +317 -0
  117. package/src/components/radial-chart/radial-chart.css +107 -0
  118. package/src/components/radial-chart/react.tsx +14 -0
  119. package/src/components/sankey-chart/README.md +48 -0
  120. package/src/components/sankey-chart/element.ts +31 -0
  121. package/src/components/sankey-chart/engine.ts +401 -0
  122. package/src/components/sankey-chart/react.tsx +14 -0
  123. package/src/components/sankey-chart/sankey-chart.css +60 -0
  124. package/src/components/scroll-island/README.md +44 -0
  125. package/src/components/scroll-island/element.ts +16 -0
  126. package/src/components/scroll-island/engine.ts +274 -0
  127. package/src/components/scroll-island/react.tsx +26 -0
  128. package/src/components/scroll-island/scroll-island.css +113 -0
  129. package/src/components/select/README.md +34 -0
  130. package/src/components/select/element.ts +26 -0
  131. package/src/components/select/engine.ts +267 -0
  132. package/src/components/select/react.tsx +33 -0
  133. package/src/components/select/select.css +135 -0
  134. package/src/components/toolbar/README.md +46 -0
  135. package/src/components/toolbar/element.ts +30 -0
  136. package/src/components/toolbar/engine.ts +230 -0
  137. package/src/components/toolbar/react.tsx +34 -0
  138. package/src/components/toolbar/toolbar.css +121 -0
  139. package/src/components/tree-view/element.ts +1 -1
  140. package/src/components/tree-view/engine.ts +174 -0
  141. package/src/components/tree-view/react.tsx +7 -3
  142. package/src/components/tree-view/tree-view.css +79 -0
  143. package/src/components/voice-dots/README.md +48 -0
  144. package/src/components/voice-dots/element.ts +45 -0
  145. package/src/components/voice-dots/engine.ts +243 -0
  146. package/src/components/voice-dots/react.tsx +52 -0
  147. package/src/components/voice-dots/voice-dots.css +69 -0
  148. package/src/elements/index.ts +39 -0
  149. package/src/index.ts +16 -0
  150. package/src/motion/liquid.ts +14 -4
  151. package/src/react/index.ts +14 -1
  152. package/dist/chunk-6P4P7NU6.js +0 -1401
  153. package/dist/chunk-6P4P7NU6.js.map +0 -1
  154. package/dist/chunk-CU6SXW3E.js.map +0 -1
  155. package/dist/store-DjkTBX7f.d.ts +0 -195
  156. /package/dist/{chunk-EY4BJKC4.js.map → chunk-NFZIX3IA.js.map} +0 -0
@@ -0,0 +1,267 @@
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
+ import { createMenu } from '../menu/engine';
7
+
8
+ /*
9
+ * Select: one choice from a list, opened as the kit's menu (it grows out of its
10
+ * button and its items flow in) instead of the system's list, so it looks and
11
+ * moves like every other menu. It enhances an authored <select> (hidden, kept in
12
+ * step: forms post it and its `change` listeners keep working) or builds one from
13
+ * `options`. The chosen label slides in out of a blur, the chevron turns, typing
14
+ * a letter on the closed button picks the next option that starts with it, and
15
+ * ↓ ↑ Enter Space open the menu. Changes made to the select's options (by the app)
16
+ * show up on their own.
17
+ */
18
+
19
+ export interface SelectOption {
20
+ value: string;
21
+ label: string;
22
+ disabled?: boolean;
23
+ /** A heading the option sits under (an <optgroup>). */
24
+ group?: string;
25
+ }
26
+
27
+ export interface SelectOptions {
28
+ /** The choices; without them, the authored <select> gives them. */
29
+ options?: SelectOption[];
30
+ value?: string;
31
+ /** Form name of the select it builds (an authored one keeps its own). */
32
+ name?: string;
33
+ /** Accessible name; the button reads "<label>: <chosen>". */
34
+ label?: string;
35
+ /** Text while nothing is chosen (default: the empty option's text, else 'Elige…'). */
36
+ placeholder?: string;
37
+ disabled?: boolean;
38
+ onChange?: (value: string) => void;
39
+ }
40
+
41
+ export interface SelectInstance extends Instance {
42
+ setValue(value: string, notify?: boolean): void;
43
+ setOptions(options: SelectOption[]): void;
44
+ /** Read the select's options again (changes to them are also noticed on their own). */
45
+ refresh(): void;
46
+ open(): void;
47
+ close(): void;
48
+ readonly value: string;
49
+ }
50
+
51
+ const CHEVRON = '<svg aria-hidden="true" data-mk-select-chevron width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 10l5 5 5-5"/></svg>';
52
+ const CHECK = '<svg aria-hidden="true" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>';
53
+ const fold = (text: string) => text.normalize('NFD').replace(/[̀-ͯ]/g, '').toLocaleLowerCase();
54
+
55
+ /** Fill a select with options (groups become <optgroup>s, in order of first appearance). */
56
+ function fillSelect(select: HTMLSelectElement, list: SelectOption[]) {
57
+ const groups = new Map<string, HTMLOptGroupElement>();
58
+ const nodes: Array<HTMLOptionElement | HTMLOptGroupElement> = [];
59
+ for (const choice of list) {
60
+ const option = new Option(choice.label, choice.value);
61
+ option.disabled = Boolean(choice.disabled);
62
+ if (!choice.group) {
63
+ nodes.push(option);
64
+ continue;
65
+ }
66
+ let group = groups.get(choice.group);
67
+ if (!group) {
68
+ group = document.createElement('optgroup');
69
+ group.label = choice.group;
70
+ groups.set(choice.group, group);
71
+ nodes.push(group);
72
+ }
73
+ group.append(option);
74
+ }
75
+ select.replaceChildren(...nodes);
76
+ }
77
+
78
+ export function createSelect(root: HTMLElement | null, options: SelectOptions = {}): SelectInstance {
79
+ if (!root) return inert({ setValue() {}, setOptions() {}, refresh() {}, open() {}, close() {}, value: '' });
80
+ registerMotion();
81
+ const scope = createScope();
82
+ const made: Element[] = [];
83
+ scope.add(() => made.forEach((element) => element.remove()));
84
+
85
+ let native = root.querySelector<HTMLSelectElement>('select:not([multiple])');
86
+ if (!native) {
87
+ native = document.createElement('select');
88
+ if (options.name) native.name = options.name;
89
+ root.append(native);
90
+ made.push(native);
91
+ } else {
92
+ // An authored select gets its own options and value back.
93
+ const select = native;
94
+ const children = [...select.childNodes];
95
+ const value = select.value;
96
+ scope.add(() => {
97
+ if (select.childNodes.length !== children.length || children.some((node, i) => select.childNodes[i] !== node)) select.replaceChildren(...children);
98
+ select.value = value;
99
+ });
100
+ }
101
+ const select = native;
102
+ if (options.options) fillSelect(select, options.options);
103
+ if (options.value !== undefined) select.value = options.value;
104
+ scope.attr(select, 'hidden', '');
105
+ scope.attr(select, 'tabindex', '-1');
106
+ scope.attr(select, 'aria-hidden', 'true');
107
+
108
+ const trigger = document.createElement('button');
109
+ trigger.type = 'button';
110
+ trigger.dataset.mkSelectTrigger = '';
111
+ trigger.setAttribute('aria-haspopup', 'menu');
112
+ trigger.setAttribute('aria-expanded', 'false');
113
+ const valueText = document.createElement('span');
114
+ valueText.dataset.mkSelectValue = '';
115
+ trigger.append(valueText);
116
+ trigger.insertAdjacentHTML('beforeend', CHEVRON);
117
+ select.after(trigger);
118
+ made.push(trigger);
119
+
120
+ const dialog = document.createElement('dialog');
121
+ dialog.className = 'mk-dialog mk-menu mk-select-menu';
122
+ dialog.dataset.mkSelectMenu = '';
123
+ dialog.setAttribute('aria-label', options.label ?? 'Opciones');
124
+ root.append(dialog);
125
+ made.push(dialog);
126
+ scope.attr(root, 'data-mk-ready', '');
127
+
128
+ const effects = new Set<gsap.core.Animation>();
129
+ const keep = (animation: gsap.core.Animation | null) => {
130
+ if (!animation) return;
131
+ effects.add(animation);
132
+ // Finished ones leave as new ones arrive: the animation's own onComplete stays untouched.
133
+ for (const effect of effects) if (effect.totalProgress() === 1) effects.delete(effect);
134
+ };
135
+ scope.add(() => {
136
+ for (const effect of effects) effect.kill();
137
+ effects.clear();
138
+ });
139
+
140
+ const choices = () => [...select.options];
141
+ const buildItems = () => {
142
+ const nodes: HTMLElement[] = [];
143
+ const item = (option: HTMLOptionElement) => {
144
+ const button = document.createElement('button');
145
+ button.type = 'button';
146
+ button.dataset.mkItem = '';
147
+ button.dataset.mkValue = option.value;
148
+ const text = document.createElement('span');
149
+ text.textContent = option.text;
150
+ button.append(text);
151
+ button.insertAdjacentHTML('beforeend', CHECK);
152
+ if (option.disabled) button.setAttribute('aria-disabled', 'true');
153
+ return button;
154
+ };
155
+ for (const child of select.children) {
156
+ if (child instanceof HTMLOptGroupElement) {
157
+ const heading = document.createElement('p');
158
+ heading.dataset.mkSelectGroup = '';
159
+ heading.setAttribute('aria-hidden', 'true');
160
+ heading.textContent = child.label;
161
+ nodes.push(heading, ...[...child.children].filter((node): node is HTMLOptionElement => node instanceof HTMLOptionElement).map(item));
162
+ } else if (child instanceof HTMLOptionElement) nodes.push(item(child));
163
+ }
164
+ dialog.replaceChildren(...nodes);
165
+ };
166
+
167
+ const placeholderText = () => options.placeholder ?? choices().find((option) => option.value === '')?.text ?? 'Elige…';
168
+ /** The items are a choice: the menu marks them plain menuitems when it opens. */
169
+ const markItems = () => {
170
+ for (const item of dialog.querySelectorAll<HTMLElement>('[data-mk-item]')) {
171
+ item.setAttribute('role', 'menuitemradio');
172
+ item.setAttribute('aria-checked', String(item.dataset.mkValue === select.value));
173
+ }
174
+ };
175
+ const paint = () => {
176
+ const chosen = select.selectedOptions[0];
177
+ const empty = !chosen || chosen.value === '';
178
+ valueText.textContent = empty ? placeholderText() : chosen.text;
179
+ root.toggleAttribute('data-mk-placeholder', empty);
180
+ if (options.label) trigger.setAttribute('aria-label', `${options.label}: ${valueText.textContent}`);
181
+ trigger.disabled = Boolean(options.disabled) || select.disabled;
182
+ markItems();
183
+ };
184
+ scope.add(() => root.removeAttribute('data-mk-placeholder'));
185
+
186
+ const choose = (value: string, notify: boolean) => {
187
+ if (value === select.value) return;
188
+ const before = choices().findIndex((option) => option.value === select.value);
189
+ select.value = value;
190
+ if (select.value !== value) return;
191
+ const after = choices().findIndex((option) => option.value === value);
192
+ paint();
193
+ if (!reduced()) {
194
+ gsap.killTweensOf(valueText);
195
+ keep(gsap.fromTo(valueText, { y: after >= before ? 6 : -6, opacity: 0, filter: 'blur(4px)' }, { y: 0, opacity: 1, filter: 'blur(0px)', duration: 0.28, ease: EASE.pop, clearProps: 'transform,opacity,filter' }));
196
+ }
197
+ if (!notify) return;
198
+ select.dispatchEvent(new Event('input', { bubbles: true }));
199
+ select.dispatchEvent(new Event('change', { bubbles: true }));
200
+ options.onChange?.(value);
201
+ root.dispatchEvent(new CustomEvent('mk:change', { bubbles: true, detail: { value } }));
202
+ };
203
+
204
+ const menu = createMenu(dialog, {
205
+ triggers: false,
206
+ onOpen: markItems,
207
+ onClose: () => trigger.setAttribute('aria-expanded', 'false'),
208
+ onSelect: (value) => choose(value, true),
209
+ });
210
+ scope.add(() => menu.destroy());
211
+ // The menu's own selection stays inside: listeners on the select hear its change.
212
+ scope.on(dialog, 'mk:select', (event) => event.stopPropagation());
213
+
214
+ const open = () => {
215
+ if (trigger.disabled || menu.isOpen) return;
216
+ trigger.setAttribute('aria-expanded', 'true');
217
+ dialog.style.setProperty('--mk-select-width', `${trigger.offsetWidth}px`);
218
+ menu.open(trigger);
219
+ };
220
+ scope.on(trigger, 'click', () => {
221
+ if (!reduced()) keep(press(trigger));
222
+ open();
223
+ });
224
+ scope.on(trigger, 'keydown', (event) => {
225
+ const key = event as KeyboardEvent;
226
+ if (key.ctrlKey || key.metaKey || key.altKey) return;
227
+ if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(key.key)) {
228
+ key.preventDefault();
229
+ open();
230
+ } else if (key.key.length === 1 && key.key.trim()) {
231
+ // Type to pick: the next option (after the chosen one) whose label starts with the letter.
232
+ const letter = fold(key.key);
233
+ const list = choices();
234
+ const from = list.findIndex((option) => option.value === select.value);
235
+ for (let step = 1; step <= list.length; step++) {
236
+ const option = list[(from + step) % list.length]!;
237
+ if (!option.disabled && option.value !== '' && fold(option.text).startsWith(letter)) {
238
+ choose(option.value, true);
239
+ break;
240
+ }
241
+ }
242
+ }
243
+ });
244
+
245
+ const refresh = () => {
246
+ buildItems();
247
+ paint();
248
+ };
249
+ // The app may change the select's options (or its disabled state) itself.
250
+ if (typeof MutationObserver === 'function') scope.observe(new MutationObserver(() => !scope.disposed && refresh())).observe(select, { childList: true, subtree: true, attributes: true, attributeFilter: ['disabled', 'label'] });
251
+ refresh();
252
+
253
+ return {
254
+ setValue: (value, notify = true) => choose(value, notify),
255
+ setOptions(next) {
256
+ fillSelect(select, next);
257
+ refresh();
258
+ },
259
+ refresh,
260
+ open,
261
+ close: () => menu.close(),
262
+ get value() {
263
+ return select.value;
264
+ },
265
+ destroy: () => scope.dispose(),
266
+ };
267
+ }
@@ -0,0 +1,33 @@
1
+ 'use client';
2
+ import { useRef, type HTMLAttributes } from 'react';
3
+ import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
4
+ import { createSelect, type SelectInstance, type SelectOptions } from './engine';
5
+
6
+ export interface SelectProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange' | 'defaultValue'>, Omit<SelectOptions, 'value'> {
7
+ /** Controlled value; without it the select keeps its own (defaultValue). */
8
+ value?: string;
9
+ defaultValue?: string;
10
+ }
11
+
12
+ /** <Select label="Equipo" options={teams} value={team} onChange={setTeam} />. */
13
+ export function Select({ options, value, defaultValue, name, label, placeholder, disabled, onChange, className, ...props }: SelectProps) {
14
+ const ref = useRef<HTMLDivElement>(null);
15
+ const instance = useRef<SelectInstance | null>(null);
16
+ const change = useLatest(onChange);
17
+ const start = useRef(value ?? defaultValue);
18
+ const key = JSON.stringify(options ?? null);
19
+ useIsomorphicLayoutEffect(() => {
20
+ const created = createSelect(ref.current, { options, value: value ?? start.current, name, label, placeholder, disabled, onChange: (next) => change.current?.(next) });
21
+ instance.current = created;
22
+ return () => {
23
+ start.current = created.value;
24
+ created.destroy();
25
+ instance.current = null;
26
+ };
27
+ // eslint-disable-next-line react-hooks/exhaustive-deps
28
+ }, [key, name, label, placeholder, disabled]);
29
+ useIsomorphicLayoutEffect(() => {
30
+ if (value !== undefined && instance.current && instance.current.value !== value) instance.current.setValue(value, false);
31
+ }, [value]);
32
+ return <div {...props} ref={ref} className={['mk-select', className].filter(Boolean).join(' ')} />;
33
+ }
@@ -0,0 +1,135 @@
1
+ /* select — a 12 px field button (surface-3) with the chosen label and a chevron that turns, opening the kit's menu:
2
+ a 16 px surface-1 panel with 8 px items, group headings and a check on the chosen one. The label slides in out of a
3
+ blur when it changes. The <select> it keeps in step is hidden. */
4
+ .mk-select,
5
+ mk-select {
6
+ display: inline-flex;
7
+ min-width: 0;
8
+ max-width: 100%;
9
+ vertical-align: middle;
10
+ }
11
+
12
+ :is(.mk-select, mk-select) > select[hidden] {
13
+ display: none;
14
+ }
15
+
16
+ [data-mk-select-trigger] {
17
+ display: inline-flex;
18
+ flex: 1;
19
+ align-items: center;
20
+ justify-content: space-between;
21
+ gap: 0.5rem;
22
+ min-width: 0;
23
+ height: 2.5rem;
24
+ padding: 0 0.55rem 0 0.8rem;
25
+ border: 0;
26
+ border-radius: 12px;
27
+ background: var(--mk-surface-3);
28
+ color: var(--mk-text);
29
+ font: inherit;
30
+ font-size: 0.875rem;
31
+ text-align: start;
32
+ cursor: pointer;
33
+ transition:
34
+ background-color 0.15s,
35
+ box-shadow 0.2s;
36
+ }
37
+
38
+ [data-mk-select-trigger]:hover:not(:disabled),
39
+ [data-mk-select-trigger][aria-expanded='true'] {
40
+ background: color-mix(in srgb, var(--mk-surface-3) 80%, var(--mk-text) 8%);
41
+ }
42
+
43
+ [data-mk-select-trigger]:focus-visible {
44
+ outline: none;
45
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--mk-accent) 35%, transparent);
46
+ }
47
+
48
+ [data-mk-select-trigger]:disabled {
49
+ opacity: 0.45;
50
+ cursor: default;
51
+ }
52
+
53
+ [data-mk-select-value] {
54
+ flex: 1;
55
+ min-width: 0;
56
+ overflow: hidden;
57
+ text-overflow: ellipsis;
58
+ white-space: nowrap;
59
+ }
60
+
61
+ :is(.mk-select, mk-select)[data-mk-placeholder] [data-mk-select-value] {
62
+ color: var(--mk-muted);
63
+ }
64
+
65
+ [data-mk-select-chevron] {
66
+ flex: none;
67
+ color: var(--mk-muted);
68
+ transition: transform 0.3s cubic-bezier(0.38, 0.49, 0, 1.5);
69
+ }
70
+
71
+ [data-mk-select-trigger][aria-expanded='true'] [data-mk-select-chevron] {
72
+ transform: rotate(180deg);
73
+ }
74
+
75
+ /* The menu: like the panels of the data table (surface-1, a hairline, 8 px items). */
76
+ .mk-dialog.mk-select-menu {
77
+ --mk-dialog-bg: var(--mk-surface-1);
78
+ --mk-dialog-radius: 16px;
79
+ --mk-dialog-shadow: 0 0 0 1px var(--mk-outline-soft), 0 16px 48px -12px color-mix(in srgb, var(--mk-shadow-color, #000) 35%, transparent);
80
+ gap: 0.125rem;
81
+ min-width: max(10rem, var(--mk-select-width, 0px));
82
+ max-height: min(20rem, 60dvh);
83
+ padding: 0.375rem;
84
+ overflow-y: auto;
85
+ -webkit-backdrop-filter: none;
86
+ backdrop-filter: none;
87
+ }
88
+
89
+ .mk-dialog.mk-select-menu [data-mk-item] {
90
+ flex: none;
91
+ gap: 0.625rem;
92
+ min-height: 2.25rem;
93
+ padding: 0 0.625rem;
94
+ border-radius: 8px;
95
+ font-family: inherit;
96
+ font-size: 0.875rem;
97
+ font-weight: 500;
98
+ letter-spacing: normal;
99
+ }
100
+
101
+ .mk-dialog.mk-select-menu [data-mk-item]:hover,
102
+ .mk-dialog.mk-select-menu [data-mk-item]:focus-visible {
103
+ background: var(--mk-surface-2);
104
+ }
105
+
106
+ .mk-dialog.mk-select-menu [data-mk-item] svg {
107
+ flex: none;
108
+ margin-left: auto;
109
+ opacity: 0;
110
+ }
111
+
112
+ .mk-dialog.mk-select-menu [data-mk-item][aria-checked='true'] {
113
+ font-weight: 600;
114
+ }
115
+
116
+ .mk-dialog.mk-select-menu [data-mk-item][aria-checked='true'] svg {
117
+ opacity: 1;
118
+ }
119
+
120
+ [data-mk-select-group] {
121
+ flex: none;
122
+ margin: 0.375rem 0.625rem 0.125rem;
123
+ color: var(--mk-muted);
124
+ font-size: 0.6875rem;
125
+ font-weight: 700;
126
+ letter-spacing: 0.04em;
127
+ text-transform: uppercase;
128
+ }
129
+
130
+ @media (prefers-reduced-motion: reduce) {
131
+ [data-mk-select-trigger],
132
+ [data-mk-select-chevron] {
133
+ transition: none;
134
+ }
135
+ }
@@ -0,0 +1,46 @@
1
+ # toolbar
2
+
3
+ Barra de herramientas con selector elástico tipo slime, etiquetas flotantes y navegación con flechas.
4
+
5
+ ## Instalación
6
+
7
+ ```tsx
8
+ import '@yewdiel21/motion-kit/css';
9
+ import { Toolbar } from '@yewdiel21/motion-kit/react';
10
+ ```
11
+
12
+ ```sh
13
+ npx @yewdiel21/motion-kit add toolbar --react --elements
14
+ ```
15
+
16
+ ## React
17
+
18
+ ```tsx
19
+ <Toolbar value={tool} onChange={setTool} aria-label="Herramientas"><button data-mk-tool="draw" aria-label="Dibujar">✎</button><button data-mk-tool="erase" aria-label="Borrar">⌫</button></Toolbar>
20
+ ```
21
+
22
+ Los callbacks del ejemplo (`ask`, `run`, `setTool`, `setVolume`) pertenecen a tu aplicación. React acepta `className`, `style` y atributos HTML. Los componentes con estado exponen `instanceRef` (excepto PillButton).
23
+
24
+ ## HTML / Astro
25
+
26
+ ```html
27
+ <mk-toolbar aria-label="Herramientas" value="draw"><button data-mk-tool="draw" aria-label="Dibujar">✎</button><button data-mk-tool="erase" aria-label="Borrar">⌫</button></mk-toolbar>
28
+ <script type="module">
29
+ import { defineElements } from '@yewdiel21/motion-kit/elements';
30
+ defineElements(['mk-toolbar']);
31
+ </script>
32
+ ```
33
+
34
+ En Astro usa un `<script>` procesado por Astro. En HTML directo el import requiere bundler o import map. También puedes usar `import { createToolbar } from '@yewdiel21/motion-kit'` y llamar a la factory con el contenedor y las mismas opciones. Al desmontar, llama a `instance.destroy()`.
35
+
36
+ ## Opciones
37
+
38
+ value: data-mk-tool elegido. orientation: vertical (por defecto) u horizontal. tips: etiquetas (true). onChange(value): selección del usuario. Instancia: setValue(value, notify?), value.
39
+
40
+ Los nombres camelCase se escriben como atributos kebab-case. Los arrays y objetos se pasan como JSON. Los booleanos aceptan atributo vacío o `"false"`.
41
+
42
+ ## Estilos y accesibilidad
43
+
44
+ Variables: --mk-toolbar-tool, --mk-toolbar-rail, --mk-toolbar-blob, --mk-toolbar-on-rail, --mk-toolbar-on-blob.
45
+
46
+ Respeta los colores del tema, foco visible y `prefers-reduced-motion`. Los gráficos incluyen una tabla accesible y selección con flechas, Home, End y Escape. Heatmap usa arriba/abajo para un día e izquierda/derecha para una semana. Toolbar usa flechas y Home/End; OrbitMenu cierra con Escape y devuelve el foco. Los controles conservan nombres accesibles; VoiceDots anuncia los estados sin leer cada fotograma.
@@ -0,0 +1,30 @@
1
+ import { MkElement, defineElement, type OptionType } from '../../elements/base';
2
+ import { createToolbar, type ToolbarInstance } from './engine';
3
+
4
+ /**
5
+ * <mk-toolbar class="mk-toolbar" aria-label="Herramientas" value="lapiz">
6
+ * <button data-mk-tool="lapiz" aria-label="Lápiz">…svg…</button> …
7
+ * </mk-toolbar>
8
+ * The chosen tool's blob stretches like slime to the next one. Emits mk:change ({value}).
9
+ */
10
+ export class MkToolbarElement extends MkElement<ToolbarInstance> {
11
+ static options: Record<string, OptionType> = { value: 'string', orientation: 'string', tips: 'boolean' };
12
+
13
+ protected create(options: Record<string, unknown>) {
14
+ return createToolbar(this, options);
15
+ }
16
+
17
+ get value() {
18
+ return this.instance?.value ?? '';
19
+ }
20
+
21
+ set value(next: string) { this.instance?.setValue(next); }
22
+ setValue(value: string, notify = false) { this.instance?.setValue(value, notify); }
23
+
24
+ attributeChangedCallback(name: string, previous: string | null, next: string | null) {
25
+ if (name === 'value' && this.instance) { if (next !== null) this.instance.setValue(next); return; }
26
+ super.attributeChangedCallback(name, previous, next);
27
+ }
28
+ }
29
+
30
+ export const defineToolbar = () => defineElement('mk-toolbar', MkToolbarElement);