@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.
- package/LICENSE +21 -0
- package/README.md +11 -2
- package/bin/looma-docs.mjs +3 -0
- package/docs/README.md +89 -0
- package/docs/card.html +79 -0
- package/docs/card.js +18 -0
- package/docs/catalog-view.js +24 -0
- package/docs/catalog-view.mjs +13 -0
- package/docs/catalog.mjs +135 -0
- package/docs/cli.mjs +26 -0
- package/docs/commands.mjs +94 -0
- package/docs/customizer-render.mjs +157 -0
- package/docs/customizer.js +494 -0
- package/docs/documentation.css +42 -0
- package/docs/example-invocations.mjs +63 -0
- package/docs/fonts.mjs +73 -0
- package/docs/framework-examples.d.mts +9 -0
- package/docs/framework-examples.mjs +225 -0
- package/docs/guides/01.theme-your-system.md +64 -0
- package/docs/guides/02.add-your-components.md +96 -0
- package/docs/guides/03.publish-your-system.md +71 -0
- package/docs/guides/04.theme-decisions.md +141 -0
- package/docs/guides/05.(Theme reference)/01.theme-colors.md +95 -0
- package/docs/guides/05.(Theme reference)/02.theme-typography.md +73 -0
- package/docs/guides/05.(Theme reference)/03.theme-spacing.md +103 -0
- package/docs/guides/05.(Theme reference)/04.theme-material.md +90 -0
- package/docs/guides/05.(Theme reference)/05.theme-motion.md +57 -0
- package/docs/guides/assets/fellowship-dark.png +0 -0
- package/docs/guides/assets/fellowship-light.png +0 -0
- package/docs/index.d.ts +59 -0
- package/docs/index.mjs +4 -0
- package/docs/local-server.mjs +65 -0
- package/docs/navigation.mjs +40 -0
- package/docs/palette.mjs +68 -0
- package/docs/pastoral.svg +54 -0
- package/docs/portrait.svg +15 -0
- package/docs/reference.html +1 -0
- package/docs/reference.js +36 -0
- package/docs/render.mjs +97 -0
- package/docs/site.mjs +244 -0
- package/docs/store.mjs +92 -0
- package/docs/theme-session.mjs +72 -0
- package/docs/theme.mjs +669 -0
- package/docs/value-editors.mjs +37 -0
- package/docs/workspace.mjs +32 -0
- package/package.json +53 -3
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { googleFonts, systemFont } from './fonts.mjs';
|
|
2
|
+
import { escapeHtml as escape } from './catalog.mjs';
|
|
3
|
+
import { componentPath, previewMarkup, exampleKey, exampleResources, cardDescription } from './render.mjs';
|
|
4
|
+
import { valueEditor } from './value-editors.mjs';
|
|
5
|
+
import { defaultRecipe, themeFields, startingPoints, startingPointLabels, startingPointFor, deriveTheme, applyRefinement, themeCSS, validateRecipe, themePalettes, materialPresets, statusPresets, refinementState } from './theme.mjs';
|
|
6
|
+
|
|
7
|
+
export function customizerResource(site, input = defaultRecipe()) {
|
|
8
|
+
const recipe = validateRecipe(input);
|
|
9
|
+
const cardFields = ['id', 'title', 'description', 'exampleTitle', 'resizable', 'category', 'tag', 'nativeRoot', 'href', 'packageName'];
|
|
10
|
+
const examples = site.catalog.components.flatMap(component => component.examples.map(example => ({ id: example.id, title: component.title, description: cardDescription(component.summary ?? component.description), exampleTitle: example.title, resizable: Boolean(example.resizable), category: component.group, tag: component.tag, packageName: component.package, nativeRoot: component.root ? `<${component.root}>` : '', href: site.base + componentPath(component).slice(1) })));
|
|
11
|
+
const toCard = example => Object.fromEntries(cardFields.map(key => [key, example[key]]));
|
|
12
|
+
const initial = ['ui-button', 'ui-input', 'ui-color-picker', 'ui-checkbox', 'ui-card', 'ui-switch', 'ui-text', 'ui-dialog', 'ui-popover'].map(tag => examples.find(example => example.tag === tag && (tag !== 'ui-button' || example.id.endsWith('/02-variant')) && (tag !== 'ui-text' || example.id.endsWith('/02-variants')) && (tag !== 'ui-card' || example.id.endsWith('/04-expressive-surface'))) ?? examples.find(example => example.tag === tag)).filter(Boolean).map(toCard);
|
|
13
|
+
if (!initial.length && examples.length) initial.push(toCard(examples[0]));
|
|
14
|
+
let specimen = 0;
|
|
15
|
+
const documentationCard = (tag, suffix, previewOnly = false) => {
|
|
16
|
+
const example = examples.find(item => item.tag === tag && (!suffix || item.id.endsWith(suffix))) ?? examples.find(item => item.tag === tag);
|
|
17
|
+
return example ? `<looma-example-card ${previewOnly ? 'preview-only' : ''} ${cardFields.filter(key => key !== 'id').map(key => `${key.replace(/[A-Z]/g, letter => '-' + letter.toLowerCase())}="${escape(String(example[key]))}"`).join(' ')} identity="${escape(example.id)}">${previewMarkup(site.catalog.components.flatMap(component => component.examples).find(item => item.id === example.id), `customizer-${specimen++}-${exampleKey(example.id)}`, site.base, example.category === 'layout')}</looma-example-card>` : '';
|
|
18
|
+
};
|
|
19
|
+
const help = (id, label, description, inline = false) => `<span${inline ? ' class="inline-help"' : ' slot="labelAction"'}><ui-icon-button id="help-${id}" size="sm" label="About ${escape(label)}"><ui-icon name="help"></ui-icon></ui-icon-button><ui-tooltip for="help-${id}" trigger="click">${escape(description)}</ui-tooltip></span>`;
|
|
20
|
+
const shortLabels = { 'body-font':'Font', 'display-font':'Heading font', 'label-size':'Field labels', 'secondary-action':'Secondary', 'tertiary-action':'Tertiary', 'selection-accent':'Selection', 'focus-accent':'Focus', 'link-accent':'Links', 'neutral-source':'Source', 'neutral-strength':'Strength', 'neutral-border-contrast':'Border contrast', 'success-source':'Success', 'danger-source':'Error', 'warning-source':'Warning', 'info-source':'Info', 'status-chroma':'Status chroma', 'control-roominess':'Roominess', 'surface-separation':'Rim width', 'corner-roundness':'Roundness' };
|
|
21
|
+
const setting = (field, control, id = `theme-${field.id}`) => `<div class="setting${field.type.includes('colour') ? ' color-setting' : field.type === 'easing' ? ' curve-setting' : ''}"><ui-form-field><label slot="label" for="${id}">${escape(shortLabels[field.id] ?? field.label)}</label>${['action','canvas','surface','text','secondary-action','tertiary-action'].includes(field.id) ? '' : help(field.id, field.label, field.description)}${control}</ui-form-field></div>`;
|
|
22
|
+
const renderField = field => {
|
|
23
|
+
const value = recipe.values[field.id];
|
|
24
|
+
if (field.id === 'label-size') {
|
|
25
|
+
const inherited = value === 'inherit', size = inherited ? recipe.values['text-size'] : value;
|
|
26
|
+
return setting(field, `<ui-stack gap="xs"><ui-select id="theme-label-source" size="sm" data-label-size-source value="${inherited ? 'inherit' : 'custom'}"><option value="inherit">Follow body</option><option value="custom">Set a separate size</option></ui-select><ui-number-input compact id="theme-label-size" size="sm" data-label-size-value label="Independent field label size" value="${escape(size)}" unit="${size.match(/[a-z]+$/i)?.[0] ?? 'px'}" min="0.1" step="1"${inherited ? ' hidden' : ''}></ui-number-input></ui-stack>`, 'theme-label-source');
|
|
27
|
+
}
|
|
28
|
+
if (field.type === 'optional-colour') {
|
|
29
|
+
return setting(field, `<ui-color-picker id="theme-${field.id}" size="sm" clearable data-token="${field.id}" data-optional-color="${field.id}" value="${escape(value === field.defaultValue ? '' : value)}" label="${escape(field.label)} colour picker"></ui-color-picker>`);
|
|
30
|
+
}
|
|
31
|
+
const editor = valueEditor(field, value, { remPixels: parseFloat(recipe.values['text-size']) });
|
|
32
|
+
const common = `size="sm" aria-label="${escape(field.label)}" data-token="${field.id}" label="${escape(field.label)}"`;
|
|
33
|
+
let control;
|
|
34
|
+
if (field.type === 'font') {
|
|
35
|
+
const selected = field.id === 'display-font' && value === recipe.values['body-font'] ? 'inherit' : recipe.fonts?.[field.id] ?? (value === systemFont ? 'system' : 'custom');
|
|
36
|
+
control = `<ui-combobox id="theme-${field.id}" size="sm" data-font-picker="${field.id}" label-visibility="sr-only" label="${escape(field.label)}" value="${escape(selected)}" placeholder="Search Google Fonts…" disclosure><template slot="option"><span class="font-option" from:data-font-preview="$option.value">{$option.label}</span></template>${field.id === 'display-font' ? '<option value="inherit">Follow body</option>' : ''}<option value="system">System font</option><option value="custom" disabled>${escape(value.split(',')[0].replaceAll('"', '').trim())}</option>${Object.values(googleFonts).map(font => `<option value="${escape(font.family)}">${escape(font.family)}</option>`).join('')}</ui-combobox><ui-text variant="caption" data-font-status="${field.id}" role="status" hidden></ui-text>`;
|
|
37
|
+
} else if (field.type === 'choice') {
|
|
38
|
+
control = `<ui-select id="theme-${field.id}" ${common} value="${escape(value)}">${Object.entries(field.choices).map(([id,label]) => `<option value="${id}">${escape(label)}</option>`).join('')}</ui-select>`;
|
|
39
|
+
} else if (field.type === 'easing') {
|
|
40
|
+
control = `<ui-input id="theme-${field.id}" size="sm" data-token="${field.id}" aria-label="${escape(field.label)}" value="${escape(value)}"></ui-input><div data-curve-for="${field.id}"${editor.kind === 'text' ? ' hidden' : ''}><ui-curve-editor ${common} data-editor-kind="curve" value="${escape(value)}"></ui-curve-editor></div>`;
|
|
41
|
+
} else if (editor.kind === 'number' || editor.kind === 'slider') {
|
|
42
|
+
const tag = editor.kind === 'slider' ? 'ui-slider' : 'ui-number-input';
|
|
43
|
+
const numeric = `<${tag} compact id="theme-${field.id}" ${common} data-editor-kind="${editor.kind}" value="${escape(editor.value)}" unit="${editor.unit}" min="${editor.min}" max="${editor.max}" step="${editor.step}"></${tag}>`;
|
|
44
|
+
control = editor.units ? `<ui-cluster wrap="nowrap" gap="xs">${numeric}<ui-select size="sm" style="--ui-select-inline-size: auto" data-unit-for="${field.id}" aria-label="${escape(field.label)} unit" value="${editor.unit}">${[...new Set([...editor.units, editor.unit])].map(unit => `<option value="${unit}">${unit}</option>`).join('')}</ui-select></ui-cluster>` : numeric;
|
|
45
|
+
} else {
|
|
46
|
+
control = field.type === 'colour' ? `<ui-color-picker id="theme-${field.id}" data-token="${field.id}" value="${escape(value)}" size="sm" label="${escape(field.label)} colour picker"></ui-color-picker>` : `<ui-input id="theme-${field.id}" ${common} value="${escape(value)}"></ui-input>`;
|
|
47
|
+
}
|
|
48
|
+
return setting(field, control);
|
|
49
|
+
};
|
|
50
|
+
const swatches = tokens => `<span class="palette-swatches" aria-hidden="true">${['--ui-accent','--ui-neutral-surface','--ui-surface','--ui-text'].map(token => `<span style="background:${escape(tokens ? tokens[token] : `var(${token})`)}"></span>`).join('')}</span>`;
|
|
51
|
+
const choice = (attributes, label, value, options, content = '') => `<ui-combobox ${attributes} size="sm" data-choice label="${escape(label)}" value="${escape(value)}" disclosure open-on-focus>${options}${content}</ui-combobox>`;
|
|
52
|
+
const renderPreset = (family, location) => {
|
|
53
|
+
const presets = { material: materialPresets, palette: themePalettes, status: statusPresets }[family];
|
|
54
|
+
const state = refinementState(recipe, family), label = { material: 'Material preset', palette: 'Palette preset', status: 'Status color relationship' }[family];
|
|
55
|
+
const origin = startingPoints[recipe.origin ?? startingPointFor(recipe)];
|
|
56
|
+
const muted = origin && Object.keys(Object.values(presets)[0].values).every(key => origin.values[key] === recipe.values[key]);
|
|
57
|
+
const options = `<option value="custom" disabled>Your ${family}</option>${Object.entries(presets).map(([id, preset]) => `<option value="${id}">${escape(preset.label)}${state.id === id && state.customized ? ' · customized' : ''}</option>`).join('')}`;
|
|
58
|
+
const preview = family === 'palette' ? `<span slot="start" data-selected-palette>${swatches()}</span><template slot="option"><span class="palette-option">${Object.keys(presets).map(id => `<template $if="$option.value = '${id}'">${swatches(deriveTheme(applyRefinement(recipe, 'palette', id), { mode: 'light' }).tokens)}</template>`).join('')}<span>{$option.label}</span></span></template>` : '';
|
|
59
|
+
if (family === 'status') return `<ui-form-field><label slot="label" for="${location}-status">${label}</label><ui-select id="${location}-status" size="sm" data-refinement-preset="status" value="${state.id}"${muted ? ' muted' : ''}>${options}</ui-select></ui-form-field>`;
|
|
60
|
+
return choice(`id="${location}-${family}" data-refinement-preset="${family}"${muted ? ' muted' : ''}`, label, state.id, options, preview);
|
|
61
|
+
};
|
|
62
|
+
const paletteDetails = `<ui-grid columns="2" min="md" gap="s">${['light', 'dark'].map(mode => `<ui-card data-theme="${mode}" padding="sm"><ui-stack gap="s"><ui-text variant="eyebrow">${mode === 'light' ? 'Light' : 'Dark'}</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="heading" as="h3">Fellowship</ui-text><ui-text>There and back.</ui-text><ui-text variant="caption">Second breakfast at nine.</ui-text><ui-input size="sm" aria-label="${mode} neutral field" value="Shared neutral boundary"></ui-input><ui-action-bar><ui-button slot="tertiary" size="sm" variant="outline" tone="accent">Outline</ui-button><ui-button slot="secondary" size="sm" preset="standard">Neutral</ui-button><ui-button slot="primary" size="sm" preset="primary">Accent</ui-button></ui-action-bar><ui-cluster><ui-button size="sm" variant="outline" tone="neutral">Neutral outline</ui-button><ui-badge tone="accent-secondary" data-extra-accent="secondary-action">Secondary accent</ui-badge><ui-badge tone="accent-tertiary" data-extra-accent="tertiary-action">Tertiary accent</ui-badge></ui-cluster><ui-card tone="accent-secondary" data-extra-accent="secondary-action"><ui-stack gap="s"><ui-text weight="medium">A supporting highlight</ui-text><ui-text variant="caption">A category or feature can carry another accent while the main action stays primary.</ui-text></ui-stack></ui-card><ui-cluster>${[['success','Success','circle-check'],['danger','Error','circle-x'],['warning','Warning','triangle-alert'],['info','Info','info']].map(([tone,label,icon]) => `<ui-badge tone="${tone}"><ui-icon name="${icon}" slot="icon"></ui-icon>${label}</ui-badge>`).join('')}</ui-cluster><ui-callout tone="tip">Tips share the informational color family.</ui-callout><ui-callout tone="info">A status surface shares the Info badge’s colors.</ui-callout><ui-list><ui-list-item current><a href="#palette">Selected collection</a></ui-list-item></ui-list><ui-text><a href="#palette">Read the guide</a></ui-text><ui-text variant="caption" data-palette-roles="${mode}"></ui-text></ui-stack></ui-card>`).join('')}</ui-grid><ui-text variant="caption" data-palette-adjustments role="status" aria-live="polite"></ui-text><ui-disclosure summary="Resolved palette values"><ui-text variant="caption" data-palette-adjustment-details></ui-text></ui-disclosure>`;
|
|
63
|
+
const paletteRelationships = `<ui-stack gap="s"><ui-text variant="heading" as="h3">How should the colors work together?</ui-text>${[
|
|
64
|
+
['preserve', 'Keep my colors', 'Keep the base colors as chosen. Warmth and intensity still refine the result.'],
|
|
65
|
+
['neighboring', 'Related colors', 'Tint neutral surfaces with a neighboring hue for a closely coordinated palette.'],
|
|
66
|
+
['complementary', 'Contrasting colors', 'Tint neutral surfaces toward the opposite hue for warm–cool contrast.'],
|
|
67
|
+
].map(([id,label,description]) => `<ui-button variant="card" tone="neutral" data-action="palette-relationship" data-relationship="${id}" aria-pressed="${recipe.values['palette-relationship'] === id}"><ui-stack gap="xs"><ui-text variant="label">${label}</ui-text><span class="palette-range" data-relationship-preview="${id}" aria-hidden="true"><span class="palette-canvas"></span><span class="palette-content"></span><span class="palette-action"></span></span><ui-text variant="caption">${description}</ui-text></ui-stack><span slot="action" data-relationship-selected="${id}"${recipe.values['palette-relationship'] === id ? '' : ' hidden'}><ui-icon name="check"></ui-icon></span></ui-button>`).join('')}</ui-stack>`;
|
|
68
|
+
const sizePreview = `<ui-stack gap="l"><ui-text size="md">Compare the same actions and fields at each size. Rhythm separates these groups and opens the lines of text; spacing adjusts the room between controls.</ui-text><ui-stack gap="xl" data-size-groups>${['sm','md','lg'].map((size,index) => `<ui-stack gap="s" data-size-sample="${size}"><ui-text weight="medium" size="md">${['Small','Medium','Large'][index]}<span data-size-readout></span></ui-text><ui-cluster data-size-actions><ui-button size="${size}" variant="solid" tone="accent">Create</ui-button><ui-button size="${size}" variant="outline" tone="accent">Save</ui-button><ui-button size="${size}" variant="outline" tone="neutral">Archive</ui-button></ui-cluster><ui-cluster><ui-input size="${size}" style="--ui-input-inline-size: 10ch" aria-label="${size} size sample" value="Frodo"></ui-input><ui-color-picker size="${size}" label="${size} swatch sample"></ui-color-picker></ui-cluster></ui-stack>`).join('')}</ui-stack><ui-card><ui-stack gap="s"><ui-text weight="medium" size="md">Grouped content</ui-text><ui-text size="md">Frodo plans the journey. Sam brings lunch. A longer paragraph makes the line rhythm visible alongside the space between groups.</ui-text></ui-stack></ui-card><div class="spacing-samples" data-spacing-samples role="img" aria-label="Spacing progression"><span></span><span></span><span></span><span></span></div></ui-stack>`;
|
|
69
|
+
const fieldsFor = ids => ids.map(id => renderField(themeFields.find(field => field.id === id))).join('');
|
|
70
|
+
const viewTitles = { material: 'Material & light', palette: 'Palette', typography: 'Typography', layout: 'Size & spacing', motion: 'Motion', export: 'Export & save', components: 'Component library' };
|
|
71
|
+
const refinement = (family, content) => `<section id="${family}" data-refinement-view="${family}" hidden tabindex="-1" aria-label="${viewTitles[family]} editor"><ui-stack gap="l"><ui-text variant="heading" as="h2">${viewTitles[family]}</ui-text>${content}</ui-stack></section>`;
|
|
72
|
+
const editor = (family, preview, controls) => refinement(family, `<div class="refinement-layout"><div class="controls-panel"><ui-card padding="sm"><div class="settings-grid">${controls}</div></ui-card></div><div class="editor-preview" data-editor-preview="${family}">${preview}</div></div>`);
|
|
73
|
+
const customFontStacks = `<div class="settings-section"><ui-disclosure summary="Custom font stacks"><div class="settings-grid">${['body-font','display-font'].map(id=>`<ui-form-field><label slot="label" for="theme-${id}-custom">${id === 'body-font' ? 'Font' : 'Heading font'} stack</label><ui-input id="theme-${id}-custom" size="sm" data-token="${id}" value="${escape(recipe.values[id])}" aria-label="${id === 'body-font' ? 'Font' : 'Heading font'} stack"></ui-input></ui-form-field>`).join('')}</div></ui-disclosure></div>`;
|
|
74
|
+
const typographyView = editor('typography', `<ui-card><ui-stack><ui-text variant="heading" as="h2">There and back again.</ui-text><ui-text>Frodo plans the journey. Sam brings lunch. Clear notes keep the Fellowship moving.</ui-text><ui-form-field><label slot="label" for="type-sample-name">Memo title</label><ui-input id="type-sample-name" value="There and back again"></ui-input><span slot="help">Shown above your notes.</span></ui-form-field><ui-text variant="caption">Notes by Sméagol · Updated today</ui-text></ui-stack></ui-card>`, `<div class="settings-section"><div class="settings-grid">${fieldsFor(['body-font','display-font'])}</div></div>${fieldsFor(['text-size','label-size','text-weight'])}${customFontStacks}`);
|
|
75
|
+
const layoutView = editor('layout', `<ui-stack>${sizePreview}</ui-stack>`, `${fieldsFor(['control-roominess','rhythm','spacing','corner-roundness'])}${fieldsFor(['scale-contrast','space-curve','line-weight'])}`);
|
|
76
|
+
const motionView = editor('motion', `<ui-stack><ui-card><ui-stack><ui-text variant="eyebrow">Motion study</ui-text><div class="motion-track"><span data-motion-object aria-hidden="true"></span></div><ui-cluster><ui-button data-action="replay-motion"><ui-icon name="play"></ui-icon>Replay motion</ui-button><ui-text variant="caption" data-motion-readout></ui-text></ui-cluster><ui-text variant="caption" data-motion-preference></ui-text></ui-stack></ui-card></ui-stack>`, `${fieldsFor(['response-time'])}<div class="settings-section"><ui-disclosure summary="Shape the curve"><ui-stack>${fieldsFor(['response-curve'])}</ui-stack></ui-disclosure></div><div class="settings-section"><ui-cluster gap="xs"><ui-text weight="medium">Motion preview</ui-text>${help('motion-preview','motion preview','Replay shows the shared curve. The popover demonstrates it in a real component. System reduced-motion preferences are respected.',true)}</ui-cluster></div><div class="settings-section"><ui-disclosure summary="Try a popover">${documentationCard('ui-popover', null, true)}</ui-disclosure></div>`);
|
|
77
|
+
const materialSurfaces = `<ui-grid columns="2" min="sm"><ui-card variant="elevated" data-material-standard><ui-stack gap="s"><ui-text weight="medium" size="md">Everyday surface</ui-text><ui-text variant="caption">A restrained lift for ordinary content.</ui-text><ui-button variant="outline" tone="neutral">Read memo</ui-button></ui-stack></ui-card><ui-card variant="elevated" treatment="expressive" data-material-expressive><ui-stack gap="s"><ui-text weight="medium" size="md">Expressive surface</ui-text><ui-text variant="caption">Finish, edge light, glow and the decorative rim.</ui-text><ui-button variant="solid" tone="accent">Join briefing</ui-button></ui-stack></ui-card></ui-grid><ui-cluster><ui-button id="material-popover-trigger" variant="outline" tone="accent">Open popover</ui-button><ui-popover for="material-popover-trigger"><ui-stack gap="s"><ui-text size="md">Floating material</ui-text><ui-text variant="caption">Adjust glass, separation and translucency with this open.</ui-text></ui-stack></ui-popover><ui-button id="material-dialog-trigger" variant="outline" tone="neutral">Open dialog</ui-button><ui-dialog id="material-preview-dialog" for="material-dialog-trigger" label="Surface and light"><ui-text size="md">Compare the backdrop, content plane and frame as the material changes.</ui-text><ui-button slot="actions" variant="solid" commandfor="material-preview-dialog" command="close">Done</ui-button></ui-dialog></ui-cluster>`;
|
|
78
|
+
const materialSample = `<ui-card treatment="expressive"><ui-stack gap="m"><ui-text variant="heading" as="h3">Fellowship briefing</ui-text><ui-text>Content stays readable while the rim reveals the scene.</ui-text><ui-input label="Office search" placeholder="Search the Fellowship office"></ui-input><ui-action-bar><ui-button slot="secondary" preset="standard">Keep draft</ui-button><ui-button slot="primary" preset="primary">Send briefing</ui-button></ui-action-bar></ui-stack></ui-card>`;
|
|
79
|
+
const lightEditor = `<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-text variant="heading">Move the light</ui-text><ui-button preset="primary">Material response</ui-button></ui-stack></ui-light-editor>`;
|
|
80
|
+
const materialView = editor('material', `<ui-stack>${materialSample}${materialSurfaces}</ui-stack>`, `<div class="settings-section">${renderPreset('material', 'refinement')}</div>${fieldsFor(['finish','translucency','glassiness'])}<div class="settings-section"><ui-cluster gap="xs"><ui-text weight="medium">Lighting</ui-text><ui-icon-button id="help-lighting" size="sm" label="About lighting"><ui-icon name="help"></ui-icon></ui-icon-button><ui-tooltip for="help-lighting" trigger="click"><span data-material-dependencies></span></ui-tooltip></ui-cluster></div><ui-stack gap="s" data-glass-hint hidden><ui-text variant="caption">No transmission to blur.</ui-text><ui-button size="sm" data-action="use-glass">Use frosted glass</ui-button></ui-stack><div class="settings-section">${lightEditor}<div class="settings-grid">${fieldsFor(['light-color','light-spread','light-strength','edge-light','glow','outer-depth','inner-depth','surface-separation'])}</div></div><div class="settings-section"><ui-disclosure summary="Try floating surfaces"><ui-stack>${documentationCard('ui-card','/04-expressive-surface')}${documentationCard('ui-tooltip')}${documentationCard('ui-popover')}${documentationCard('ui-dialog')}</ui-stack></ui-disclosure></div>`);
|
|
81
|
+
const statusControls = ['success','danger','warning','info'].map(role => fieldsFor([role + '-source'])).join('');
|
|
82
|
+
const paletteView = refinement('palette', `<div class="refinement-layout"><div class="controls-panel"><ui-card padding="sm"><ui-stack gap="s">${renderPreset('palette', 'refinement')}<ui-cluster gap="xs"><ui-text weight="medium">Base colors</ui-text>${help('base-colors','base colors',['action','canvas','surface','text'].map(id => { const f=themeFields.find(field=>field.id===id); return f.label+': '+f.description; }).join(' '),true)}</ui-cluster><div class="color-row" data-base-colors>${fieldsFor(['action','canvas','surface','text'])}</div><ui-cluster gap="xs"><ui-text weight="medium">Supporting accents</ui-text>${help('supporting-colors','supporting accents','Optional second and third accents. Choose None inside a picker to leave it unset. Components use these only when that accent is chosen.',true)}</ui-cluster><div class="color-row" data-supporting-colors>${fieldsFor(['secondary-action','tertiary-action'])}</div><ui-text weight="medium">Use colors for</ui-text><div class="settings-grid">${fieldsFor(['selection-accent','focus-accent','link-accent'])}</div><ui-text weight="medium">Neutrals & borders</ui-text><div class="settings-grid">${fieldsFor(['neutral-source','neutral-strength','neutral-border-contrast'])}</div><ui-text weight="medium">Status colors</ui-text>${renderPreset('status', 'refinement')}<div class="settings-grid">${statusControls}${fieldsFor(['status-chroma'])}</div><ui-disclosure summary="Color character"><div class="settings-grid">${fieldsFor(['warmth','color-intensity'])}</div>${paletteRelationships}</ui-disclosure></ui-stack></ui-card></div><div class="palette-preview"><ui-stack>${paletteDetails}</ui-stack></div></div>`);
|
|
83
|
+
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('');
|
|
84
|
+
const links = ['ui-menu','ui-menu-item','ui-tooltip','ui-icon-button','ui-action-bar','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','ui-checkbox','ui-badge','ui-icon','ui-callout','ui-popover','ui-dialog','ui-list','ui-list-item'].map(tag => `<link rel="component" href="@threadlabs/looma/components/${tag}/${tag}.html">`).join('\n');
|
|
85
|
+
return `${links}\n${exampleResources(site.catalog.components.flatMap(component => component.examples), site.catalog)}\n<link rel="component" href="./card.html">\n<link rel="component" href="./preview-choice.html">\n<template component="looma-customizer" module="./customizer.js"><defs>
|
|
86
|
+
<state name="previews" 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>
|
|
87
|
+
<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>
|
|
88
|
+
</defs>
|
|
89
|
+
<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 ?? '')}">
|
|
90
|
+
<ui-stack gap="l">
|
|
91
|
+
<div class="summary-panel" data-customizer-summary><ui-card padding="sm"><div class="summary-toolbar"><div class="summary-theme">${choice('id="theme-start" data-preset label-visibility="sr-only"', 'Starting point', startingPointFor(recipe), `<option value="custom" disabled>Your theme</option>${Object.entries(startingPointLabels).map(([id,label]) => `<option value="${id}">${label}</option>`).join('')}`)}</div><nav aria-label="Theme controls" class="summary-sections">${[['palette','Palette'],['material','Material'],['typography','Type'],['layout','Size'],['motion','Motion']].map(([id,label]) => `<div class="summary-choice"><ui-button variant="ghost" tone="neutral" size="sm" data-action="refine-${id}" aria-label="${label}"><ui-stack gap="xs"><span class="summary-heading">${id === 'palette' ? swatches() : ''}<span>${label}</span></span><span data-summary="${id}" class="summary-value"></span></ui-stack></ui-button></div>`).join('')}</nav><div class="summary-actions"><ui-cluster gap="xs" wrap="nowrap"><ui-icon-button id="theme-undo" size="sm" data-action="undo" label="Undo" disabled><ui-icon name="undo"></ui-icon></ui-icon-button><ui-tooltip for="theme-undo">Undo</ui-tooltip><ui-icon-button id="theme-redo" size="sm" data-action="redo" label="Redo" disabled><ui-icon name="redo"></ui-icon></ui-icon-button><ui-tooltip for="theme-redo">Redo</ui-tooltip><ui-button size="sm" data-action="reset">Reset</ui-button><ui-icon-button id="theme-tools" size="sm" label="Theme tools"><ui-icon name="ellipsis"></ui-icon></ui-icon-button><ui-menu for="theme-tools" label="Theme tools"><ui-menu-item data-action="back">Overview</ui-menu-item><ui-menu-item data-action="refine-components">Components</ui-menu-item><ui-menu-item data-action="refine-export">Export & save</ui-menu-item></ui-menu></ui-cluster></div><span class="summary-active-value" data-active-summary></span><span class="summary-status" role="status" aria-live="polite" data-message></span></div></ui-card></div>
|
|
92
|
+
${materialView}${paletteView}${typographyView}${layoutView}${motionView}
|
|
93
|
+
${refinement('export', `<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>
|
|
94
|
+
<input type="file" accept="application/json,.json" hidden data-recipe-file>
|
|
95
|
+
<ui-button data-action="save" variant="solid" hidden>Save to repository</ui-button>
|
|
96
|
+
<ui-disclosure summary="Use your theme"><ui-text>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></ui-text></ui-disclosure>
|
|
97
|
+
<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>`)}
|
|
98
|
+
<div class="customizer-layout" data-customizer-overview>
|
|
99
|
+
<section data-theme-sampler><ui-stack gap="xs"><ui-text variant="heading" as="h2">Theme sampler</ui-text>${documentationCard('ui-cluster', '/99-theme-sampler', true)}</ui-stack></section>
|
|
100
|
+
<noscript>Enable JavaScript to edit the theme. The component reference and example previews remain available.</noscript>
|
|
101
|
+
</div>${refinement('components', ` <section class="examples"><ui-stack><ui-form-field><label slot="label" for="example-query">Search components</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>
|
|
102
|
+
<ui-grid columns="2" min="sm"><section $each="card of $previews" $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" removable><looma-preview-choice slot="preview" from:identity="$card.id" from:namespace="concat('selected-', $card.id)"></looma-preview-choice></looma-example-card></section></ui-grid>
|
|
103
|
+
<ui-button data-action="more-examples">Show more components</ui-button></ui-stack></section>
|
|
104
|
+
`)}</ui-stack></div>
|
|
105
|
+
<style>
|
|
106
|
+
:host { background-color: var(--ui-surface); }
|
|
107
|
+
.summary-panel { container-type: inline-size; position: sticky; inset-block-start: var(--_editor-chrome-height, 4.25rem); z-index: 3; }
|
|
108
|
+
.font-option { font-family: var(--_font-preview-family, inherit); font-weight: 400; }
|
|
109
|
+
.summary-theme { inline-size: 8rem; max-inline-size: 100%; min-inline-size: 0; }
|
|
110
|
+
.summary-toolbar { display: grid !important; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center !important; gap: var(--ui-space-2) !important; }
|
|
111
|
+
.summary-sections { display: flex !important; flex-wrap: wrap !important; gap: var(--ui-space-1) !important; }
|
|
112
|
+
.summary-heading { display: flex !important; align-items: center !important; gap: var(--ui-space-2) !important; }
|
|
113
|
+
.summary-active-value { display: none; grid-column: 1 / -1; color: var(--ui-text-secondary); font-size: var(--ui-font-size-sm); }
|
|
114
|
+
.curve-setting { grid-column: 1 / -1; }
|
|
115
|
+
.summary-status { grid-column: 1 / -1; font-size: var(--ui-font-size-md); color: var(--ui-text-secondary); }
|
|
116
|
+
.summary-status:empty { display: none; }
|
|
117
|
+
.summary-choice { flex: 0 1 auto; min-inline-size: 0; }
|
|
118
|
+
.summary-value { display: block; max-inline-size: 11rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--ui-font-size-sm); color: var(--ui-text-secondary); }
|
|
119
|
+
.controls-panel { grid-column: 2; grid-row: 1; min-inline-size: 0; }
|
|
120
|
+
.refinement-layout > .editor-preview, .refinement-layout > .palette-preview { grid-column: 1; grid-row: 1; }
|
|
121
|
+
@container (max-width: 16rem) { .summary-toolbar { grid-template-columns: minmax(0, 1fr); } .summary-actions { grid-column: 1 !important; grid-row: 2 !important; } .summary-sections { grid-row: 3 !important; } }
|
|
122
|
+
.settings-grid { display: grid !important; grid-template-columns: repeat(auto-fit, minmax(min(100%, 9rem), 1fr)); gap: var(--ui-space-3) !important; align-items: start !important; }
|
|
123
|
+
.settings-section { grid-column: 1 / -1; }
|
|
124
|
+
.setting { min-inline-size: 0; }
|
|
125
|
+
[data-base-colors] { display: grid !important; grid-template-columns: repeat(auto-fit, minmax(min(100%, 4.25rem), max-content)); }
|
|
126
|
+
[data-base-colors] .color-setting { inline-size: auto; }
|
|
127
|
+
.color-row { display: flex !important; flex-wrap: wrap !important; gap: var(--ui-space-4) !important; }
|
|
128
|
+
.color-setting { inline-size: 5.25rem; }
|
|
129
|
+
[data-editor-preview="material"] { padding: var(--ui-space-4) !important; background-color: var(--ui-surface); background-image: repeating-linear-gradient(0deg, transparent 0 15px, color-mix(in srgb, var(--ui-text) 24%, transparent) 14px 16px), repeating-linear-gradient(90deg, transparent 0 15px, color-mix(in srgb, var(--ui-text) 24%, transparent) 14px 16px); }
|
|
130
|
+
@media (max-width: 40rem) { .summary-toolbar { grid-template-columns: minmax(0, 1fr) auto; } .summary-sections { grid-column: 1 / -1; grid-row: 2; flex-wrap: wrap !important; } .summary-actions { grid-column: 2; grid-row: 1; } .summary-value, .summary-heading .palette-swatches { display: none; } .summary-choice { flex: 0 1 auto; } .summary-active-value { display: block; } .summary-status { font-size: var(--ui-font-size-sm); } }
|
|
131
|
+
|
|
132
|
+
[data-customizer-overview][hidden], [data-refinement-view][hidden] { display: none !important; }
|
|
133
|
+
.customizer-layout { display: grid !important; grid-template-columns: minmax(0, 1fr); gap: var(--ui-space-6) !important; align-items: start !important; }
|
|
134
|
+
aside { grid-row: 1 / span 2; position: sticky; inset-block-start: calc(var(--_editor-chrome-height, 4.25rem) + var(--_customizer-summary-height, 8rem) + var(--ui-space-2)); max-block-size: calc(100dvh - var(--_editor-chrome-height, 4.25rem) - var(--ui-space-4)); overflow: auto; padding: max(var(--ui-space-2), calc(var(--ui-focus-width) * 2)) !important; scroll-padding: max(var(--ui-space-2), calc(var(--ui-focus-width) * 2)); }
|
|
135
|
+
.examples { min-inline-size: 0; }
|
|
136
|
+
.refinement-layout { display: grid !important; grid-template-columns: minmax(0, 1.15fr) minmax(20rem, 0.85fr); gap: var(--ui-space-6) !important; align-items: start !important; }
|
|
137
|
+
[data-refinement-view] { scroll-margin-block-start: calc(var(--_editor-chrome-height, 4.25rem) + var(--_customizer-summary-height, 8rem) + var(--ui-space-2)); }
|
|
138
|
+
.palette-preview, .editor-preview { position: sticky; inset-block-start: calc(var(--_editor-chrome-height, 4.25rem) + var(--_customizer-summary-height, 8rem) + var(--ui-space-2)); max-block-size: calc(100dvh - var(--_editor-chrome-height, 4.25rem) - var(--ui-space-4)); overflow: auto; }
|
|
139
|
+
.spacing-samples { display: flex !important; align-items: center !important; gap: 0 !important; padding-block: var(--ui-space-3) !important; }
|
|
140
|
+
.spacing-samples > span { display: block !important; block-size: var(--ui-space-1); inline-size: var(--ui-space-1); border-radius: 50%; background: var(--ui-accent); }
|
|
141
|
+
.spacing-samples > span:nth-child(2) { margin-inline-start: var(--ui-space-2) !important; }
|
|
142
|
+
.spacing-samples > span:nth-child(3) { margin-inline-start: var(--ui-space-3) !important; }
|
|
143
|
+
.spacing-samples > span:nth-child(4) { margin-inline-start: var(--ui-space-4) !important; }
|
|
144
|
+
.palette-option { display: flex; align-items: center; gap: var(--ui-space-2); }
|
|
145
|
+
.palette-swatches { display: flex; gap: 0.125rem; flex: none; }
|
|
146
|
+
.palette-swatches > span { display: block; inline-size: 0.65rem; block-size: 0.65rem; border-radius: var(--ui-radius-xs); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ui-text) 12%, transparent); }
|
|
147
|
+
.palette-range { display: flex !important; min-block-size: var(--ui-control-size-sm); border: var(--ui-border-width) solid var(--ui-control-border); border-radius: var(--ui-radius-sm); overflow: hidden; }
|
|
148
|
+
.palette-range > span { flex: 1; min-inline-size: var(--ui-space-5); }
|
|
149
|
+
.palette-canvas { background: var(--_palette-canvas, var(--ui-surface)); }
|
|
150
|
+
.palette-content { background: var(--_palette-content, var(--ui-surface-raised)); }
|
|
151
|
+
.palette-action { background: var(--_palette-action, var(--ui-accent)); }
|
|
152
|
+
.motion-track { overflow: clip; display: flex; align-items: center; block-size: 5rem; padding: var(--ui-space-3); border-radius: var(--ui-radius-md); background: var(--ui-surface-muted); }
|
|
153
|
+
[data-motion-object] { display: block; inline-size: 2rem; block-size: 2rem; border-radius: var(--ui-radius-sm); background: var(--ui-accent-solid); box-shadow: var(--ui-action-shadow); }
|
|
154
|
+
@media (max-width: 64rem) { .controls-panel, .refinement-layout > .editor-preview, .refinement-layout > .palette-preview { grid-column: 1; grid-row: auto; } [data-base-colors] { gap: var(--ui-space-2) !important; } [data-base-colors] .color-setting { inline-size: auto; } .customizer-layout, .refinement-layout { grid-template-columns: minmax(0, 1fr); } aside { grid-row: auto; order: 1; position: static; max-block-size: none; } .palette-preview, .editor-preview { position: sticky; inset-block-start: calc(var(--_editor-chrome-height, 4.25rem) + var(--_customizer-summary-height, 8rem) + var(--ui-space-2)); z-index: 1; background: var(--ui-surface); max-block-size: none; overflow: visible; padding: 0 !important; } .editor-preview { position: static; } [data-theme-sampler] { order: 2; } .examples { order: 3; } }
|
|
155
|
+
@media (max-width: 64rem) { .palette-preview[data-page-flow], .editor-preview[data-page-flow] { position: static; } }
|
|
156
|
+
</style></template>`;
|
|
157
|
+
}
|