@divlet/web 0.0.0-stage → 0.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 (44) hide show
  1. package/README.md +12 -2
  2. package/dist/LICENSE +21 -0
  3. package/dist/board.css +948 -0
  4. package/dist/board.js +427 -0
  5. package/dist/index.d.ts +4 -0
  6. package/dist/index.html +73 -0
  7. package/dist/index.js +3 -0
  8. package/dist/lib/actions.js +472 -0
  9. package/dist/lib/activity.js +311 -0
  10. package/dist/lib/artboards.js +56 -0
  11. package/dist/lib/ask.js +350 -0
  12. package/dist/lib/attrs.js +144 -0
  13. package/dist/lib/board-host.d.ts +14 -0
  14. package/dist/lib/board-host.js +2 -0
  15. package/dist/lib/board-session.d.ts +53 -0
  16. package/dist/lib/board-session.js +24 -0
  17. package/dist/lib/canvas.js +1594 -0
  18. package/dist/lib/chat.js +156 -0
  19. package/dist/lib/clip.js +231 -0
  20. package/dist/lib/crumbbar.js +56 -0
  21. package/dist/lib/crumbs.js +63 -0
  22. package/dist/lib/css.js +238 -0
  23. package/dist/lib/expandbar.js +64 -0
  24. package/dist/lib/file-picker.js +103 -0
  25. package/dist/lib/help.js +273 -0
  26. package/dist/lib/host.js +16 -0
  27. package/dist/lib/http-services.js +24 -0
  28. package/dist/lib/http-session.js +45 -0
  29. package/dist/lib/http.js +17 -0
  30. package/dist/lib/inspector.js +857 -0
  31. package/dist/lib/layers.js +298 -0
  32. package/dist/lib/menu.js +119 -0
  33. package/dist/lib/palette.js +292 -0
  34. package/dist/lib/panels.js +27 -0
  35. package/dist/lib/project.js +178 -0
  36. package/dist/lib/resize.js +114 -0
  37. package/dist/lib/shortcuts.js +91 -0
  38. package/dist/lib/sidebar.js +85 -0
  39. package/dist/lib/store.js +249 -0
  40. package/dist/lib/suggest.js +130 -0
  41. package/dist/lib/toolbar.js +54 -0
  42. package/dist/lib/typography.js +120 -0
  43. package/dist/lib/util.js +177 -0
  44. package/package.json +37 -4
@@ -0,0 +1,120 @@
1
+ // Native font-family inspection/editing. CSSOM is a view of authored CSS, not a project validator or font-byte store.
2
+ import { el, icon } from './util.js';
3
+ import { openMenu } from './menu.js';
4
+
5
+ const GENERICS = ['system-ui', 'sans-serif', 'serif', 'monospace'];
6
+ function familyStyle(value) {
7
+ const style = document.createElement('span').style;
8
+ style.setProperty('font-family', value);
9
+ return style;
10
+ }
11
+ function parts(value) {
12
+ const result = [];
13
+ let start = 0, quote = '', depth = 0, escaped = false;
14
+ for (let i = 0; i < value.length; i++) {
15
+ const char = value[i];
16
+ if (escaped) { escaped = false; continue; }
17
+ if (char === '\\') { escaped = true; continue; }
18
+ if (quote) { if (char === quote) quote = ''; continue; }
19
+ if (char === '"' || char === "'") quote = char;
20
+ else if (char === '(') depth++;
21
+ else if (char === ')') depth--;
22
+ else if (char === ',' && depth === 0) { result.push(value.slice(start, i).trim()); start = i + 1; }
23
+ }
24
+ result.push(value.slice(start).trim());
25
+ return result.filter(Boolean);
26
+ }
27
+ const key = value => value.toLowerCase();
28
+ const canonical = value => familyStyle(value).getPropertyValue('font-family');
29
+
30
+ /** List literal face-family declarations from authored project/frame sheets; never fetch URLs or inspect font payloads. */
31
+ export function declaredFamilies(sources) {
32
+ const families = new Map();
33
+ const walk = rules => {
34
+ for (const rule of rules) {
35
+ if (rule.type === CSSRule.FONT_FACE_RULE) {
36
+ const family = rule.style.getPropertyValue('font-family');
37
+ if (family && parts(family).length === 1 && (/^["']/.test(family) || !family.includes('('))) families.set(key(family), family);
38
+ } else if (rule.cssRules) walk(rule.cssRules);
39
+ }
40
+ };
41
+ for (const css of sources) {
42
+ const sheet = new CSSStyleSheet();
43
+ sheet.replaceSync(css || '');
44
+ walk(sheet.cssRules);
45
+ }
46
+ return [...families.values()];
47
+ }
48
+
49
+ /** Native inline edits retain classes, priority and literal fallback stacks; ambiguous shorthand edits refuse. */
50
+ export function familyPatch(node, next, families) {
51
+ if (['font', 'all'].some(name => Object.hasOwn(node.style, name))) throw new Error('Edit the inline font/all shorthand before choosing a family; its other settings must be preserved.');
52
+ if (next === null) return { 'font-family': null };
53
+ const current = document.createElement('span').style;
54
+ current.cssText = `font-family:${node.style['font-family'] || ''}`;
55
+ let value = next;
56
+ if (!GENERICS.includes(next)) {
57
+ const authored = current.getPropertyValue('font-family');
58
+ const stack = /^(inherit|initial|unset|revert|revert-layer)$/i.test(authored) ? [] : parts(authored);
59
+ if (stack.some(item => !/^["']/.test(item) && item.includes('('))) throw new Error('The font family uses a dynamic CSS value. Choose a generic family or edit its fallback explicitly.');
60
+ const declared = new Set(families.map(key));
61
+ const fallback = declared.has(key(stack[0] || '')) ? stack.slice(1) : stack;
62
+ value = [next, ...fallback.filter(item => key(item) !== key(next))].join(', ');
63
+ }
64
+ return { 'font-family': value + (current.getPropertyPriority('font-family') ? ' !important' : '') };
65
+ }
66
+
67
+ export function fontFamilyControl(nodes, { css, apply, upload }) {
68
+ const families = declaredFamilies(css);
69
+ const values = nodes.map(node => ['font', 'all'].some(name => Object.hasOwn(node.style, name)) ? 'Font shorthand' : node.style['font-family'] ?? null);
70
+ const mixed = !values.every(value => value === values[0]);
71
+ const value = values[0], primary = value ? parts(canonical(value.replace(/\s*!important\s*$/i, '')))[0] : null;
72
+ const shown = mixed ? 'Mixed' : value === 'Font shorthand' ? 'Inline shorthand' : primary || value || 'Default';
73
+ // A rare family longer than the viewport is scrollable as one whole control, never ellipsized or squashed.
74
+ const scroll = el('div', 'font-family-scroll');
75
+ scroll.tabIndex = 0;
76
+ scroll.setAttribute('role', 'group');
77
+ scroll.setAttribute('aria-label', 'Font family control');
78
+ scroll.addEventListener('keydown', event => {
79
+ if (event.target === scroll && ['ArrowLeft', 'ArrowRight', 'Home', 'End', ' '].includes(event.key)) event.stopPropagation();
80
+ });
81
+ const button = el('button', 'pick font-family');
82
+ button.type = 'button';
83
+ button.dataset.key = 'font-family';
84
+ button.dataset.tip = mixed ? 'Mixed authored font families' : value || 'Default · use stylesheet or inherited family';
85
+ button.setAttribute('aria-label', `Font family: ${shown}`);
86
+ button.setAttribute('aria-haspopup', 'menu');
87
+ button.append(el('span', 'pick-k', 'Family'), el('span', `pick-v${!value || mixed ? ' faint' : ''}`, shown), icon('chevron', 'icon pick-caret'));
88
+ const choose = next => apply(classes => classes, next === null ? 'default font family' : `font family ${next}`, { style: node => familyPatch(node, next, families) });
89
+ button.addEventListener('click', () => openMenu([
90
+ { label: 'Default (stylesheet / inherited)', checked: !mixed && value === null, run: () => choose(null) },
91
+ ...GENERICS.map(family => ({ label: family, checked: !mixed && key(primary || '') === family, run: () => choose(family) })),
92
+ ...(families.length ? ['-', ...families.map(family => ({ label: family, checked: !mixed && key(primary || '') === key(family), hint: 'Declared @font-face family; availability follows its CSS conditions', run: () => choose(family) }))] : []),
93
+ ], { anchor: button, label: 'Font family' }));
94
+ scroll.append(button);
95
+ const control = el('div', 'font-family-control');
96
+ const note = el('small', 'caption font-family-note', 'Edits set inline CSS; stylesheet priority still applies.');
97
+ note.id = 'font-family-cascade-note';
98
+ button.setAttribute('aria-describedby', note.id);
99
+ const row = el('div', 'font-family-row');
100
+ row.append(scroll);
101
+ if (upload) {
102
+ row.classList.add('with-upload');
103
+ const picker = document.createElement('input');
104
+ picker.type = 'file'; picker.accept = '.ttf,.otf,.woff,.woff2,font/ttf,font/otf,font/woff,font/woff2'; picker.hidden = true;
105
+ const add = el('button', 'tool mini font-upload');
106
+ add.type = 'button'; add.dataset.key = 'font-upload'; add.setAttribute('aria-label', 'Upload font');
107
+ add.dataset.tip = 'Add a TTF/OTF/WOFF or null-transform WOFF2 font to project CSS, then choose it in Family. Existing family declarations are never replaced.';
108
+ add.append(icon('upload'));
109
+ add.disabled = upload.pending; add.setAttribute('aria-busy', String(upload.pending));
110
+ add.addEventListener('click', () => picker.click());
111
+ picker.addEventListener('change', async () => {
112
+ const file = picker.files?.[0]; picker.value = '';
113
+ if (!file || upload.pending) return;
114
+ await upload.run(file);
115
+ });
116
+ row.append(add, picker);
117
+ }
118
+ control.append(row, note);
119
+ return control;
120
+ }
@@ -0,0 +1,177 @@
1
+ /** The interface size chosen in Settings (1 = 100%); popups placed from on-screen rectangles measure with it. */
2
+ export const uiScale = () => Number.parseFloat(document.documentElement.style.getPropertyValue('--ui')) || 1;
3
+ export const $ = (selector, root = document) => root.querySelector(selector);
4
+ export const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
5
+ export const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
6
+ export const round = (value, digits = 0) => { const f = 10 ** digits; return Math.round(value * f) / f; };
7
+
8
+ export function el(tag, className, content) {
9
+ const node = document.createElement(tag);
10
+ if (className) node.className = className;
11
+ if (content !== undefined) node.textContent = content;
12
+ return node;
13
+ }
14
+
15
+ const ICONS = {
16
+ select: '<path d="m5 3 11 7.5-5 1-2.5 5L5 3Z"/>',
17
+ inspect: '<path d="M3 7V3h4M13 3h4v4M17 13v4h-4M7 17H3v-4 M8.5 7.5h3.0a1.0 1.0 0 0 1 1.0 1.0v3.0a1.0 1.0 0 0 1 -1.0 1.0h-3.0a1.0 1.0 0 0 1 -1.0 -1.0v-3.0a1.0 1.0 0 0 1 1.0 -1.0Z"/>',
18
+ play: '<path d="M6.5 4.5v11l9-5.5-9-5.5Z"/>',
19
+ plus: '<path d="M10 4v12M4 10h12"/>',
20
+ minus: '<path d="M4 10h12"/>',
21
+ fit: '<path d="M3 7V4a1 1 0 0 1 1-1h3M13 3h3a1 1 0 0 1 1 1v3M17 13v3a1 1 0 0 1-1 1h-3M7 17H4a1 1 0 0 1-1-1v-3"/>',
22
+ undo: '<path d="M7 4 3.5 7.5 7 11 M4 7.5h7a4.5 4.5 0 0 1 0 9H8"/>',
23
+ redo: '<path d="m13 4 3.5 3.5L13 11 M16 7.5H9a4.5 4.5 0 0 0 0 9h3"/>',
24
+ layers: '<path d="m10 3 7 3.8-7 3.8-7-3.8L10 3Z m3 10.2 7 3.8 7-3.8M3 13.6 10 17.4l7-3.8"/>',
25
+ artboard: '<path d="M3.5 7.5h13 M5.5 3.5h9.0a2.0 2.0 0 0 1 2.0 2.0v9.0a2.0 2.0 0 0 1 -2.0 2.0h-9.0a2.0 2.0 0 0 1 -2.0 -2.0v-9.0a2.0 2.0 0 0 1 2.0 -2.0Z"/>',
26
+ expand: '<path d="M12 3h5v5M17 3l-6 6M8 17H3v-5M3 17l6-6"/>',
27
+ close: '<path d="m5 5 10 10M15 5 5 15"/>',
28
+ site: '<path d="M8 4H5a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 5 16h9a1.5 1.5 0 0 0 1.5-1.5V12M11 3.5h5.5V9M16.5 3.5 9 11"/>',
29
+ back: '<path d="m9 5-5 5 5 5M4 10h12"/>',
30
+ copy: '<path d="M13 7V5.5A1.5 1.5 0 0 0 11.5 4h-6A1.5 1.5 0 0 0 4 5.5v6A1.5 1.5 0 0 0 5.5 13H7 M8.5 7.0h6.0a1.5 1.5 0 0 1 1.5 1.5v6.0a1.5 1.5 0 0 1 -1.5 1.5h-6.0a1.5 1.5 0 0 1 -1.5 -1.5v-6.0a1.5 1.5 0 0 1 1.5 -1.5Z"/>',
31
+ trash: '<path d="M4 6h12M8 6V4h4v2M6 6l.7 10h6.6L14 6"/>',
32
+ chevron: '<path d="m7 5 5 5-5 5"/>',
33
+ search: '<path d="m13 13 3.5 3.5 M4.0 9.0a5.0 5.0 0 1 0 10.0 0a5.0 5.0 0 1 0 -10.0 0Z"/>',
34
+ agent: '<path d="M10 3v4M10 13v4M3 10h4M13 10h4M10 10h.01"/>',
35
+ user: '<path d="M4 16.5c.8-3 3.3-4.5 6-4.5s5.2 1.5 6 4.5 M7.0 7.5a3.0 3.0 0 1 0 6.0 0a3.0 3.0 0 1 0 -6.0 0Z"/>',
36
+ file: '<path d="M6 3h5.5L15 6.5V16a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1Z M11 3v4h4"/>',
37
+ alertDot: '<path d="M10 7v3.5M10 13v.01 M3.5 10.0a6.5 6.5 0 1 0 13.0 0a6.5 6.5 0 1 0 -13.0 0Z"/>',
38
+ divlet: '<path d="M8 4.5 2.5 10 8 15.5" fill="none"/><path d="M10.5 4.5H12L17.5 10 12 15.5h-1.5Z" fill="currentColor" stroke="none"/>',
39
+ alignL: '<path d="M4 3v14 M7.3 5.0h7.4a0.8 0.8 0 0 1 0.8 0.8v1.9a0.8 0.8 0 0 1 -0.8 0.8h-7.4a0.8 0.8 0 0 1 -0.8 -0.8v-1.9a0.8 0.8 0 0 1 0.8 -0.8Z M7.3 11.5h3.9a0.8 0.8 0 0 1 0.8 0.8v1.9a0.8 0.8 0 0 1 -0.8 0.8h-3.9a0.8 0.8 0 0 1 -0.8 -0.8v-1.9a0.8 0.8 0 0 1 0.8 -0.8Z"/>',
40
+ alignCH: '<path d="M10 3v14 M5.3 5.0h9.4a0.8 0.8 0 0 1 0.8 0.8v1.9a0.8 0.8 0 0 1 -0.8 0.8h-9.4a0.8 0.8 0 0 1 -0.8 -0.8v-1.9a0.8 0.8 0 0 1 0.8 -0.8Z M7.3 11.5h5.4a0.8 0.8 0 0 1 0.8 0.8v1.9a0.8 0.8 0 0 1 -0.8 0.8h-5.4a0.8 0.8 0 0 1 -0.8 -0.8v-1.9a0.8 0.8 0 0 1 0.8 -0.8Z"/>',
41
+ alignR: '<path d="M16 3v14 M5.3 5.0h7.4a0.8 0.8 0 0 1 0.8 0.8v1.9a0.8 0.8 0 0 1 -0.8 0.8h-7.4a0.8 0.8 0 0 1 -0.8 -0.8v-1.9a0.8 0.8 0 0 1 0.8 -0.8Z M8.8 11.5h3.9a0.8 0.8 0 0 1 0.8 0.8v1.9a0.8 0.8 0 0 1 -0.8 0.8h-3.9a0.8 0.8 0 0 1 -0.8 -0.8v-1.9a0.8 0.8 0 0 1 0.8 -0.8Z"/>',
42
+ alignT: '<path d="M3 4h14 M5.8 6.5h1.9a0.8 0.8 0 0 1 0.8 0.8v7.4a0.8 0.8 0 0 1 -0.8 0.8h-1.9a0.8 0.8 0 0 1 -0.8 -0.8v-7.4a0.8 0.8 0 0 1 0.8 -0.8Z M12.3 6.5h1.9a0.8 0.8 0 0 1 0.8 0.8v3.9a0.8 0.8 0 0 1 -0.8 0.8h-1.9a0.8 0.8 0 0 1 -0.8 -0.8v-3.9a0.8 0.8 0 0 1 0.8 -0.8Z"/>',
43
+ alignCV: '<path d="M3 10h14 M5.8 4.5h1.9a0.8 0.8 0 0 1 0.8 0.8v9.4a0.8 0.8 0 0 1 -0.8 0.8h-1.9a0.8 0.8 0 0 1 -0.8 -0.8v-9.4a0.8 0.8 0 0 1 0.8 -0.8Z M12.3 6.5h1.9a0.8 0.8 0 0 1 0.8 0.8v5.4a0.8 0.8 0 0 1 -0.8 0.8h-1.9a0.8 0.8 0 0 1 -0.8 -0.8v-5.4a0.8 0.8 0 0 1 0.8 -0.8Z"/>',
44
+ alignB: '<path d="M3 16h14 M5.8 4.5h1.9a0.8 0.8 0 0 1 0.8 0.8v7.4a0.8 0.8 0 0 1 -0.8 0.8h-1.9a0.8 0.8 0 0 1 -0.8 -0.8v-7.4a0.8 0.8 0 0 1 0.8 -0.8Z M12.3 8.0h1.9a0.8 0.8 0 0 1 0.8 0.8v3.9a0.8 0.8 0 0 1 -0.8 0.8h-1.9a0.8 0.8 0 0 1 -0.8 -0.8v-3.9a0.8 0.8 0 0 1 0.8 -0.8Z"/>',
45
+ distH: '<path d="M4 3v14M16 3v14 M8.8 6.0h2.4a0.8 0.8 0 0 1 0.8 0.8v6.4a0.8 0.8 0 0 1 -0.8 0.8h-2.4a0.8 0.8 0 0 1 -0.8 -0.8v-6.4a0.8 0.8 0 0 1 0.8 -0.8Z"/>',
46
+ distV: '<path d="M3 4h14M3 16h14 M6.8 8.0h6.4a0.8 0.8 0 0 1 0.8 0.8v2.4a0.8 0.8 0 0 1 -0.8 0.8h-6.4a0.8 0.8 0 0 1 -0.8 -0.8v-2.4a0.8 0.8 0 0 1 0.8 -0.8Z"/>',
47
+ heading: '<path d="M5 4v12M15 4v12M5 10h10"/>',
48
+ text: '<path d="M4 5h12M10 5v11"/>',
49
+ box: '<rect x="3.5" y="3.5" width="13" height="13" rx="2" stroke-dasharray="2 2"/>',
50
+ rectangle: '<path d="M3.5 5.5h13.0v9.0h-13.0Z"/>',
51
+ button: '<path d="M6.5 6.5h7.0a3.5 3.5 0 0 1 3.5 3.5v0.0a3.5 3.5 0 0 1 -3.5 3.5h-7.0a3.5 3.5 0 0 1 -3.5 -3.5v-0.0a3.5 3.5 0 0 1 3.5 -3.5Z"/>',
52
+ image: '<path d="m4 14 4-3.5 3 2.5 2.5-2 3 3 M6.3 8.5a1.2 1.2 0 1 0 2.4 0a1.2 1.2 0 1 0 -2.4 0Z M5.0 4.5h10.0a1.5 1.5 0 0 1 1.5 1.5v8.0a1.5 1.5 0 0 1 -1.5 1.5h-10.0a1.5 1.5 0 0 1 -1.5 -1.5v-8.0a1.5 1.5 0 0 1 1.5 -1.5Z"/>',
53
+ link: '<path d="M8.5 11.5a3 3 0 0 0 4.2 0l2.3-2.3a3 3 0 0 0-4.2-4.2l-.8.8M11.5 8.5a3 3 0 0 0-4.2 0L5 10.8A3 3 0 0 0 9.2 15l.8-.8"/>',
54
+ list: '<path d="M8 5.5h8.5M8 10h8.5M8 14.5h8.5 M3.6 5.5a0.9 0.9 0 1 0 1.8 0a0.9 0.9 0 1 0 -1.8 0Z M3.6 10.0a0.9 0.9 0 1 0 1.8 0a0.9 0.9 0 1 0 -1.8 0Z M3.6 14.5a0.9 0.9 0 1 0 1.8 0a0.9 0.9 0 1 0 -1.8 0Z"/>',
55
+ row: '<path d="M3.8 6.0h3.9a1.3 1.3 0 0 1 1.3 1.3v5.4a1.3 1.3 0 0 1 -1.3 1.3h-3.9a1.3 1.3 0 0 1 -1.3 -1.3v-5.4a1.3 1.3 0 0 1 1.3 -1.3Z M12.3 6.0h3.9a1.3 1.3 0 0 1 1.3 1.3v5.4a1.3 1.3 0 0 1 -1.3 1.3h-3.9a1.3 1.3 0 0 1 -1.3 -1.3v-5.4a1.3 1.3 0 0 1 1.3 -1.3Z"/>',
56
+ grid: '<path d="M4.2 3.0h3.6a1.2 1.2 0 0 1 1.2 1.2v3.6a1.2 1.2 0 0 1 -1.2 1.2h-3.6a1.2 1.2 0 0 1 -1.2 -1.2v-3.6a1.2 1.2 0 0 1 1.2 -1.2Z M12.2 3.0h3.6a1.2 1.2 0 0 1 1.2 1.2v3.6a1.2 1.2 0 0 1 -1.2 1.2h-3.6a1.2 1.2 0 0 1 -1.2 -1.2v-3.6a1.2 1.2 0 0 1 1.2 -1.2Z M4.2 11.0h3.6a1.2 1.2 0 0 1 1.2 1.2v3.6a1.2 1.2 0 0 1 -1.2 1.2h-3.6a1.2 1.2 0 0 1 -1.2 -1.2v-3.6a1.2 1.2 0 0 1 1.2 -1.2Z M12.2 11.0h3.6a1.2 1.2 0 0 1 1.2 1.2v3.6a1.2 1.2 0 0 1 -1.2 1.2h-3.6a1.2 1.2 0 0 1 -1.2 -1.2v-3.6a1.2 1.2 0 0 1 1.2 -1.2Z"/>',
57
+ more: '<path d="M3.9 10.0a1.1 1.1 0 1 0 2.2 0a1.1 1.1 0 1 0 -2.2 0Z M8.9 10.0a1.1 1.1 0 1 0 2.2 0a1.1 1.1 0 1 0 -2.2 0Z M13.9 10.0a1.1 1.1 0 1 0 2.2 0a1.1 1.1 0 1 0 -2.2 0Z"/>',
58
+ parent: '<path d="M14.5 15.5h-6a3 3 0 0 1-3-3V4.5 m2.5 7.5 3-3 3 3"/>',
59
+ child: '<path d="M5.5 4.5v6a3 3 0 0 0 3 3h7 m12.5 10.5 3 3-3 3"/>',
60
+ wrap: '<rect x="3" y="3" width="14" height="14" rx="2.5" stroke-dasharray="2.2 2"/><rect x="6.5" y="6.5" width="7" height="7" rx="1.2"/>',
61
+ unwrap: '<path d="M3 6V4a1 1 0 0 1 1-1h2M14 3h2a1 1 0 0 1 1 1v2M17 14v2a1 1 0 0 1-1 1h-2M6 17H4a1 1 0 0 1-1-1v-2 M7.7 6.5h4.6a1.2 1.2 0 0 1 1.2 1.2v4.6a1.2 1.2 0 0 1 -1.2 1.2h-4.6a1.2 1.2 0 0 1 -1.2 -1.2v-4.6a1.2 1.2 0 0 1 1.2 -1.2Z"/>',
62
+ help: '<path d="M8 8a2 2 0 1 1 2.8 1.8c-.5.3-.8.7-.8 1.3v.4M10 14v.01 M3.0 10.0a7.0 7.0 0 1 0 14.0 0a7.0 7.0 0 1 0 -14.0 0Z"/>',
63
+ keyboard: '<path d="M5.5 8h.01M8.5 8h.01M11.5 8h.01M14.5 8h.01M6.5 12h7 M4.5 5.0h11.0a2.0 2.0 0 0 1 2.0 2.0v6.0a2.0 2.0 0 0 1 -2.0 2.0h-11.0a2.0 2.0 0 0 1 -2.0 -2.0v-6.0a2.0 2.0 0 0 1 2.0 -2.0Z"/>',
64
+ swap: '<path d="M6 3.5v13M3.5 6 6 3.5 8.5 6M14 16.5v-13M11.5 14l2.5 2.5 2.5-2.5"/>',
65
+ hug: '<path d="M4 3.5h12M4 16.5h12M10 6.5v7M7.5 9 10 6.5 12.5 9M7.5 11 10 13.5l2.5-2.5"/>',
66
+ sun: '<path d="M10 2.5v1.5M10 16v1.5M2.5 10H4M16 10h1.5M4.7 4.7l1 1M14.3 14.3l1 1M4.7 15.3l1-1M14.3 5.7l1-1 M6.8 10.0a3.2 3.2 0 1 0 6.4 0a3.2 3.2 0 1 0 -6.4 0Z"/>',
67
+ moon: '<path d="M15.5 12.5A6.5 6.5 0 0 1 7.5 4.5a6.5 6.5 0 1 0 8 8Z"/>',
68
+ auto: '<circle cx="10" cy="10" r="6.5"/><path d="M10 3.5v13a6.5 6.5 0 0 0 0-13Z" fill="currentColor" stroke="none"/>',
69
+ tidy: '<path d="M3.5 5.0h2.0a1.0 1.0 0 0 1 1.0 1.0v8.0a1.0 1.0 0 0 1 -1.0 1.0h-2.0a1.0 1.0 0 0 1 -1.0 -1.0v-8.0a1.0 1.0 0 0 1 1.0 -1.0Z M9.0 5.0h2.0a1.0 1.0 0 0 1 1.0 1.0v5.0a1.0 1.0 0 0 1 -1.0 1.0h-2.0a1.0 1.0 0 0 1 -1.0 -1.0v-5.0a1.0 1.0 0 0 1 1.0 -1.0Z M14.5 5.0h2.0a1.0 1.0 0 0 1 1.0 1.0v6.5a1.0 1.0 0 0 1 -1.0 1.0h-2.0a1.0 1.0 0 0 1 -1.0 -1.0v-6.5a1.0 1.0 0 0 1 1.0 -1.0Z"/>',
70
+ undoKind: '<path d="M7 4 3.5 7.5 7 11 M4 7.5h7a4.5 4.5 0 0 1 0 9H8"/>',
71
+ pencil: '<path d="M12.5 4.5 15.5 7.5 7 16H4v-3l8.5-8.5Z m11 6 3 3"/>',
72
+ tag: '<path d="M3.5 3.5h6l7 7-6 6-7-7v-6Z M6.0 7.0a1.0 1.0 0 1 0 2.0 0a1.0 1.0 0 1 0 -2.0 0Z"/>',
73
+ hash: '<path d="M8 3 6.5 17M13.5 3 12 17M3.5 7.5h13M3 12.5h13"/>',
74
+ rotate: '<path d="M15.5 9.5A5.5 5.5 0 1 1 13.8 5.6 M14.5 2.5v3.5H11"/>',
75
+ check: '<path d="m4.5 10.5 3.5 3.5 7.5-8"/>',
76
+ arrowUp: '<path d="M10 16V4M5 9l5-5 5 5"/>',
77
+ arrowRight: '<path d="M4 10h12M11 5l5 5-5 5"/>',
78
+ wrapLines: '<path d="M3.5 5.5h13M3.5 10h10.5a2.5 2.5 0 0 1 0 5H11M12.5 13.5 11 15l1.5 1.5M3.5 14.5h4"/>',
79
+ textL: '<path d="M3.5 5h13M3.5 8.5h8M3.5 12h13M3.5 15.5h8"/>',
80
+ textC: '<path d="M3.5 5h13M6 8.5h8M3.5 12h13M6 15.5h8"/>',
81
+ textR: '<path d="M3.5 5h13M8.5 8.5h8M3.5 12h13M8.5 15.5h8"/>',
82
+ textJ: '<path d="M3.5 5h13M3.5 8.5h13M3.5 12h13M3.5 15.5h13"/>',
83
+ between: '<path d="M3.5 3.5v13M16.5 3.5v13 M6.6 7.5h1.3a0.6 0.6 0 0 1 0.6 0.6v3.8a0.6 0.6 0 0 1 -0.6 0.6h-1.3a0.6 0.6 0 0 1 -0.6 -0.6v-3.8a0.6 0.6 0 0 1 0.6 -0.6Z M12.1 7.5h1.3a0.6 0.6 0 0 1 0.6 0.6v3.8a0.6 0.6 0 0 1 -0.6 0.6h-1.3a0.6 0.6 0 0 1 -0.6 -0.6v-3.8a0.6 0.6 0 0 1 0.6 -0.6Z"/>',
84
+ arrowDown: '<path d="M10 4v12M5 11l5 5 5-5"/>',
85
+ sparkle: '<path d="M9 2.8c.5 3.6 2 5.1 5.6 5.6-3.6.5-5.1 2-5.6 5.6-.5-3.6-2-5.1-5.6-5.6C7 7.9 8.5 6.4 9 2.8Z M15.2 12.6v4.2M13.1 14.7h4.2"/>',
86
+ send: '<path d="M10 15.5v-11M5.5 9 10 4.5 14.5 9"/>',
87
+ stop: '<rect x="5.5" y="5.5" width="9" height="9" rx="1.8" fill="currentColor" stroke="none"/>',
88
+ cut: '<path d="M7.4 12.8 14 3.5M12.6 12.8 6 3.5 M3.8 14.5a2.2 2.2 0 1 0 4.4 0a2.2 2.2 0 1 0 -4.4 0Z M11.8 14.5a2.2 2.2 0 1 0 4.4 0a2.2 2.2 0 1 0 -4.4 0Z"/>',
89
+ paste: '<path d="M8 4V2.8h4V4M7.5 9h5M7.5 12h5 M6.1 4.0h7.8a1.6 1.6 0 0 1 1.6 1.6v9.8a1.6 1.6 0 0 1 -1.6 1.6h-7.8a1.6 1.6 0 0 1 -1.6 -1.6v-9.8a1.6 1.6 0 0 1 1.6 -1.6Z"/>',
90
+ code: '<path d="m6.5 6-3.5 4 3.5 4M13.5 6l3.5 4-3.5 4M11.3 4l-2.6 12"/>',
91
+ download: '<path d="M10 3.5v9M6 9l4 4 4-4M4 16.5h12"/>',
92
+ upload: '<path d="M10 13V4M6 7.5l4-4 4 4M4 16.5h12"/>',
93
+ chat: '<path d="M4 5.5A1.5 1.5 0 0 1 5.5 4h9A1.5 1.5 0 0 1 16 5.5v6a1.5 1.5 0 0 1-1.5 1.5H9.2L6 15.8V13h-.5A1.5 1.5 0 0 1 4 11.5v-6Z"/>',
94
+ command: '<path d="M7 7V5.5a1.5 1.5 0 1 0-1.5 1.5H7Zm0 0h6m-6 0v6m6-6V5.5A1.5 1.5 0 1 1 14.5 7H13Zm0 0v6m0 0v1.5a1.5 1.5 0 1 0 1.5-1.5H13Zm0 0H7m0 0v1.5A1.5 1.5 0 1 1 5.5 13H7Z"/>',
95
+ enter: '<path d="M15.5 4.5v5a2 2 0 0 1-2 2h-9M7.5 8.5l-3 3 3 3"/>',
96
+ component: '<path d="m10 2.8 3.2 3.2L10 9.2 6.8 6 10 2.8ZM10 10.8l3.2 3.2-3.2 3.2-3.2-3.2 3.2-3.2ZM6 6.8 9.2 10 6 13.2 2.8 10 6 6.8ZM14 6.8l3.2 3.2-3.2 3.2-3.2-3.2L14 6.8Z"/>',
97
+ instance: '<path d="M10 2.8 17.2 10 10 17.2 2.8 10 10 2.8Z"/>',
98
+ lock: '<path d="M7.5 9V7a2.5 2.5 0 0 1 5 0v2 M6.6 9.0h6.8a1.6 1.6 0 0 1 1.6 1.6v3.8a1.6 1.6 0 0 1 -1.6 1.6h-6.8a1.6 1.6 0 0 1 -1.6 -1.6v-3.8a1.6 1.6 0 0 1 1.6 -1.6Z"/>',
99
+ unlock: '<path d="M7.5 9V7a2.5 2.5 0 0 1 4.8-.9 M6.6 9.0h6.8a1.6 1.6 0 0 1 1.6 1.6v3.8a1.6 1.6 0 0 1 -1.6 1.6h-6.8a1.6 1.6 0 0 1 -1.6 -1.6v-3.8a1.6 1.6 0 0 1 1.6 -1.6Z"/>',
100
+ folder: '<path d="M3 6.2A1.7 1.7 0 0 1 4.7 4.5h3.1l1.6 1.8h5.9A1.7 1.7 0 0 1 17 8v6.3a1.7 1.7 0 0 1-1.7 1.7H4.7A1.7 1.7 0 0 1 3 14.3V6.2Z"/>',
101
+ folderUp: '<path d="M3 6.2A1.7 1.7 0 0 1 4.7 4.5h3.1l1.6 1.8h5.9A1.7 1.7 0 0 1 17 8v6.3a1.7 1.7 0 0 1-1.7 1.7H4.7A1.7 1.7 0 0 1 3 14.3V6.2ZM10 13V9M8 10.8 10 9l2 1.8"/>',
102
+ clock: '<path d="M10 6.3V10l2.6 1.6 M3.5 10.0a6.5 6.5 0 1 0 13.0 0a6.5 6.5 0 1 0 -13.0 0Z"/>',
103
+ gear: '<g transform="scale(.8333)" stroke-width="1.8"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></g>',
104
+ };
105
+
106
+ /**
107
+ * The key a shortcut means on any keyboard layout: a Latin letter as typed, otherwise the
108
+ * physical key (Ф on a Russian layout is A), so single-letter and Ctrl shortcuts always work.
109
+ */
110
+ export function shortcutKey(event) {
111
+ const key = String(event.key || '');
112
+ const code = String(event.code || '');
113
+ if (/^[a-z]$/i.test(key)) return key.toLowerCase();
114
+ if (/^Key[A-Z]$/.test(code)) return code.slice(3).toLowerCase();
115
+ if (/^Digit\d$/.test(code) && !event.shiftKey) return code.slice(5);
116
+ if (code === 'Slash' && event.shiftKey) return '?';
117
+ if (code === 'Equal' || code === 'NumpadAdd') return event.shiftKey || code === 'NumpadAdd' ? '+' : '=';
118
+ if (code === 'Minus' || code === 'NumpadSubtract') return '-';
119
+ return key.toLowerCase();
120
+ }
121
+
122
+ export function icon(name, className = 'icon') {
123
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
124
+ svg.setAttribute('viewBox', '0 0 20 20');
125
+ svg.setAttribute('class', className);
126
+ svg.setAttribute('aria-hidden', 'true');
127
+ svg.innerHTML = ICONS[name] || '';
128
+ return svg;
129
+ }
130
+
131
+ /** Human label for a key combination, e.g. "Mod+Shift+Z" → "Ctrl ⇧ Z" or "⌘ ⇧ Z". */
132
+ export const isMac = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
133
+ export function keys(combo) {
134
+ return combo.split('+').map(part => ({ Mod: isMac ? '⌘' : 'Ctrl', Shift: '⇧', Alt: isMac ? '⌥' : 'Alt', Enter: '↵', Up: '↑', Down: '↓', Left: '←', Right: '→', Delete: 'Del', Escape: 'Esc' })[part] || part).join(' ');
135
+ }
136
+
137
+ export function button(iconName, tip, handler, className = '') {
138
+ const node = el('button', `tool ${className}`.trim());
139
+ node.type = 'button';
140
+ node.setAttribute('aria-label', tip.replace(/ · .*/, ''));
141
+ node.dataset.tip = tip;
142
+ if (iconName) node.append(icon(iconName));
143
+ if (handler) node.addEventListener('click', handler);
144
+ return node;
145
+ }
146
+
147
+ export function ago(iso) {
148
+ const seconds = Math.max(0, Math.round((Date.now() - new Date(iso).getTime()) / 1000));
149
+ if (seconds < 5) return 'now';
150
+ if (seconds < 60) return `${seconds}s`;
151
+ if (seconds < 3600) return `${Math.round(seconds / 60)}m`;
152
+ if (seconds < 86400) return `${Math.round(seconds / 3600)}h`;
153
+ return `${Math.round(seconds / 86400)}d`;
154
+ }
155
+
156
+ /**
157
+ * Re-rendering a panel between pointerdown and pointerup swallows the click (the pressed
158
+ * element is gone). Panels ask `pressing()` before replacing their content and get
159
+ * `after()` called once the pointer is released and the click has been dispatched.
160
+ */
161
+ export function pressGuard(panel, after) {
162
+ let down = false;
163
+ panel.addEventListener('pointerdown', () => { down = true; }, true);
164
+ const release = () => { if (!down) return; down = false; setTimeout(after, 0); };
165
+ window.addEventListener('pointerup', release, true);
166
+ window.addEventListener('pointercancel', release, true);
167
+ window.addEventListener('blur', release);
168
+ return () => down;
169
+ }
170
+
171
+ export function debounce(fn, wait) {
172
+ let timer;
173
+ const wrapped = (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), wait); };
174
+ wrapped.flush = (...args) => { clearTimeout(timer); fn(...args); };
175
+ wrapped.cancel = () => clearTimeout(timer);
176
+ return wrapped;
177
+ }
package/package.json CHANGED
@@ -1,6 +1,39 @@
1
1
  {
2
2
  "name": "@divlet/web",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.1",
4
+ "description": "The divlet browser board, static client assets and transport-free host ports",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/llblab/divlet.git",
10
+ "directory": "web"
11
+ },
12
+ "engines": {
13
+ "node": ">=22.19.0"
14
+ },
15
+ "exports": {
16
+ ".": {
17
+ "divlet-source": "./index.ts",
18
+ "default": "./dist/index.js"
19
+ },
20
+ "./session": {
21
+ "divlet-source": "./lib/board-session.ts",
22
+ "default": "./dist/lib/board-session.js"
23
+ },
24
+ "./host": {
25
+ "divlet-source": "./lib/board-host.ts",
26
+ "default": "./dist/lib/board-host.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "README.md"
32
+ ],
33
+ "scripts": {
34
+ "prepack": "node ../scripts/build.mjs"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ }
39
+ }