@threadlabs/looma 0.35.0 → 0.35.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/docs/README.md CHANGED
@@ -17,7 +17,7 @@ pnpm exec looma-docs init --title "The Fellowship"
17
17
  pnpm exec looma-docs dev
18
18
  ```
19
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.
20
+ Use the tool versions accepted by Looma's optional peer dependencies (Markupress 0.8.5 or newer on the supported 0.8 line). 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
21
 
22
22
  Initialization creates three project-owned files:
23
23
 
@@ -46,6 +46,8 @@ The generated values follow rules. Small and large control heights follow a rati
46
46
 
47
47
  Choose a coordinated palette to change related paper, ink and action seeds together without changing geometry or motion. Palette seeds remain available for precise edits; the light/dark range previews and resolved values explain readability adjustments.
48
48
 
49
+ The customizer uses Starting point, Palette and Material as its controls sidebar, with documentation navigation left out of this workspace. Detailed type, size and motion controls, example selection and exports open when needed.
50
+
49
51
  The main customizer offers Material and Palette presets. **Refine material & light** opens a focused view with a movable light source, real component samples and bounded surface controls. **Refine palette** opens the coordinated light/dark ranges and their four color seeds. Both views edit the same theme as the main customizer. Returning to the overview, navigating to a component, or reloading keeps the result.
50
52
 
51
53
  A refined preset keeps its name with “customized” beside it. **Restore material preset** resets only material and light; **Restore palette preset** resets only the palette. These family choices are recorded in the recipe's optional `presets` mapping. The `values` mapping remains the complete source of the generated theme; a preset is a named combination of those values.
package/docs/card.html CHANGED
@@ -1,3 +1,4 @@
1
+ <link rel="component" href="@threadlabs/looma/components/ui-stack/ui-stack.html">
1
2
  <link rel="component" href="@threadlabs/looma/components/ui-form-field/ui-form-field.html">
2
3
  <link rel="component" href="@threadlabs/looma/components/ui-input/ui-input.html">
3
4
  <link rel="component" href="@threadlabs/looma/components/ui-button/ui-button.html">
@@ -23,7 +24,7 @@
23
24
  <div class="preview"><iframe $ref="frame" from:title="$title" from:src="$src" sandbox="allow-scripts" from:loading="$loading" width="100%" height="208"></iframe></div>
24
25
  <section $if="$reference" class="example-code"><h4>Code</h4><slot name="code"></slot></section>
25
26
  <div $if="$resizable" class="preview-width"> <ui-form-field><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></div>
26
- <div $if="not $reference" class="body">
27
+ <div $if="not $reference" class="body"><ui-stack gap="xs">
27
28
  <div class="meta"><span>{$category}</span><code>{$nativeRoot}</code></div>
28
29
  <h3><a from:href="$href">{$title}</a></h3>
29
30
  <p class="description">{$description}</p>
@@ -31,7 +32,7 @@
31
32
  <p class="example-caption" $if="$exampleTitle">{$exampleTitle}</p>
32
33
 
33
34
  <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>
34
- </div>
35
+ </ui-stack></div>
35
36
  </article>
36
37
  <style>
37
38
  /* The existing documentation catalog card, shared by the directory, reference and customizer. */
@@ -65,13 +66,13 @@
65
66
  footer { display: flex !important; align-items: center !important; justify-content: space-between !important; gap: var(--ui-space-2) !important; }
66
67
  .preview-width { padding: var(--ui-space-4) !important; }
67
68
  .body { padding: var(--ui-space-5) !important; }
68
- .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; }
69
+ .meta { display: flex !important; align-items: center !important; justify-content: space-between !important; gap: var(--ui-space-2) !important; margin: 0 !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; }
69
70
  .meta code { padding: 0 !important; background: transparent; color: inherit; text-transform: none; }
70
71
  h3 { margin: 0 !important; padding: 0 !important; font-size: var(--ui-font-size-lg); letter-spacing: -.035em; }
71
72
  h3 a { color: var(--ui-text); text-decoration: none; }
72
- .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); }
73
- .owner { margin: var(--ui-space-2) 0 !important; color: var(--ui-text-secondary); font-size: var(--ui-font-size-xs); overflow-wrap: anywhere; }
74
- .example-caption { margin: var(--ui-space-2) 0 !important; color: var(--ui-text-secondary); font-size: var(--ui-font-size-xs); }
73
+ .description { margin: 0 !important; color: var(--ui-text-secondary); font-size: var(--ui-font-size-sm); line-height: var(--ui-line-height); }
74
+ .owner { margin: 0 !important; color: var(--ui-text-secondary); font-size: var(--ui-font-size-xs); overflow-wrap: anywhere; }
75
+ .example-caption { margin: 0 !important; color: var(--ui-text-secondary); font-size: var(--ui-font-size-xs); }
75
76
  @media (prefers-reduced-motion: reduce) { :host { transition: none; } :host:hover { transform: none; } }
76
77
  </style>
77
78
  </template>
@@ -6,6 +6,7 @@ export function catalogResource(groups) {
6
6
  <link rel="component" href="@threadlabs/looma/components/ui-input/ui-input.html">
7
7
  <link rel="component" href="@threadlabs/looma/components/ui-select/ui-select.html">
8
8
  <link rel="component" href="@threadlabs/looma/components/ui-cluster/ui-cluster.html">
9
+ <link rel="component" href="@threadlabs/looma/components/ui-stack/ui-stack.html">
9
10
  <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
+ <ui-stack gap="xl"><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></ui-stack></template>`;
11
12
  }
package/docs/catalog.json CHANGED
@@ -443,7 +443,7 @@
443
443
  ],
444
444
  "schemaVersion": 1,
445
445
  "package": "@threadlabs/looma",
446
- "version": "0.35.0",
446
+ "version": "0.35.1",
447
447
  "groups": [
448
448
  {
449
449
  "id": "layout",
@@ -17970,7 +17970,7 @@
17970
17970
  {
17971
17971
  "name": "snap",
17972
17972
  "property": "snap",
17973
- "description": "Snaps scrolling to items: start comes to rest with an item's start edge at the reel's start, and center with an item centred in the reel. Unset, the reel scrolls freely.",
17973
+ "description": "Snaps scrolling to items: start comes to rest with an item's start edge at the reel's start, and center with an item centred in the reel. Unset, the reel scrolls freely. Wrap a component item in a plain element when snapping: the wrapper receives snap alignment while the component keeps ownership of its root styles.",
17974
17974
  "type": "keyword",
17975
17975
  "options": [
17976
17976
  "start",
@@ -18003,7 +18003,7 @@
18003
18003
  },
18004
18004
  {
18005
18005
  "name": "snap",
18006
- "description": "Snaps scrolling to items: start comes to rest with an item's start edge at the reel's start, and center with an item centred in the reel. Unset, the reel scrolls freely.",
18006
+ "description": "Snaps scrolling to items: start comes to rest with an item's start edge at the reel's start, and center with an item centred in the reel. Unset, the reel scrolls freely. Wrap a component item in a plain element when snapping: the wrapper receives snap alignment while the component keeps ownership of its root styles.",
18007
18007
  "type": "\"start\" | \"center\"",
18008
18008
  "options": [
18009
18009
  "start",
@@ -18031,8 +18031,8 @@
18031
18031
  "id": "@threadlabs/looma/ui-reel/01-default",
18032
18032
  "name": "01-default",
18033
18033
  "title": "Default",
18034
- "description": "With no layout properties supplied, Reel uses its base gap, item width, and snap behavior.",
18035
- "markup": "<ui-reel item-width=\"sm\" aria-label=\"Items\">\n <ui-card padding=\"sm\">Frodo</ui-card>\n <ui-card padding=\"sm\">Sam</ui-card>\n <ui-card padding=\"sm\">Sméagol</ui-card>\n <ui-card padding=\"sm\">Merry</ui-card>\n</ui-reel>",
18034
+ "description": "Scroll through the office desks. Small fixed-width items overflow the preview and scroll freely.",
18035
+ "markup": "<ui-reel item-width=\"sm\" aria-label=\"Fellowship office desks\">\n <div><ui-card padding=\"sm\">Frodo</ui-card></div>\n <div><ui-card padding=\"sm\">Sam</ui-card></div>\n <div><ui-card padding=\"sm\">Sméagol</ui-card></div>\n <div><ui-card padding=\"sm\">Merry</ui-card></div>\n <div><ui-card padding=\"sm\">Pippin</ui-card></div>\n <div><ui-card padding=\"sm\">Gandalf</ui-card></div>\n <div><ui-card padding=\"sm\">Aragorn</ui-card></div>\n <div><ui-card padding=\"sm\">Legolas</ui-card></div>\n</ui-reel>",
18036
18036
  "resizable": false,
18037
18037
  "bindings": "",
18038
18038
  "frameworks": {}
@@ -18041,8 +18041,8 @@
18041
18041
  "id": "@threadlabs/looma/ui-reel/02-gap-item-width-and-snap",
18042
18042
  "name": "02-gap-item-width-and-snap",
18043
18043
  "title": "Gap, item width, and snap",
18044
- "description": "These three properties control spacing, each child's basis, and the scroll snap position.",
18045
- "markup": "<ui-reel gap=\"m\" item-width=\"md\" snap=\"center\" aria-label=\"Items\">\n <ui-card padding=\"sm\">Frodo</ui-card>\n <ui-card padding=\"sm\">Sam</ui-card>\n <ui-card padding=\"sm\">Sméagol</ui-card>\n <ui-card padding=\"sm\">Merry</ui-card>\n</ui-reel>",
18044
+ "description": "Scroll the office desks sideways: medium-width items snap to the center when you release.",
18045
+ "markup": "<ui-reel gap=\"m\" item-width=\"md\" snap=\"center\" aria-label=\"Fellowship office desks\">\n <div><ui-card padding=\"sm\">Frodo</ui-card></div>\n <div><ui-card padding=\"sm\">Sam</ui-card></div>\n <div><ui-card padding=\"sm\">Sméagol</ui-card></div>\n <div><ui-card padding=\"sm\">Merry</ui-card></div>\n <div><ui-card padding=\"sm\">Pippin</ui-card></div>\n <div><ui-card padding=\"sm\">Gandalf</ui-card></div>\n <div><ui-card padding=\"sm\">Aragorn</ui-card></div>\n <div><ui-card padding=\"sm\">Legolas</ui-card></div>\n</ui-reel>",
18046
18046
  "resizable": false,
18047
18047
  "bindings": "",
18048
18048
  "frameworks": {}
@@ -19473,8 +19473,8 @@
19473
19473
  "id": "@threadlabs/looma/ui-separator/01-default-horizontal",
19474
19474
  "name": "01-default-horizontal",
19475
19475
  "title": "Default horizontal",
19476
- "description": "With orientation omitted, Separator renders horizontally.",
19477
- "markup": "<div>\n <span>Above</span>\n <ui-separator> </ui-separator>\n <span>Below</span>\n</div>",
19476
+ "description": "Stack supplies breathing room around a horizontal divider.",
19477
+ "markup": "<ui-stack gap=\"s\">\n <span>Above</span>\n <ui-separator> </ui-separator>\n <span>Below</span>\n</ui-stack>",
19478
19478
  "resizable": false,
19479
19479
  "bindings": "",
19480
19480
  "frameworks": {}
@@ -19484,7 +19484,7 @@
19484
19484
  "name": "02-orientation-vertical",
19485
19485
  "title": "orientation=\"vertical\"",
19486
19486
  "description": "Vertical orientation divides inline content; the parent supplies the row layout and height.",
19487
- "markup": "<div style=\"display: flex; align-items: center; height: 3rem; gap: 1rem\">\n <span>Before</span>\n <ui-separator orientation=\"vertical\"> </ui-separator>\n <span>After</span>\n</div>",
19487
+ "markup": "<ui-cluster gap=\"s\">\n <span>Before</span>\n <ui-separator orientation=\"vertical\"> </ui-separator>\n <span>After</span>\n</ui-cluster>",
19488
19488
  "resizable": false,
19489
19489
  "bindings": "",
19490
19490
  "frameworks": {}
@@ -32,13 +32,13 @@ export function customizerResource(site, input = defaultRecipe()) {
32
32
  };
33
33
  const paletteDetails = `<ui-cluster>${['light', 'dark'].map(mode => `<ui-card data-theme="${mode}" padding="sm"><ui-text variant="label">${mode === 'light' ? 'Light' : 'Dark'} range</ui-text><div class="palette-range" role="img" aria-label="${mode} canvas, content and action range"><span class="palette-canvas"></span><span class="palette-content"></span><span class="palette-action"></span></div><ui-text variant="caption">Fellowship office</ui-text></ui-card>`).join('')}</ui-cluster><p data-palette-adjustments role="status" aria-live="polite"></p><ui-disclosure summary="Resolved palette values"><p data-palette-adjustment-details></p></ui-disclosure>`;
34
34
  const supplements = {
35
- 'Colour': `${renderPreset('palette', 'overview')}<p>One coordinated palette generates readable light and dark ranges.</p><ui-button data-action="refine-palette">Refine palette</ui-button>`,
36
- 'Material': `${renderPreset('material', 'overview')}<p>Shared material and light shape the whole design system.</p><ui-button data-action="refine-material">Refine material & light</ui-button>`,
35
+ 'Colour': `${renderPreset('palette', 'overview')}<ui-text variant="caption">Coordinated light and dark ranges.</ui-text><ui-button data-action="refine-palette">Refine palette</ui-button>`,
36
+ 'Material': `${renderPreset('material', 'overview')}<ui-text variant="caption">Material and light, shared everywhere.</ui-text><ui-button data-action="refine-material">Refine material & light</ui-button>`,
37
37
  'Size & spacing': `<ui-cluster><ui-button size="sm">Small</ui-button><ui-button>Medium</ui-button><ui-button size="lg">Large</ui-button></ui-cluster><ui-disclosure summary="How the scale grows"><ui-stack><p>Control size anchors one shared height. Size ratio separates small from large. Density and spacing curve change breathing room independently.</p>${themeFields.filter(item => ['scale-contrast','space-curve'].includes(item.id)).map(renderField).join('')}</ui-stack></ui-disclosure>`,
38
38
  };
39
- const families = [...new Set(themeFields.map(field => field.family))].map(family => `<ui-disclosure summary="${family}"${family === 'Colour' ? ' open' : ''}><ui-stack>${themeFields.filter(item => item.family === family && !['scale-contrast', 'space-curve'].includes(item.id) && !['Colour','Material'].includes(family)).map(renderField).join('')}${supplements[family] ?? ''}</ui-stack></ui-disclosure>`).join('');
39
+ const families = [...new Set(themeFields.map(field => field.family))].filter(family => !['Colour', 'Material'].includes(family)).map(family => `<ui-disclosure summary="${family}"><ui-stack>${themeFields.filter(item => item.family === family && !['scale-contrast', 'space-curve'].includes(item.id)).map(renderField).join('')}${supplements[family] ?? ''}</ui-stack></ui-disclosure>`).join('');
40
40
  const refinement = (family, content) => `<section data-refinement-view="${family}" hidden tabindex="-1" aria-label="${family === 'material' ? 'Material and light refinement' : 'Palette refinement'}"><ui-stack gap="xl"><ui-cluster justify="between"><ui-button data-action="back">Back to customization</ui-button><ui-button data-action="restore-${family}">Restore ${family} preset</ui-button></ui-cluster><ui-text variant="heading" as="h2">${family === 'material' ? 'Refine material & light' : 'Refine your palette'}</ui-text><p>These are advanced refinements of the same persistent theme. Every change applies throughout your documentation.</p>${renderPreset(family, 'refinement')}${content}</ui-stack></section>`;
41
- const materialView = refinement('material', `<div class="refinement-layout"><ui-stack><ui-light-editor data-light-editor x="${recipe.values['light-x']}" y="${recipe.values['light-y']}" label="Theme light position"><ui-stack gap="sm"><ui-cluster><ui-button preset="primary">Join briefing</ui-button><ui-button preset="standard">Read memo</ui-button></ui-cluster><ui-input label="Office search" placeholder="Search the Fellowship office"></ui-input></ui-stack></ui-light-editor><ui-card variant="elevated" treatment="expressive"><ui-text variant="eyebrow">Fellowship office</ui-text><ui-text variant="heading" as="h3">Second breakfast briefing</ui-text><ui-text>Sam has prepared the agenda. Frodo has the maps, and Sméagol is taking minutes.</ui-text><ui-cluster><ui-button preset="primary">Join briefing</ui-button><ui-button preset="quiet">Read agenda</ui-button></ui-cluster></ui-card><ui-card variant="elevated"><ui-text variant="label">Standard elevated surface</ui-text><ui-text>Subtle lift stays independent from expressive treatment.</ui-text></ui-card></ui-stack><ui-stack><ui-text variant="label">Surface response</ui-text>${themeFields.filter(field => ['finish','edge-light','glow'].includes(field.id)).map(renderField).join('')}<ui-text variant="label">Light source</ui-text>${themeFields.filter(field => ['light-spread','light-strength'].includes(field.id)).map(renderField).join('')}<ui-disclosure summary="Depth, glass and separation"><ui-stack>${themeFields.filter(field => ['outer-depth','inner-depth','glassiness','surface-separation','translucency'].includes(field.id)).map(renderField).join('')}</ui-stack></ui-disclosure></ui-stack></div>`);
41
+ const materialView = refinement('material', `<div class="refinement-layout"><ui-stack><ui-light-editor data-light-editor x="${recipe.values['light-x']}" y="${recipe.values['light-y']}" label="Theme light position"><ui-stack gap="s"><ui-cluster><ui-button preset="primary">Join briefing</ui-button><ui-button preset="standard">Read memo</ui-button></ui-cluster><ui-input label="Office search" placeholder="Search the Fellowship office"></ui-input></ui-stack></ui-light-editor><ui-card variant="elevated" treatment="expressive"><ui-text variant="eyebrow">Fellowship office</ui-text><ui-text variant="heading" as="h3">Second breakfast briefing</ui-text><ui-text>Sam has prepared the agenda. Frodo has the maps, and Sméagol is taking minutes.</ui-text><ui-cluster><ui-button preset="primary">Join briefing</ui-button><ui-button preset="quiet">Read agenda</ui-button></ui-cluster></ui-card><ui-card variant="elevated"><ui-text variant="label">Standard elevated surface</ui-text><ui-text>Subtle lift stays independent from expressive treatment.</ui-text></ui-card></ui-stack><ui-stack><ui-text variant="label">Surface response</ui-text>${themeFields.filter(field => ['finish','edge-light','glow'].includes(field.id)).map(renderField).join('')}<ui-text variant="label">Light source</ui-text>${themeFields.filter(field => ['light-spread','light-strength'].includes(field.id)).map(renderField).join('')}<ui-disclosure summary="Depth, glass and separation"><ui-stack>${themeFields.filter(field => ['outer-depth','inner-depth','glassiness','surface-separation','translucency'].includes(field.id)).map(renderField).join('')}</ui-stack></ui-disclosure></ui-stack></div>`);
42
42
  const paletteView = refinement('palette', `<div class="refinement-layout"><ui-stack>${paletteDetails}<ui-card><ui-text variant="eyebrow">Fellowship office</ui-text><ui-text variant="heading" as="h3">There and back by Friday</ui-text><ui-text>Choose a coordinated range, then refine action, paper and ink. Readable roles follow automatically in both modes.</ui-text><ui-cluster><ui-button preset="primary">Share route</ui-button><ui-button preset="standard">Save draft</ui-button></ui-cluster></ui-card></ui-stack><ui-stack>${themeFields.filter(field => field.family === 'Colour').map(renderField).join('')}</ui-stack></div>`);
43
43
  const options = site.catalog.groups.map(group => `<optgroup label="${escape(group.label)}">${examples.filter(example => example.category === group.id).map(example => `<option value="${escape(example.id)}" data-description="${escape(example.category + ' · ' + example.tag)}" data-example="${escape(JSON.stringify(example))}" data-search="${escape(`${example.title} ${example.tag} ${example.description} ${example.exampleTitle}`.toLocaleLowerCase())}">${escape(example.title)} · ${escape(example.exampleTitle)}</option>`).join('')}</optgroup>`).join('');
44
44
  const links = ['ui-stack','ui-grid','ui-cluster','ui-form-field','ui-input','ui-select','ui-color-picker','ui-button','ui-disclosure','ui-textarea','ui-number-input','ui-slider','ui-curve-editor','ui-card','ui-text','ui-light-editor','ui-combobox'].map(tag => `<link rel="component" href="@threadlabs/looma/components/${tag}/${tag}.html">`).join('\n');
@@ -46,19 +46,21 @@ export function customizerResource(site, input = defaultRecipe()) {
46
46
  <state name="cards" type="list" value="${escape(JSON.stringify(initial))}"><prop type="object">${cardFields.map(name => `<prop name="${name}" required type="${name === 'resizable' ? 'boolean' : 'string'}"></prop>`).join('')}</prop></state>
47
47
  </defs>
48
48
  <div data-looma-customizer data-recipe="${escape(JSON.stringify(recipe))}" data-base="${escape(site.base)}" data-mode="${site.config.local ? 'local' : 'public'}" data-revision="${escape(site.config.revision ?? '')}">
49
- <ui-stack gap="xl"><p>Shape your design system through shared decisions. These are the actual examples from the component documentation. Changes apply throughout your documentation and are remembered in this browser until reset. Save to repository writes the theme to your project; downloads let you use it elsewhere.</p>
50
- <p role="status" aria-live="polite" data-message>Your theme is remembered in this browser. Repository files change only when you save.</p>
49
+ <ui-stack gap="l"><ui-text>Choose a starting point, refine shared decisions, and see them on real documentation examples.</ui-text>
50
+ <ui-text variant="caption" role="status" aria-live="polite" data-message>Changes are remembered across the documentation.</ui-text>
51
51
  ${materialView}${paletteView}
52
52
  <div class="customizer-layout" data-customizer-overview>
53
- <aside><ui-stack><ui-form-field><label slot="label" for="theme-start">Starting point</label><ui-select id="theme-start" data-preset value="${startingPointFor(recipe)}"><option value="custom" disabled>Your theme</option>${Object.entries(startingPointLabels).map(([id,label]) => `<option value="${id}">${label}</option>`).join('')}</ui-select></ui-form-field>
54
- ${families}
55
- <ui-cluster><ui-button data-action="css" variant="solid">Download CSS</ui-button><ui-button data-action="recipe">Download recipe</ui-button><ui-button data-action="import">Import recipe</ui-button><ui-button data-action="reset" variant="ghost">Reset changes</ui-button></ui-cluster>
53
+ <aside aria-label="Theme controls"><ui-stack><ui-form-field><label slot="label" for="theme-start">Starting point</label><ui-select id="theme-start" data-preset value="${startingPointFor(recipe)}"><option value="custom" disabled>Your theme</option>${Object.entries(startingPointLabels).map(([id,label]) => `<option value="${id}">${label}</option>`).join('')}</ui-select></ui-form-field>
54
+ ${supplements.Colour}${supplements.Material}
55
+ <ui-disclosure summary="Type, size & motion"><ui-stack>${families}</ui-stack></ui-disclosure>
56
+ <ui-button data-action="reset" variant="ghost">Reset changes</ui-button>
57
+ <ui-disclosure summary="Export & save"><ui-stack><ui-cluster><ui-button data-action="css" variant="solid">Download CSS</ui-button><ui-button data-action="recipe">Download recipe</ui-button><ui-button data-action="import">Import recipe</ui-button></ui-cluster>
56
58
  <input type="file" accept="application/json,.json" hidden data-recipe-file>
57
59
  <ui-button data-action="save" variant="solid" hidden>Save to repository</ui-button>
58
60
  <ui-disclosure summary="Use your theme"><p>Every export includes light and dark. Leave data-theme off the page to follow system preference, or set it to light or dark on the page or a section. <a href="${escape(site.base)}design-system/">Installation and mode guidance</a></p></ui-disclosure>
59
- <ui-disclosure summary="Theme CSS"><ui-textarea data-css readonly rows="12" aria-label="Generated theme CSS" value="${escape(themeCSS(recipe))}"></ui-textarea></ui-disclosure>
61
+ <ui-disclosure summary="Theme CSS"><ui-textarea data-css readonly rows="12" aria-label="Generated theme CSS" value="${escape(themeCSS(recipe))}"></ui-textarea></ui-disclosure></ui-stack></ui-disclosure>
60
62
  </ui-stack></aside>
61
- <section class="examples"><ui-stack><ui-form-field><label slot="label" for="example-query">Find components and examples</label><ui-input id="example-query" type="search" data-example-query placeholder="Name, tag or purpose"></ui-input></ui-form-field><p role="status" aria-live="polite" data-example-count></p><ui-combobox id="example-choice" data-add-example label="Choose a component example" placeholder="Search buttons, fields, dialogs…" disclosure clearable>${options}</ui-combobox><ui-button data-action="add" disabled>Add to preview</ui-button>
63
+ <section class="examples"><ui-stack><ui-form-field><label slot="label" for="example-query">Filter previews</label><ui-input id="example-query" type="search" data-example-query placeholder="Name, tag or purpose"></ui-input></ui-form-field><ui-text variant="caption" role="status" aria-live="polite" data-example-count></ui-text><ui-disclosure summary="Add an example"><ui-stack><ui-combobox id="example-choice" data-add-example label="Choose a component example" placeholder="Search buttons, fields, dialogs…" disclosure clearable>${options}</ui-combobox><ui-button data-action="add" disabled>Add to preview</ui-button></ui-stack></ui-disclosure>
62
64
  <ui-grid columns="2" min="sm"><section $each="card of $cards" $key="$card.id"><looma-example-card ${cardFields.filter(name => name !== 'id').map(name => `from:${name.replace(/[A-Z]/g, letter => '-' + letter.toLowerCase())}="$card.${name}"`).join(' ')} from:identity="$card.id" loading="eager" removable></looma-example-card></section></ui-grid>
63
65
  <noscript>Enable JavaScript to edit the theme. The component reference and example previews remain available.</noscript>
64
66
  </ui-stack></section></div></ui-stack></div>
@@ -71,10 +71,13 @@ export function connect(host) {
71
71
  };
72
72
  const filterExamples = () => {
73
73
  const words = root.querySelector('[data-example-query]').value.toLocaleLowerCase().trim().split(/\s+/).filter(Boolean);
74
- const options = [...root.querySelectorAll('[data-add-example] option')];
75
- for (const card of root.querySelectorAll('[data-example-card]')) card.closest('section').hidden = !words.every(word => card.textContent.toLocaleLowerCase().includes(word) || card.dataset.componentCard.includes(word));
76
- const matches = options.filter(option => words.every(word => option.dataset.search.includes(word))).length;
77
- root.querySelector('[data-example-count]').textContent = matches ? `${matches} matching examples` : 'No examples match. Try another name or purpose.';
74
+ let visible = 0;
75
+ for (const card of root.querySelectorAll('[data-example-card]')) {
76
+ const matches = words.every(word => card.textContent.toLocaleLowerCase().includes(word) || card.dataset.componentCard.includes(word));
77
+ card.closest('section').hidden = !matches;
78
+ if (matches) visible++;
79
+ }
80
+ root.querySelector('[data-example-count]').textContent = visible ? `${visible} example${visible === 1 ? '' : 's'} in preview` : 'No previews match. Clear the filter or add another example.';
78
81
  componentRegistry.update(root.querySelector('[data-action="add"]'), { disabled: !selectedExample || host.state.cards.some(item => item.id === selectedExample) });
79
82
  };
80
83
  const refinedRecipe = values => {
package/docs/site.mjs CHANGED
@@ -120,11 +120,12 @@ export async function prepareDocumentation({ root, config = {}, catalog }) {
120
120
  }
121
121
  const gallery = combined.groups.map(group => {
122
122
  const components = combined.components.filter(c => c.group === group.id && !c.navigationParent);
123
- return components.length ? `<section data-catalog-group="${escapeHtml(group.id)}">\n\n## ${escapeHtml(group.label)}\n\n<ui-grid columns="3" min="sm">${components.map(component => `<div data-category="${escapeHtml(component.group)}" data-catalog-search="${escapeHtml(`${component.tag} ${component.title} ${component.description}`)}">${cardMarkup(component.examples[0], base, component)}</div>`).join('')}</ui-grid>\n\n</section>` : '';
123
+ // A standalone opening tag makes this a Markdown HTML block, keeping native card wrappers inside Grid.
124
+ return components.length ? `<section data-catalog-group="${escapeHtml(group.id)}">\n\n## ${escapeHtml(group.label)}\n\n<ui-grid columns="3" min="sm">\n${components.map(component => `<div data-category="${escapeHtml(component.group)}" data-catalog-search="${escapeHtml(`${component.tag} ${component.title} ${component.description}`)}">${cardMarkup(component.examples[0], base, component)}</div>`).join('\n')}\n</ui-grid>\n\n</section>` : '';
124
125
  }).join('\n\n');
125
126
  await write(path.join(content, 'components/index.md'), `---\ntitle: Components\n---\n\n# Components\n\nExplore the live examples that also power your theme customizer.\n\n<link rel="component" href="${relativeHref(path.join(content, 'components/index.md'), card)}">\n<link rel="component" href="@threadlabs/looma/components/ui-grid/ui-grid.html">\n<link rel="component" href="${relativeHref(path.join(content, 'components/index.md'), path.join(generated, 'catalog-view.html'))}">\n\n<looma-catalog>\n\n${gallery}\n\n</looma-catalog>\n`);
126
127
  await write(path.join(content, 'index.md'), `---\ntitle: ${JSON.stringify(config.title ?? 'Your design system')}\n---\n\n# ${config.title ?? 'Your design system'}\n\nWeave your design system from Looma and your own HTML Next components.\n\n[Explore components](/components/) · [Customize your theme](/customize/)\n`);
127
- if (combined.components.some(component => component.group === 'editor')) await write(path.join(content, 'editor.md'), `---\ntitle: Editor\n---\n\n# Editor\n\nWriting tools and editor components, separate from the general component reference.\n\n${combined.components.filter(component => component.group === 'editor' && !component.navigationParent).map(component => `- [${component.title}](${componentPath(component)})`).join('\n')}\n`);
128
+ if (combined.components.some(component => component.group === 'editor')) await write(path.join(content, 'editor.md'), `---\ntitle: Editor\nsidebar: false\n---\n\n# Editor\n\nWriting tools and editor components, separate from the general component reference.\n\n${combined.components.filter(component => component.group === 'editor' && !component.navigationParent).map(component => `- [${component.title}](${componentPath(component)})`).join('\n')}\n`);
128
129
  await write(path.join(content, 'design-system.md'), await readFile(own('README.md'), 'utf8'));
129
130
  if (config.content) {
130
131
  const authored = path.resolve(root, config.content);
@@ -143,7 +144,7 @@ export async function prepareDocumentation({ root, config = {}, catalog }) {
143
144
  const customizer = path.join(generated, 'customizer.html');
144
145
  await write(customizer, customizerResource({ catalog: combined, config, base }, config.recipe));
145
146
  const customizerPage = path.join(content, 'customize.md');
146
- await write(customizerPage, `---\ntitle: Customize\n---\n\n# Weave your design system\n\n<link rel="component" href="${relativeHref(customizerPage, customizer)}">\n<looma-customizer></looma-customizer>\n`);
147
+ await write(customizerPage, `---\ntitle: Customize\nsidebar: false\noutline: false\nprevious: false\nnext: false\n---\n\n# Weave your design system\n\n<link rel="component" href="${relativeHref(customizerPage, customizer)}">\n<looma-customizer></looma-customizer>\n`);
147
148
  await write(path.join(generated, 'catalog.json'), JSON.stringify(combined));
148
149
  const redirects = {};
149
150
  for (const component of combined.components) {
package/docs/theme.mjs CHANGED
@@ -227,6 +227,8 @@ export function deriveTheme(input, { mode } = {}) {
227
227
  tokens['--ui-selection-text'] = tokens['--ui-accent-subtle-text'];
228
228
  tokens['--ui-selection-border'] = mix(tokens['--ui-accent'], tokens['--ui-surface-raised'], .2);
229
229
  tokens['--ui-control-surface'] = tokens['--ui-surface-raised'];
230
+ // A hint is quieter than body copy; resolve it against its field, not every tinted content surface.
231
+ tokens['--ui-control-placeholder'] = readableColour(mix(tokens['--ui-text'], tokens['--ui-control-surface'], .5), [tokens['--ui-control-surface']], 4.5, ink);
230
232
  tokens['--ui-control-border'] = readableColour(mix(tokens['--ui-text'], tokens['--ui-surface-raised'], .43), readingSurfaces, 3, ink);
231
233
  tokens['--ui-control-border-hover'] = tokens['--ui-control-border'];
232
234
  tokens['--ui-border-strong'] = tokens['--ui-control-border'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadlabs/looma",
3
- "version": "0.35.0",
3
+ "version": "0.35.1",
4
4
  "type": "module",
5
5
  "description": "Looma components as HTML, Vue, and plain DOM, with design tokens.",
6
6
  "license": "MIT",
@@ -127,7 +127,7 @@
127
127
  "@tiptap/pm": ">=2 <3",
128
128
  "@tiptap/vue-3": ">=2 <3",
129
129
  "vue": "^3.5.43",
130
- "markupress": "^0.8.4",
130
+ "markupress": "^0.8.5",
131
131
  "@nextwebwg/htmlkit": "1.0.0-alpha.69",
132
132
  "vite": "^8.3.4"
133
133
  },
@@ -59,10 +59,9 @@
59
59
  max-inline-size: 100%;
60
60
  /* Tabs that do not fit scroll sideways rather than wrapping or squeezing. */
61
61
  overflow-x: auto;
62
- /* overflow-x: auto would make the block axis scrollable too; a sideways swipe must not scroll the
63
- strip or the page vertically. */
62
+ /* Contain sideways overscroll without trapping vertical page scrolling. */
64
63
  overflow-y: hidden;
65
- overscroll-behavior: contain;
64
+ overscroll-behavior-inline: contain;
66
65
  scrollbar-width: thin;
67
66
  border-block-end: var(--ui-border-width) solid var(--ui-border-divider);
68
67
  }
@@ -204,7 +204,8 @@
204
204
  border-inline-end-width: var(--ui-border-width);
205
205
  border-block-end-width: var(--ui-border-width);
206
206
  border-end-end-radius: var(--ui-border-width);
207
- transform: rotate(45deg) scaleX(1.25) scaleY(0.85);
207
+ /* Stretch in screen axes after rotation so both arrow shoulders stay level. */
208
+ transform: scale(1.25, 0.85) rotate(45deg);
208
209
  }
209
210
 
210
211
  :scope:where([data-component~="ui-tooltip"])[data-ui-actual-placement="bottom"] .surface:where(:not([data-component], [data-slotted]))::after {
package/tokens.css CHANGED
@@ -251,7 +251,7 @@
251
251
  --ui-control-surface: var(--ui-surface);
252
252
  --ui-control-surface-disabled: var(--ui-surface-muted);
253
253
  --ui-control-border-hover: color-mix(in oklab, var(--ui-text) 50%, var(--ui-surface));
254
- --ui-control-placeholder: var(--ui-text-secondary);
254
+ --ui-control-placeholder: color-mix(in oklab, var(--ui-text) 58%, var(--ui-control-surface));
255
255
  --ui-control-focus: var(--ui-focus-ring);
256
256
  /* Derived from the ring, not a near-white tint, so the halo shows on any surface. */
257
257
  --ui-control-focus-halo: color-mix(in srgb, var(--ui-focus-ring) 24%, transparent);
package/vue/UiReel.vue CHANGED
@@ -40,7 +40,7 @@ const propValidityContract = {
40
40
  required: false,
41
41
  target: { attribute: 'snap' },
42
42
  description:
43
- 'Snaps scrolling to items: start comes to rest with an item\'s start edge at the reel\'s start, and center with an item centred in the reel. Unset, the reel\n scrolls freely.',
43
+ 'Snaps scrolling to items: start comes to rest with an item\'s start edge at the reel\'s start, and center with an item centred in the reel. Unset, the reel\n scrolls freely. Wrap a component item in a plain element when snapping: the wrapper receives snap alignment while the component keeps ownership of its\n root styles.',
44
44
  values: ['start', 'center'],
45
45
  },
46
46
  },
package/vue/UiTabs.vue CHANGED
@@ -395,10 +395,9 @@ useComponentHost(() => import('../components/ui-tabs/ui-tabs.js'), {
395
395
  max-inline-size: 100%;
396
396
  /* Tabs that do not fit scroll sideways rather than wrapping or squeezing. */
397
397
  overflow-x: auto;
398
- /* overflow-x: auto would make the block axis scrollable too; a sideways swipe must not scroll the
399
- strip or the page vertically. */
398
+ /* Contain sideways overscroll without trapping vertical page scrolling. */
400
399
  overflow-y: hidden;
401
- overscroll-behavior: contain;
400
+ overscroll-behavior-inline: contain;
402
401
  scrollbar-width: thin;
403
402
  border-block-end: var(--ui-border-width) solid var(--ui-border-divider);
404
403
  }
package/vue/UiTooltip.vue CHANGED
@@ -580,7 +580,8 @@ useComponentHost(() => import('../components/ui-tooltip/ui-tooltip.js'), {
580
580
  border-inline-end-width: var(--ui-border-width);
581
581
  border-block-end-width: var(--ui-border-width);
582
582
  border-end-end-radius: var(--ui-border-width);
583
- transform: rotate(45deg) scaleX(1.25) scaleY(0.85);
583
+ /* Stretch in screen axes after rotation so both arrow shoulders stay level. */
584
+ transform: scale(1.25, 0.85) rotate(45deg);
584
585
  }
585
586
 
586
587
  :global(:scope[data-ui-actual-placement="bottom"] .surface:where([data-hn-ui-tooltip])::after) {
@@ -58,7 +58,7 @@ var UiReel_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE_
58
58
  },
59
59
  required: false,
60
60
  target: { attribute: "snap" },
61
- description: "Snaps scrolling to items: start comes to rest with an item's start edge at the reel's start, and center with an item centred in the reel. Unset, the reel\n scrolls freely.",
61
+ description: "Snaps scrolling to items: start comes to rest with an item's start edge at the reel's start, and center with an item centred in the reel. Unset, the reel\n scrolls freely. Wrap a component item in a plain element when snapping: the wrapper receives snap alignment while the component keeps ownership of its\n root styles.",
62
62
  values: ["start", "center"]
63
63
  }
64
64
  } };
@@ -128,6 +128,6 @@ var UiReel_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE_
128
128
  }]]);
129
129
  };
130
130
  }
131
- }), [["__scopeId", "data-v-ace220e1"]]);
131
+ }), [["__scopeId", "data-v-fbdba47c"]]);
132
132
  //#endregion
133
133
  export { UiReel_default as t };
@@ -329,6 +329,6 @@ var UiTabs_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE_
329
329
  }]]);
330
330
  };
331
331
  }
332
- }), [["__scopeId", "data-v-f1705832"]]);
332
+ }), [["__scopeId", "data-v-502b66c9"]]);
333
333
  //#endregion
334
334
  export { UiTabs_default as t };
@@ -292,6 +292,6 @@ var UiTooltip_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PU
292
292
  }]]);
293
293
  };
294
294
  }
295
- }), [["__scopeId", "data-v-4cc4aab6"]]);
295
+ }), [["__scopeId", "data-v-21fb192b"]]);
296
296
  //#endregion
297
297
  export { UiTooltip_default as t };
@@ -2726,7 +2726,8 @@ input[type="range"] {
2726
2726
  border-inline-end-width: var(--ui-border-width);
2727
2727
  border-block-end-width: var(--ui-border-width);
2728
2728
  border-end-end-radius: var(--ui-border-width);
2729
- transform: rotate(45deg) scaleX(1.25) scaleY(0.85);
2729
+ /* Stretch in screen axes after rotation so both arrow shoulders stay level. */
2730
+ transform: scale(1.25, 0.85) rotate(45deg);
2730
2731
  }
2731
2732
  :scope[data-ui-actual-placement="bottom"] .surface:where([data-hn-ui-tooltip])::after {
2732
2733
  inset-block-start: calc(
@@ -10934,10 +10935,9 @@ input:where([data-hn-ui-switch]),input:where([data-hn-ui-switch])::after {
10934
10935
  max-inline-size: 100%;
10935
10936
  /* Tabs that do not fit scroll sideways rather than wrapping or squeezing. */
10936
10937
  overflow-x: auto;
10937
- /* overflow-x: auto would make the block axis scrollable too; a sideways swipe must not scroll the
10938
- strip or the page vertically. */
10938
+ /* Contain sideways overscroll without trapping vertical page scrolling. */
10939
10939
  overflow-y: hidden;
10940
- overscroll-behavior: contain;
10940
+ overscroll-behavior-inline: contain;
10941
10941
  scrollbar-width: thin;
10942
10942
  border-block-end: var(--ui-border-width) solid var(--ui-border-divider);
10943
10943
  }
@@ -16093,7 +16093,8 @@ html[data-ui-input-modality="touch"]
16093
16093
  border-inline-end-width: var(--ui-border-width);
16094
16094
  border-block-end-width: var(--ui-border-width);
16095
16095
  border-end-end-radius: var(--ui-border-width);
16096
- transform: rotate(45deg) scaleX(1.25) scaleY(0.85);
16096
+ /* Stretch in screen axes after rotation so both arrow shoulders stay level. */
16097
+ transform: scale(1.25, 0.85) rotate(45deg);
16097
16098
  }
16098
16099
 
16099
16100
  :scope:where([data-component~="ui-tooltip"])[data-ui-actual-placement="bottom"] .surface:where(:not([data-component], [data-slotted]))::after {