@threadlabs/looma-docs 0.0.0-stage → 0.52.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 (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -2
  3. package/bin/looma-docs.mjs +3 -0
  4. package/docs/README.md +89 -0
  5. package/docs/card.html +79 -0
  6. package/docs/card.js +18 -0
  7. package/docs/catalog-view.js +24 -0
  8. package/docs/catalog-view.mjs +13 -0
  9. package/docs/catalog.mjs +135 -0
  10. package/docs/cli.mjs +26 -0
  11. package/docs/commands.mjs +94 -0
  12. package/docs/customizer-render.mjs +157 -0
  13. package/docs/customizer.js +494 -0
  14. package/docs/documentation.css +42 -0
  15. package/docs/example-invocations.mjs +63 -0
  16. package/docs/fonts.mjs +73 -0
  17. package/docs/framework-examples.d.mts +9 -0
  18. package/docs/framework-examples.mjs +225 -0
  19. package/docs/guides/01.theme-your-system.md +64 -0
  20. package/docs/guides/02.add-your-components.md +96 -0
  21. package/docs/guides/03.publish-your-system.md +71 -0
  22. package/docs/guides/04.theme-decisions.md +141 -0
  23. package/docs/guides/05.(Theme reference)/01.theme-colors.md +95 -0
  24. package/docs/guides/05.(Theme reference)/02.theme-typography.md +73 -0
  25. package/docs/guides/05.(Theme reference)/03.theme-spacing.md +103 -0
  26. package/docs/guides/05.(Theme reference)/04.theme-material.md +90 -0
  27. package/docs/guides/05.(Theme reference)/05.theme-motion.md +57 -0
  28. package/docs/guides/assets/fellowship-dark.png +0 -0
  29. package/docs/guides/assets/fellowship-light.png +0 -0
  30. package/docs/index.d.ts +59 -0
  31. package/docs/index.mjs +4 -0
  32. package/docs/local-server.mjs +65 -0
  33. package/docs/navigation.mjs +40 -0
  34. package/docs/palette.mjs +68 -0
  35. package/docs/pastoral.svg +54 -0
  36. package/docs/portrait.svg +15 -0
  37. package/docs/reference.html +1 -0
  38. package/docs/reference.js +36 -0
  39. package/docs/render.mjs +97 -0
  40. package/docs/site.mjs +244 -0
  41. package/docs/store.mjs +92 -0
  42. package/docs/theme-session.mjs +72 -0
  43. package/docs/theme.mjs +669 -0
  44. package/docs/value-editors.mjs +37 -0
  45. package/docs/workspace.mjs +32 -0
  46. package/package.json +53 -3
package/docs/fonts.mjs ADDED
@@ -0,0 +1,73 @@
1
+ /** A focused searchable Google Fonts collection; system fonts never require a request. */
2
+ export const googleFonts = Object.fromEntries([
3
+ ['Inter', 'sans-serif'], ['Roboto', 'sans-serif'], ['Open Sans', 'sans-serif'],
4
+ ['Source Sans 3', 'sans-serif'], ['Lato', 'sans-serif'], ['Nunito Sans', 'sans-serif'],
5
+ ['DM Sans', 'sans-serif'], ['Manrope', 'sans-serif'], ['Work Sans', 'sans-serif'],
6
+ ['IBM Plex Sans', 'sans-serif'], ['Public Sans', 'sans-serif'], ['Atkinson Hyperlegible', 'sans-serif'],
7
+ ['Outfit', 'sans-serif'], ['Fraunces', 'serif'], ['Merriweather', 'serif'], ['Lora', 'serif'],
8
+ ['Source Serif 4', 'serif'], ['Newsreader', 'serif'], ['Libre Baskerville', 'serif'], ['Crimson Pro', 'serif'],
9
+ ['EB Garamond', 'serif'], ['Playfair Display', 'serif'], ['Roboto Mono', 'monospace'],
10
+ ['IBM Plex Mono', 'monospace'], ['JetBrains Mono', 'monospace'], ['Fira Code', 'monospace'],
11
+ ].map(([family, category]) => [family, { family, category, weights: ['Lato', 'Atkinson Hyperlegible', 'Libre Baskerville'].includes(family) ? '400;700' : '400;500;600;700' }]));
12
+
13
+ export const systemFont = 'system-ui, sans-serif';
14
+ export const googleFontStack = family => `"${family}", ${googleFonts[family].category}`;
15
+ export function googleFontURL(fonts = {}) {
16
+ const families = [...new Set(Object.values(fonts))].sort();
17
+ if (!families.length) return '';
18
+ return 'https://fonts.googleapis.com/css2?' + families.map(family => {
19
+ if (!Object.hasOwn(googleFonts, family)) throw new Error('Choose a supported Google font.');
20
+ return 'family=' + encodeURIComponent(family).replaceAll('%20', '+') + ':wght@' + googleFonts[family].weights;
21
+ }).join('&') + '&display=swap';
22
+ }
23
+
24
+ /** Load and confirm actual faces before reporting a selected font as ready. */
25
+ const fontLoads = new WeakMap();
26
+ export async function loadGoogleFonts(document, fonts) {
27
+ const url = googleFontURL(fonts);
28
+ let link = document.head.querySelector('link[data-looma-google-fonts]');
29
+ if (!url) { fontLoads.get(link)?.cancel(); link?.remove(); return; }
30
+ if (link?.href !== url) {
31
+ fontLoads.get(link)?.cancel(); link?.remove(); link = document.createElement('link');
32
+ link.rel = 'stylesheet'; link.dataset.loomaGoogleFonts = '';
33
+ let cancel;
34
+ const loaded = new Promise((resolve, reject) => {
35
+ cancel = () => resolve(false);
36
+ link.onload = () => resolve(true); link.onerror = () => reject(new Error('The Google font could not load. Check your connection or select the system font.'));
37
+ });
38
+ fontLoads.set(link, { loaded, cancel }); link.href = url; document.head.append(link);
39
+ }
40
+ if (await fontLoads.get(link)?.loaded === false) return;
41
+ for (const family of new Set(Object.values(fonts))) {
42
+ const faces = await document.fonts.load(`400 16px "${family}"`);
43
+ if (!faces.length) throw new Error(`${family} is unavailable; the fallback is currently displayed.`);
44
+ }
45
+ }
46
+
47
+ /** Request only a font name's glyphs; selected fonts use the unrestricted stylesheet above. */
48
+ export function googleFontPreviewURL(family) {
49
+ if (!Object.hasOwn(googleFonts, family)) throw new Error('Choose a supported Google font.');
50
+ return 'https://fonts.googleapis.com/css2?family=' + encodeURIComponent(family).replaceAll('%20', '+') + ':wght@400&display=swap&text=' + encodeURIComponent(family);
51
+ }
52
+
53
+ const previewLoads = new WeakMap();
54
+ /** Isolate subset faces under aliases so document typography always uses full selected fonts. */
55
+ export function loadGoogleFontPreview(document, family) {
56
+ let loads = previewLoads.get(document);
57
+ if (!loads) { loads = new Map(); previewLoads.set(document, loads); }
58
+ if (!loads.has(family)) {
59
+ const loaded = (async () => {
60
+ const response = await document.defaultView.fetch(googleFontPreviewURL(family));
61
+ if (!response.ok) throw new Error('Font preview unavailable.');
62
+ const source = (await response.text()).match(/src\s*:\s*([^;]+);/)?.[1];
63
+ if (!source) throw new Error('Font preview unavailable.');
64
+ const alias = `Looma Font Preview ${family}`;
65
+ const face = new document.defaultView.FontFace(alias, source, { weight: '400', style: 'normal', display: 'swap' });
66
+ await face.load(); document.fonts.add(face);
67
+ return alias;
68
+ })();
69
+ loads.set(family, loaded);
70
+ loaded.catch(() => { if (loads.get(family) === loaded) loads.delete(family); });
71
+ }
72
+ return loads.get(family);
73
+ }
@@ -0,0 +1,9 @@
1
+ import type { TypeNode } from '@nextwebwg/html-next';
2
+
3
+ export type Framework = 'html-next' | 'vue' | 'react' | 'preact' | 'svelte' | 'angular';
4
+ export interface FrameworkExample { language: 'html' | 'vue' | 'tsx' | 'svelte' | 'text'; code: string; notice?: string; }
5
+ export const frameworkLabels: Readonly<Record<Framework, string>>;
6
+ export function frameworkExamples(
7
+ example: { markup: string; frameworks?: Partial<Record<Framework, FrameworkExample>> },
8
+ components: readonly { tag: string; package: string; root?: string; resource?: string; importSource?: string; attributes?: readonly { name: string; property?: string; type?: string; valueType?: TypeNode }[] }[],
9
+ ): Record<Framework, FrameworkExample>;
@@ -0,0 +1,225 @@
1
+ import { camel, componentName, exampleInvocations, nativeBooleans } from './example-invocations.mjs';
2
+
3
+ const escapeHtml = value => String(value).replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;');
4
+ const indent = depth => ' '.repeat(depth);
5
+ const textValue = node => node.preserve ? node.value : node.value.replace(/[\t\n\r\f ]+/g, ' ');
6
+ const formattingSpace = node => node?.kind === 'text' && !node.preserve && /^[\t\n\r\f ]*$/.test(node.value);
7
+ const key = (value, quote = JSON.stringify) => /^[a-zA-Z_$][\w$]*$/.test(value) ? value : quote(value);
8
+ const singleQuoted = value => `'${JSON.stringify(value).slice(1, -1).replaceAll('\\"', '"').replaceAll("'", "\\'")}'`;
9
+ const kebab = value => value.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
10
+ const jsx = target => target === 'react' || target === 'preact';
11
+ const voidElements = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
12
+ const reactAttributes = { class: 'className', for: 'htmlFor', readonly: 'readOnly', tabindex: 'tabIndex', autofocus: 'autoFocus', maxlength: 'maxLength', minlength: 'minLength', colspan: 'colSpan', rowspan: 'rowSpan', srcset: 'srcSet', crossorigin: 'crossOrigin', spellcheck: 'spellCheck', contenteditable: 'contentEditable', allowfullscreen: 'allowFullScreen', formnovalidate: 'formNoValidate', novalidate: 'noValidate', playsinline: 'playsInline' };
13
+ const numericAttributes = new Set(['cols', 'rows', 'size', 'span', 'rowSpan', 'colSpan', 'tabIndex', 'maxLength', 'minLength', 'start']);
14
+ const inlineElements = new Set('a abbr b bdi bdo br button cite code del em i img input ins kbd label mark q s samp small span strong sub sup time u var wbr'.split(' '));
15
+ const reserved = new Set('await break case catch class const continue debugger default delete do else enum export extends false finally for function if implements import in instanceof interface let new null package private protected public return static super switch this throw true try typeof var void while with yield arguments eval'.split(' '));
16
+
17
+ export const frameworkLabels = { 'html-next': 'HTML Next', vue: 'Vue', react: 'React', preact: 'Preact', svelte: 'Svelte', angular: 'Angular' };
18
+
19
+ function vueExpression(value) {
20
+ if (typeof value === 'string') return singleQuoted(value);
21
+ if (Array.isArray(value)) return `[${value.map(vueExpression).join(', ')}]`;
22
+ if (value && typeof value === 'object') {
23
+ const fields = Object.entries(value).map(([name, item]) => `${key(name, singleQuoted)}: ${vueExpression(item)}`);
24
+ return fields.length ? `{ ${fields.join(', ')} }` : '{}';
25
+ }
26
+ return JSON.stringify(value);
27
+ }
28
+
29
+ // Derive block layout from the tree. Mixed text and inline adjacency stay together.
30
+ function layout(nodes, owner) {
31
+ const structural = nodes.some(node => node.kind === 'slot' || node.kind === 'element' && (node.component || !inlineElements.has(node.tag)));
32
+ const labels = nodes.filter(node => node.kind === 'text' && !formattingSpace(node));
33
+ const namedSlot = node => node.kind === 'element' && node.attributes.some(attribute => attribute.name === 'slot' && attribute.value);
34
+ const slottedLabel = owner?.tag.includes('-') && labels.length === 1 && !labels[0].preserve
35
+ && nodes.some(namedSlot) && nodes.every(node => node.kind !== 'element' || namedSlot(node));
36
+ const multiline = slottedLabel || structural
37
+ && !nodes.some(node => node.kind === 'text' && !formattingSpace(node));
38
+ if (!multiline) return { multiline, nodes };
39
+ const displayed = nodes.filter(node => !formattingSpace(node)).map(node => {
40
+ if (!slottedLabel || node.kind !== 'text') return node;
41
+ return { ...node, value: node.value.replace(/^[\t\n\r\f ]+|[\t\n\r\f ]+$/g, '') };
42
+ });
43
+ return { multiline, nodes: displayed };
44
+ }
45
+
46
+ function text(node, target) {
47
+ const value = textValue(node);
48
+ if (target === 'html-next') return escapeHtml(value);
49
+ if (jsx(target)) {
50
+ if (node.preserve || /[\n\r\t]/.test(node.value)) {
51
+ return `{${JSON.stringify(value)}}`;
52
+ }
53
+ return escapeHtml(value).replace(/[{}]/g, char => `{${JSON.stringify(char)}}`);
54
+ }
55
+ const escaped = escapeHtml(value);
56
+ if (target === 'angular') return escaped.replaceAll('{', '&#123;').replaceAll('}', '&#125;').replaceAll('@', '&#64;');
57
+ return target === 'svelte' ? escaped.replaceAll('{', "{'{'}") : escaped.replaceAll('{', '&#123;');
58
+ }
59
+
60
+ function attribute(value, target, component) {
61
+ let name = value.property;
62
+ let data = value.value;
63
+ if (target === 'html-next') return data === true || data === '' ? name : `${name}="${escapeHtml(data)}"`;
64
+ if (jsx(target)) {
65
+ name = reactAttributes[name] ?? name;
66
+ if (name === 'style') {
67
+ data = Object.fromEntries(String(data).split(';').filter(part => part.includes(':')).map(part => {
68
+ const at = part.indexOf(':');
69
+ const property = part.slice(0, at).trim();
70
+ return [property.startsWith('--') ? property : camel(property), part.slice(at + 1).trim()];
71
+ }));
72
+ } else if (!component && numericAttributes.has(name) && /^[-+]?\d+$/.test(data)) data = Number(data);
73
+ if (data === true) return name;
74
+ return typeof data === 'string' ? `${name}="${escapeHtml(data)}"` : `${name}={${JSON.stringify(data)}}`;
75
+ }
76
+ if (target === 'angular') {
77
+ if (component?.attributes?.some(prop => (prop.property ?? prop.name) === name)) return `[${name}]="${escapeHtml(vueExpression(data))}"`;
78
+ if (data === true) return name;
79
+ return `${value.name}="${escapeHtml(data)}"`;
80
+ }
81
+ if (target === 'svelte') {
82
+ if (data === true) return name;
83
+ return typeof data === 'string' ? `${name}="${escapeHtml(data).replaceAll('{', '&#123;')}"` : `${name}={${JSON.stringify(data)}}`;
84
+ }
85
+ name = component ? kebab(name) : value.name;
86
+ if (data === true && nativeBooleans.has(name) && !component) return name;
87
+ return typeof data === 'string' ? `${name}="${escapeHtml(data).replaceAll('{', '&#123;')}"` : `:${name}="${escapeHtml(vueExpression(data))}"`;
88
+ }
89
+
90
+ function snippetBindings(model) {
91
+ const taken = new Set([...reserved, ...model.components.map(component => componentName(component.tag))]);
92
+ const bindings = new Map();
93
+ function visit(nodes) {
94
+ for (const node of nodes) {
95
+ if (node.kind === 'slot') {
96
+ let base = camel(node.name.replace(/[^\w$-]/g, '-')).replace(/-/g, '');
97
+ if (!/^[a-zA-Z_$]/.test(base)) base = `slot${base}`;
98
+ if (reserved.has(base)) base += 'Content';
99
+ let name = base;
100
+ if (taken.has(name)) {
101
+ const owner = componentName(node.owner.tag).replace(/^Ui/, '');
102
+ base = owner[0].toLowerCase() + owner.slice(1) + base[0].toUpperCase() + base.slice(1);
103
+ name = base;
104
+ for (let index = 2; taken.has(name); index++) name = `${base}${index}`;
105
+ }
106
+ taken.add(name);
107
+ bindings.set(node, name);
108
+ }
109
+ if (node.children) visit(node.children);
110
+ }
111
+ }
112
+ visit(model.nodes);
113
+ return bindings;
114
+ }
115
+
116
+ /** Print the public invocation API, keeping framework-specific syntax out of the parsed model. */
117
+ function printer(model, target) {
118
+ const bindings = ['svelte', 'angular'].includes(target) ? snippetBindings(model) : new Map();
119
+
120
+ function sequence(nodes, depth, forceLines = false, owner) {
121
+ const arranged = layout(nodes, owner);
122
+ const content = arranged.nodes.map(node => (arranged.multiline ? indent(depth) : '') + render(node, depth)).join(arranged.multiline ? '\n' : '');
123
+ return (forceLines && !arranged.multiline ? indent(depth) : '') + content;
124
+ }
125
+
126
+ function reactValue(nodes, depth) {
127
+ const content = [...nodes];
128
+ const boundaryIndentation = node => formattingSpace(node) && /[\n\r]/.test(node.value);
129
+ while (boundaryIndentation(content[0])) content.shift();
130
+ while (boundaryIndentation(content.at(-1))) content.pop();
131
+ const arranged = layout(content);
132
+ if (!arranged.nodes.length) return 'null';
133
+ if (arranged.nodes.length === 1) {
134
+ const node = arranged.nodes[0];
135
+ if (node.kind === 'text') return JSON.stringify(textValue(node));
136
+ if (node.kind === 'comment') return `null /*${node.value}*/`;
137
+ return render(node, depth);
138
+ }
139
+ return arranged.multiline
140
+ ? `<>\n${sequence(content, depth + 1)}\n${indent(depth)}</>`
141
+ : `<>${sequence(content, depth)}</>`;
142
+ }
143
+
144
+ function render(node, depth) {
145
+ if (node.kind === 'text') return text(node, target);
146
+ if (node.kind === 'comment') return jsx(target) ? `{/*${node.value}*/}` : `<!--${node.value}-->`;
147
+ if (node.kind === 'slot' && target === 'angular') {
148
+ return `<ng-container ngProjectAs="[slot='${node.name}']">\n${sequence(node.children, depth + 1, true)}\n${indent(depth)}</ng-container>`;
149
+ }
150
+ if (node.kind === 'slot') {
151
+ return `<template #${node.name}>\n${sequence(node.children, depth + 1, true)}\n${indent(depth)}</template>`;
152
+ }
153
+ const structural = target === 'angular' && node.component?.angularHost === 'ng-template';
154
+ const declared = value => node.component.attributes?.some(prop => (prop.property ?? prop.name) === value.property);
155
+ const attrs = node.attributes.filter(value => !structural || declared(value)).map(value => attribute(value, target, node.component));
156
+ if (structural) {
157
+ const forwarded = node.attributes.filter(value => !declared(value));
158
+ if (forwarded.length) attrs.push(`[hnAttributes]="${escapeHtml(`{ ${forwarded.map(value => `${key(value.name, singleQuoted)}: ${vueExpression(value.value)}`).join(', ')} }`)}"`);
159
+ }
160
+ if (node.slots.length && target === 'svelte') {
161
+ attrs.push(`slots={{ ${node.slots.map(slot => slot.name === bindings.get(slot) ? slot.name : `${key(slot.name)}: ${bindings.get(slot)}`).join(', ')} }}`);
162
+ }
163
+ if (node.slots.length && jsx(target)) {
164
+ attrs.push(`slots={{\n${node.slots.map(slot => `${indent(depth + 2)}${key(slot.name)}: ${reactValue(slot.children, depth + 2)},`).join('\n')}\n${indent(depth + 1)}}}`);
165
+ }
166
+ let tag = node.tag;
167
+ let templates = '';
168
+ if (target === 'angular' && node.component) {
169
+ tag = node.component.angularHost ?? node.component.root;
170
+ if (!tag) throw new Error(`Missing Angular native host for ${node.component.tag}.`);
171
+ attrs.unshift(node.component.tag);
172
+ if (tag === 'ng-template' && node.slots.length) {
173
+ templates = node.slots.map(slot => `<ng-template #${bindings.get(slot)}>\n${sequence(slot.children, depth + 1, true)}\n${indent(depth)}</ng-template>\n${indent(depth)}`).join('');
174
+ attrs.push(`[slots]="${escapeHtml(`{ ${node.slots.map(slot => `${key(slot.name, singleQuoted)}: ${bindings.get(slot)}`).join(', ')} }`)}"`);
175
+ }
176
+ }
177
+ const wrapped = attrs.length > 0;
178
+ const start = wrapped ? `<${tag}\n${attrs.map(value => indent(depth + 1) + value).join('\n')}\n${indent(depth)}` : `<${tag}`;
179
+ const children = target === 'vue' || target === 'angular' && tag !== 'ng-template' ? node.children : node.children.filter(child => child.kind !== 'slot');
180
+ const owner = target === 'html-next' ? node : undefined;
181
+ const arranged = layout(children, owner);
182
+ if (!arranged.nodes.length && target !== 'angular' && (node.component || jsx(target))) return `${start}${wrapped ? '' : ' '}/>`;
183
+ if (voidElements.has(tag)) return templates + `${start}>`;
184
+ const content = sequence(children, depth + 1, false, owner);
185
+ return arranged.multiline
186
+ ? `${templates}${start}>\n${content}\n${indent(depth)}</${tag}>`
187
+ : `${templates}${start}>${content}</${tag}>`;
188
+ }
189
+
190
+ function body(depth) {
191
+ const arranged = layout(model.nodes);
192
+ if (jsx(target)) return reactValue(model.nodes, depth);
193
+ return (arranged.multiline ? '' : indent(depth)) + sequence(model.nodes, depth);
194
+ }
195
+
196
+ return {
197
+ body,
198
+ snippets: () => [...bindings].map(([slot, name]) => `{#snippet ${name}()}\n${sequence(slot.children, 1, true)}\n{/snippet}`).join('\n\n'),
199
+ };
200
+ }
201
+
202
+ /** Present invocations using the public APIs produced by each Vite framework target. */
203
+ export function frameworkExamples(example, components) {
204
+ const model = exampleInvocations(example.markup, components);
205
+ const importSource = component => component.importSource ?? component.package;
206
+ const imports = [...new Set(model.components.map(importSource))].map(owner => {
207
+ const names = model.components.filter(component => importSource(component) === owner).map(component => componentName(component.tag)).sort();
208
+ const line = `import { ${names.join(', ')} } from ${JSON.stringify(owner)};`;
209
+ return line.length <= 100 ? line : `import {\n${names.map(name => ` ${name},`).join('\n')}\n} from ${JSON.stringify(owner)};`;
210
+ }).join('\n');
211
+ const links = model.components.map(component => `<link\n rel="component"\n href="${escapeHtml(component.resource ?? `${component.package}/components/${component.tag}/${component.tag}.html`)}"\n>`).join('\n');
212
+ const html = printer(exampleInvocations(example.markup, []), 'html-next').body(0);
213
+ const codes = { 'html-next': { language: 'html', code: [links, html].filter(Boolean).join('\n\n') } };
214
+ for (const target of ['vue', 'react', 'preact', 'svelte']) {
215
+ const output = printer(model, target);
216
+ const script = imports ? `<script${target === 'vue' ? ' setup' : ''}>\n${imports.split('\n').map(line => ' ' + line).join('\n')}\n</script>\n\n` : '';
217
+ codes[target] = jsx(target)
218
+ ? { language: 'tsx', code: `${target === 'preact' ? '/** @jsxImportSource preact */\n' : ''}${imports ? imports + '\n\n' : ''}export default function Example() {\n return ${output.body(1)};\n}` }
219
+ : { language: target, code: script + (target === 'vue' ? `<template>\n${output.body(1)}\n</template>` : [output.snippets(), output.body(0)].filter(Boolean).join('\n\n')) };
220
+ }
221
+ const angular = printer(model, 'angular');
222
+ const template = angular.body(2).replaceAll('\\', '\\\\').replaceAll('`', '\\`').replaceAll('${', '\\${');
223
+ codes.angular = { language: 'typescript', code: `import { Component } from "@angular/core";\n${imports ? imports + '\n' : ''}\n@Component({\n selector: "example-view",\n standalone: true,\n imports: [${model.components.map(component => componentName(component.tag)).join(', ')}],\n template: \`\n${template}\n \`,\n})\nexport default class Example {}` };
224
+ return { ...codes, ...example.frameworks };
225
+ }
@@ -0,0 +1,64 @@
1
+ ---
2
+ title: Theme your system
3
+ order: 1
4
+ ---
5
+
6
+ <div data-looma-guide>
7
+
8
+ # Theme your system
9
+
10
+ Open **Customize** in your local docs site. You can try a starting theme, refine its shared decisions, and see the result on real components.
11
+
12
+ ## Choose a starting point
13
+
14
+ Pick a starting theme in the summary bar. Each choice sets a coordinated identity across color, typography, shape, spacing, material, and motion.
15
+
16
+ - **Minimal** keeps neutral colors and restrained depth.
17
+ - **Gold** combines warm actions with rounded, polished surfaces.
18
+ - **Bookish** pairs wine and coral accents with reading-focused type.
19
+ - **Editorial** uses flat ink-and-paper surfaces and serif headings.
20
+ - **Night** uses cool accents and glass material.
21
+ - **Garden** brings forest, clay, and gold together with soft corners.
22
+
23
+ ## Refine one family at a time
24
+
25
+ Open a summary on the overview to edit that subject. Choose **Overview** to return.
26
+
27
+ ::: table
28
+
29
+ - Editor: Palette
30
+ Try: Change your brand seeds, then compare light and dark specimens.
31
+
32
+ - Editor: Typography
33
+ Try: Choose a body font and a heading font. Check a heading beside a form label.
34
+
35
+ - Editor: Size & spacing
36
+ Try: Adjust control roominess and spacing while comparing the size ladder.
37
+
38
+ - Editor: Material & light
39
+ Try: Move the light source and compare matte and polished surfaces.
40
+
41
+ - Editor: Motion
42
+ Try: Replay the motion study and open the example popover.
43
+
44
+ :::
45
+
46
+ Edits apply immediately. Use **Undo**, **Redo**, or **Reset** in the summary bar as you explore. [Theme decisions](/theme-decisions/) explains the detailed controls and derivation rules.
47
+
48
+ ## Check your own components
49
+
50
+ Choose **Browse components** to find an example in your discovered catalog. Preview your common forms, navigation, and content alongside one another. [Add your components](/add-your-components/) shows how to include your own definitions and examples.
51
+
52
+ ![Fellowship UI with the same component definitions in a dark theme.](./assets/fellowship-dark.png)
53
+
54
+ Every generated theme includes light and dark palettes. Use the appearance button to compare both. In your application, leave `data-theme` off the document root to follow the system, or set it to `light` or `dark` for a fixed mode.
55
+
56
+ ## Save and continue
57
+
58
+ Open **Export & save**. On your local site, saving updates the configured JSON recipe and CSS. A static site offers downloads. Import a recipe to continue editing it later.
59
+
60
+ Load the CSS after Looma's styles in your application. Re-save your recipe after a Looma upgrade to pick up updated derivation rules. Keep handwritten overrides in their own file.
61
+
62
+ [Next: add your components](/add-your-components/)
63
+
64
+ </div>
@@ -0,0 +1,96 @@
1
+ ---
2
+ title: Add your components
3
+ order: 2
4
+ ---
5
+
6
+ <div data-looma-guide>
7
+
8
+ # Add your components
9
+
10
+ Build your design-system components in HTML Next. Their definitions, description attributes, modules and HTML examples are the source for the generated documentation and customizer cards. Use Looma’s public semantic properties to make your components respond to the same theme.
11
+
12
+ 1. Create a directory for your HTML Next component resources, such as `app/components`.
13
+ 2. Opt that directory into `components` in `looma.docs.json`, with your package's name.
14
+ 3. Put `docs.json` and an `examples/` directory beside each resource, as shown below.
15
+ 4. Run `looma-docs dev`. Your components appear beside Looma's in the reference,
16
+ search and customizer example picker. Resources under registered directories
17
+ are rediscovered when the local server rebuilds.
18
+
19
+ ## Register a discovery root
20
+
21
+ The configuration can combine your name, authored guides and discovery roots:
22
+
23
+ ```json
24
+ {
25
+ "title": "The Fellowship",
26
+ "base": "/",
27
+ "outDir": ".looma/site",
28
+ "content": "docs",
29
+ "theme": { "recipe": "looma.theme.json", "css": "looma.theme.css" },
30
+ "components": [{ "directory": "app/components", "packageName": "fellowship-ui" }],
31
+ "overrides": ["styles/design-system-overrides.css"]
32
+ }
33
+ ```
34
+
35
+ `content` and `overrides` are optional. Create files you configure before building. Markdown guidance supplements the generated reference; it cannot replace an API page. Relative component and asset dependencies keep their original project locations, and code examples remain literal. Local web fonts referenced by override CSS are embedded in the documentation's theme stylesheet so isolated examples render the same fonts on static hosts. Google font selections travel in the generated CSS import. Load custom or self-hosted faces in the consuming application; their stacks only select families.
36
+
37
+ ## Write a component
38
+
39
+ A resource can use any valid HTML Next component tag:
40
+
41
+ ```html
42
+ <template component="office-status">
43
+ <defs>
44
+ <prop name="label" type="string" default="Ready">The team's current status.</prop>
45
+ </defs>
46
+ <p>{$label}<slot></slot></p>
47
+ </template>
48
+ ```
49
+
50
+ ## Describe it in the catalog
51
+
52
+ Place a `docs.json` beside it to join a shared category:
53
+
54
+ ```json
55
+ {
56
+ "title": "Office status",
57
+ "summary": "The office’s current status.",
58
+ "description": "A status for the Fellowship office.",
59
+ "group": "display"
60
+ }
61
+ ```
62
+
63
+ An optional `summary` supplies the short card text; `description` supplies the full reference introduction. Cards use `description` when no summary is authored.
64
+
65
+ Looma's category IDs are listed in its published catalog. Components without a category appear in **Project components**. A discovery root can declare additional `groups` as `{ "id": "…", "label": "…" }` entries. The top-level `categories` mapping explicitly maps category IDs when combining catalogs.
66
+
67
+ A component card should demonstrate its defining feature, including a non-default configuration when that makes the behavior clear. A feature demonstration does not require changing the component’s default.
68
+
69
+ ## Add a live example
70
+
71
+ Put authored examples in an `examples/` directory beside the resource. Each `.html` example starts with a metadata comment:
72
+
73
+ ```html
74
+ <!--
75
+ title: Ready for second breakfast
76
+ description: Sam has finished the morning checklist.
77
+ -->
78
+ <office-status label="Ready for second breakfast"></office-status>
79
+ ```
80
+
81
+ The first example also appears in the component directory, so keep it representative at a narrow card width. Full variations belong in the later reference examples. Optional `.behavior.js` or `.behavior.ts` companions export a default setup function receiving the example root; return a cleanup function when needed. Select controls through example-local data attributes: generated IDs and their label, trigger and command references are namespaced for each instance. Add `preview: resizable` to expose the shared card's width control, or `preview: stretch` when the sample needs the available inline width, such as percentage-based loading placeholders. Framework-specific `.snippet.html` and `.vue` companions provide literal source views.
82
+
83
+ Discovery reads definitions and descriptions without executing their controllers. New resources under a selected root appear after the local server rebuilds. External components keep their own contracts and supported token hooks; discovery does not make arbitrary components respond to Looma's tokens.
84
+
85
+ Author components and documentation examples in HTML Next. Vue, React, Preact and Svelte applications compile those definitions with the HTML Next Vite plugin or converter; framework conversion belongs in the consuming application. See the [loading guide](https://threadlabs.studio/looma/getting-started/) for framework setup, imports and styles.
86
+
87
+
88
+ ## Combine installed catalogs
89
+
90
+ A library can publish the versioned catalog shape exposed by `@threadlabs/looma/docs/catalog.json`. Add package exports or relative JSON files under `catalogs` in the configuration. Component identities are package-qualified; duplicate tags, conflicting categories and missing compound parents fail with ownership information instead of silently replacing a component.
91
+
92
+ Use the `@threadlabs/looma-docs` entrypoint for programmatic discovery, catalog merging, theme validation, and build/dev/preview integration. Its TypeScript definitions describe the configuration and catalog interfaces. The browser card renderer, catalog data and compiled example companions ship in the package.
93
+
94
+ [Next: publish your system](/publish-your-system/)
95
+
96
+ </div>
@@ -0,0 +1,71 @@
1
+ ---
2
+ title: Publish your system
3
+ order: 3
4
+ ---
5
+
6
+ <div data-looma-guide>
7
+
8
+ # Publish your system
9
+
10
+ Give your documentation its own name, then build a static site for your team.
11
+
12
+ ## Name and brand the site
13
+
14
+ `looma-docs init --title "Fellowship UI"` sets your site's name. To rename an
15
+ existing local documentation site, edit `title` in `looma.docs.json`; the local
16
+ server rebuilds the header, generated landing page and page titles with that
17
+ name. You do not need to fork Looma or change component imports.
18
+
19
+ You can also supply your logo and navigation:
20
+
21
+ ```json
22
+ {
23
+ "title": "Fellowship UI",
24
+ "logo": {
25
+ "light": "/assets/fellowship-light.svg",
26
+ "dark": "/assets/fellowship-dark.svg"
27
+ },
28
+ "nav": [{ "label": "Our application", "href": "https://example.com" }]
29
+ }
30
+ ```
31
+
32
+ Merge these fields into your existing configuration. Serve the logo URLs from
33
+ your application's public assets, such as `public/assets/`. Leave `logo` unset
34
+ for a text-only brand. To author your own introduction, set `content` to your
35
+ Markdown directory and add its `index.md`; it replaces the generated landing
36
+ page without replacing component reference pages.
37
+
38
+ The site name is yours. Component ownership stays accurate: built-in cards
39
+ still identify `@threadlabs/looma`, while your discovered components identify
40
+ the `packageName` you configure below. That is package attribution, not the
41
+ name of your design system. Shared CSS and recipe filenames are configured
42
+ under `theme`; changing a site title does not require renaming those files.
43
+
44
+ A navigation link with `"position": "start"` appears before the generated component and
45
+ editor sections. Other links keep their order after those sections:
46
+
47
+ ```json
48
+ { "nav": [{ "label": "Get started", "href": "/getting-started/", "position": "start" }] }
49
+ ```
50
+
51
+ For documentation-site Markdown syntax and publishing options, see the
52
+ [Markupress documentation](https://threadlabs-studio.github.io/markupress/).
53
+
54
+ ## Build and preview
55
+
56
+ ```sh
57
+ pnpm exec looma-docs build
58
+ pnpm exec looma-docs preview
59
+ ```
60
+
61
+ `build` writes a static MarkupKit site to `outDir`, which defaults to `.looma/site`. Deploy that directory to your static host. `preview` serves the built files. All commands accept `--root`; `dev` and `preview` also accept `--port`.
62
+
63
+ ## Save your theme
64
+
65
+ Commit `looma.docs.json`, the theme recipe, and the generated CSS with your components. Add `.looma/` to your ignore file. Keep handwritten CSS in a separate overrides file so saving a theme preserves it.
66
+
67
+ The local editor writes only the configured recipe and CSS inside the project. It rejects symlink paths, generated or dependency directories, cross-origin save requests and stale file revisions. A competing edit leaves your unsaved preview in place; reload and reconcile it before saving again. Examples are pre-rendered directly in the documentation page and share its styles and HTML Next runtime. Project component roots and example companions are trusted application code, so opt in only to sources you would run in your application. Example IDs and their label, trigger and command relationships are namespaced per specimen; companions can select stable authored hooks such as data-example-id. Width controls resize the inline stage; viewport media queries follow the documentation window.
68
+
69
+ The writer stages complete files, serializes saves with a project lock, and rolls back the recipe if replacing the CSS fails. If a process stops during a save or build, close the old editor before removing its `.looma/theme-write.lock` or `.looma/docs-build.lock`; restart and save the loaded recipe to regenerate a consistent pair. Change the server base, output directory or theme-file paths by restarting `dev`.
70
+
71
+ </div>