@muten/shadcn 0.0.7 → 0.1.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 (205) hide show
  1. package/AGENTS.md +47 -0
  2. package/README.md +47 -0
  3. package/kit.css +2763 -0
  4. package/motion.css +31 -1
  5. package/nova.css +1387 -0
  6. package/package.json +6 -2
  7. package/registry/accordion.muten +11 -10
  8. package/registry/agenda.js +35 -0
  9. package/registry/agenda.muten +10 -0
  10. package/registry/alert-dialog.muten +9 -12
  11. package/registry/alert.muten +8 -8
  12. package/registry/app-shell.js +9 -0
  13. package/registry/app-shell.muten +12 -0
  14. package/registry/attachment.muten +13 -9
  15. package/registry/avatar.muten +24 -5
  16. package/registry/badge.muten +23 -8
  17. package/registry/band.muten +16 -0
  18. package/registry/bar-chart.muten +7 -0
  19. package/registry/bell.muten +3 -0
  20. package/registry/bento.muten +6 -9
  21. package/registry/booking.js +21 -0
  22. package/registry/booking.muten +13 -0
  23. package/registry/breadcrumb.muten +9 -7
  24. package/registry/button-group.muten +13 -4
  25. package/registry/button.muten +21 -8
  26. package/registry/calendar.js +28 -93
  27. package/registry/calendar.muten +9 -8
  28. package/registry/canvas-viewport.js +12 -5
  29. package/registry/canvas-viewport.muten +9 -9
  30. package/registry/card-field.muten +4 -0
  31. package/registry/card.muten +12 -8
  32. package/registry/carousel.js +5 -36
  33. package/registry/carousel.muten +12 -5
  34. package/registry/chatbot.muten +9 -0
  35. package/registry/checklist.muten +6 -0
  36. package/registry/choice-list.muten +16 -0
  37. package/registry/code.js +14 -12
  38. package/registry/code.muten +6 -7
  39. package/registry/collapsible.muten +14 -10
  40. package/registry/color-picker.js +460 -41
  41. package/registry/color-picker.muten +7 -6
  42. package/registry/combobox.js +5 -0
  43. package/registry/combobox.muten +6 -23
  44. package/registry/command-palette.muten +8 -12
  45. package/registry/command.js +32 -0
  46. package/registry/command.muten +32 -11
  47. package/registry/context-menu.muten +9 -8
  48. package/registry/count-up.muten +5 -10
  49. package/registry/date-field.js +5 -0
  50. package/registry/date-field.muten +7 -0
  51. package/registry/device-frame.muten +8 -0
  52. package/registry/dialog.muten +6 -12
  53. package/registry/dirty-bar.js +19 -0
  54. package/registry/dirty-bar.muten +13 -0
  55. package/registry/drawer.muten +6 -11
  56. package/registry/dropdown-menu.muten +37 -13
  57. package/registry/empty.muten +11 -7
  58. package/registry/feature-grid.muten +10 -12
  59. package/registry/feature-rows.muten +12 -8
  60. package/registry/field-behavior.js +33 -0
  61. package/registry/field-behavior.muten +9 -0
  62. package/registry/field.muten +26 -7
  63. package/registry/file-input.js +36 -21
  64. package/registry/file-input.muten +8 -7
  65. package/registry/fixed-card.muten +25 -0
  66. package/registry/folder-picker.js +13 -15
  67. package/registry/folder-picker.muten +5 -8
  68. package/registry/gradient-blinds.muten +6 -13
  69. package/registry/hover-card.muten +7 -9
  70. package/registry/image-upload.js +5 -0
  71. package/registry/image-upload.muten +8 -0
  72. package/registry/indicator.muten +5 -0
  73. package/registry/input-group.muten +27 -4
  74. package/registry/input-otp.js +8 -35
  75. package/registry/input-otp.muten +6 -6
  76. package/registry/input.muten +8 -7
  77. package/registry/inspector.muten +13 -15
  78. package/registry/island.js +7 -0
  79. package/registry/island.muten +5 -0
  80. package/registry/item.muten +20 -7
  81. package/registry/kbd.muten +3 -3
  82. package/registry/kit/board.js +300 -0
  83. package/registry/kit/collections.js +148 -0
  84. package/registry/kit/core.js +989 -0
  85. package/registry/kit/dates.js +106 -0
  86. package/registry/kit/menus.js +90 -0
  87. package/registry/kit/overlays.js +114 -0
  88. package/registry/kit/paint.js +211 -0
  89. package/registry/kit/qrcode.js +2291 -0
  90. package/registry/kit/structure.js +393 -0
  91. package/registry/kit/table.js +198 -0
  92. package/registry/kit/values.js +179 -0
  93. package/registry/kit-layer.js +2 -0
  94. package/registry/kit-layer.muten +5 -0
  95. package/registry/kit-skeleton.js +24 -0
  96. package/registry/kit-skeleton.muten +16 -0
  97. package/registry/kit-widget.js +79 -0
  98. package/registry/kit-widget.muten +4 -0
  99. package/registry/link-btn.muten +9 -7
  100. package/registry/live-stack.js +29 -0
  101. package/registry/live-stack.muten +7 -0
  102. package/registry/logo-cloud.muten +6 -11
  103. package/registry/member-list.muten +6 -0
  104. package/registry/menubar.muten +10 -14
  105. package/registry/message-scroller.js +12 -0
  106. package/registry/message-scroller.muten +8 -4
  107. package/registry/message.muten +27 -10
  108. package/registry/messaging.muten +11 -0
  109. package/registry/modal-behavior.js +56 -0
  110. package/registry/modal-behavior.muten +4 -0
  111. package/registry/modal.muten +29 -0
  112. package/registry/money-field.js +5 -0
  113. package/registry/money-field.muten +7 -0
  114. package/registry/nav-button.muten +14 -0
  115. package/registry/navigation-menu.muten +16 -14
  116. package/registry/navpill.muten +15 -9
  117. package/registry/new-appointment.js +8 -0
  118. package/registry/new-appointment.muten +10 -0
  119. package/registry/number-field.js +34 -24
  120. package/registry/number-field.muten +8 -6
  121. package/registry/order-board.js +6 -0
  122. package/registry/order-board.muten +21 -0
  123. package/registry/overlay-behavior.js +13 -0
  124. package/registry/overlay-behavior.muten +4 -0
  125. package/registry/pagination-nav.js +6 -0
  126. package/registry/pagination-nav.muten +8 -0
  127. package/registry/pagination.muten +12 -9
  128. package/registry/password-field.muten +3 -0
  129. package/registry/permission-matrix.muten +6 -0
  130. package/registry/phone-field.js +5 -0
  131. package/registry/phone-field.muten +7 -0
  132. package/registry/phone-shell.js +7 -0
  133. package/registry/phone-shell.muten +9 -0
  134. package/registry/pill.muten +6 -5
  135. package/registry/plans.muten +4 -0
  136. package/registry/popover.muten +16 -9
  137. package/registry/pricing.muten +21 -21
  138. package/registry/progress.muten +5 -5
  139. package/registry/queue.muten +3 -0
  140. package/registry/radio-group.muten +16 -8
  141. package/registry/range-slider.js +9 -24
  142. package/registry/range-slider.muten +4 -5
  143. package/registry/resizable.js +34 -27
  144. package/registry/resizable.muten +16 -5
  145. package/registry/roving-behavior.js +16 -0
  146. package/registry/roving-behavior.muten +3 -0
  147. package/registry/row-menu.js +5 -0
  148. package/registry/row-menu.muten +5 -0
  149. package/registry/save-button.muten +11 -0
  150. package/registry/scale-slider.js +7 -0
  151. package/registry/scale-slider.muten +9 -0
  152. package/registry/scroll-area.muten +8 -4
  153. package/registry/segmented-control.muten +8 -8
  154. package/registry/separator.muten +3 -3
  155. package/registry/share-button.js +7 -0
  156. package/registry/share-button.muten +10 -0
  157. package/registry/sheet.muten +6 -10
  158. package/registry/sidebar.muten +23 -15
  159. package/registry/skeleton.muten +6 -4
  160. package/registry/slider.js +73 -27
  161. package/registry/slider.muten +6 -6
  162. package/registry/sortable-behavior.js +43 -0
  163. package/registry/sortable-behavior.muten +2 -0
  164. package/registry/sortable-list.js +10 -26
  165. package/registry/sortable-list.muten +6 -7
  166. package/registry/sortable.muten +15 -0
  167. package/registry/spark.js +14 -0
  168. package/registry/spark.muten +3 -0
  169. package/registry/spinner.muten +4 -3
  170. package/registry/split-button.muten +12 -10
  171. package/registry/stamp-card.muten +7 -0
  172. package/registry/stat-band.muten +6 -9
  173. package/registry/stat.muten +23 -0
  174. package/registry/status-bar.muten +7 -6
  175. package/registry/stepper.muten +17 -14
  176. package/registry/store-qr.js +5 -0
  177. package/registry/store-qr.muten +5 -0
  178. package/registry/swatch.muten +8 -8
  179. package/registry/switch.muten +14 -6
  180. package/registry/table-view.muten +10 -0
  181. package/registry/tabs-behavior.js +27 -0
  182. package/registry/tabs-behavior.muten +3 -0
  183. package/registry/tabs.muten +19 -10
  184. package/registry/tag-input.js +4 -27
  185. package/registry/tag-input.muten +6 -6
  186. package/registry/text.muten +7 -0
  187. package/registry/time-field.js +5 -0
  188. package/registry/time-field.muten +6 -0
  189. package/registry/timeline.muten +14 -14
  190. package/registry/toaster.js +34 -22
  191. package/registry/toaster.muten +10 -8
  192. package/registry/toggle-group.muten +11 -6
  193. package/registry/toggle.muten +8 -5
  194. package/registry/toolbar.muten +11 -6
  195. package/registry/tooltip.muten +6 -9
  196. package/registry/tree-view.js +27 -7
  197. package/registry/tree-view.muten +8 -13
  198. package/registry/vector-field.muten +9 -8
  199. package/registry/virtual-list.js +5 -0
  200. package/registry/virtual-list.muten +6 -0
  201. package/registry/weekly-hours.muten +4 -0
  202. package/registry/wordline.muten +6 -0
  203. package/registry/zoom-control.muten +9 -9
  204. package/registry.json +872 -406
  205. package/registry/context-menu.js +0 -34
@@ -0,0 +1,106 @@
1
+ // @muten/shadcn · dates & files (kit): the reference artifact's date and time fields (a field-like button that opens
2
+ // the wheels in the ONE adaptive modal) and the image upload (validate, decode first, crop square, shrink to WebP,
3
+ // upload with progress). Generic: values, ranges, limits and the upload address come in.
4
+
5
+ const pad = (n) => String(n).padStart(2, '0');
6
+
7
+ // DateField: i {label, value "yyyy-mm-dd" | "today", min "today" | "", years 2} → on.change("yyyy-mm-dd")
8
+ export function dateField(root, i, core, on) {
9
+ const { $, ui, today, DAYLONG, MON, makeWheel, openModal, closeModal, CONTENT, body, foot } = core;
10
+ const MONTHS = MON.map((m) => m[0].toUpperCase() + m.slice(1));
11
+ const parse = (s) => { if (!s || s === 'today') return new Date(today); const d = new Date(String(s) + 'T00:00:00'); return Number.isNaN(d.getTime()) ? new Date(today) : d; };
12
+ let value = parse(i.value), draft = null, wheels = [];
13
+ const min = i.min === 'today' ? today : i.min ? parse(i.min) : null;
14
+ const YEARS = Array.from({ length: Number(i.years) || 2 }, (_, k) => today.getFullYear() + k);
15
+ const longDate = (d) => `${DAYLONG[d.getDay()]} ${d.getDate()} de ${MON[d.getMonth()]} de ${d.getFullYear()}`;
16
+ const id = 'dp' + Math.random().toString(36).slice(2, 7), kind = 'date' + id;
17
+ root.innerHTML = `<div data-slot="field" class="cn-field cn-field-orientation-vertical group/field"><span class="cn-label" id="${id}-l">${i.label || 'Fecha'}</span><button class="cn-button cn-button-variant-outline cn-button-size-default cx-picker-field" aria-labelledby="${id}-l ${id}-v"><svg><use href="#cal"/></svg><span id="${id}-v"></span><svg class="cx-pf-chev"><use href="#chev"/></svg></button></div>`;
18
+ const out = $(`#${id}-v`, root), paint = () => { out.textContent = longDate(value); };
19
+ const daysIn = (y, m) => new Date(y, m + 1, 0).getDate();
20
+ CONTENT[kind] = () => {
21
+ document.getElementById('mdl-title').textContent = i.title || 'Elige la fecha'; document.getElementById('mdl-desc').textContent = 'Arrastra cada rueda, toca un valor o usa las flechas.';
22
+ body.innerHTML = '<div class="cx-wheels"><div class="cx-wheel" data-w="d"></div><div class="cx-wheel" data-flex="2" data-w="m"></div><div class="cx-wheel" data-w="y"></div></div><p class="cx-wheel-sum"></p>';
23
+ foot.innerHTML = ui.button({ label: 'Cancelar', size: 'default', attrs: 'data-dismiss', cls: 'cx-exit' }) + ui.button({ label: 'Listo', variant: 'default', size: 'default', attrs: `data-dp-ok="${id}"`, cls: 'cx-go' });
24
+ draft = { y: value.getFullYear(), m: value.getMonth(), d: value.getDate() };
25
+ const sumEl = $('.cx-wheel-sum', body);
26
+ const sum = () => { const d = new Date(draft.y, draft.m, draft.d), past = min && d < min; sumEl.textContent = past ? 'Esa fecha ya pasó' : longDate(d); sumEl.style.color = past ? 'var(--destructive)' : ''; $(`[data-dp-ok]`, foot).disabled = !!past; if (min) wD.offs((k) => new Date(draft.y, draft.m, k + 1) < min); };
27
+ const days = () => Array.from({ length: daysIn(draft.y, draft.m) }, (_, k) => k + 1);
28
+ const wD = makeWheel($('[data-w=d]', body), days(), draft.d - 1, (k) => { draft.d = k + 1; sum(); }, 'Día');
29
+ const fixDays = () => { const max = daysIn(draft.y, draft.m); if (draft.d > max) draft.d = max; wD.relabel(days()); sum(); };
30
+ const wM = makeWheel($('[data-w=m]', body), MONTHS, draft.m, (k) => { draft.m = k; fixDays(); }, 'Mes');
31
+ const wY = makeWheel($('[data-w=y]', body), YEARS, Math.max(0, YEARS.indexOf(draft.y)), (k) => { draft.y = YEARS[k]; fixDays(); }, 'Año');
32
+ wheels = [wD, wM, wY]; sum();
33
+ };
34
+ $('button', root).addEventListener('click', () => { openModal(kind, 'auto'); requestAnimationFrame(() => requestAnimationFrame(() => { wheels.forEach((w) => w.place()); $('.cx-wheel', body)?.focus({ preventScroll: true }); })); });
35
+ document.addEventListener('click', (e) => { const ok = e.target.closest(`[data-dp-ok="${id}"]`); if (!ok || ok.disabled) return; value = new Date(draft.y, draft.m, draft.d); paint(); closeModal(); on?.change?.(`${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}`); });
36
+ paint();
37
+ }
38
+
39
+ // TimeField: i {label, value "10:30", from 9, to 19, step 15} → on.change("HH:MM")
40
+ export function timeField(root, i, core, on) {
41
+ const { $, ui, makeWheel, openModal, closeModal, CONTENT, body, foot } = core;
42
+ const from = Number(i.from ?? 9), to = Number(i.to ?? 19), step = Number(i.step) || 15;
43
+ const HOURS = Array.from({ length: to - from + 1 }, (_, k) => k + from), MINS = Array.from({ length: Math.ceil(60 / step) }, (_, k) => k * step);
44
+ const [h0, m0] = String(i.value || `${pad(from)}:00`).split(':').map(Number);
45
+ let value = { h: HOURS.includes(h0) ? h0 : from, m: MINS.includes(m0) ? m0 : 0 }, draft = null, wheels = [];
46
+ const hm = (t) => `${pad(t.h)}:${pad(t.m)}`, part = (t) => (t.h < 12 ? 'mañana' : 'tarde');
47
+ const id = 'tp' + Math.random().toString(36).slice(2, 7), kind = 'time' + id;
48
+ root.innerHTML = `<div data-slot="field" class="cn-field cn-field-orientation-vertical group/field"><span class="cn-label" id="${id}-l">${i.label || 'Hora'}</span><button class="cn-button cn-button-variant-outline cn-button-size-default cx-picker-field" aria-labelledby="${id}-l ${id}-v"><svg><use href="#clock"/></svg><span id="${id}-v"></span><svg class="cx-pf-chev"><use href="#chev"/></svg></button></div>`;
49
+ const out = $(`#${id}-v`, root), paint = () => { out.textContent = `${hm(value)} · ${part(value)}`; };
50
+ CONTENT[kind] = () => {
51
+ document.getElementById('mdl-title').textContent = i.title || 'Elige la hora'; document.getElementById('mdl-desc').textContent = i.description || `Horario: ${pad(from)}:00 a ${pad(to)}:${pad(MINS.at(-1))}. Arrastra o toca.`;
52
+ body.innerHTML = '<div class="cx-wheels"><div class="cx-wheel" data-w="h"></div><div class="cx-wheel" data-w="mi"></div></div><p class="cx-wheel-sum"></p>';
53
+ foot.innerHTML = ui.button({ label: 'Cancelar', size: 'default', attrs: 'data-dismiss', cls: 'cx-exit' }) + ui.button({ label: 'Listo', variant: 'default', size: 'default', attrs: `data-tp-ok="${id}"`, cls: 'cx-go' });
54
+ draft = { ...value };
55
+ const sumEl = $('.cx-wheel-sum', body), sum = () => { sumEl.textContent = `${hm(draft)} · ${part(draft)}`; };
56
+ wheels = [makeWheel($('[data-w=h]', body), HOURS.map(pad), HOURS.indexOf(draft.h), (k) => { draft.h = HOURS[k]; sum(); }, 'Hora'), makeWheel($('[data-w=mi]', body), MINS.map(pad), MINS.indexOf(draft.m), (k) => { draft.m = MINS[k]; sum(); }, 'Minutos')];
57
+ sum();
58
+ };
59
+ $('button', root).addEventListener('click', () => { openModal(kind, 'auto'); requestAnimationFrame(() => requestAnimationFrame(() => { wheels.forEach((w) => w.place()); $('.cx-wheel', body)?.focus({ preventScroll: true }); })); });
60
+ document.addEventListener('click', (e) => { const ok = e.target.closest(`[data-tp-ok="${id}"]`); if (!ok) return; value = { ...draft }; paint(); closeModal(); on?.change?.(hm(value)); });
61
+ paint();
62
+ }
63
+
64
+ // ImageUpload: i {title, hint, maxMb 10, size 1200, uploadUrl, sample "on"} → on.ready(json {dataUrl | url, width, height, kb})
65
+ // Without uploadUrl the processed image is handed back as a data URL; with it, the WebP is POSTed with real progress.
66
+ export function imageUpload(root, i, core, on) {
67
+ const { $ } = core;
68
+ const MAX = (Number(i.maxMb) || 10) * 1024 * 1024, SIZE = Number(i.size) || 1200, idle = i.hint || `JPG, PNG o WebP · hasta ${Number(i.maxMb) || 10} MB`;
69
+ root.innerHTML = `<div class="cx-drop" tabindex="0" role="button"><input type="file" accept="image/*" hidden><div class="cx-drop-empty"><svg><use href="#upload"/></svg><b>${i.title || 'Sube la foto'}</b><span>Toca, arrastra o pega aquí</span></div><canvas class="cx-drop-img" width="600" height="600" hidden></canvas><div class="cx-drop-bar" hidden><i></i></div></div><p class="cx-note cx-drop-msg" style="text-align:left">${idle}</p><div class="cx-line cx-line-start">${i.sample === 'on' ? '<button data-slot="button" class="cn-button cn-button-variant-outline cn-button-size-sm group/button" data-drop-sample><svg><use href="#image"/></svg>Usar una de ejemplo</button>' : ''}<button data-slot="button" class="cn-button cn-button-variant-ghost cn-button-size-sm group/button" data-drop-remove hidden><svg><use href="#trash"/></svg>Quitar</button></div>`;
70
+ const drop = $('.cx-drop', root), dropFile = $('input', root), dropMsg = $('.cx-drop-msg', root), dropCanvas = $('canvas', root), dropBar = $('.cx-drop-bar', root), dropProg = $('.cx-drop-bar i', root), removeB = $('[data-drop-remove]', root);
71
+ const say = (text, tone) => { dropMsg.textContent = text; if (tone) dropMsg.dataset.tone = tone; else dropMsg.removeAttribute('data-tone'); };
72
+ const coverDraw = (ctx, bmp, size) => { const s = Math.min(bmp.width, bmp.height); ctx.drawImage(bmp, (bmp.width - s) / 2, (bmp.height - s) / 2, s, s, 0, 0, size, size); };
73
+ const progress = (p) => { dropProg.style.width = p + '%'; say(`Subiendo… ${Math.round(p)} %`); };
74
+ const takeFile = async (file) => {
75
+ if (!file) return;
76
+ if (!file.type.startsWith('image/')) return say(`«${file.name}» no es una imagen. Usa JPG, PNG o WebP.`, 'bad');
77
+ if (file.size > MAX) return say(`Pesa ${(file.size / 1048576).toFixed(1).replace('.', ',')} MB; el tope es ${Number(i.maxMb) || 10} MB.`, 'bad');
78
+ say('Preparando la imagen…');
79
+ let bmp; try { bmp = await createImageBitmap(file); } catch { return say('No pudimos leer esa imagen. Prueba con otra.', 'bad'); }
80
+ coverDraw(dropCanvas.getContext('2d'), bmp, dropCanvas.width);
81
+ const out = document.createElement('canvas'); out.width = out.height = Math.min(SIZE, Math.min(bmp.width, bmp.height)); coverDraw(out.getContext('2d'), bmp, out.width);
82
+ const blob = await new Promise((r) => out.toBlob(r, 'image/webp', 0.86));
83
+ dropCanvas.hidden = false; drop.setAttribute('data-has', ''); removeB.hidden = false; dropBar.hidden = false; progress(0);
84
+ const done = (extra) => { dropBar.hidden = true; say(`Lista · ${out.width} × ${out.height} px, ${Math.max(1, Math.round(blob.size / 1024))} KB`, 'ok'); on?.ready?.(JSON.stringify({ width: out.width, height: out.height, kb: Math.round(blob.size / 1024), ...extra })); };
85
+ if (i.uploadUrl) {
86
+ const xhr = new XMLHttpRequest(); xhr.open('POST', i.uploadUrl); xhr.upload.onprogress = (e) => { if (e.lengthComputable) progress((e.loaded / e.total) * 100); };
87
+ xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { progress(100); setTimeout(() => done({ url: xhr.responseText }), 250); } else say('No se pudo subir. Inténtalo otra vez.', 'bad'); };
88
+ xhr.onerror = () => say('No se pudo subir. Revisa tu conexión.', 'bad'); xhr.send(blob);
89
+ } else { const r = new FileReader(); r.onload = () => { progress(100); setTimeout(() => done({ dataUrl: r.result }), 200); }; r.readAsDataURL(blob); }
90
+ };
91
+ drop.addEventListener('click', () => { if (!drop.hasAttribute('data-has')) dropFile.click(); });
92
+ drop.addEventListener('keydown', (e) => { if ((e.key === 'Enter' || e.key === ' ') && !drop.hasAttribute('data-has')) { e.preventDefault(); dropFile.click(); } });
93
+ dropFile.addEventListener('change', () => { takeFile(dropFile.files[0]); dropFile.value = ''; });
94
+ ['dragenter', 'dragover'].forEach((t) => drop.addEventListener(t, (e) => { e.preventDefault(); drop.setAttribute('data-over', ''); }));
95
+ ['dragleave', 'drop'].forEach((t) => drop.addEventListener(t, () => drop.removeAttribute('data-over')));
96
+ drop.addEventListener('drop', (e) => { e.preventDefault(); takeFile(e.dataTransfer.files[0]); });
97
+ document.addEventListener('paste', (e) => { if (e.target.closest?.('input, textarea')) return; const f = [...(e.clipboardData?.files || [])].find((x) => x.type.startsWith('image/')); if (f && root.isConnected) { drop.scrollIntoView({ block: 'center', behavior: 'smooth' }); takeFile(f); } });
98
+ removeB.onclick = () => { drop.removeAttribute('data-has'); dropCanvas.hidden = true; dropBar.hidden = true; removeB.hidden = true; say(idle); on?.ready?.(''); };
99
+ $('[data-drop-sample]', root)?.addEventListener('click', () => {
100
+ const c = document.createElement('canvas'); c.width = 1600; c.height = 1100; const g = c.getContext('2d');
101
+ const gr = g.createLinearGradient(0, 0, 1600, 1100); gr.addColorStop(0, '#d9b48c'); gr.addColorStop(1, '#5e3f27'); g.fillStyle = gr; g.fillRect(0, 0, 1600, 1100);
102
+ g.fillStyle = 'rgba(255,255,255,.18)'; for (let k = 0; k < 9; k++) { g.beginPath(); g.arc(300 + k * 130, 550 + Math.sin(k) * 160, 60 + (k % 3) * 30, 0, 7); g.fill(); }
103
+ g.fillStyle = '#fff'; g.font = '600 110px sans-serif'; g.textAlign = 'center'; g.fillText('El Roble', 800, 590);
104
+ c.toBlob((b) => takeFile(new File([b], 'ejemplo.png', { type: 'image/png' })), 'image/png');
105
+ });
106
+ }
@@ -0,0 +1,90 @@
1
+ // @muten/shadcn · menus (kit): the reference artifact's Combobox (type, it filters and marks the match, arrows, Enter,
2
+ // «Usar …» for something not in the list), the row menu ⋯ (arrows, Home/End, type-ahead, Esc returns focus) and the
3
+ // store QR (with the mark in the middle and «Copiar enlace»). Generic: options, items and the link come in.
4
+
5
+ export function combobox(root, i, core, on) {
6
+ const { $, $$, lib, place, show, hide, fold } = core;
7
+ const OPTIONS = String(i.options || '').split(',').map((x) => x.trim()).filter(Boolean);
8
+ const allowNew = i.allowNew !== 'off', id = 'cb' + Math.random().toString(36).slice(2, 7);
9
+ root.innerHTML = `<div data-slot="field" class="cn-field cn-field-orientation-vertical group/field"><label data-slot="label" class="cn-label" for="${id}">${i.label || ''}</label><div data-slot="input-group" class="cn-input-group group/input-group cx-combo"><input data-slot="input-group-control" class="cn-input cn-input-group-input" id="${id}" role="combobox" aria-expanded="false" aria-controls="${id}-list" aria-autocomplete="list" autocomplete="off" placeholder="${i.placeholder || ''}"><div data-slot="input-group-addon" class="cn-input-group-addon cn-input-group-addon-align-inline-end"><svg><use href="#updown"/></svg></div></div>${i.hint === 'off' ? '' : `<p data-slot="field-description" class="cn-field-description" data-cb-out>Elegida: ${i.value || 'ninguna'}</p>`}</div>`;
10
+ const cIn = $('input', root), cGroup = $('.cx-combo', root), cOut = $('[data-cb-out]', root);
11
+ const cList = document.createElement('div'); cList.className = 'cx-combo-list pg-pop'; cList.id = `${id}-list`; cList.setAttribute('role', 'listbox'); cList.hidden = true; lib.appendChild(cList);
12
+ let cOpts = [], cHl = 0, cValue = i.value || '';
13
+ cIn.value = cValue;
14
+ const mark = (name, typed) => { const k = fold(name).indexOf(fold(typed)); return !typed || k < 0 ? name : name.slice(0, k) + '<mark>' + name.slice(k, k + typed.length) + '</mark>' + name.slice(k + typed.length); };
15
+ const cRender = () => {
16
+ const typed = cIn.value.trim();
17
+ const hits = OPTIONS.filter((c) => fold(c).includes(fold(typed)));
18
+ cOpts = hits.map((c) => ({ value: c, html: `<span>${mark(c, typed)}</span>` + (c === cValue ? '<svg class="cx-tick"><use href="#check"/></svg>' : '') }));
19
+ if (allowNew && typed && !OPTIONS.some((c) => fold(c) === fold(typed))) cOpts.push({ value: typed, html: `<span>Usar «<b>${typed.replace(/</g, '&lt;')}</b>»</span>`, isNew: true });
20
+ cHl = Math.min(cHl, Math.max(0, cOpts.length - 1));
21
+ cList.innerHTML = cOpts.map((o, k) => `<div class="cx-opt${o.isNew ? ' cx-opt-new' : ''}" role="option" id="${id}-o${k}" data-k="${k}" aria-selected="${o.value === cValue}"${k === cHl ? ' data-hl' : ''}>${o.html}</div>`).join('') || '<div class="cx-opt" style="color:var(--muted-foreground)">Escribe para buscar</div>';
22
+ cIn.setAttribute('aria-activedescendant', cOpts.length ? `${id}-o${cHl}` : '');
23
+ $('[data-hl]', cList)?.scrollIntoView({ block: 'nearest' });
24
+ };
25
+ const cOpen = () => { if (!cList.hidden && cList.dataset.state === 'open') return; cRender(); cList.hidden = false; place(cList, cGroup, 'start', 4, cGroup.offsetWidth); show(cList); cIn.setAttribute('aria-expanded', 'true'); };
26
+ const cClose = () => { hide(cList, 110); cIn.setAttribute('aria-expanded', 'false'); };
27
+ const cPick = (k) => { const o = cOpts[k]; if (!o) return; cValue = o.value; cIn.value = o.value; if (cOut) cOut.textContent = `Elegida: ${o.value}${o.isNew ? ' (escrita por ti)' : ''}`; cClose(); on?.change?.(o.value); };
28
+ cIn.addEventListener('focus', cOpen);
29
+ cIn.addEventListener('input', () => { cHl = 0; cOpen(); cRender(); });
30
+ cIn.addEventListener('keydown', (e) => {
31
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); if (cList.hidden) return cOpen(); cHl = (cHl + (e.key === 'ArrowDown' ? 1 : -1) + cOpts.length) % Math.max(1, cOpts.length); cRender(); }
32
+ else if (e.key === 'Enter') { if (!cList.hidden) { e.preventDefault(); cPick(cHl); } }
33
+ else if (e.key === 'Escape') { if (!cList.hidden) { e.stopPropagation(); cClose(); } }
34
+ else if (e.key === 'Tab') cClose();
35
+ });
36
+ cList.addEventListener('pointerdown', (e) => e.preventDefault());
37
+ cList.addEventListener('pointermove', (e) => { const o = e.target.closest('[role=option]'); if (!o) return; const k = +o.dataset.k; if (k !== cHl) { cHl = k; $$('[role=option]', cList).forEach((x) => x.toggleAttribute('data-hl', x === o)); } });
38
+ cList.addEventListener('click', (e) => { const o = e.target.closest('[role=option]'); if (o) cPick(+o.dataset.k); });
39
+ cGroup.addEventListener('pointerdown', (e) => { if (e.target !== cIn) { e.preventDefault(); cIn.focus(); cOpen(); } });
40
+ document.addEventListener('pointerdown', (e) => { if (!cList.hidden && !cList.contains(e.target) && !cGroup.contains(e.target)) cClose(); });
41
+ addEventListener('scroll', () => { if (!cList.hidden) place(cList, cGroup, 'start', 4, cGroup.offsetWidth); }, { passive: true });
42
+ }
43
+
44
+ // Row menu ⋯: the trigger button (in the row's actions) and its menu. i {label, items [{id, label, icon, danger, separator}]}
45
+ export function rowMenu(root, i, core, on) {
46
+ const { $, lib, place, show, hide, fold, ui } = core;
47
+ const items = Array.isArray(i.items) ? i.items : [];
48
+ root.innerHTML = ui.button({ icon: 'more', variant: 'ghost', size: 'icon-sm', attrs: `data-menu aria-haspopup="menu" aria-expanded="false" aria-label="${i.label || 'Más opciones'}" title="Más opciones"` });
49
+ const btn = $('[data-menu]', root);
50
+ const menu = document.createElement('div'); menu.dataset.slot = 'dropdown-menu-content'; menu.className = 'cn-dropdown-menu-content pg-pop'; menu.setAttribute('role', 'menu'); menu.hidden = true; menu.style.minWidth = '184px';
51
+ menu.innerHTML = items.map((it) => `${it.separator ? '<div data-slot="dropdown-menu-separator" class="cn-dropdown-menu-separator"></div>' : ''}<div data-slot="dropdown-menu-item" ${it.danger ? 'data-variant="destructive"' : ''} class="cn-dropdown-menu-item group/dropdown-menu-item" role="menuitem" tabindex="-1" data-act="${it.id}">${it.icon ? ui.icon(it.icon) : ''}${it.label}</div>`).join('');
52
+ lib.appendChild(menu);
53
+ const all = () => [...menu.querySelectorAll('[role=menuitem]')];
54
+ let open = false;
55
+ const close = (refocus) => { if (!open) return; open = false; hide(menu, 120); btn.setAttribute('aria-expanded', 'false'); if (refocus) btn.focus(); };
56
+ btn.addEventListener('click', (e) => { e.stopPropagation(); if (open) return close(); open = true; menu.hidden = false; place(menu, btn, 'end', 4); show(menu); btn.setAttribute('aria-expanded', 'true'); all()[0]?.focus({ preventScroll: true }); });
57
+ document.addEventListener('pointerdown', (e) => { if (open && !menu.contains(e.target) && !btn.contains(e.target)) close(); });
58
+ menu.addEventListener('pointermove', (e) => { const m = e.target.closest('[role=menuitem]'); if (m && document.activeElement !== m) m.focus({ preventScroll: true }); });
59
+ menu.addEventListener('keydown', (e) => {
60
+ const list = all(), k = list.indexOf(document.activeElement);
61
+ const to = { ArrowDown: k + 1, ArrowUp: k - 1, Home: 0, End: list.length - 1 }[e.key];
62
+ if (to !== undefined) { e.preventDefault(); list[(to + list.length) % list.length].focus(); return; }
63
+ if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); close(true); return; }
64
+ if (e.key === 'Tab') { close(); return; }
65
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); document.activeElement.click(); return; }
66
+ if (e.key.length === 1) { const ch = fold(e.key); [...list.slice(k + 1), ...list.slice(0, k + 1)].find((m) => fold(m.textContent.trim()).startsWith(ch))?.focus(); }
67
+ });
68
+ menu.addEventListener('click', (e) => { const m = e.target.closest('[role=menuitem]'); if (!m) return; close(true); on?.pick?.(m.dataset.act); });
69
+ addEventListener('scroll', () => { if (open) place(menu, btn, 'end', 4); }, { passive: true });
70
+ }
71
+
72
+ // Store QR: the code with the shop's mark in the middle, the name, the link and «Copiar enlace».
73
+ export function storeQr(root, i, core) {
74
+ const { $ } = core;
75
+ const url = i.url || '', shown = url.replace(/^https?:\/\//, '');
76
+ root.innerHTML = `<div class="cx-qr-card"><div class="cx-qr" role="img" aria-label="Código QR de ${shown}"></div><div class="cx-qr-side"><b>${i.name || ''}</b><span class="cx-qr-url">${shown}</span><button data-slot="button" class="cn-button cn-button-variant-outline cn-button-size-sm group/button cx-copy"><span class="cx-btn-label"><svg><use href="#copyi"/></svg>Copiar enlace</span></button></div></div>`;
77
+ const qrBox = $('.cx-qr', root);
78
+ import('@muten/shadcn/registry/kit/qrcode.js').then(({ default: qrcode }) => {
79
+ const qr = qrcode(0, 'H'); qr.addData(url); qr.make();
80
+ const n = qr.getModuleCount(); let d = '';
81
+ for (let r = 0; r < n; r++) for (let c = 0; c < n; c++) if (qr.isDark(r, c)) d += `M${c} ${r}h1v1h-1z`;
82
+ qrBox.innerHTML = `<svg viewBox="0 0 ${n} ${n}" shape-rendering="crispEdges" aria-hidden="true"><path d="${d}" fill="var(--qr-ink)"/></svg>${i.mark ? `<div class="cx-qr-mark"><span>${i.mark}</span></div>` : ''}`;
83
+ });
84
+ const copyBtn = $('.cx-copy', root), copyLabel = $('.cx-btn-label', copyBtn), copyIdle = copyLabel.innerHTML;
85
+ copyBtn.onclick = () => {
86
+ const done = (ok) => { copyBtn.toggleAttribute('data-done', ok); copyLabel.innerHTML = ok ? '<svg><use href="#check"/></svg>Copiado' : 'Selecciónalo y copia'; setTimeout(() => { copyBtn.removeAttribute('data-done'); copyLabel.innerHTML = copyIdle; }, 1800); };
87
+ const fallback = () => { getSelection().selectAllChildren($('.cx-qr-url', root)); done(false); };
88
+ try { navigator.clipboard.writeText(url).then(() => done(true), fallback); } catch { fallback(); }
89
+ };
90
+ }
@@ -0,0 +1,114 @@
1
+ // kit/overlays.js - the one behaviour behind every floating piece made of muten parts: Popover, HoverCard,
2
+ // DropdownMenu, ContextMenu, Menubar and NavigationMenu. The parts write a wrapper (.cx-ov) holding a trigger
3
+ // (.cx-ov-trigger) and a content panel (.cx-ov-content) side by side; this module opens and closes the panel.
4
+ // · the panel rises to the browser's top layer (the popover API), so no overflow or stacking context clips it,
5
+ // and it never leaves its place in the DOM (muten keeps owning and updating it)
6
+ // · it is placed next to its trigger with the kit's place() and flips up when it does not fit below
7
+ // · it closes on a click outside, on Esc (focus goes back to the trigger), on Tab out, and when the page scrolls
8
+ // it follows its trigger
9
+ // · a menu moves with the arrows, Home/End and the first letter; Enter or Space picks; a picked item closes it
10
+ // (a check or radio item stays open, as in shadcn)
11
+ // kinds: popover (click) · hover (hover or focus, with a short delay) · menu (click, keyboard menu) ·
12
+ // context (right click or a long press, placed at the pointer) · menubar / navmenu (one open at a time in
13
+ // their bar, hover moves between them once one is open)
14
+
15
+ const ITEMS = '[role="menuitem"]:not([aria-disabled="true"]), [role="menuitemcheckbox"]:not([aria-disabled="true"]), [role="menuitemradio"]:not([aria-disabled="true"])';
16
+ const KEEP_OPEN = '[role="menuitemcheckbox"], [role="menuitemradio"]';
17
+ const openBars = new WeakMap();
18
+
19
+ export function overlay(host, kind, core) {
20
+ const { place, fold } = core;
21
+ const wrap = host.parentElement;
22
+ const trigger = wrap.querySelector(':scope > .cx-ov-trigger');
23
+ const panel = wrap.querySelector(':scope > .cx-ov-content');
24
+ if (!trigger || !panel) return;
25
+ const isMenu = kind === 'menu' || kind === 'context' || kind === 'menubar';
26
+ const bar = kind === 'menubar' || kind === 'navmenu' ? wrap.parentElement : null;
27
+ const button = () => trigger.querySelector('button, a, [tabindex]') || trigger;
28
+ panel.setAttribute('popover', 'manual');
29
+ panel.hidden = false;
30
+ if (isMenu) panel.setAttribute('role', 'menu');
31
+ else if (kind !== 'navmenu') panel.setAttribute('role', 'dialog');
32
+ const btn = button();
33
+ if (kind !== 'context') { btn.setAttribute('aria-haspopup', isMenu ? 'menu' : 'dialog'); btn.setAttribute('aria-expanded', 'false'); }
34
+
35
+ let open = false, at = null, hoverTimer = 0;
36
+ const items = () => [...panel.querySelectorAll(ITEMS)].filter((x) => x.offsetParent !== null || panel.matches(':popover-open'));
37
+ const align = () => panel.dataset.align || (kind === 'menubar' || kind === 'navmenu' ? 'start' : 'center');
38
+ const anchor = () => (at ? { getBoundingClientRect: () => ({ left: at.x, right: at.x, top: at.y, bottom: at.y, width: 0, height: 0 }) } : trigger);
39
+ const position = () => { if (open) place(panel, anchor(), at ? 'start' : align(), Number(panel.dataset.gap) || (kind === 'navmenu' ? 8 : 4)); };
40
+ const show = (focusFirst) => {
41
+ if (open) return; open = true;
42
+ if (bar) { const other = openBars.get(bar); if (other && other !== api) other.close(false); openBars.set(bar, api); }
43
+ try { panel.showPopover(); } catch { /* already in the top layer */ }
44
+ panel.removeAttribute('data-closed'); panel.setAttribute('data-open', ''); panel.dataset.state = 'open';
45
+ position();
46
+ if (kind !== 'context') btn.setAttribute('aria-expanded', 'true');
47
+ trigger.dataset.open = '';
48
+ if (isMenu && focusFirst) items()[0]?.focus({ preventScroll: true });
49
+ else if (kind === 'popover') panel.querySelector('input, textarea, button, [tabindex]:not([tabindex="-1"])')?.focus({ preventScroll: true });
50
+ };
51
+ const close = (refocus) => {
52
+ if (!open) return; open = false; at = null;
53
+ if (bar && openBars.get(bar) === api) openBars.delete(bar);
54
+ panel.removeAttribute('data-open'); panel.setAttribute('data-closed', ''); panel.dataset.state = 'closed';
55
+ if (kind !== 'context') btn.setAttribute('aria-expanded', 'false');
56
+ delete trigger.dataset.open;
57
+ const done = () => { if (!open) { try { panel.hidePopover(); } catch { /* not open */ } } };
58
+ requestAnimationFrame(() => { const runs = panel.getAnimations(); if (!runs.length) return done(); Promise.all(runs.map((a) => a.finished.catch(() => {}))).then(done); });
59
+ setTimeout(done, 500);
60
+ if (refocus) btn.focus({ preventScroll: true });
61
+ };
62
+ const api = { close, show, trigger };
63
+
64
+ // ── opening ──
65
+ if (kind === 'hover' || kind === 'navmenu') {
66
+ const enter = () => { clearTimeout(hoverTimer); hoverTimer = setTimeout(() => show(false), kind === 'navmenu' && bar && openBars.get(bar) ? 0 : (Number(panel.dataset.delay) || 250)); };
67
+ const leave = () => { clearTimeout(hoverTimer); hoverTimer = setTimeout(() => close(false), 180); };
68
+ [trigger, panel].forEach((x) => { x.addEventListener('pointerenter', (e) => { if (e.pointerType !== 'touch') enter(); }); x.addEventListener('pointerleave', (e) => { if (e.pointerType !== 'touch') leave(); }); });
69
+ trigger.addEventListener('focusin', () => show(false));
70
+ wrap.addEventListener('focusout', (e) => { if (!wrap.contains(e.relatedTarget) && !panel.contains(e.relatedTarget)) close(false); });
71
+ }
72
+ if (kind === 'popover' || kind === 'menu' || kind === 'menubar' || kind === 'navmenu') {
73
+ trigger.addEventListener('click', (e) => { if (kind === 'navmenu' && !panel.childElementCount) return; e.preventDefault(); if (open) close(false); else show(e.detail === 0); });
74
+ trigger.addEventListener('keydown', (e) => { if (isMenu && (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); show(true); } });
75
+ }
76
+ if (kind === 'menubar') {
77
+ trigger.addEventListener('pointerenter', () => { const other = openBars.get(bar); if (other && other !== api) { show(false); } });
78
+ }
79
+ if (kind === 'context') {
80
+ trigger.addEventListener('contextmenu', (e) => { e.preventDefault(); close(false); at = { x: e.clientX, y: e.clientY }; open = false; show(true); });
81
+ let press = 0;
82
+ trigger.addEventListener('pointerdown', (e) => { if (e.pointerType !== 'touch') return; press = setTimeout(() => { at = { x: e.clientX, y: e.clientY }; show(true); navigator.vibrate?.(8); }, 500); });
83
+ ['pointerup', 'pointercancel', 'pointermove'].forEach((t) => trigger.addEventListener(t, () => clearTimeout(press)));
84
+ trigger.addEventListener('keydown', (e) => { if (e.key === 'ContextMenu' || (e.shiftKey && e.key === 'F10')) { e.preventDefault(); const r = trigger.getBoundingClientRect(); at = { x: r.left + 12, y: r.top + 12 }; show(true); } });
85
+ }
86
+
87
+ // ── closing and following ──
88
+ document.addEventListener('pointerdown', (e) => { if (open && !panel.contains(e.target) && !trigger.contains(e.target)) close(false); }, true);
89
+ panel.addEventListener('keydown', (e) => {
90
+ if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); close(true); return; }
91
+ if (e.key === 'Tab' && isMenu) { close(false); return; }
92
+ if (!isMenu) return;
93
+ const list = items(), k = list.indexOf(document.activeElement);
94
+ const to = { ArrowDown: k + 1, ArrowUp: k - 1, Home: 0, End: list.length - 1 }[e.key];
95
+ if (to !== undefined) { e.preventDefault(); list[(to + list.length) % list.length]?.focus(); return; }
96
+ if (bar && (e.key === 'ArrowRight' || e.key === 'ArrowLeft')) {
97
+ e.preventDefault(); const menus = [...bar.querySelectorAll(':scope > .cx-ov')]; const i = menus.indexOf(wrap);
98
+ const next = menus[(i + (e.key === 'ArrowRight' ? 1 : -1) + menus.length) % menus.length];
99
+ close(false); next.querySelector(':scope > .cx-ov-trigger button')?.click(); return;
100
+ }
101
+ if (e.key === 'Enter' || e.key === ' ') { if (document.activeElement && list.includes(document.activeElement)) { e.preventDefault(); document.activeElement.click(); } return; }
102
+ if (e.key.length === 1 && /\S/.test(e.key)) { const ch = fold(e.key); [...list.slice(k + 1), ...list.slice(0, k + 1)].find((m) => fold(m.textContent.trim()).startsWith(ch))?.focus(); }
103
+ });
104
+ trigger.addEventListener('keydown', (e) => { if (e.key === 'Escape' && open) { e.preventDefault(); close(true); } });
105
+ if (isMenu) {
106
+ panel.addEventListener('pointermove', (e) => { const m = e.target.closest(ITEMS); if (m && document.activeElement !== m) m.focus({ preventScroll: true }); });
107
+ panel.addEventListener('click', (e) => { const m = e.target.closest(ITEMS); if (m && !m.matches(KEEP_OPEN)) close(true); });
108
+ } else {
109
+ panel.addEventListener('click', (e) => { if (e.target.closest('[data-ovclose]')) close(true); });
110
+ }
111
+ addEventListener('scroll', position, { passive: true, capture: true });
112
+ addEventListener('resize', position);
113
+ return api;
114
+ }
@@ -0,0 +1,211 @@
1
+ var PAINT = (() => {
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var paint_exports = {};
20
+ __export(paint_exports, {
21
+ ANIM_LABELS: () => ANIM_LABELS,
22
+ MAX_STOPS: () => MAX_STOPS,
23
+ PAIRS: () => PAIRS,
24
+ SPEEDS: () => SPEEDS,
25
+ barCss: () => barCss,
26
+ hexToHsl: () => hexToHsl,
27
+ hslToHex: () => hslToHex,
28
+ isHex: () => isHex,
29
+ legacyPaint: () => legacyPaint,
30
+ mixHex: () => mixHex,
31
+ paintAnim: () => paintAnim,
32
+ paintBase: () => paintBase,
33
+ paintBg: () => paintBg,
34
+ paintCss: () => paintCss,
35
+ paintMain: () => paintMain,
36
+ paintSize: () => paintSize,
37
+ parsePaint: () => parsePaint,
38
+ printPaint: () => printPaint
39
+ });
40
+ const SPEEDS = [24, 14, 7];
41
+ const MAX_STOPS = 4;
42
+ const ANIM_LABELS = {
43
+ none: { es: "Nada", en: "None" },
44
+ flow: { es: "Fluir", en: "Flow" },
45
+ breathe: { es: "Respirar", en: "Breathe" },
46
+ spin: { es: "Girar", en: "Spin" },
47
+ hue: { es: "Tono", en: "Hue" }
48
+ };
49
+ const PAIRS = [
50
+ { name: { es: "Aurora", en: "Aurora" }, colours: ["#6B46F2", "#ff5ca8", "#ffb36b"] },
51
+ { name: { es: "Atardecer", en: "Sunset" }, colours: ["#f97316", "#db2777"] },
52
+ { name: { es: "Durazno", en: "Peach" }, colours: ["#ffd6a5", "#ff7eb3"] },
53
+ { name: { es: "Oro", en: "Gold" }, colours: ["#fbbf24", "#b45309"] },
54
+ { name: { es: "Brasa", en: "Ember" }, colours: ["#1c0f0a", "#c2410c"] },
55
+ { name: { es: "Vino", en: "Wine" }, colours: ["#2a0b16", "#9f1239"] },
56
+ { name: { es: "Lavanda", en: "Lavender" }, colours: ["#e9d5ff", "#7c3aed"] },
57
+ { name: { es: "Noche", en: "Night" }, colours: ["#0f172a", "#4338ca"] },
58
+ { name: { es: "Oc\xE9ano", en: "Ocean" }, colours: ["#0ea5e9", "#1e3a8a"] },
59
+ { name: { es: "Hielo", en: "Ice" }, colours: ["#e0f2fe", "#38bdf8"] },
60
+ { name: { es: "Menta", en: "Mint" }, colours: ["#a7f3d0", "#0d9488"] },
61
+ { name: { es: "Bosque", en: "Forest" }, colours: ["#14532d", "#65a30d"] }
62
+ ];
63
+ const TYPES = ["linear", "radial", "conic"];
64
+ const ANIMS = ["none", "flow", "breathe", "spin", "hue"];
65
+ const HEX = /^#[0-9a-f]{6}$/i;
66
+ const FALLBACK = "#4b3aa8";
67
+ function isHex(value) {
68
+ return HEX.test(String(value ?? "").trim());
69
+ }
70
+ function hslToHex(h, s, l) {
71
+ const sat = s / 100;
72
+ const light = l / 100;
73
+ const chroma = sat * Math.min(light, 1 - light);
74
+ const channel = (n) => {
75
+ const k = (n + h / 30) % 12;
76
+ const value = light - chroma * Math.max(-1, Math.min(k - 3, Math.min(9 - k, 1)));
77
+ return Math.round(255 * value).toString(16).padStart(2, "0");
78
+ };
79
+ return `#${channel(0)}${channel(8)}${channel(4)}`;
80
+ }
81
+ function hexToHsl(hex) {
82
+ const clean = String(hex ?? "").trim();
83
+ if (!HEX.test(clean)) return hexToHsl(FALLBACK);
84
+ const [r, g, b] = [1, 3, 5].map((at) => parseInt(clean.slice(at, at + 2), 16) / 255);
85
+ const max = Math.max(r, g, b);
86
+ const min = Math.min(r, g, b);
87
+ const light = (max + min) / 2;
88
+ let hue = 0;
89
+ let sat = 0;
90
+ if (max !== min) {
91
+ const span = max - min;
92
+ sat = light > 0.5 ? span / (2 - max - min) : span / (max + min);
93
+ if (max === r) hue = (g - b) / span + (g < b ? 6 : 0);
94
+ else if (max === g) hue = (b - r) / span + 2;
95
+ else hue = (r - g) / span + 4;
96
+ hue *= 60;
97
+ }
98
+ return { h: Math.round(hue), s: Math.round(sat * 100), l: Math.round(light * 100) };
99
+ }
100
+ function solidOf(hex) {
101
+ const colour = (isHex(hex) ? hex : FALLBACK).toLowerCase();
102
+ return { type: "solid", angle: 150, stops: [{ c: colour, p: 0 }, { c: colour, p: 100 }], anim: "none", speed: 1 };
103
+ }
104
+ function mixHex(from, to, weight) {
105
+ const channels = (hex) => [1, 3, 5].map((at) => parseInt(hex.slice(at, at + 2), 16));
106
+ const a = channels(isHex(from) ? from : FALLBACK);
107
+ const b = channels(isHex(to) ? to : FALLBACK);
108
+ const w = Math.max(0, Math.min(1, weight));
109
+ return `#${a.map((value, i) => Math.round(value + (b[i] - value) * w).toString(16).padStart(2, "0")).join("")}`;
110
+ }
111
+ function parsePaint(text) {
112
+ const raw = String(text ?? "").trim();
113
+ if (raw === "" || isHex(raw)) return solidOf(raw);
114
+ const [body, animPart] = raw.split("~");
115
+ const parts = body.split("|").map((part) => part.trim()).filter((part) => part !== "");
116
+ const [typeWord, angleWord] = (parts[0] ?? "").split(/\s+/);
117
+ const stops = parts.slice(1).map((part) => {
118
+ const [c, p] = part.split(/\s+/);
119
+ return { c: String(c ?? "").toLowerCase(), p: Number(p) };
120
+ }).filter((stop) => isHex(stop.c) && Number.isFinite(stop.p));
121
+ if (stops.length < 2) return solidOf(stops[0]?.c ?? FALLBACK);
122
+ const type = TYPES.find((each) => each === typeWord) ?? "linear";
123
+ let anim = "none";
124
+ let speed = 1;
125
+ if (animPart) {
126
+ const [animWord, secondsWord] = animPart.trim().split(/\s+/);
127
+ anim = ANIMS.find((each) => each === animWord) ?? "none";
128
+ const at = SPEEDS.indexOf(Number(secondsWord));
129
+ speed = at < 0 ? 1 : at;
130
+ }
131
+ return { type, angle: Number(angleWord) || 0, stops, anim, speed };
132
+ }
133
+ const byPosition = (stops) => [...stops].sort((a, b) => a.p - b.p);
134
+ function printPaint(paint) {
135
+ if (paint.type === "solid") return paint.stops[0].c;
136
+ const stops = byPosition(paint.stops).map((stop) => `${stop.c} ${Math.round(stop.p)}`).join(" | ");
137
+ const anim = paint.anim !== "none" ? ` ~${paint.anim} ${SPEEDS[paint.speed] ?? SPEEDS[1]}` : "";
138
+ return `${paint.type} ${Math.round(paint.angle)} | ${stops}${anim}`;
139
+ }
140
+ function paintMain(text) {
141
+ const paint = parsePaint(text);
142
+ let best = paint.stops[0].c;
143
+ let score = -1;
144
+ for (const stop of paint.stops) {
145
+ const hsl = hexToHsl(stop.c);
146
+ const vivid = hsl.s * (1 - Math.abs(hsl.l - 50) / 50);
147
+ if (vivid > score) {
148
+ score = vivid;
149
+ best = stop.c;
150
+ }
151
+ }
152
+ return best;
153
+ }
154
+ function paintBase(text) {
155
+ return parsePaint(text).stops[0].c;
156
+ }
157
+ function paintCss(value) {
158
+ const paint = typeof value === "object" && value !== null ? value : parsePaint(String(value ?? ""));
159
+ if (paint.type === "solid") return { background: paint.stops[0].c, backgroundSize: "", animation: "" };
160
+ const list = byPosition(paint.stops).map((stop) => `${stop.c} ${stop.p}%`).join(", ");
161
+ const angle = paint.anim === "spin" ? `calc(var(--pk-angle) + ${paint.angle}deg)` : `${paint.angle}deg`;
162
+ const background = paint.type === "radial" ? `radial-gradient(120% 90% at 50% 25%, ${list})` : paint.type === "conic" ? `conic-gradient(from ${angle} at 50% 40%, ${list})` : `linear-gradient(${angle}, ${list})`;
163
+ const seconds = SPEEDS[paint.speed] ?? SPEEDS[1];
164
+ const animation = {
165
+ none: "",
166
+ flow: `pk-flow ${seconds}s ease-in-out infinite`,
167
+ breathe: `pk-breathe ${seconds / 2}s ease-in-out infinite`,
168
+ spin: `pk-spin ${seconds}s linear infinite`,
169
+ hue: `pk-hue ${seconds * 1.5}s linear infinite`
170
+ }[paint.anim] ?? "";
171
+ return { background, backgroundSize: paint.anim === "flow" ? "220% 220%" : "", animation };
172
+ }
173
+ function barCss(paint) {
174
+ return `linear-gradient(90deg, ${byPosition(paint.stops).map((stop) => `${stop.c} ${stop.p}%`).join(", ")})`;
175
+ }
176
+ function paintBg(text) {
177
+ return paintCss(text).background;
178
+ }
179
+ function paintSize(text) {
180
+ return paintCss(text).backgroundSize || "auto";
181
+ }
182
+ function paintAnim(text) {
183
+ return paintCss(text).animation || "none";
184
+ }
185
+ const PRESETS = {
186
+ sunset: "linear 160 | #ff8a5b 0 | #f5583f 60 | #c0392b 100",
187
+ grape: "linear 160 | #b06de8 0 | #7b5cf0 60 | #4b3bd6 100",
188
+ ocean: "linear 160 | #4fd1c5 0 | #3182ce 70 | #2c5282 100",
189
+ mint: "linear 160 | #a7f3d0 0 | #34d399 70 | #059669 100",
190
+ peach: "linear 160 | #ffd9a0 0 | #ff9a8b 60 | #e0567f 100",
191
+ night: "linear 160 | #3b3a52 0 | #1f1d2e 70 | #0f0e17 100",
192
+ aurora: "linear 165 | #6B46F2 0 | #ff5ca8 60 | #ff9a5b 92 | #ffd0a8 100",
193
+ dusk: "linear 170 | #241a5e 0 | #a84bc0 60 | #e06a8a 82 | #ff9a6b 100",
194
+ reef: "linear 168 | #00d0b0 0 | #1ba0e0 38 | #2b6cff 70 | #6B46F2 100",
195
+ blush: "linear 150 | #ffe0b0 0 | #ff9ec4 42 | #c86bff 100",
196
+ citrus: "linear 135 | #ffe14d 0 | #ff8a3d 42 | #ff5a8a 100",
197
+ berry: "linear 140 | #ff4d9d 0 | #a44bff 48 | #4b6bff 100",
198
+ lime: "linear 140 | #bff24a 0 | #34d399 48 | #00b8c2 100"
199
+ };
200
+ function legacyPaint(grad, mode, dir, colour) {
201
+ if (mode !== "custom" && PRESETS[grad]) return `${PRESETS[grad]} ~flow ${SPEEDS[1]}`;
202
+ const base = isHex(colour) ? colour.toLowerCase() : FALLBACK;
203
+ const light = mixHex(base, "#ffffff", 0.38);
204
+ const dark = mixHex(base, "#000000", 0.22);
205
+ if (dir === "radial") return `radial 0 | ${light} 0 | ${base} 62 | ${dark} 100`;
206
+ return `linear ${dir === "up" ? 0 : 180} | ${light} 0 | ${base} 55 | ${dark} 100`;
207
+ }
208
+ return __toCommonJS(paint_exports);
209
+ })();
210
+
211
+ export default PAINT;