@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.
Files changed (37) hide show
  1. package/README.md +15 -0
  2. package/bin/looma-docs.mjs +3 -0
  3. package/docs/README.md +116 -0
  4. package/docs/card.html +63 -0
  5. package/docs/card.js +32 -0
  6. package/docs/catalog-view.js +18 -0
  7. package/docs/catalog-view.mjs +11 -0
  8. package/docs/catalog.json +22974 -0
  9. package/docs/catalog.mjs +131 -0
  10. package/docs/cli.mjs +26 -0
  11. package/docs/commands.mjs +94 -0
  12. package/docs/customizer-render.mjs +40 -0
  13. package/docs/customizer.js +118 -0
  14. package/docs/documentation.css +8 -0
  15. package/docs/examples/ui-action-bar/examples/01-form-footer.behavior.js +7 -0
  16. package/docs/examples/ui-button/examples/08-loading.behavior.js +21 -0
  17. package/docs/examples/ui-checkbox/examples/02-checked-indeterminate-and-disabled.behavior.js +30 -0
  18. package/docs/examples/ui-combobox/examples/06-selected-values.behavior.js +11 -0
  19. package/docs/examples/ui-search-shell/examples/02-label-dismissible-status-and-footer.behavior.js +34 -0
  20. package/docs/examples/ui-toast/examples/01-authored.behavior.js +15 -0
  21. package/docs/examples/ui-toast/examples/02-danger.behavior.js +2 -0
  22. package/docs/examples/ui-tree/examples/03-max-depth-2-and-hover-expand-delay-300.behavior.js +30 -0
  23. package/docs/examples/ui-tree/examples/06-move-without-dragging.behavior.js +2 -0
  24. package/docs/examples/ui-tree/examples/07-multiple-selection.behavior.js +16 -0
  25. package/docs/examples/ui-tree-item/examples/02-container.behavior.js +1 -0
  26. package/docs/index.d.ts +54 -0
  27. package/docs/index.mjs +4 -0
  28. package/docs/local-server.mjs +68 -0
  29. package/docs/preview.js +20 -0
  30. package/docs/reference.html +1 -0
  31. package/docs/reference.js +36 -0
  32. package/docs/render.mjs +84 -0
  33. package/docs/site.mjs +198 -0
  34. package/docs/store.mjs +92 -0
  35. package/docs/theme.mjs +100 -0
  36. package/docs/workspace.mjs +32 -0
  37. package/package.json +28 -4
package/README.md CHANGED
@@ -58,3 +58,18 @@ stylesheet is the only stylesheet to include. Without a build, link components o
58
58
  Components read design tokens (`--ui-*` custom properties) from `tokens.css` and a theme
59
59
  (`theme-light.css`, `theme-dark.css`, `theme-high-contrast.css`), and each exposes its own
60
60
  `--ui-<component>-*` properties. Set them on the component or any ancestor.
61
+
62
+
63
+ ## Weave your design system
64
+
65
+ The package includes the documentation generator and theme customizer used by Looma's public site. Browse real component cards, generate a theme from shared semantic decisions, and document your own HTML Next components beside Looma's.
66
+
67
+ Install the optional documentation tools, then initialize your project:
68
+
69
+ ```sh
70
+ pnpm add -D markupress @nextwebwg/htmlkit@1.0.0-alpha.69 vite
71
+ pnpm exec looma-docs init
72
+ pnpm exec looma-docs dev
73
+ ```
74
+
75
+ The local editor saves `looma.theme.json` and generated `looma.theme.css`; import that CSS in your application. Static builds offer CSS and recipe downloads. See the [design-system guide](https://threadlabs.studio/looma/design-system/) for configuration, discovery, saving and recovery. Programmatic tooling is available through `@threadlabs/looma/docs`.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ // Keep the command linkable before the workspace build creates docs/.
3
+ import '../docs/cli.mjs';
package/docs/README.md ADDED
@@ -0,0 +1,116 @@
1
+ ---
2
+ title: Weave your design system
3
+ ---
4
+
5
+ # Weave your design system
6
+
7
+ Looma ships the documentation generator and theme customizer used by its public site. Run it in your project to browse Looma and your own HTML Next components as one design system. The customizer uses the same authored component cards as the reference.
8
+
9
+ ## Start a local design system
10
+
11
+ In a project with a `package.json`, install Looma and its documentation tools:
12
+
13
+ ```sh
14
+ pnpm add @threadlabs/looma @nextwebwg/html-next@1.0.0-alpha.69
15
+ pnpm add -D markupress @nextwebwg/htmlkit@1.0.0-alpha.69 vite
16
+ pnpm exec looma-docs init --title "The Fellowship"
17
+ pnpm exec looma-docs dev
18
+ ```
19
+
20
+ Use the tool versions accepted by Looma's optional peer dependencies. Regular component imports do not load the documentation server. Vue and Tiptap remain optional; install them when your own authored documentation mounts the Vue editor.
21
+
22
+ Initialization creates three project-owned files:
23
+
24
+ | File | Purpose |
25
+ | --- | --- |
26
+ | `looma.docs.json` | Documentation title, content, discovery roots and theme paths. |
27
+ | `looma.theme.json` | Editable semantic decisions. |
28
+ | `looma.theme.css` | Generated theme, including the shared derivation rules. |
29
+
30
+ Import `./looma.theme.css` after Looma's tokens and palette in your application's entry point. The initializer prints that import; it leaves existing application files intact. Commit the configuration, recipe and CSS. Add `.looma/` to your project's ignore file: it contains disposable generated documentation and tooling files.
31
+
32
+ Open the local address printed by `dev`, choose **Customize**, and save your theme. Saving updates those shared theme files and rebuilds the documentation. Your application consumes the same CSS import. A static or public site instead offers CSS and recipe downloads.
33
+
34
+ ```sh
35
+ pnpm exec looma-docs build
36
+ pnpm exec looma-docs preview
37
+ ```
38
+
39
+ `build` produces a static HTMLKit site in the configured output directory. `preview` serves that output without a repository writer. All commands accept `--root`; `dev` and `preview` also accept `--port`.
40
+
41
+ ## Change the rule, keep the family consistent
42
+
43
+ The authoring model has twenty shared decisions, grouped as Colour, Type, Size & spacing, Material and Motion. The controls expose shared intent: a control size, spaciousness, corners, inner and outer depth, and response pace.
44
+
45
+ The generated values follow rules. Small and large control heights follow a ratio around the shared control size. Spacing follows an independent power curve: linear growth at 1, quadratic at 2. Depth generates families of shadows; it does not add motion amplitude. Fields and buttons with the same size setting use the same box height, while content can still grow when it needs more room.
46
+
47
+ Lengths retain their CSS units, such as `rem`; times retain `ms` or `s`. The downloaded CSS includes the canonical shared recipe as well as your inputs. Import a downloaded JSON recipe to continue editing it. Keep hand-written CSS in a separate overrides file so regenerating the theme does not replace it.
48
+
49
+ Changes remain a preview until saved. Starting points update all selected cards. Reset returns to the loaded recipe. Invalid input keeps the last valid preview and explains which decision needs attention.
50
+
51
+ ## Add your components
52
+
53
+ Opt into source directories in `looma.docs.json`:
54
+
55
+ ```json
56
+ {
57
+ "title": "The Fellowship",
58
+ "base": "/",
59
+ "outDir": ".looma/site",
60
+ "content": "docs",
61
+ "theme": { "recipe": "looma.theme.json", "css": "looma.theme.css" },
62
+ "components": [{ "directory": "app/components", "packageName": "fellowship-ui" }],
63
+ "overrides": ["styles/design-system-overrides.css"]
64
+ }
65
+ ```
66
+
67
+ `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.
68
+
69
+ A resource can use any valid HTML Next component tag:
70
+
71
+ ```html
72
+ <template component="office-status">
73
+ <defs>
74
+ <prop name="label" type="string" default="Ready">The team's current status.</prop>
75
+ </defs>
76
+ <p>{$label}<slot></slot></p>
77
+ </template>
78
+ ```
79
+
80
+ Place a `docs.json` beside it to join a shared category:
81
+
82
+ ```json
83
+ {
84
+ "title": "Office status",
85
+ "description": "A status for the Fellowship office.",
86
+ "group": "display"
87
+ }
88
+ ```
89
+
90
+ 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.
91
+
92
+ Put authored examples in an `examples/` directory beside the resource. Each `.html` example starts with a metadata comment:
93
+
94
+ ```html
95
+ <!--
96
+ title: Ready for second breakfast
97
+ description: Sam has finished the morning checklist.
98
+ -->
99
+ <office-status label="Ready for second breakfast"></office-status>
100
+ ```
101
+
102
+ The example appears in both the reference and the customizer's selection list. Optional `.behavior.js` or `.behavior.ts` companions export a default setup function receiving the example root; return a cleanup function when needed. Add `preview: resizable` to expose the shared card's width control. Framework-specific `.snippet.html` and `.vue` companions provide literal source views.
103
+
104
+ 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 invent a Vue adapter or make arbitrary components respond to Looma's tokens.
105
+
106
+ ## Combine installed catalogs
107
+
108
+ 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.
109
+
110
+ 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.
111
+
112
+ ## Saving and recovery
113
+
114
+ 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 run in isolated frames and receive theme CSS, never the local editing capability.
115
+
116
+ 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`.
package/docs/card.html ADDED
@@ -0,0 +1,63 @@
1
+ <link rel="component" href="@threadlabs/looma/components/ui-form-field/ui-form-field.html">
2
+ <link rel="component" href="@threadlabs/looma/components/ui-input/ui-input.html">
3
+ <link rel="component" href="@threadlabs/looma/components/ui-button/ui-button.html">
4
+ <template component="looma-example-card" module="./card.js">
5
+ <defs>
6
+ <prop name="identity" type="string" default="">Stable catalog example identity.</prop>
7
+ <prop name="title" type="string" default="Example">The documented component name.</prop>
8
+ <prop name="description" type="string" default="">The component's authored description.</prop>
9
+ <prop name="exampleTitle" type="string" default="">The selected authored example.</prop>
10
+ <prop name="src" type="string" default="">The compiled isolated example page.</prop>
11
+ <prop name="resizable" type="boolean" default="false">Expose a width control for responsive examples.</prop>
12
+ <prop name="category" type="string" default="forms">The shared category identity.</prop>
13
+ <prop name="tag" type="string" default="">The component's authored tag.</prop>
14
+ <prop name="packageName" type="string" default="">The package that owns this component.</prop>
15
+ <prop name="nativeRoot" type="string" default="">The component's semantic native root.</prop>
16
+ <prop name="href" type="string" default="">The component reference page.</prop>
17
+ <prop name="removable" type="boolean" default="false">Offer removal when this card is selected in the customizer.</prop>
18
+ <prop name="loading" type="keyword" values="lazy, eager" default="lazy">Load visible catalog cards lazily; selected customizer cards eagerly.</prop>
19
+ </defs>
20
+ <article from:data-example-card="$identity" from:data-component-card="$tag" from:data-category="$category">
21
+ <div class="preview"><iframe $ref="frame" from:title="$title" from:src="$src" sandbox="allow-scripts" from:loading="$loading" width="100%" height="208"></iframe></div>
22
+ <div class="body">
23
+ <div class="meta"><span>{$category}</span><code>{$nativeRoot}</code></div>
24
+ <h3><a from:href="$href">{$title}</a></h3>
25
+ <p class="description">{$description}</p>
26
+ <p class="owner" $if="$packageName">{$packageName}</p>
27
+ <p class="example-caption" $if="$exampleTitle">{$exampleTitle}</p>
28
+ <ui-form-field $if="$resizable"><label slot="label">Preview width</label><span slot="help">Explore the example at a narrower width.</span><ui-input $ref="width" type="number" min="240" max="1600" value="720" aria-label="Preview width in pixels" size="sm"></ui-input></ui-form-field>
29
+ <footer><ui-button $if="$href" as="a" from:href="$href" variant="link" size="sm">View component</ui-button><ui-button $if="$removable" $ref="remove" variant="ghost" size="sm">Remove</ui-button><slot name="actions"></slot></footer>
30
+ </div>
31
+ </article>
32
+ <style>
33
+ /* The existing documentation catalog card, shared by the directory, reference and customizer. */
34
+ :host {
35
+ --_card-accent: var(--ui-accent);
36
+ position: relative;
37
+ overflow: hidden;
38
+ background: var(--ui-surface-raised);
39
+ border: var(--ui-border-width) solid var(--ui-border);
40
+ border-radius: var(--ui-radius-lg);
41
+ box-shadow: var(--ui-shadow-sm);
42
+ transition: border-color var(--ui-motion-fast) var(--ui-motion-ease), box-shadow var(--ui-motion-fast) var(--ui-motion-ease), transform var(--ui-motion-fast) var(--ui-motion-ease);
43
+ }
44
+ :host([category="display"]) { --_card-accent: var(--ui-success); }
45
+ :host([category="overlay"]) { --_card-accent: var(--ui-danger); }
46
+ :host([category="layout"]) { --_card-accent: var(--ui-info); }
47
+ :host([category="editor"]) { --_card-accent: var(--ui-warning); }
48
+ :host:hover { border-color: color-mix(in srgb, var(--_card-accent) 45%, var(--ui-border)); box-shadow: var(--ui-shadow-md); transform: translateY(-2px); }
49
+ .preview { position: relative; min-block-size: 13rem; overflow: hidden; border-block-end: var(--ui-border-width) solid var(--ui-border); background: color-mix(in srgb, var(--_card-accent) 5%, var(--ui-surface)); }
50
+ .preview::before { position: absolute; inset-block-start: 0; inset-inline-start: 0; inline-size: 3.5rem; block-size: 3px; background: var(--_card-accent); content: ""; z-index: 1; }
51
+ iframe { display: block !important; max-inline-size: 100%; border: 0; }
52
+ footer { display: flex !important; align-items: center !important; justify-content: space-between !important; gap: var(--ui-space-2) !important; }
53
+ .body { padding: var(--ui-space-5) !important; }
54
+ .meta { display: flex !important; align-items: center !important; justify-content: space-between !important; gap: var(--ui-space-2) !important; margin-block-end: var(--ui-space-2) !important; color: var(--ui-text-secondary); font-size: var(--ui-font-size-xs); font-weight: var(--ui-font-bold); letter-spacing: .09em; text-transform: uppercase; }
55
+ .meta code { padding: 0 !important; background: transparent; color: inherit; text-transform: none; }
56
+ h3 { margin: 0 !important; padding: 0 !important; font-size: var(--ui-font-size-lg); letter-spacing: -.035em; }
57
+ h3 a { color: var(--ui-text); text-decoration: none; }
58
+ .description { margin: var(--ui-space-2) 0 !important; color: var(--ui-text-secondary); font-size: var(--ui-font-size-sm); line-height: var(--ui-line-height); }
59
+ .owner { margin: var(--ui-space-2) 0 !important; color: var(--ui-text-secondary); font-size: var(--ui-font-size-xs); overflow-wrap: anywhere; }
60
+ .example-caption { margin: var(--ui-space-2) 0 !important; color: var(--ui-text-secondary); font-size: var(--ui-font-size-xs); }
61
+ @media (prefers-reduced-motion: reduce) { :host { transition: none; } :host:hover { transform: none; } }
62
+ </style>
63
+ </template>
package/docs/card.js ADDED
@@ -0,0 +1,32 @@
1
+ /** Only theme data and measured height cross the opaque preview boundary. */
2
+ export function connect(host) {
3
+ const frame = host.refs.frame;
4
+ const width = host.refs.width;
5
+ const remove = host.refs.remove;
6
+ const removeCard = () => host.element.dispatchEvent(new CustomEvent("looma-example-remove", { bubbles: true, detail: { id: host.props.identity.value } }));
7
+ const view = frame.ownerDocument.defaultView;
8
+ let theme = '';
9
+ const send = () => frame.contentWindow?.postMessage({ type: 'looma-theme', css: theme }, '*');
10
+ const message = event => {
11
+ if (event.source !== frame.contentWindow) return;
12
+ if (event.data?.type === 'looma-preview-ready') send();
13
+ if (event.data?.type === 'looma-preview-height' && Number.isFinite(event.data.height)) frame.height = String(Math.max(120, Math.min(2400, Math.ceil(event.data.height))));
14
+ };
15
+ const update = event => { theme = event.detail.css; send(); };
16
+ const resize = event => {
17
+ const value = Number(event.detail?.value ?? event.target.value);
18
+ if (value >= 240 && value <= 1600) frame.width = String(value);
19
+ };
20
+ host.element.addEventListener('looma-theme-preview', update);
21
+ width?.addEventListener('input', resize);
22
+ remove?.addEventListener('click', removeCard);
23
+ view.addEventListener('message', message);
24
+ frame.addEventListener('load', send);
25
+ return () => {
26
+ host.element.removeEventListener('looma-theme-preview', update);
27
+ width?.removeEventListener('input', resize);
28
+ remove?.removeEventListener('click', removeCard);
29
+ view.removeEventListener('message', message);
30
+ frame.removeEventListener('load', send);
31
+ };
32
+ }
@@ -0,0 +1,18 @@
1
+ /** Native catalog entries own their visibility; component previews keep their own layout. */
2
+ export function connect(host) {
3
+ const { query, category, count } = host.refs;
4
+ const groups = [...host.element.querySelectorAll('[data-catalog-group]')];
5
+ const items = [...host.element.querySelectorAll('[data-catalog-search]')];
6
+ const filter = () => {
7
+ const words = query.value.toLocaleLowerCase().trim().split(/\s+/).filter(Boolean);
8
+ let visible = 0;
9
+ for (const item of items) {
10
+ item.hidden = Boolean(category.value && item.dataset.category !== category.value) || !words.every(word => item.dataset.catalogSearch.toLocaleLowerCase().includes(word));
11
+ if (!item.hidden) visible++;
12
+ }
13
+ for (const group of groups) group.hidden = ![...group.querySelectorAll('[data-catalog-search]')].some(item => !item.hidden);
14
+ count.textContent = visible === 0 ? 'No components match. Try another name or category.' : `${visible} component${visible === 1 ? '' : 's'}`;
15
+ };
16
+ query.addEventListener('input', filter); category.addEventListener('change', filter); filter();
17
+ return () => { query.removeEventListener('input', filter); category.removeEventListener('change', filter); };
18
+ }
@@ -0,0 +1,11 @@
1
+ import { escapeHtml as escape } from './catalog.mjs';
2
+
3
+ /** Catalog filtering wraps the same server-rendered documentation cards. */
4
+ export function catalogResource(groups) {
5
+ return `<link rel="component" href="@threadlabs/looma/components/ui-form-field/ui-form-field.html">
6
+ <link rel="component" href="@threadlabs/looma/components/ui-input/ui-input.html">
7
+ <link rel="component" href="@threadlabs/looma/components/ui-select/ui-select.html">
8
+ <link rel="component" href="@threadlabs/looma/components/ui-cluster/ui-cluster.html">
9
+ <template component="looma-catalog" module="./catalog-view.js"><defs></defs>
10
+ <div><ui-cluster align="end"><ui-form-field><label slot="label" for="catalog-query">Find a component</label><ui-input id="catalog-query" $ref="query" type="search" placeholder="Name, tag or purpose"></ui-input></ui-form-field><ui-form-field><label slot="label" for="catalog-category">Category</label><ui-select id="catalog-category" $ref="category"><option value="">All categories</option>${groups.map(group => `<option value="${escape(group.id)}">${escape(group.label)}</option>`).join('')}</ui-select></ui-form-field></ui-cluster><p $ref="count" role="status" aria-live="polite"></p><slot></slot></div></template>`;
11
+ }