@threadlabs/looma 0.34.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/components/ui-badge/ui-badge.html +11 -3
- package/components/ui-button/ui-button.html +29 -4
- package/components/ui-card/ui-card.html +15 -13
- package/components/ui-checkbox/ui-checkbox.html +4 -2
- package/components/ui-dialog/ui-dialog.html +2 -0
- package/components/ui-icon-button/ui-icon-button.html +22 -8
- package/components/ui-input/ui-input.html +2 -1
- package/components/ui-input-group/ui-input-group.html +4 -1
- package/components/ui-light-editor/ui-light-editor.html +45 -0
- package/components/ui-light-editor/ui-light-editor.js +53 -0
- package/components/ui-menu/ui-menu.html +2 -0
- package/components/ui-number-input/ui-number-input.html +2 -1
- package/components/ui-number-input/ui-number-input.js +6 -3
- package/components/ui-popover/ui-popover.html +2 -0
- package/components/ui-reel/ui-reel.html +2 -1
- package/components/ui-select/ui-select.html +9 -7
- package/components/ui-switch/ui-switch.html +3 -0
- package/components/ui-tabs/ui-tabs.html +2 -3
- package/components/ui-tabs/ui-tabs.js +0 -9
- package/components/ui-textarea/ui-textarea.html +2 -1
- package/components/ui-tooltip/ui-tooltip.html +2 -1
- package/dist/index.js +24 -23
- package/docs/README.md +29 -3
- package/docs/card.html +7 -6
- package/docs/catalog-view.mjs +2 -1
- package/docs/catalog.json +575 -53
- package/docs/customizer-render.mjs +31 -16
- package/docs/customizer.js +72 -19
- package/docs/index.d.ts +4 -1
- package/docs/index.mjs +1 -1
- package/docs/render.mjs +1 -1
- package/docs/site.mjs +33 -22
- package/docs/theme-session.mjs +4 -2
- package/docs/theme.mjs +61 -7
- package/package.json +2 -2
- package/styles/ui-badge.css +11 -6
- package/styles/ui-button.css +29 -4
- package/styles/ui-card.css +9 -9
- package/styles/ui-checkbox.css +4 -2
- package/styles/ui-dialog.css +4 -0
- package/styles/ui-icon-button.css +22 -8
- package/styles/ui-input-group.css +4 -1
- package/styles/ui-input.css +2 -1
- package/styles/ui-light-editor.css +28 -0
- package/styles/ui-menu.css +2 -0
- package/styles/ui-popover.css +2 -0
- package/styles/ui-select.css +9 -7
- package/styles/ui-switch.css +3 -0
- package/styles/ui-tabs.css +2 -3
- package/styles/ui-textarea.css +2 -1
- package/styles/ui-tooltip.css +2 -1
- package/theme-light.css +1 -0
- package/theme-system.css +43 -7
- package/tokens.css +5 -1
- package/vanilla/UiCard.d.ts +2 -1
- package/vanilla/UiCard.js +12 -12
- package/vanilla/UiLightEditor.d.ts +29 -0
- package/vanilla/UiLightEditor.js +102 -0
- package/vanilla/UiNumberInput.js +27 -20
- package/vanilla/index.js +1 -0
- package/vue/UiBadge.vue +29 -4
- package/vue/UiButton.vue +33 -4
- package/vue/UiCard.d.ts +8 -2
- package/vue/UiCard.vue +41 -17
- package/vue/UiCheckbox.vue +4 -2
- package/vue/UiDialog.vue +4 -0
- package/vue/UiIconButton.vue +26 -11
- package/vue/UiInput.vue +2 -1
- package/vue/UiInputGroup.vue +4 -1
- package/vue/UiLightEditor.d.ts +75 -0
- package/vue/UiLightEditor.js +2 -0
- package/vue/UiLightEditor.vue +402 -0
- package/vue/UiMenu.vue +2 -0
- package/vue/UiNumberInput.vue +32 -3
- package/vue/UiPopover.vue +2 -0
- package/vue/UiReel.vue +1 -1
- package/vue/UiSelect.vue +16 -10
- package/vue/UiSwitch.vue +3 -0
- package/vue/UiTabs.vue +2 -3
- package/vue/UiTextarea.vue +2 -1
- package/vue/UiTooltip.vue +2 -1
- package/vue/chunks/UiBadge.js +1 -1
- package/vue/chunks/UiButton.js +1 -1
- package/vue/chunks/UiCard.js +33 -5
- package/vue/chunks/UiCheckbox.js +1 -1
- package/vue/chunks/UiDialog.js +1 -1
- package/vue/chunks/UiIconButton.js +1 -1
- package/vue/chunks/UiInput.js +1 -1
- package/vue/chunks/UiInputGroup.js +1 -1
- package/vue/chunks/UiLightEditor.js +347 -0
- package/vue/chunks/UiMenu.js +1 -1
- package/vue/chunks/UiNumberInput.js +21 -8
- package/vue/chunks/UiPopover.js +1 -1
- package/vue/chunks/UiReel.js +2 -2
- package/vue/chunks/UiSelect.js +1 -1
- package/vue/chunks/UiSwitch.js +1 -1
- package/vue/chunks/UiTabs.js +1 -1
- package/vue/chunks/UiTextarea.js +1 -1
- package/vue/chunks/UiTooltip.js +1 -1
- package/vue/components.css +312 -71
- package/vue/index.d.ts +1 -0
- package/vue/index.js +2 -1
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { escapeHtml as escape } from './catalog.mjs';
|
|
2
2
|
import { componentPath, previewPath } from './render.mjs';
|
|
3
3
|
import { valueEditor } from './value-editors.mjs';
|
|
4
|
-
import { defaultRecipe, themeFields, startingPointLabels, startingPointFor, themeCSS, validateRecipe, themePalettes,
|
|
4
|
+
import { defaultRecipe, themeFields, startingPointLabels, startingPointFor, themeCSS, validateRecipe, themePalettes, materialPresets, refinementState } from './theme.mjs';
|
|
5
5
|
|
|
6
6
|
export function customizerResource(site, input = defaultRecipe()) {
|
|
7
7
|
const recipe = validateRecipe(input);
|
|
8
8
|
const cardFields = ['id', 'title', 'description', 'exampleTitle', 'src', 'resizable', 'category', 'tag', 'nativeRoot', 'href', 'packageName'];
|
|
9
9
|
const examples = site.catalog.components.flatMap(component => component.examples.map(example => ({ id: example.id, title: component.title, description: component.description, exampleTitle: example.title, src: site.base + previewPath(example).slice(1), 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) })));
|
|
10
10
|
const toCard = example => Object.fromEntries(cardFields.map(key => [key, example[key]]));
|
|
11
|
-
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-
|
|
11
|
+
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);
|
|
12
12
|
if (!initial.length && examples.length) initial.push(toCard(examples[0]));
|
|
13
13
|
const renderField = field => {
|
|
14
14
|
const value = recipe.values[field.id], editor = valueEditor(field, value);
|
|
@@ -25,42 +25,57 @@ export function customizerResource(site, input = defaultRecipe()) {
|
|
|
25
25
|
}
|
|
26
26
|
return `<ui-form-field><label slot="label" for="theme-${field.id}">${escape(field.label)}</label><span slot="help">${escape(field.description)}</span>${control}</ui-form-field>`;
|
|
27
27
|
};
|
|
28
|
+
const renderPreset = (family, location) => {
|
|
29
|
+
const presets = family === 'material' ? materialPresets : themePalettes;
|
|
30
|
+
const state = refinementState(recipe, family), label = family === 'material' ? 'Material' : 'Palette';
|
|
31
|
+
return `<ui-form-field><label slot="label" for="${location}-${family}">${label} preset</label><ui-select id="${location}-${family}" data-refinement-preset="${family}" aria-label="${label} preset" value="${state.id}"><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('')}</ui-select></ui-form-field>`;
|
|
32
|
+
};
|
|
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>`;
|
|
28
34
|
const supplements = {
|
|
29
|
-
'Colour':
|
|
30
|
-
'Material':
|
|
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>`,
|
|
31
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>`,
|
|
32
38
|
};
|
|
33
|
-
const families = [...new Set(themeFields.map(field => field.family))].map(family => `<ui-disclosure summary="${family}"
|
|
34
|
-
const
|
|
35
|
-
const
|
|
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
|
+
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="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
|
+
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
|
+
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
|
+
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');
|
|
36
45
|
return `${links}\n<link rel="component" href="./card.html">\n<template component="looma-customizer" module="./customizer.js"><defs>
|
|
37
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>
|
|
38
47
|
</defs>
|
|
39
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 ?? '')}">
|
|
40
|
-
<ui-stack gap="
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<ui-
|
|
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
|
+
${materialView}${paletteView}
|
|
52
|
+
<div class="customizer-layout" data-customizer-overview>
|
|
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>
|
|
45
58
|
<input type="file" accept="application/json,.json" hidden data-recipe-file>
|
|
46
59
|
<ui-button data-action="save" variant="solid" hidden>Save to repository</ui-button>
|
|
47
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>
|
|
48
|
-
<
|
|
49
|
-
<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>
|
|
50
62
|
</ui-stack></aside>
|
|
51
|
-
<section class="examples"><ui-stack><ui-form-field><label slot="label" for="example-query">
|
|
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>
|
|
52
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>
|
|
53
65
|
<noscript>Enable JavaScript to edit the theme. The component reference and example previews remain available.</noscript>
|
|
54
66
|
</ui-stack></section></div></ui-stack></div>
|
|
55
67
|
<style>
|
|
68
|
+
[data-customizer-overview][hidden], [data-refinement-view][hidden] { display: none !important; }
|
|
56
69
|
.customizer-layout { display: grid !important; grid-template-columns: minmax(16rem, 20rem) minmax(0, 1fr); gap: var(--ui-space-6) !important; align-items: start !important; }
|
|
57
70
|
aside { position: sticky; inset-block-start: 5rem; max-block-size: calc(100dvh - 6rem); overflow: auto; padding-inline-end: var(--ui-space-2) !important; }
|
|
58
71
|
.examples { min-inline-size: 0; }
|
|
72
|
+
.refinement-layout { display: grid !important; grid-template-columns: minmax(0, 1.2fr) minmax(16rem, 1fr); gap: var(--ui-space-6) !important; align-items: start !important; }
|
|
73
|
+
[data-refinement-view] { scroll-margin-block-start: 5rem; }
|
|
59
74
|
.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; }
|
|
60
75
|
.palette-range > span { flex: 1; min-inline-size: var(--ui-space-5); }
|
|
61
76
|
.palette-canvas { background: var(--ui-surface); }
|
|
62
77
|
.palette-content { background: var(--ui-surface-raised); }
|
|
63
78
|
.palette-action { background: var(--ui-accent); }
|
|
64
|
-
@media (max-width: 64rem) { .customizer-layout { grid-template-columns: minmax(0, 1fr); } aside { position: static; max-block-size: none; } }
|
|
79
|
+
@media (max-width: 64rem) { .customizer-layout, .refinement-layout { grid-template-columns: minmax(0, 1fr); } aside { position: static; max-block-size: none; } }
|
|
65
80
|
</style></template>`;
|
|
66
81
|
}
|
package/docs/customizer.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { documentationTheme, themeEvent } from './theme-session.mjs';
|
|
2
2
|
import { componentRegistry } from '@nextwebwg/html-next/runtime';
|
|
3
|
-
import { startingPoints, startingPointFor, themeCSS, validateRecipe, themeFields, themePalettes,
|
|
3
|
+
import { startingPoints, startingPointFor, themeCSS, validateRecipe, themeFields, themePalettes, materialPresets, applyRefinement, refinementState, deriveTheme } from './theme.mjs';
|
|
4
4
|
|
|
5
5
|
import { valueEditor, editorValue, convertValueUnit } from './value-editors.mjs';
|
|
6
6
|
|
|
@@ -19,6 +19,8 @@ export function connect(host) {
|
|
|
19
19
|
let changed = false;
|
|
20
20
|
let saving = false;
|
|
21
21
|
let alive = true;
|
|
22
|
+
let selectedExample = '';
|
|
23
|
+
let refinementTrigger;
|
|
22
24
|
const abort = new AbortController();
|
|
23
25
|
const say = (text, error = false) => { message.textContent = text; message.setAttribute('role', error ? 'alert' : 'status'); };
|
|
24
26
|
const sayApplied = text => say(theme?.storageAvailable ? text : 'Theme applies to this page. Browser storage is unavailable; download CSS or a recipe to keep it.');
|
|
@@ -41,13 +43,21 @@ export function connect(host) {
|
|
|
41
43
|
}
|
|
42
44
|
};
|
|
43
45
|
const paint = () => {
|
|
44
|
-
const css = themeCSS(recipe);
|
|
45
|
-
|
|
46
|
+
const css = theme?.css || themeCSS(recipe);
|
|
47
|
+
for (const family of ['material','palette']) {
|
|
48
|
+
const state = refinementState(recipe, family), presets = family === 'material' ? materialPresets : themePalettes;
|
|
49
|
+
for (const select of root.querySelectorAll(`[data-refinement-preset="${family}"]`)) {
|
|
50
|
+
for (const option of select.options) if (presets[option.value]) option.textContent = presets[option.value].label + (state.id === option.value && state.customized ? ' · customized' : '');
|
|
51
|
+
updateControl(select, state.id);
|
|
52
|
+
}
|
|
53
|
+
componentRegistry.update(root.querySelector(`[data-action="restore-${family}"]`), { disabled: state.id === 'custom' || !state.customized });
|
|
54
|
+
}
|
|
55
|
+
componentRegistry.update(root.querySelector('[data-light-editor]'), { x: Number(recipe.values['light-x']), y: Number(recipe.values['light-y']) });
|
|
46
56
|
const adjustments = ['light', 'dark'].flatMap(mode => deriveTheme(recipe, { mode }).adjustments.map(item => ({ ...item, mode })));
|
|
47
57
|
const report = root.querySelector('[data-palette-adjustments]');
|
|
48
58
|
report.textContent = adjustments.length ? 'Paired ranges are derived for readability. Your seed colors stay editable; resolved values are shown below.' : 'Both ranges preserve your seeds with readable text and essential outlines.';
|
|
49
59
|
root.querySelector('[data-palette-adjustment-details]').textContent = adjustments.length ? adjustments.map(item => `${item.mode} ${fields.get(item.id).label.toLowerCase()} ${item.requested} → ${item.resolved}`).join('; ') + '. Seed colors remain editable.' : 'Both ranges preserve your seeds with readable text and essential outlines.';
|
|
50
|
-
for (const card of root.querySelectorAll('[data-example-card]')) card.dispatchEvent(new CustomEvent('looma-theme-preview', { detail: { css
|
|
60
|
+
for (const card of root.querySelectorAll('[data-example-card]')) card.dispatchEvent(new CustomEvent('looma-theme-preview', { detail: { css } }));
|
|
51
61
|
updateControl(root.querySelector('[data-css]'), css);
|
|
52
62
|
};
|
|
53
63
|
const show = (next, persist = true) => {
|
|
@@ -61,24 +71,38 @@ export function connect(host) {
|
|
|
61
71
|
};
|
|
62
72
|
const filterExamples = () => {
|
|
63
73
|
const words = root.querySelector('[data-example-query]').value.toLocaleLowerCase().trim().split(/\s+/).filter(Boolean);
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
root.querySelector('[data-
|
|
72
|
-
|
|
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.';
|
|
81
|
+
componentRegistry.update(root.querySelector('[data-action="add"]'), { disabled: !selectedExample || host.state.cards.some(item => item.id === selectedExample) });
|
|
82
|
+
};
|
|
83
|
+
const refinedRecipe = values => {
|
|
84
|
+
const presets = { ...recipe.presets };
|
|
85
|
+
for (const family of ['material', 'palette']) {
|
|
86
|
+
const state = refinementState(recipe, family);
|
|
87
|
+
if (!presets[family] && state.id !== 'custom') presets[family] = state.id;
|
|
88
|
+
}
|
|
89
|
+
return { ...recipe, presets, values };
|
|
73
90
|
};
|
|
74
91
|
const input = event => {
|
|
75
92
|
if (event.target.matches('[data-example-query]')) { filterExamples(); return; }
|
|
93
|
+
if (event.target.matches('[data-light-editor]')) {
|
|
94
|
+
try {
|
|
95
|
+
show(refinedRecipe({ ...recipe.values, 'light-x': String(event.detail.x), 'light-y': String(event.detail.y) }));
|
|
96
|
+
sayApplied('Light position remembered throughout your design system.');
|
|
97
|
+
} catch (error) { say(error.message, true); }
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
76
100
|
const control = event.target.closest?.('[data-token]');
|
|
77
101
|
if (!control) return;
|
|
78
102
|
const field = fields.get(control.dataset.token);
|
|
79
103
|
const raw = event.detail?.value ?? control.value;
|
|
80
104
|
const value = control.dataset.editorKind ? editorValue(field, raw) : String(raw);
|
|
81
|
-
const next = {
|
|
105
|
+
const next = refinedRecipe({ ...recipe.values, [control.dataset.token]: value });
|
|
82
106
|
try {
|
|
83
107
|
recipe = validateRecipe(next);
|
|
84
108
|
theme?.apply(recipe); changed = true;
|
|
@@ -99,12 +123,31 @@ export function connect(host) {
|
|
|
99
123
|
if (!button || (button.dataset.action === 'save' && saving)) return;
|
|
100
124
|
try {
|
|
101
125
|
switch (button.dataset.action) {
|
|
126
|
+
case 'refine-material': case 'refine-palette': {
|
|
127
|
+
const family = button.dataset.action.slice(7);
|
|
128
|
+
refinementTrigger = button;
|
|
129
|
+
root.querySelector('[data-customizer-overview]').hidden = true;
|
|
130
|
+
for (const section of root.querySelectorAll('[data-refinement-view]')) section.hidden = section.dataset.refinementView !== family;
|
|
131
|
+
const section = root.querySelector(`[data-refinement-view="${family}"]`);
|
|
132
|
+
section.focus(); section.scrollIntoView({ block: 'start' });
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
case 'back':
|
|
136
|
+
for (const section of root.querySelectorAll('[data-refinement-view]')) section.hidden = true;
|
|
137
|
+
root.querySelector('[data-customizer-overview]').hidden = false;
|
|
138
|
+
refinementTrigger?.focus(); break;
|
|
139
|
+
case 'restore-material': case 'restore-palette': {
|
|
140
|
+
const family = button.dataset.action.slice(8), state = refinementState(recipe, family);
|
|
141
|
+
if (state.id !== 'custom') { show(applyRefinement(recipe, family, state.id)); sayApplied(`${family === 'material' ? 'Material' : 'Palette'} preset restored; other theme decisions are unchanged.`); }
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
102
144
|
case 'add': {
|
|
103
|
-
const
|
|
104
|
-
|
|
145
|
+
const option = [...root.querySelectorAll('[data-add-example] option')].find(option => option.value === selectedExample);
|
|
146
|
+
if (!option) break;
|
|
105
147
|
const selected = JSON.parse(option.dataset.example);
|
|
106
148
|
const card = Object.fromEntries(['id','title','description','exampleTitle','src','resizable','category','tag','nativeRoot','href','packageName'].map(key => [key, selected[key]]));
|
|
107
149
|
if (!host.state.cards.some(item => item.id === card.id)) host.state.cards = [...host.state.cards, card];
|
|
150
|
+
say(`${card.title} · ${card.exampleTitle} added to preview.`);
|
|
108
151
|
view.requestAnimationFrame(() => { paint(); filterExamples(); }); break;
|
|
109
152
|
}
|
|
110
153
|
case 'css': {
|
|
@@ -144,7 +187,11 @@ export function connect(host) {
|
|
|
144
187
|
sayApplied(`${field.label} units updated and remembered in this browser.`);
|
|
145
188
|
} catch (error) { updateUnits(); say(error.message, true); }
|
|
146
189
|
}
|
|
147
|
-
if (event.target.matches('[data-
|
|
190
|
+
if (event.target.matches('[data-refinement-preset]')) {
|
|
191
|
+
const family = event.target.dataset.refinementPreset;
|
|
192
|
+
show(applyRefinement(recipe, family, event.target.value));
|
|
193
|
+
sayApplied(`${family === 'material' ? 'Material' : 'Coordinated palette'} preset applied and remembered in both modes.`);
|
|
194
|
+
}
|
|
148
195
|
if (event.target.matches('[data-preset]')) { show(structuredClone(startingPoints[event.target.value])); sayApplied('Starting point applied throughout the documentation and remembered in this browser.'); }
|
|
149
196
|
if (event.target === file && file.files[0]) {
|
|
150
197
|
try { show(JSON.parse(await file.files[0].text())); sayApplied('Recipe applied throughout the documentation and remembered in this browser.'); }
|
|
@@ -157,9 +204,15 @@ export function connect(host) {
|
|
|
157
204
|
const ready = event => {
|
|
158
205
|
if (event.data?.type === 'looma-preview-ready' && [...root.querySelectorAll('iframe')].some(frame => frame.contentWindow === event.source)) paint();
|
|
159
206
|
};
|
|
160
|
-
const remove = event => { host.state.cards = host.state.cards.filter(card => card.id !== event.detail.id); };
|
|
207
|
+
const remove = event => { host.state.cards = host.state.cards.filter(card => card.id !== event.detail.id); view.requestAnimationFrame(filterExamples); };
|
|
208
|
+
const selectExample = event => {
|
|
209
|
+
if (!event.target.matches('[data-add-example]')) return;
|
|
210
|
+
selectedExample = event.detail.value ?? '';
|
|
211
|
+
filterExamples();
|
|
212
|
+
};
|
|
161
213
|
root.addEventListener('looma-example-remove', remove);
|
|
162
214
|
root.addEventListener('input', input);
|
|
215
|
+
root.addEventListener('value-change', selectExample);
|
|
163
216
|
root.addEventListener('blur', blur, true);
|
|
164
217
|
root.addEventListener('change', change);
|
|
165
218
|
root.addEventListener('click', click);
|
|
@@ -177,5 +230,5 @@ export function connect(host) {
|
|
|
177
230
|
root.querySelector('[data-action="save"]').hidden = false;
|
|
178
231
|
}).catch(error => { if (alive && error.name !== 'AbortError') say(error.message, true); });
|
|
179
232
|
}
|
|
180
|
-
return () => { alive = false; abort.abort(); root.removeEventListener('looma-example-remove', remove); root.removeEventListener('input', input); root.removeEventListener('blur', blur, true); root.removeEventListener('change', change); root.removeEventListener('click', click); view.removeEventListener('message', ready); view.removeEventListener(themeEvent, synchronize); };
|
|
233
|
+
return () => { alive = false; abort.abort(); root.removeEventListener('looma-example-remove', remove); root.removeEventListener('input', input); root.removeEventListener('value-change', selectExample); root.removeEventListener('blur', blur, true); root.removeEventListener('change', change); root.removeEventListener('click', click); view.removeEventListener('message', ready); view.removeEventListener(themeEvent, synchronize); };
|
|
181
234
|
}
|
package/docs/index.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export interface DocumentationCatalog {
|
|
|
17
17
|
resources?: { tag: string; package: string; resource: string }[]; components: DocumentationComponent[];
|
|
18
18
|
}
|
|
19
19
|
export interface DiscoveryRoot { directory: string; packageName: string; groups?: DocumentationGroup[] }
|
|
20
|
-
export interface ThemeRecipe { schemaVersion: 1; values: Record<string, string
|
|
20
|
+
export interface ThemeRecipe { schemaVersion: 1; values: Record<string, string>; presets?: Partial<Record<'material' | 'palette', string>> }
|
|
21
21
|
export interface ThemeField { family: string; id: string; label: string; property: string; type: string; defaultValue: string; description: string; min?: number; max?: number }
|
|
22
22
|
export interface DocumentationConfiguration {
|
|
23
23
|
title?: string; base?: string; origin?: string; outDir?: string; content?: string;
|
|
@@ -50,6 +50,9 @@ export function previewProjectDocumentation(input?: DocumentationInput & { port?
|
|
|
50
50
|
export const themeFields: readonly ThemeField[];
|
|
51
51
|
export const startingPoints: Record<string, ThemeRecipe>;
|
|
52
52
|
export function defaultRecipe(): ThemeRecipe;
|
|
53
|
+
export const materialPresets: Record<string, { label: string; values: Record<string, string> }>;
|
|
54
|
+
export function applyRefinement(input: ThemeRecipe, family: 'material' | 'palette', id: string): ThemeRecipe;
|
|
55
|
+
export function refinementState(input: ThemeRecipe, family: 'material' | 'palette'): { id: string; customized: boolean };
|
|
53
56
|
export function validateRecipe(input: unknown): ThemeRecipe;
|
|
54
57
|
export interface ResolvedTheme { tokens: Record<string, string>; adjustments: { id: string; requested: string; resolved: string; reason: string }[] }
|
|
55
58
|
export function deriveTheme(input: ThemeRecipe, options?: { mode?: 'light' | 'dark' }): ResolvedTheme;
|
package/docs/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { readCatalog, discoverComponents, mergeCatalogs } from './catalog.mjs';
|
|
2
2
|
export { prepareDocumentation, documentationOptions, documentationPlugin, buildDocumentation } from './site.mjs';
|
|
3
|
-
export { defaultRecipe, startingPoints, themeFields, validateRecipe, deriveTheme, themeCSS } from './theme.mjs';
|
|
3
|
+
export { defaultRecipe, startingPoints, themeFields, validateRecipe, deriveTheme, themeCSS, materialPresets, applyRefinement, refinementState } from './theme.mjs';
|
|
4
4
|
export { initializeDocumentation, buildProjectDocumentation, devProjectDocumentation, previewProjectDocumentation, readConfiguration } from './commands.mjs';
|
package/docs/render.mjs
CHANGED
|
@@ -8,7 +8,7 @@ const literal = value => escape(value).replaceAll('{', '\\{');
|
|
|
8
8
|
const code = value => `<code>${literal(value ?? '—')}</code>`;
|
|
9
9
|
const table = (head, rows) => `<table><thead><tr>${head.map(h => `<th>${literal(h)}</th>`).join('')}</tr></thead><tbody>${rows.map(row => `<tr>${row.map(value => `<td>${value}</td>`).join('')}</tr>`).join('')}</tbody></table>`;
|
|
10
10
|
export const exampleKey = id => createHash('sha256').update(id).digest('hex').slice(0, 20);
|
|
11
|
-
export const componentPath = component =>
|
|
11
|
+
export const componentPath = component => `/${component.group === 'editor' ? 'editor' : 'components'}/${component.navigationParent ?? component.tag}/` + (component.navigationParent ? `#${component.tag}-api` : '');
|
|
12
12
|
export const previewPath = example => `/_looma/preview/${exampleKey(example.id)}/`;
|
|
13
13
|
|
|
14
14
|
export function apiBody(api, prefix = '') {
|
package/docs/site.mjs
CHANGED
|
@@ -96,9 +96,10 @@ export async function prepareDocumentation({ root, config = {}, catalog }) {
|
|
|
96
96
|
const resourceByTag = new Map(combined.resources.map(resource => [resource.tag, resource]));
|
|
97
97
|
for (const component of combined.components) {
|
|
98
98
|
if (!component.navigationParent) {
|
|
99
|
-
const
|
|
99
|
+
const section = component.group === 'editor' ? ['editor'] : ['components', groupDirectories.get(component.group)];
|
|
100
|
+
const page = path.join(content, ...section, component.tag + '.md');
|
|
100
101
|
await write(page, referencePage(component, combined, relativeHref(page, card), base));
|
|
101
|
-
generatedReferences.add(
|
|
102
|
+
generatedReferences.add(`${component.group === 'editor' ? 'editor' : 'components'}/${component.tag}.md`);
|
|
102
103
|
}
|
|
103
104
|
for (const example of component.examples) {
|
|
104
105
|
const key = exampleKey(example.id);
|
|
@@ -119,10 +120,12 @@ export async function prepareDocumentation({ root, config = {}, catalog }) {
|
|
|
119
120
|
}
|
|
120
121
|
const gallery = combined.groups.map(group => {
|
|
121
122
|
const components = combined.components.filter(c => c.group === group.id && !c.navigationParent);
|
|
122
|
-
|
|
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>` : '';
|
|
123
125
|
}).join('\n\n');
|
|
124
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`);
|
|
125
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`);
|
|
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`);
|
|
126
129
|
await write(path.join(content, 'design-system.md'), await readFile(own('README.md'), 'utf8'));
|
|
127
130
|
if (config.content) {
|
|
128
131
|
const authored = path.resolve(root, config.content);
|
|
@@ -134,20 +137,19 @@ export async function prepareDocumentation({ root, config = {}, catalog }) {
|
|
|
134
137
|
let exists = false;
|
|
135
138
|
try { await readFile(target); exists = true; } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
136
139
|
// A project's landing page and footer are branding/content, never API overrides.
|
|
137
|
-
if (exists && !['index.md', '_footer.md'].includes(path.relative(content, target))) throw new Error(`Authored/generated documentation collision: ${source} would replace ${target}. Add guidance under another path.`);
|
|
140
|
+
if (exists && !['index.md', 'editor.md', '_footer.md'].includes(path.relative(content, target))) throw new Error(`Authored/generated documentation collision: ${source} would replace ${target}. Add guidance under another path.`);
|
|
138
141
|
await mkdir(path.dirname(target), { recursive: true }); await cp(source, target);
|
|
139
142
|
}
|
|
140
143
|
}
|
|
141
144
|
const customizer = path.join(generated, 'customizer.html');
|
|
142
145
|
await write(customizer, customizerResource({ catalog: combined, config, base }, config.recipe));
|
|
143
146
|
const customizerPage = path.join(content, 'customize.md');
|
|
144
|
-
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`);
|
|
145
148
|
await write(path.join(generated, 'catalog.json'), JSON.stringify(combined));
|
|
146
149
|
const redirects = {};
|
|
147
150
|
for (const component of combined.components) {
|
|
148
151
|
const target = componentPath(component);
|
|
149
152
|
redirects[`/components/${component.group}/${component.tag}/`] = target;
|
|
150
|
-
if (component.group === 'editor') redirects[`/editor/${component.tag}/`] = target;
|
|
151
153
|
if (component.navigationParent) redirects[`/parts/${component.tag}/`] = target;
|
|
152
154
|
}
|
|
153
155
|
return { root, config, base, catalog: combined, examples, generated, content, previews, redirects };
|
|
@@ -220,20 +222,29 @@ export async function buildDocumentation({ root = process.cwd(), config = {}, ca
|
|
|
220
222
|
|
|
221
223
|
async function isolatePreviewScripts(site, { outDir, writeFile: writeOutput }) {
|
|
222
224
|
const { build } = await tool('vite', site.root);
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
225
|
+
// Preview entries and outputs are independent. Keep compiler memory bounded
|
|
226
|
+
// and retain the build lock until every in-flight bundle has settled.
|
|
227
|
+
const remaining = [...site.examples];
|
|
228
|
+
const results = await Promise.allSettled(Array.from({ length: Math.min(2, remaining.length) }, async () => {
|
|
229
|
+
try {
|
|
230
|
+
for (let example; (example = remaining.pop());) {
|
|
231
|
+
const htmlFile = path.join(outDir, '_looma/preview', exampleKey(example.id), 'index.html');
|
|
232
|
+
let html = await readFile(htmlFile, 'utf8');
|
|
233
|
+
const scripts = [...html.matchAll(/<script\b[^>]*type="module"[^>]*src="([^"]+)"[^>]*><\/script>/g)];
|
|
234
|
+
if (!scripts.length) throw new Error(`No browser module found for isolated preview ${example.id}.`);
|
|
235
|
+
for (const [tag, url] of scripts) {
|
|
236
|
+
const input = path.join(outDir, url.slice(site.base.length));
|
|
237
|
+
const bundle = await build({ configFile: false, logLevel: 'silent', build: { write: false, minify: true, lib: { entry: input, name: 'LoomaPreview', formats: ['iife'] }, rollupOptions: { output: { codeSplitting: false } } } });
|
|
238
|
+
const code = (Array.isArray(bundle) ? bundle[0] : bundle).output.find(output => output.type === 'chunk').code;
|
|
239
|
+
const name = `${exampleKey(example.id)}.js`;
|
|
240
|
+
await writeOutput('_looma/runtime/' + name, code);
|
|
241
|
+
html = html.replace(tag, `<script src="${site.base}_looma/runtime/${name}" defer></script>`);
|
|
242
|
+
}
|
|
243
|
+
html = html.replace(/<link\b[^>]*rel="modulepreload"[^>]*>/g, '');
|
|
244
|
+
await writeFile(htmlFile, html);
|
|
245
|
+
}
|
|
246
|
+
} catch (error) { remaining.length = 0; throw error; }
|
|
247
|
+
}));
|
|
248
|
+
const failed = results.find(result => result.status === 'rejected');
|
|
249
|
+
if (failed) throw failed.reason;
|
|
239
250
|
}
|
package/docs/theme-session.mjs
CHANGED
|
@@ -8,6 +8,7 @@ export function createThemeSession(view, { base, baseline, systemCSS }) {
|
|
|
8
8
|
const key = `looma:documentation-theme:v1:${base}`;
|
|
9
9
|
let original = validateRecipe(baseline);
|
|
10
10
|
let selected;
|
|
11
|
+
let portableCSS = '';
|
|
11
12
|
let storageAvailable = true;
|
|
12
13
|
const style = view.document.createElement('style');
|
|
13
14
|
style.dataset.loomaDocumentationTheme = '';
|
|
@@ -15,9 +16,10 @@ export function createThemeSession(view, { base, baseline, systemCSS }) {
|
|
|
15
16
|
const read = value => { try { return value ? validateRecipe(JSON.parse(value)) : undefined; } catch { return undefined; } };
|
|
16
17
|
try { selected = read(view.localStorage.getItem(key)); } catch { storageAvailable = false; }
|
|
17
18
|
const paint = () => {
|
|
19
|
+
portableCSS = selected ? themeCSS(selected, systemCSS) : '';
|
|
18
20
|
view.document.documentElement.toggleAttribute('data-looma-documentation-theme', Boolean(selected));
|
|
19
21
|
style.textContent = selected ? themeCSS(selected, systemCSS, { scope: 'documentation' }) : '';
|
|
20
|
-
view.dispatchEvent(new view.CustomEvent(themeEvent, { detail: { css:
|
|
22
|
+
view.dispatchEvent(new view.CustomEvent(themeEvent, { detail: { css: portableCSS } }));
|
|
21
23
|
};
|
|
22
24
|
const persist = () => {
|
|
23
25
|
try { if (selected) view.localStorage.setItem(key, JSON.stringify(selected)); else view.localStorage.removeItem(key); storageAvailable = true; } catch { storageAvailable = false; }
|
|
@@ -27,7 +29,7 @@ export function createThemeSession(view, { base, baseline, systemCSS }) {
|
|
|
27
29
|
get recipe() { return structuredClone(selected ?? original); },
|
|
28
30
|
get hasOverride() { return selected !== undefined; },
|
|
29
31
|
get storageAvailable() { return storageAvailable; },
|
|
30
|
-
get css() { return
|
|
32
|
+
get css() { return portableCSS; },
|
|
31
33
|
apply(recipe) { selected = validateRecipe(recipe); persist(); },
|
|
32
34
|
reset() { selected = undefined; persist(); return structuredClone(original); },
|
|
33
35
|
baseline(recipe) { original = validateRecipe(recipe); },
|