image-to-pixel 3.0.0 → 3.0.1

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 (68) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +69 -69
  3. package/dist/editor.css.map +1 -1
  4. package/dist/editor.esm.js +1 -1
  5. package/dist/editor.esm.js.map +1 -1
  6. package/dist/fonts/README.md +6 -6
  7. package/dist/fonts/geist-LICENSE.txt +93 -93
  8. package/dist/image-to-pixel.bundle.esm.js +13 -13
  9. package/dist/image-to-pixel.bundle.esm.js.map +2 -2
  10. package/dist/image-to-pixel.cjs +2 -2
  11. package/dist/image-to-pixel.cjs.map +2 -2
  12. package/dist/image-to-pixel.esm.js +2 -2
  13. package/dist/image-to-pixel.esm.js.map +2 -2
  14. package/dist/image-to-pixel.js +2 -2
  15. package/dist/image-to-pixel.js.map +2 -2
  16. package/dist/image-to-pixel.min.js +11 -11
  17. package/dist/image-to-pixel.min.js.map +2 -2
  18. package/dist/video.esm.js +1 -1
  19. package/dist/video.esm.js.map +1 -1
  20. package/lib/core/color.js +69 -69
  21. package/lib/core/cpu/pipeline.js +21 -21
  22. package/lib/core/cpu/stages.js +227 -227
  23. package/lib/core/dither/bluenoise-data.js +3 -3
  24. package/lib/core/dither/kernels.js +27 -27
  25. package/lib/core/dither/matrices.js +95 -95
  26. package/lib/core/engine.js +158 -158
  27. package/lib/core/export/canvas.js +22 -22
  28. package/lib/core/export/crc32.js +15 -15
  29. package/lib/core/export/gif.js +153 -153
  30. package/lib/core/export/png.js +94 -94
  31. package/lib/core/export/scale.js +103 -103
  32. package/lib/core/export/sheet.js +95 -95
  33. package/lib/core/export/zip.js +55 -55
  34. package/lib/core/gpu/backend.js +159 -159
  35. package/lib/core/gpu/shaders.js +258 -258
  36. package/lib/core/palette/builtin.js +36 -36
  37. package/lib/core/palette/extract.js +69 -69
  38. package/lib/core/palette/lospec.js +24 -24
  39. package/lib/core/palette/parse.js +112 -112
  40. package/lib/core/plan.js +58 -58
  41. package/lib/core/presets.js +42 -42
  42. package/lib/core/settings.js +274 -274
  43. package/lib/core/source.js +89 -89
  44. package/lib/core/video/frames.js +144 -144
  45. package/lib/core/video/mux-mp4.js +131 -131
  46. package/lib/core/video/mux-webm.js +127 -127
  47. package/lib/editor/controls.js +139 -139
  48. package/lib/editor/dom.js +65 -65
  49. package/lib/editor/exporter.js +185 -185
  50. package/lib/editor/icons.js +61 -61
  51. package/lib/editor/index.js +793 -793
  52. package/lib/editor/inspector.js +328 -328
  53. package/lib/editor/library.js +182 -182
  54. package/lib/editor/persist.js +18 -18
  55. package/lib/editor/samples.js +90 -90
  56. package/lib/editor/stage.js +239 -239
  57. package/lib/editor/store.js +80 -80
  58. package/lib/editor/styles/editor.css +385 -385
  59. package/lib/editor/styles/fonts/README.md +6 -6
  60. package/lib/editor/styles/fonts/geist-LICENSE.txt +93 -93
  61. package/lib/editor/styles/tokens.css +89 -89
  62. package/lib/editor/ui.js +97 -97
  63. package/lib/index.js +3 -1
  64. package/lib/video.js +92 -92
  65. package/package.json +92 -92
  66. package/types/editor.d.ts +34 -34
  67. package/types/index.d.ts +196 -196
  68. package/types/video.d.ts +43 -43
@@ -1,139 +1,139 @@
1
- // Inspector controls. Each returns { el, set(value) } so the inspector can sync from state
2
- // without rebuilding (which would steal focus mid-drag).
3
-
4
- import { h, nextId } from './dom.js';
5
- import { icon } from './icons.js';
6
-
7
- /**
8
- * Collapsible inspector section: a header button (works the same in every browser, unlike
9
- * <details>) plus an optional reset icon that appears when the section differs from its defaults.
10
- * @param {string} title
11
- * @param {{ open?: boolean, onToggle?: (open: boolean) => void, onReset?: () => void, resetLabel?: string }} opts
12
- */
13
- export function section(title, opts = {}, ...children) {
14
- const id = nextId('sec');
15
- const hintEl = h('span.hint', {});
16
- const toggle = h('button.itp-sec-toggle', { type: 'button', 'aria-controls': id },
17
- h('span.title', { text: title }), hintEl, h('span.chev', { html: icon('chevron') }));
18
- const reset = opts.onReset ? h('button.itp-btn.small.icon.itp-sec-reset', {
19
- type: 'button', hidden: true, title: opts.resetLabel || `Reset ${title.toLowerCase()}`, 'aria-label': opts.resetLabel || `Reset ${title.toLowerCase()} to defaults`,
20
- html: icon('reset'), onclick: () => opts.onReset(),
21
- }) : null;
22
- const body = h('div.body', { id }, ...children);
23
- const el = h('section.itp-sec', {}, h('div.itp-sec-head', {}, toggle, reset), body);
24
- const setOpen = open => {
25
- el.dataset.open = String(open);
26
- toggle.setAttribute('aria-expanded', String(open));
27
- body.hidden = !open;
28
- };
29
- toggle.addEventListener('click', () => { const open = el.dataset.open !== 'true'; setOpen(open); opts.onToggle?.(open); });
30
- setOpen(opts.open !== false);
31
- return {
32
- el, body,
33
- setHint(t) { hintEl.textContent = t; },
34
- /** Show the reset icon when the section's settings differ from the defaults. */
35
- setChanged(changed) { if (reset) reset.hidden = !changed; },
36
- setOpen,
37
- get open() { return el.dataset.open === 'true'; },
38
- };
39
- }
40
-
41
- /** Labelled field wrapper. */
42
- export function field(label, control, { help = '', value = null, id } = {}) {
43
- const lab = h('div.lab', {}, h('label', { for: id || control.id || null, text: label }), value);
44
- return h('div.itp-field', {}, lab, control, help ? h('div.help', { text: help }) : null);
45
- }
46
-
47
- /**
48
- * Slider + number box.
49
- * @param {{ label: string, min: number, max: number, step?: number, value: number, unit?: string,
50
- * scale?: number, help?: string, onInput: (v:number)=>void, onCommit?: ()=>void }} o
51
- * scale: display multiplier (0..1 shown as 0..100 with scale 100)
52
- */
53
- export function slider(o) {
54
- const id = nextId('sl');
55
- const k = o.scale || 1;
56
- const step = o.step || 1;
57
- const range = h('input', { type: 'range', id, min: o.min * k, max: o.max * k, step: step * k, 'aria-label': o.label });
58
- const num = h('input.itp-input', { type: 'number', min: o.min * k, max: o.max * k, step: step * k, 'aria-label': `${o.label} value` });
59
- const paint = () => { const p = (range.value - range.min) / (range.max - range.min) * 100; range.style.setProperty('--p', `${p}%`); };
60
- const set = v => { const d = round(v * k, step * k); range.value = d; num.value = d; paint(); };
61
- range.addEventListener('input', () => { num.value = range.value; paint(); o.onInput(Number(range.value) / k); });
62
- range.addEventListener('change', () => o.onCommit && o.onCommit());
63
- num.addEventListener('change', () => {
64
- const v = Math.min(o.max * k, Math.max(o.min * k, Number(num.value) || 0));
65
- set(v / k); o.onInput(v / k); o.onCommit && o.onCommit();
66
- });
67
- range.addEventListener('dblclick', () => { if (o.reset != null) { set(o.reset); o.onInput(o.reset); o.onCommit && o.onCommit(); } });
68
- set(o.value);
69
- const el = field(o.label, h('div.itp-slider', {}, range, num), { help: o.help, id });
70
- return { el, set, input: range };
71
- }
72
-
73
- const round = (v, step) => {
74
- const d = Math.max(0, -Math.floor(Math.log10(step) + 1e-9));
75
- return Number((Math.round(v / step) * step).toFixed(d));
76
- };
77
-
78
- /** <select> with options [{ value, label, group? }]. */
79
- export function select(o) {
80
- const id = nextId('se');
81
- const el = h('select.itp-select', { id, 'aria-label': o.label });
82
- const groups = new Map();
83
- for (const opt of o.options) {
84
- const node = h('option', { value: opt.value, text: opt.label });
85
- if (opt.group) {
86
- if (!groups.has(opt.group)) { const g = h('optgroup', { label: opt.group }); groups.set(opt.group, g); el.append(g); }
87
- groups.get(opt.group).append(node);
88
- } else el.append(node);
89
- }
90
- el.addEventListener('change', () => o.onChange(el.value));
91
- const wrapped = o.bare ? el : field(o.label, el, { help: o.help, id });
92
- return { el: wrapped, select: el, set(v) { el.value = v; } };
93
- }
94
-
95
- /** Segmented toggle [{ value, label, icon?, title? }]. */
96
- export function segmented(o) {
97
- const btns = o.options.map(opt => h('button', {
98
- type: 'button', 'aria-pressed': 'false', title: opt.title || null,
99
- html: (opt.icon ? icon(opt.icon) : '') + (opt.label ? `<span>${opt.label}</span>` : ''),
100
- onclick: () => o.onChange(opt.value),
101
- }));
102
- const seg = h('div.itp-seg', { role: 'group', 'aria-label': o.label }, ...btns);
103
- const set = v => btns.forEach((b, i) => b.setAttribute('aria-pressed', String(o.options[i].value === v)));
104
- set(o.value);
105
- const el = o.bare ? seg : field(o.label, seg, { help: o.help });
106
- return { el, set };
107
- }
108
-
109
- /** On/off switch. */
110
- export function toggle(o) {
111
- const input = h('input', { type: 'checkbox', role: 'switch' });
112
- input.addEventListener('change', () => o.onChange(input.checked));
113
- const lab = h('label.itp-switch', {}, h('span', { text: o.label }), input);
114
- const el = o.help ? h('div.itp-field', {}, lab, h('div.help', { text: o.help })) : lab;
115
- return { el, set(v) { input.checked = !!v; } };
116
- }
117
-
118
- /** Colour input with hex readout. */
119
- export function colorInput(o) {
120
- const input = h('input', { type: 'color', 'aria-label': o.label });
121
- const hex = h('span.mono', {});
122
- input.addEventListener('input', () => { hex.textContent = input.value; o.onChange(input.value); });
123
- const box = h('label.itp-color', {}, input, hex);
124
- const set = v => { input.value = v; hex.textContent = v; };
125
- set(o.value || '#000000');
126
- return { el: o.bare ? box : field(o.label, box), set };
127
- }
128
-
129
- /** Number box (no slider). */
130
- export function numberInput(o) {
131
- const id = nextId('nu');
132
- const input = h('input.itp-input.num', { type: 'number', id, min: o.min, max: o.max, step: o.step || 1, 'aria-label': o.label });
133
- input.addEventListener('change', () => {
134
- const v = Math.min(o.max, Math.max(o.min, Number(input.value) || o.min));
135
- if (input.value !== '' || !input.placeholder) input.value = v;
136
- o.onChange(v);
137
- });
138
- return { el: o.bare ? input : field(o.label, input, { help: o.help, id }), input, set(v) { input.value = v; } };
139
- }
1
+ // Inspector controls. Each returns { el, set(value) } so the inspector can sync from state
2
+ // without rebuilding (which would steal focus mid-drag).
3
+
4
+ import { h, nextId } from './dom.js';
5
+ import { icon } from './icons.js';
6
+
7
+ /**
8
+ * Collapsible inspector section: a header button (works the same in every browser, unlike
9
+ * <details>) plus an optional reset icon that appears when the section differs from its defaults.
10
+ * @param {string} title
11
+ * @param {{ open?: boolean, onToggle?: (open: boolean) => void, onReset?: () => void, resetLabel?: string }} opts
12
+ */
13
+ export function section(title, opts = {}, ...children) {
14
+ const id = nextId('sec');
15
+ const hintEl = h('span.hint', {});
16
+ const toggle = h('button.itp-sec-toggle', { type: 'button', 'aria-controls': id },
17
+ h('span.title', { text: title }), hintEl, h('span.chev', { html: icon('chevron') }));
18
+ const reset = opts.onReset ? h('button.itp-btn.small.icon.itp-sec-reset', {
19
+ type: 'button', hidden: true, title: opts.resetLabel || `Reset ${title.toLowerCase()}`, 'aria-label': opts.resetLabel || `Reset ${title.toLowerCase()} to defaults`,
20
+ html: icon('reset'), onclick: () => opts.onReset(),
21
+ }) : null;
22
+ const body = h('div.body', { id }, ...children);
23
+ const el = h('section.itp-sec', {}, h('div.itp-sec-head', {}, toggle, reset), body);
24
+ const setOpen = open => {
25
+ el.dataset.open = String(open);
26
+ toggle.setAttribute('aria-expanded', String(open));
27
+ body.hidden = !open;
28
+ };
29
+ toggle.addEventListener('click', () => { const open = el.dataset.open !== 'true'; setOpen(open); opts.onToggle?.(open); });
30
+ setOpen(opts.open !== false);
31
+ return {
32
+ el, body,
33
+ setHint(t) { hintEl.textContent = t; },
34
+ /** Show the reset icon when the section's settings differ from the defaults. */
35
+ setChanged(changed) { if (reset) reset.hidden = !changed; },
36
+ setOpen,
37
+ get open() { return el.dataset.open === 'true'; },
38
+ };
39
+ }
40
+
41
+ /** Labelled field wrapper. */
42
+ export function field(label, control, { help = '', value = null, id } = {}) {
43
+ const lab = h('div.lab', {}, h('label', { for: id || control.id || null, text: label }), value);
44
+ return h('div.itp-field', {}, lab, control, help ? h('div.help', { text: help }) : null);
45
+ }
46
+
47
+ /**
48
+ * Slider + number box.
49
+ * @param {{ label: string, min: number, max: number, step?: number, value: number, unit?: string,
50
+ * scale?: number, help?: string, onInput: (v:number)=>void, onCommit?: ()=>void }} o
51
+ * scale: display multiplier (0..1 shown as 0..100 with scale 100)
52
+ */
53
+ export function slider(o) {
54
+ const id = nextId('sl');
55
+ const k = o.scale || 1;
56
+ const step = o.step || 1;
57
+ const range = h('input', { type: 'range', id, min: o.min * k, max: o.max * k, step: step * k, 'aria-label': o.label });
58
+ const num = h('input.itp-input', { type: 'number', min: o.min * k, max: o.max * k, step: step * k, 'aria-label': `${o.label} value` });
59
+ const paint = () => { const p = (range.value - range.min) / (range.max - range.min) * 100; range.style.setProperty('--p', `${p}%`); };
60
+ const set = v => { const d = round(v * k, step * k); range.value = d; num.value = d; paint(); };
61
+ range.addEventListener('input', () => { num.value = range.value; paint(); o.onInput(Number(range.value) / k); });
62
+ range.addEventListener('change', () => o.onCommit && o.onCommit());
63
+ num.addEventListener('change', () => {
64
+ const v = Math.min(o.max * k, Math.max(o.min * k, Number(num.value) || 0));
65
+ set(v / k); o.onInput(v / k); o.onCommit && o.onCommit();
66
+ });
67
+ range.addEventListener('dblclick', () => { if (o.reset != null) { set(o.reset); o.onInput(o.reset); o.onCommit && o.onCommit(); } });
68
+ set(o.value);
69
+ const el = field(o.label, h('div.itp-slider', {}, range, num), { help: o.help, id });
70
+ return { el, set, input: range };
71
+ }
72
+
73
+ const round = (v, step) => {
74
+ const d = Math.max(0, -Math.floor(Math.log10(step) + 1e-9));
75
+ return Number((Math.round(v / step) * step).toFixed(d));
76
+ };
77
+
78
+ /** <select> with options [{ value, label, group? }]. */
79
+ export function select(o) {
80
+ const id = nextId('se');
81
+ const el = h('select.itp-select', { id, 'aria-label': o.label });
82
+ const groups = new Map();
83
+ for (const opt of o.options) {
84
+ const node = h('option', { value: opt.value, text: opt.label });
85
+ if (opt.group) {
86
+ if (!groups.has(opt.group)) { const g = h('optgroup', { label: opt.group }); groups.set(opt.group, g); el.append(g); }
87
+ groups.get(opt.group).append(node);
88
+ } else el.append(node);
89
+ }
90
+ el.addEventListener('change', () => o.onChange(el.value));
91
+ const wrapped = o.bare ? el : field(o.label, el, { help: o.help, id });
92
+ return { el: wrapped, select: el, set(v) { el.value = v; } };
93
+ }
94
+
95
+ /** Segmented toggle [{ value, label, icon?, title? }]. */
96
+ export function segmented(o) {
97
+ const btns = o.options.map(opt => h('button', {
98
+ type: 'button', 'aria-pressed': 'false', title: opt.title || null,
99
+ html: (opt.icon ? icon(opt.icon) : '') + (opt.label ? `<span>${opt.label}</span>` : ''),
100
+ onclick: () => o.onChange(opt.value),
101
+ }));
102
+ const seg = h('div.itp-seg', { role: 'group', 'aria-label': o.label }, ...btns);
103
+ const set = v => btns.forEach((b, i) => b.setAttribute('aria-pressed', String(o.options[i].value === v)));
104
+ set(o.value);
105
+ const el = o.bare ? seg : field(o.label, seg, { help: o.help });
106
+ return { el, set };
107
+ }
108
+
109
+ /** On/off switch. */
110
+ export function toggle(o) {
111
+ const input = h('input', { type: 'checkbox', role: 'switch' });
112
+ input.addEventListener('change', () => o.onChange(input.checked));
113
+ const lab = h('label.itp-switch', {}, h('span', { text: o.label }), input);
114
+ const el = o.help ? h('div.itp-field', {}, lab, h('div.help', { text: o.help })) : lab;
115
+ return { el, set(v) { input.checked = !!v; } };
116
+ }
117
+
118
+ /** Colour input with hex readout. */
119
+ export function colorInput(o) {
120
+ const input = h('input', { type: 'color', 'aria-label': o.label });
121
+ const hex = h('span.mono', {});
122
+ input.addEventListener('input', () => { hex.textContent = input.value; o.onChange(input.value); });
123
+ const box = h('label.itp-color', {}, input, hex);
124
+ const set = v => { input.value = v; hex.textContent = v; };
125
+ set(o.value || '#000000');
126
+ return { el: o.bare ? box : field(o.label, box), set };
127
+ }
128
+
129
+ /** Number box (no slider). */
130
+ export function numberInput(o) {
131
+ const id = nextId('nu');
132
+ const input = h('input.itp-input.num', { type: 'number', id, min: o.min, max: o.max, step: o.step || 1, 'aria-label': o.label });
133
+ input.addEventListener('change', () => {
134
+ const v = Math.min(o.max, Math.max(o.min, Number(input.value) || o.min));
135
+ if (input.value !== '' || !input.placeholder) input.value = v;
136
+ o.onChange(v);
137
+ });
138
+ return { el: o.bare ? input : field(o.label, input, { help: o.help, id }), input, set(v) { input.value = v; } };
139
+ }
package/lib/editor/dom.js CHANGED
@@ -1,65 +1,65 @@
1
- // Tiny DOM helpers. No framework: the editor is small enough to wire by hand.
2
-
3
- /** Create an element: h('button.itp-btn.icon', { onclick, title }, child…) */
4
- export function h(tag, props = {}, ...children) {
5
- const [name, ...classes] = tag.split('.');
6
- const el = document.createElement(name || 'div');
7
- if (classes.length) el.className = classes.join(' ');
8
- for (const [k, v] of Object.entries(props || {})) {
9
- if (v == null || v === false) continue;
10
- if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2), v);
11
- else if (k === 'html') el.innerHTML = v;
12
- else if (k === 'text') el.textContent = v;
13
- else if (k === 'style' && typeof v === 'object') Object.assign(el.style, v);
14
- else if (k === 'dataset') Object.assign(el.dataset, v);
15
- else if (k in el && typeof v !== 'string' && k !== 'list') el[k] = v;
16
- else el.setAttribute(k, v === true ? '' : v);
17
- }
18
- append(el, children);
19
- return el;
20
- }
21
-
22
- export function append(el, children) {
23
- for (const c of children.flat(Infinity)) {
24
- if (c == null || c === false) continue;
25
- el.append(c instanceof Node ? c : document.createTextNode(String(c)));
26
- }
27
- return el;
28
- }
29
-
30
- /** Replace all children. */
31
- export function render(el, ...children) { el.replaceChildren(); return append(el, children); }
32
-
33
- export const $ = (sel, root = document) => root.querySelector(sel);
34
-
35
- /** Element from an HTML string (single root). */
36
- export function html(str) {
37
- const t = document.createElement('template');
38
- t.innerHTML = str.trim();
39
- return t.content.firstElementChild;
40
- }
41
-
42
- export const esc = s => String(s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
43
-
44
- let uid = 0;
45
- export const nextId = (p = 'itp') => `${p}-${++uid}`;
46
-
47
- /** Debounce helper. */
48
- export function debounce(fn, ms) {
49
- let t;
50
- return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); };
51
- }
52
-
53
- /** Read a File as text. */
54
- export const readText = file => file.text();
55
-
56
- /** Open a file picker and resolve with the chosen files. */
57
- export function pickFiles({ accept = '', multiple = false } = {}) {
58
- return new Promise(res => {
59
- const input = h('input', { type: 'file', accept, multiple, style: { display: 'none' } });
60
- input.addEventListener('change', () => { res([...input.files]); input.remove(); });
61
- input.addEventListener('cancel', () => { res([]); input.remove(); });
62
- document.body.append(input);
63
- input.click();
64
- });
65
- }
1
+ // Tiny DOM helpers. No framework: the editor is small enough to wire by hand.
2
+
3
+ /** Create an element: h('button.itp-btn.icon', { onclick, title }, child…) */
4
+ export function h(tag, props = {}, ...children) {
5
+ const [name, ...classes] = tag.split('.');
6
+ const el = document.createElement(name || 'div');
7
+ if (classes.length) el.className = classes.join(' ');
8
+ for (const [k, v] of Object.entries(props || {})) {
9
+ if (v == null || v === false) continue;
10
+ if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2), v);
11
+ else if (k === 'html') el.innerHTML = v;
12
+ else if (k === 'text') el.textContent = v;
13
+ else if (k === 'style' && typeof v === 'object') Object.assign(el.style, v);
14
+ else if (k === 'dataset') Object.assign(el.dataset, v);
15
+ else if (k in el && typeof v !== 'string' && k !== 'list') el[k] = v;
16
+ else el.setAttribute(k, v === true ? '' : v);
17
+ }
18
+ append(el, children);
19
+ return el;
20
+ }
21
+
22
+ export function append(el, children) {
23
+ for (const c of children.flat(Infinity)) {
24
+ if (c == null || c === false) continue;
25
+ el.append(c instanceof Node ? c : document.createTextNode(String(c)));
26
+ }
27
+ return el;
28
+ }
29
+
30
+ /** Replace all children. */
31
+ export function render(el, ...children) { el.replaceChildren(); return append(el, children); }
32
+
33
+ export const $ = (sel, root = document) => root.querySelector(sel);
34
+
35
+ /** Element from an HTML string (single root). */
36
+ export function html(str) {
37
+ const t = document.createElement('template');
38
+ t.innerHTML = str.trim();
39
+ return t.content.firstElementChild;
40
+ }
41
+
42
+ export const esc = s => String(s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
43
+
44
+ let uid = 0;
45
+ export const nextId = (p = 'itp') => `${p}-${++uid}`;
46
+
47
+ /** Debounce helper. */
48
+ export function debounce(fn, ms) {
49
+ let t;
50
+ return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); };
51
+ }
52
+
53
+ /** Read a File as text. */
54
+ export const readText = file => file.text();
55
+
56
+ /** Open a file picker and resolve with the chosen files. */
57
+ export function pickFiles({ accept = '', multiple = false } = {}) {
58
+ return new Promise(res => {
59
+ const input = h('input', { type: 'file', accept, multiple, style: { display: 'none' } });
60
+ input.addEventListener('change', () => { res([...input.files]); input.remove(); });
61
+ input.addEventListener('cancel', () => { res([]); input.remove(); });
62
+ document.body.append(input);
63
+ input.click();
64
+ });
65
+ }