@threadlabs/looma 0.33.0 → 0.33.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.
- package/README.md +15 -0
- package/bin/looma-docs.mjs +3 -0
- package/docs/README.md +116 -0
- package/docs/card.html +63 -0
- package/docs/card.js +32 -0
- package/docs/catalog-view.js +18 -0
- package/docs/catalog-view.mjs +11 -0
- package/docs/catalog.json +22974 -0
- package/docs/catalog.mjs +131 -0
- package/docs/cli.mjs +26 -0
- package/docs/commands.mjs +94 -0
- package/docs/customizer-render.mjs +40 -0
- package/docs/customizer.js +118 -0
- package/docs/documentation.css +8 -0
- package/docs/examples/ui-action-bar/examples/01-form-footer.behavior.js +7 -0
- package/docs/examples/ui-button/examples/08-loading.behavior.js +21 -0
- package/docs/examples/ui-checkbox/examples/02-checked-indeterminate-and-disabled.behavior.js +30 -0
- package/docs/examples/ui-combobox/examples/06-selected-values.behavior.js +11 -0
- package/docs/examples/ui-search-shell/examples/02-label-dismissible-status-and-footer.behavior.js +34 -0
- package/docs/examples/ui-toast/examples/01-authored.behavior.js +15 -0
- package/docs/examples/ui-toast/examples/02-danger.behavior.js +2 -0
- package/docs/examples/ui-tree/examples/03-max-depth-2-and-hover-expand-delay-300.behavior.js +30 -0
- package/docs/examples/ui-tree/examples/06-move-without-dragging.behavior.js +2 -0
- package/docs/examples/ui-tree/examples/07-multiple-selection.behavior.js +16 -0
- package/docs/examples/ui-tree-item/examples/02-container.behavior.js +1 -0
- package/docs/index.d.ts +54 -0
- package/docs/index.mjs +4 -0
- package/docs/local-server.mjs +68 -0
- package/docs/preview.js +20 -0
- package/docs/reference.html +1 -0
- package/docs/reference.js +36 -0
- package/docs/render.mjs +84 -0
- package/docs/site.mjs +198 -0
- package/docs/store.mjs +92 -0
- package/docs/theme.mjs +100 -0
- package/docs/workspace.mjs +32 -0
- package/package.json +28 -4
package/docs/theme.mjs
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/** The shared authoring schema; ids remain stable in saved recipes. */
|
|
2
|
+
export const themeFields = [
|
|
3
|
+
['Colour', 'action', 'Action', '--ui-accent', 'colour', '#5b55d6', 'Primary commands and selected controls.'],
|
|
4
|
+
['Colour', 'canvas', 'Canvas', '--ui-surface', 'colour', '#f6f6f8', 'The ground behind surfaces.'],
|
|
5
|
+
['Colour', 'surface', 'Surface', '--ui-surface-raised', 'colour', '#ffffff', 'Fields, cards and floating surfaces.'],
|
|
6
|
+
['Colour', 'text', 'Text', '--ui-text', 'colour', '#1a1a1a', 'Readable content; supporting text follows this and Surface.'],
|
|
7
|
+
['Type', 'body-font', 'Body font', '--ui-font-sans', 'font', 'system-ui, sans-serif', 'The shared interface typeface.'],
|
|
8
|
+
['Type', 'display-font', 'Display font', '--ui-font-display', 'font', 'system-ui, sans-serif', 'The heading typeface.'],
|
|
9
|
+
['Type', 'text-size', 'Text size', '--ui-font-size', 'positive-length', '16px', 'Base type and choice-mark size. Accepts native CSS lengths, such as 1rem.'],
|
|
10
|
+
['Type', 'text-weight', 'Emphasis weight', '--ui-font-medium', 'weight', '500', 'Control-label emphasis; body and headings keep their roles.'],
|
|
11
|
+
['Size & spacing', 'control-size', 'Control size', '--ui-control-size', 'positive-length', '40px', 'One shared height for buttons, fields and swatches.'],
|
|
12
|
+
['Size & spacing', 'density', 'Density', '--ui-density', 'positive', '1', 'More or less breathing room throughout the interface.'],
|
|
13
|
+
['Size & spacing', 'scale-contrast', 'Size ratio', '--ui-scale-ratio', 'ratio', '1.2', 'The multiplier between small, medium and large controls.'],
|
|
14
|
+
['Size & spacing', 'space-curve', 'Spacing curve', '--ui-space-curve', 'nonnegative', '1', 'Spacing growth: 1 is linear, 2 is quadratic. Independent of control sizes.'],
|
|
15
|
+
['Size & spacing', 'corner-roundness', 'Corner roundness', '--ui-radius-md', 'length', '8px', 'One corner family across controls and surfaces.'],
|
|
16
|
+
['Size & spacing', 'line-weight', 'Line weight', '--ui-border-width', 'length', '1px', 'Shared edge weight; focus stays independently visible.'],
|
|
17
|
+
['Material', 'outer-depth', 'Outer depth', '--ui-outer-depth', 'nonnegative', '0.6', 'How strongly controls and floating surfaces rise above the page.'],
|
|
18
|
+
['Material', 'inner-depth', 'Inner depth', '--ui-inner-depth', 'nonnegative', '0.3', 'How strongly fields and pressed controls sit into their surface.'],
|
|
19
|
+
['Material', 'translucency', 'Overlay translucency', '--ui-overlay-translucency', 'fraction', '0', 'How much background comes through floating surfaces.'],
|
|
20
|
+
['Material', 'edge-light', 'Edge light', '--ui-edge-light', 'fraction', '0', 'Light caught by surface edges, independent of depth.'],
|
|
21
|
+
['Motion', 'response-time', 'Response time', '--ui-motion-fast', 'time', '120ms', 'Shared feedback pace; floating transitions take proportionally longer.'],
|
|
22
|
+
['Motion', 'response-curve', 'Response curve', '--ui-motion-ease', 'easing', 'cubic-bezier(0.2,0,0.2,1)', 'How movement accelerates and settles.'],
|
|
23
|
+
].map(([family, id, label, property, type, defaultValue, description]) => Object.freeze({ family, id, label, property, type, defaultValue, description }));
|
|
24
|
+
|
|
25
|
+
export const defaultRecipe = () => ({ schemaVersion: 1, values: Object.fromEntries(themeFields.map(field => [field.id, field.defaultValue])) });
|
|
26
|
+
const starting = values => ({ schemaVersion: 1, values: { ...defaultRecipe().values, ...values } });
|
|
27
|
+
export const startingPoints = {
|
|
28
|
+
clear: defaultRecipe(),
|
|
29
|
+
library: starting({ action: '#2e1228', canvas: '#f7f7f5', surface: '#fffefd', text: '#0d0c0c', 'body-font': 'Arial, sans-serif', 'display-font': 'Georgia, serif', 'text-weight': '400', 'control-size': '42px', 'scale-contrast': '1.18', 'outer-depth': '1', 'inner-depth': '1', translucency: '0.18', 'edge-light': '1', 'response-time': '150ms', 'response-curve': 'ease' }),
|
|
30
|
+
editorial: starting({ action: '#182c24', canvas: '#eee9dc', surface: '#fffdf5', text: '#182c24', 'corner-roundness': '0px', 'outer-depth': '0', 'inner-depth': '0', 'body-font': 'Georgia, serif', 'display-font': 'Georgia, serif', 'text-weight': '400', 'line-weight': '2px', 'response-time': '90ms', 'response-curve': 'linear' }),
|
|
31
|
+
night: starting({ action: '#a7b8ff', canvas: '#11141c', surface: '#202634', text: '#edf1fb', 'corner-roundness': '12px', 'outer-depth': '1.4', 'inner-depth': '0.45', translucency: '0.2', 'edge-light': '0.65', 'response-time': '210ms', 'response-curve': 'cubic-bezier(0.2,0.8,0.2,1)' }),
|
|
32
|
+
garden: starting({ action: '#256342', canvas: '#eef5e7', surface: '#fcfff6', text: '#173121', 'corner-roundness': '22px', 'outer-depth': '0.35', 'inner-depth': '0', 'edge-light': '0.2', 'control-size': '46px', 'scale-contrast': '1.24', density: '1.05', 'response-time': '180ms', 'response-curve': 'cubic-bezier(0.16,1,0.3,1)' }),
|
|
33
|
+
};
|
|
34
|
+
export const startingPointLabels = { clear: 'Clear', library: 'Library', editorial: 'Editorial', night: 'Night', garden: 'Garden' };
|
|
35
|
+
const scalar = /^[+]?(?:\d+(?:\.\d*)?|\.\d+)$/;
|
|
36
|
+
const length = /^(?:0|[+]?(?:\d+(?:\.\d*)?|\.\d+)(?:px|rem|em|ex|ch|cap|ic|lh|rlh|vw|vh|vi|vb|vmin|vmax|svw|svh|lvw|lvh|dvw|dvh|cm|mm|Q|in|pt|pc))$/;
|
|
37
|
+
const font = /^(?:"[^"\\;{}<>@\x00-\x1f]+"|'[^'\\;{}<>@\x00-\x1f]+'|[\p{L}_-][\p{L}\p{N}_ -]*)(?:\s*,\s*(?:"[^"\\;{}<>@\x00-\x1f]+"|'[^'\\;{}<>@\x00-\x1f]+'|[\p{L}_-][\p{L}\p{N}_ -]*))*$/u;
|
|
38
|
+
const time = /^[+]?(?:\d+(?:\.\d*)?|\.\d+)(?:ms|s)$/;
|
|
39
|
+
function easing(value) {
|
|
40
|
+
if (/^(ease|linear|ease-in|ease-out|ease-in-out|step-start|step-end)$/.test(value)) return true;
|
|
41
|
+
const match = /^cubic-bezier\(\s*([\d.+-]+)\s*,\s*([\d.+-]+)\s*,\s*([\d.+-]+)\s*,\s*([\d.+-]+)\s*\)$/.exec(value);
|
|
42
|
+
return Boolean(match && match.slice(1).every(n => Number.isFinite(Number(n))) && [1, 3].every(i => Number(match[i]) >= 0 && Number(match[i]) <= 1));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Validate the whole recipe before previewing or writing any output. */
|
|
46
|
+
export function validateRecipe(input) {
|
|
47
|
+
if (!input || input.schemaVersion !== 1 || !input.values || typeof input.values !== 'object' || Array.isArray(input.values)) throw new Error('Unsupported theme recipe version or values.');
|
|
48
|
+
const known = new Set(themeFields.map(field => field.id));
|
|
49
|
+
for (const key of Object.keys(input.values)) if (!known.has(key)) throw new Error(`Unknown theme decision ${key}.`);
|
|
50
|
+
const values = { ...defaultRecipe().values, ...input.values };
|
|
51
|
+
for (const field of themeFields) {
|
|
52
|
+
const value = values[field.id];
|
|
53
|
+
let valid = typeof value === 'string' && value.length > 0;
|
|
54
|
+
if (valid) switch (field.type) {
|
|
55
|
+
case 'colour': valid = /^#[\da-f]{6}$/i.test(value); break;
|
|
56
|
+
case 'font': valid = font.test(value); break;
|
|
57
|
+
case 'length': valid = length.test(value) && Number.isFinite(parseFloat(value)); break;
|
|
58
|
+
case 'positive-length': valid = length.test(value) && Number.isFinite(parseFloat(value)) && parseFloat(value) > 0; break;
|
|
59
|
+
case 'time': valid = time.test(value) && Number.isFinite(parseFloat(value)); break;
|
|
60
|
+
case 'easing': valid = easing(value); break;
|
|
61
|
+
default: {
|
|
62
|
+
const number = Number(value); valid = scalar.test(value) && Number.isFinite(number);
|
|
63
|
+
if (field.type === 'positive') valid &&= number > 0;
|
|
64
|
+
if (field.type === 'ratio') valid &&= number >= 1;
|
|
65
|
+
if (field.type === 'fraction') valid &&= number >= 0 && number <= 1;
|
|
66
|
+
if (field.type === 'weight') valid &&= number >= 1 && number <= 1000;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!valid) throw new Error(`${field.label}: enter a valid ${field.type.replaceAll('-', ' ')} value.`);
|
|
70
|
+
}
|
|
71
|
+
return { schemaVersion: 1, values };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Match presets by their complete semantic recipe, including locally saved themes. */
|
|
75
|
+
export function startingPointFor(input) {
|
|
76
|
+
const recipe = JSON.stringify(validateRecipe(input));
|
|
77
|
+
return Object.entries(startingPoints).find(([, value]) => JSON.stringify(value) === recipe)?.[0] ?? 'custom';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const rgb = hex => [1, 3, 5].map(i => parseInt(hex.slice(i, i + 2), 16));
|
|
81
|
+
const mix = (a, b, weight) => '#' + rgb(a).map((n, i) => Math.round(n * weight + rgb(b)[i] * (1 - weight)).toString(16).padStart(2, '0')).join('');
|
|
82
|
+
const luminance = hex => rgb(hex).map(n => { const s = n / 255; return s <= .04045 ? s / 12.92 : ((s + .055) / 1.055) ** 2.4; }).reduce((sum, n, i) => sum + n * [.2126, .7152, .0722][i], 0);
|
|
83
|
+
const contrast = (a, b) => (Math.max(luminance(a), luminance(b)) + .05) / (Math.min(luminance(a), luminance(b)) + .05);
|
|
84
|
+
const foreground = (fill, light, dark) => [light, dark, '#ffffff', '#000000'].sort((a, b) => contrast(fill, b) - contrast(fill, a))[0];
|
|
85
|
+
|
|
86
|
+
/** Shared public seeds; combine with theme-system.css for a standalone exported theme. */
|
|
87
|
+
export function themeCSS(input, systemCSS = '') {
|
|
88
|
+
const { values } = validateRecipe(input);
|
|
89
|
+
const tokens = Object.fromEntries(themeFields.map(field => [field.property, values[field.id]]));
|
|
90
|
+
tokens['--ui-on-accent'] = foreground(values.action, values.surface, values.text);
|
|
91
|
+
tokens['--ui-surface-sunken'] = mix(values.surface, values.canvas, .92);
|
|
92
|
+
let muted = values.text;
|
|
93
|
+
for (let weight = .72; weight <= 1; weight += .01) { muted = mix(values.text, values.surface, weight); if (contrast(muted, values.surface) >= 4.5) break; }
|
|
94
|
+
tokens['--ui-text-secondary'] = muted;
|
|
95
|
+
let danger = '#c92d1e';
|
|
96
|
+
for (let weight = 0; weight <= 1; weight += .02) { danger = mix('#c92d1e', values.text, 1 - weight); if (Math.min(contrast(danger, values.surface), contrast(danger, values.canvas)) >= 4.5) break; }
|
|
97
|
+
tokens['--ui-danger'] = danger; tokens['--ui-on-danger'] = foreground(danger, values.surface, values.text);
|
|
98
|
+
tokens['--ui-focus-width'] = 'max(2px, calc(var(--ui-border-width) * 2))';
|
|
99
|
+
return `/* Looma shared semantic theme. */\n${systemCSS}\n:root, [data-theme], [data-contrast] {\n${Object.entries(tokens).map(([name, value]) => ` ${name}: ${value};`).join('\n')}\n}\n@media (prefers-reduced-motion: reduce) {\n :root, [data-theme], [data-contrast] { --ui-motion-fast: 0ms; --ui-motion-base: 0ms; --ui-motion-press: 0ms; --ui-press-scale: 1; }\n}\n`;
|
|
100
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { lstat, mkdir, open, realpath, unlink } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export async function rejectSymlink(file) {
|
|
5
|
+
try { if ((await lstat(file)).isSymbolicLink()) throw new Error(`Looma's generated path cannot be a symlink: ${file}.`); }
|
|
6
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Generated sources and tooling wrappers stay in owned cache directories. */
|
|
10
|
+
export async function cacheDirectory(root, name = '') {
|
|
11
|
+
root = await realpath(path.resolve(root));
|
|
12
|
+
if (name && !/^[a-z][a-z-]*$/.test(name)) throw new Error('Invalid Looma cache directory.');
|
|
13
|
+
const cache = path.join(root, '.looma');
|
|
14
|
+
await rejectSymlink(cache); await mkdir(cache, { recursive: true });
|
|
15
|
+
const directory = path.join(cache, name);
|
|
16
|
+
await rejectSymlink(directory); await mkdir(directory, { recursive: true });
|
|
17
|
+
return directory;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** One complete preparation/build owns the generated tree, including across CLI processes. */
|
|
21
|
+
export async function withBuildLock(root, action) {
|
|
22
|
+
const lockFile = path.join(await cacheDirectory(root), 'docs-build.lock');
|
|
23
|
+
await rejectSymlink(lockFile);
|
|
24
|
+
let lock;
|
|
25
|
+
try { lock = await open(lockFile, 'wx', 0o600); }
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error.code === 'EEXIST') throw new Error('Another documentation build holds .looma/docs-build.lock. Retry after it finishes. If the old process stopped, close it before removing that lock.');
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
try { await lock.writeFile(JSON.stringify({ pid: process.pid })); return await action(); }
|
|
31
|
+
finally { try { await lock.close(); } finally { await unlink(lockFile); } }
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@threadlabs/looma",
|
|
3
|
-
"version": "0.33.
|
|
3
|
+
"version": "0.33.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Looma components as HTML, Vue, and plain DOM, with design tokens.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -83,7 +83,14 @@
|
|
|
83
83
|
"import": "./editor/expressmark.js",
|
|
84
84
|
"default": "./editor/expressmark.js"
|
|
85
85
|
},
|
|
86
|
-
"./theme-system.css": "./theme-system.css"
|
|
86
|
+
"./theme-system.css": "./theme-system.css",
|
|
87
|
+
"./docs/catalog.json": "./docs/catalog.json",
|
|
88
|
+
"./docs/*": "./docs/*",
|
|
89
|
+
"./docs": {
|
|
90
|
+
"types": "./docs/index.d.ts",
|
|
91
|
+
"import": "./docs/index.mjs",
|
|
92
|
+
"default": "./docs/index.mjs"
|
|
93
|
+
}
|
|
87
94
|
},
|
|
88
95
|
"files": [
|
|
89
96
|
"dist/",
|
|
@@ -96,7 +103,9 @@
|
|
|
96
103
|
"*.css",
|
|
97
104
|
"README.md",
|
|
98
105
|
"LICENSE",
|
|
99
|
-
"EXPRESSIVE-MARKDOWN.md"
|
|
106
|
+
"EXPRESSIVE-MARKDOWN.md",
|
|
107
|
+
"docs/",
|
|
108
|
+
"bin/"
|
|
100
109
|
],
|
|
101
110
|
"sideEffects": [
|
|
102
111
|
"./dist/index.js",
|
|
@@ -117,7 +126,10 @@
|
|
|
117
126
|
"@tiptap/core": ">=2 <3",
|
|
118
127
|
"@tiptap/pm": ">=2 <3",
|
|
119
128
|
"@tiptap/vue-3": ">=2 <3",
|
|
120
|
-
"vue": "^3.5.43"
|
|
129
|
+
"vue": "^3.5.43",
|
|
130
|
+
"markupress": "^0.8.0",
|
|
131
|
+
"@nextwebwg/htmlkit": "1.0.0-alpha.69",
|
|
132
|
+
"vite": "^8.3.4"
|
|
121
133
|
},
|
|
122
134
|
"peerDependenciesMeta": {
|
|
123
135
|
"@tiptap/core": {
|
|
@@ -131,6 +143,15 @@
|
|
|
131
143
|
},
|
|
132
144
|
"vue": {
|
|
133
145
|
"optional": true
|
|
146
|
+
},
|
|
147
|
+
"markupress": {
|
|
148
|
+
"optional": true
|
|
149
|
+
},
|
|
150
|
+
"@nextwebwg/htmlkit": {
|
|
151
|
+
"optional": true
|
|
152
|
+
},
|
|
153
|
+
"vite": {
|
|
154
|
+
"optional": true
|
|
134
155
|
}
|
|
135
156
|
},
|
|
136
157
|
"devDependencies": {
|
|
@@ -185,6 +206,9 @@
|
|
|
185
206
|
"engines": {
|
|
186
207
|
"node": ">=22.22.2 <23 || >=24.15 <25"
|
|
187
208
|
},
|
|
209
|
+
"bin": {
|
|
210
|
+
"looma-docs": "./bin/looma-docs.mjs"
|
|
211
|
+
},
|
|
188
212
|
"scripts": {
|
|
189
213
|
"build": "node build.mjs",
|
|
190
214
|
"lint": "vue-tsc -p tsconfig.json --noEmit",
|