@threadlabs/looma 0.33.1 → 0.34.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/components/shared/overlay.js +8 -2
- package/components/shared/surface-material.js +21 -0
- package/components/ui-button/ui-button.html +63 -47
- package/components/ui-card/ui-card.html +44 -2
- package/components/ui-curve-editor/curve.js +10 -0
- package/components/ui-curve-editor/ui-curve-editor.html +181 -0
- package/components/ui-curve-editor/ui-curve-editor.js +51 -0
- package/components/ui-dialog/ui-dialog.html +62 -1
- package/components/ui-dialog/ui-dialog.js +9 -1
- package/components/ui-icon-button/ui-icon-button.html +75 -23
- package/components/ui-menu/ui-menu.html +60 -0
- package/components/ui-menu/ui-menu.js +1 -1
- package/components/ui-number-input/arithmetic.js +47 -0
- package/components/ui-number-input/ui-number-input.html +62 -0
- package/components/ui-number-input/ui-number-input.js +44 -0
- package/components/ui-popover/ui-popover.html +64 -1
- package/components/ui-popover/ui-popover.js +1 -1
- package/components/ui-slider/ui-slider.html +84 -0
- package/components/ui-slider/ui-slider.js +29 -0
- package/components/ui-tabs/ui-tabs.html +6 -1
- package/components/ui-text/ui-text.html +31 -6
- package/components/ui-tooltip/ui-tooltip.html +60 -0
- package/components/ui-tooltip/ui-tooltip.js +1 -1
- package/dist/index.js +46 -43
- package/docs/README.md +34 -4
- package/docs/card.html +16 -2
- package/docs/card.js +17 -2
- package/docs/catalog.json +1154 -144
- package/docs/customizer-render.mjs +35 -9
- package/docs/customizer.js +83 -20
- package/docs/documentation.css +9 -1
- package/docs/index.d.ts +4 -2
- package/docs/index.mjs +1 -1
- package/docs/preview.js +2 -0
- package/docs/render.mjs +15 -6
- package/docs/site.mjs +50 -9
- package/docs/theme-session.mjs +51 -0
- package/docs/theme.mjs +134 -15
- package/docs/value-editors.mjs +33 -0
- package/package.json +2 -2
- package/styles/ui-button.css +45 -45
- package/styles/ui-card.css +47 -0
- package/styles/ui-curve-editor.css +109 -0
- package/styles/ui-dialog.css +132 -0
- package/styles/ui-icon-button.css +63 -20
- package/styles/ui-menu.css +72 -0
- package/styles/ui-number-input.css +34 -0
- package/styles/ui-popover.css +74 -0
- package/styles/ui-slider.css +53 -0
- package/styles/ui-tabs.css +6 -1
- package/styles/ui-text.css +23 -1
- package/styles/ui-tooltip.css +72 -0
- package/theme-system.css +26 -10
- package/tokens.css +8 -2
- package/vanilla/UiButton.d.ts +1 -0
- package/vanilla/UiButton.js +195 -130
- package/vanilla/UiCard.d.ts +1 -1
- package/vanilla/UiCard.js +1 -1
- package/vanilla/UiCurveEditor.d.ts +28 -0
- package/vanilla/UiCurveEditor.js +134 -0
- package/vanilla/UiIconButton.d.ts +2 -1
- package/vanilla/UiIconButton.js +58 -31
- package/vanilla/UiNumberInput.d.ts +33 -0
- package/vanilla/UiNumberInput.js +79 -0
- package/vanilla/UiSlider.d.ts +33 -0
- package/vanilla/UiSlider.js +125 -0
- package/vanilla/UiText.d.ts +2 -2
- package/vanilla/UiText.js +45 -11
- package/vanilla/index.js +3 -0
- package/vue/UiButton.d.ts +6 -0
- package/vue/UiButton.vue +155 -56
- package/vue/UiCard.d.ts +2 -2
- package/vue/UiCard.vue +55 -5
- package/vue/UiCurveEditor.d.ts +53 -0
- package/vue/UiCurveEditor.js +2 -0
- package/vue/UiCurveEditor.vue +427 -0
- package/vue/UiDialog.vue +177 -1
- package/vue/UiIconButton.d.ts +8 -2
- package/vue/UiIconButton.vue +164 -34
- package/vue/UiMenu.vue +89 -0
- package/vue/UiNumberInput.d.ts +83 -0
- package/vue/UiNumberInput.js +2 -0
- package/vue/UiNumberInput.vue +370 -0
- package/vue/UiPopover.vue +92 -1
- package/vue/UiSlider.d.ts +83 -0
- package/vue/UiSlider.js +2 -0
- package/vue/UiSlider.vue +412 -0
- package/vue/UiTabs.vue +6 -1
- package/vue/UiText.d.ts +7 -5
- package/vue/UiText.vue +104 -22
- package/vue/UiTooltip.vue +89 -0
- package/vue/chunks/UiActionBar.js +1 -1
- package/vue/chunks/UiAffordanceScope.js +2 -2
- package/vue/chunks/UiAvatar.js +2 -2
- package/vue/chunks/UiAvatarGroup.js +2 -2
- package/vue/chunks/UiBadge.js +2 -2
- package/vue/chunks/UiBreadcrumbItem.js +2 -2
- package/vue/chunks/UiBreadcrumbs.js +2 -2
- package/vue/chunks/UiButton.js +94 -11
- package/vue/chunks/UiCallout.js +2 -2
- package/vue/chunks/UiCard.js +6 -5
- package/vue/chunks/UiCheckbox.js +2 -2
- package/vue/chunks/UiCluster.js +2 -2
- package/vue/chunks/UiColorPicker.js +2 -2
- package/vue/chunks/UiCombobox.js +2 -2
- package/vue/chunks/UiContainer.js +2 -2
- package/vue/chunks/UiContextMenu.js +2 -2
- package/vue/chunks/UiCover.js +2 -2
- package/vue/chunks/UiCurveEditor.js +413 -0
- package/vue/chunks/UiDescriptionItem.js +2 -2
- package/vue/chunks/UiDescriptionList.js +2 -2
- package/vue/chunks/UiDialog.js +4 -4
- package/vue/chunks/UiDisclosure.js +2 -2
- package/vue/chunks/UiEditable.js +2 -2
- package/vue/chunks/UiEditorBlockActions.js +2 -2
- package/vue/chunks/UiEditorChipEditor.js +2 -2
- package/vue/chunks/UiEditorCodeLanguage.js +2 -2
- package/vue/chunks/UiEditorFootnoteEditor.js +2 -2
- package/vue/chunks/UiEditorImageToolbar.js +3 -3
- package/vue/chunks/UiEditorInsertTableGrid.js +2 -2
- package/vue/chunks/UiEditorLinkCard.js +3 -3
- package/vue/chunks/UiEditorLinkForm.js +3 -3
- package/vue/chunks/UiEditorMentionMenu.js +2 -2
- package/vue/chunks/UiEditorMentionMenuItem.js +2 -2
- package/vue/chunks/UiEditorSlashMenu.js +2 -2
- package/vue/chunks/UiEditorSlashMenuGroup.js +2 -2
- package/vue/chunks/UiEditorSlashMenuItem.js +2 -2
- package/vue/chunks/UiEditorSurface.js +2 -2
- package/vue/chunks/UiEditorTableContextMenu.js +2 -2
- package/vue/chunks/UiEditorTableOverlay.js +2 -2
- package/vue/chunks/UiEditorTableToolbar.js +2 -2
- package/vue/chunks/UiEditorToolbar.js +2 -2
- package/vue/chunks/UiFloatingActionButton.js +2 -2
- package/vue/chunks/UiFormField.js +2 -2
- package/vue/chunks/UiGrid.js +2 -2
- package/vue/chunks/UiIcon.js +2 -2
- package/vue/chunks/UiIconButton.js +77 -11
- package/vue/chunks/UiImage.js +2 -2
- package/vue/chunks/UiInput.js +2 -2
- package/vue/chunks/UiInputGroup.js +2 -2
- package/vue/chunks/UiList.js +2 -2
- package/vue/chunks/UiListItem.js +2 -2
- package/vue/chunks/UiListbox.js +2 -2
- package/vue/chunks/UiMenu.js +3 -3
- package/vue/chunks/UiMenuGroup.js +2 -2
- package/vue/chunks/UiMenuItem.js +2 -2
- package/vue/chunks/UiMeter.js +2 -2
- package/vue/chunks/UiNavItem.js +2 -2
- package/vue/chunks/UiNumberInput.js +390 -0
- package/vue/chunks/UiPageHeader.js +2 -2
- package/vue/chunks/UiPopover.js +4 -4
- package/vue/chunks/UiRadio.js +2 -2
- package/vue/chunks/UiRadioGroup.js +2 -2
- package/vue/chunks/UiReel.js +2 -2
- package/vue/chunks/UiScrollArea.js +2 -2
- package/vue/chunks/UiSearchResultRow.js +2 -2
- package/vue/chunks/UiSearchShell.js +2 -2
- package/vue/chunks/UiSection.js +2 -2
- package/vue/chunks/UiSelect.js +2 -2
- package/vue/chunks/UiSeparator.js +2 -2
- package/vue/chunks/UiSidebar.js +2 -2
- package/vue/chunks/UiSlider.js +396 -0
- package/vue/chunks/UiSpinner.js +2 -2
- package/vue/chunks/UiStack.js +2 -2
- package/vue/chunks/UiStatusMessage.js +2 -2
- package/vue/chunks/UiSwatch.js +2 -2
- package/vue/chunks/UiSwitch.js +2 -2
- package/vue/chunks/UiSwitcher.js +2 -2
- package/vue/chunks/UiTable.js +2 -2
- package/vue/chunks/UiTabs.js +3 -3
- package/vue/chunks/UiText.js +53 -12
- package/vue/chunks/UiTextarea.js +2 -2
- package/vue/chunks/UiToast.js +2 -2
- package/vue/chunks/UiToastRegion.js +2 -2
- package/vue/chunks/UiTooltip.js +3 -3
- package/vue/chunks/UiTopBar.js +1 -1
- package/vue/chunks/UiTree.js +2 -2
- package/vue/chunks/UiTreeItem.js +2 -2
- package/vue/chunks/_plugin-vue_export-helper.js +68 -2
- package/vue/chunks/props.js +24 -1
- package/vue/components.css +2215 -1203
- package/vue/editor/index.js +623 -211
- package/vue/index.d.ts +3 -0
- package/vue/index.js +5 -2
package/docs/theme.mjs
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
const bounds = {
|
|
2
|
+
'text-size': [14, 24], 'control-size': [32, 64], 'corner-roundness': [0, 32],
|
|
3
|
+
'line-weight': [.5, 3], density: [.75, 1.5], 'scale-contrast': [1, 1.5],
|
|
4
|
+
'space-curve': [.5, 2], 'text-weight': [400, 700], 'outer-depth': [0, 2], 'inner-depth': [0, 2],
|
|
5
|
+
'response-time': [0, 600],
|
|
6
|
+
};
|
|
7
|
+
|
|
1
8
|
/** The shared authoring schema; ids remain stable in saved recipes. */
|
|
2
9
|
export const themeFields = [
|
|
3
10
|
['Colour', 'action', 'Action', '--ui-accent', 'colour', '#5b55d6', 'Primary commands and selected controls.'],
|
|
@@ -16,22 +23,40 @@ export const themeFields = [
|
|
|
16
23
|
['Size & spacing', 'line-weight', 'Line weight', '--ui-border-width', 'length', '1px', 'Shared edge weight; focus stays independently visible.'],
|
|
17
24
|
['Material', 'outer-depth', 'Outer depth', '--ui-outer-depth', 'nonnegative', '0.6', 'How strongly controls and floating surfaces rise above the page.'],
|
|
18
25
|
['Material', 'inner-depth', 'Inner depth', '--ui-inner-depth', 'nonnegative', '0.3', 'How strongly fields and pressed controls sit into their surface.'],
|
|
26
|
+
['Material', 'glassiness', 'Glassiness', '--ui-glassiness', 'fraction', '0', 'Frosted blur, richer background colour and light-catching glass edges. Content stays readable.'],
|
|
27
|
+
['Material', 'surface-separation', 'Surface separation', '--ui-surface-separation', 'fraction', '0', 'One material offset, scaled by popup purpose and actual size: quiet tooltips through substantial dialogs.'],
|
|
19
28
|
['Material', 'translucency', 'Overlay translucency', '--ui-overlay-translucency', 'fraction', '0', 'How much background comes through floating surfaces.'],
|
|
20
29
|
['Material', 'edge-light', 'Edge light', '--ui-edge-light', 'fraction', '0', 'Light caught by surface edges, independent of depth.'],
|
|
21
30
|
['Motion', 'response-time', 'Response time', '--ui-motion-fast', 'time', '120ms', 'Shared feedback pace; floating transitions take proportionally longer.'],
|
|
22
31
|
['Motion', 'response-curve', 'Response curve', '--ui-motion-ease', 'easing', 'cubic-bezier(0.2,0,0.2,1)', 'How movement accelerates and settles.'],
|
|
23
|
-
].map(([family, id, label, property, type, defaultValue, description]) => Object.freeze({ family, id, label, property, type, defaultValue, description }));
|
|
32
|
+
].map(([family, id, label, property, type, defaultValue, description]) => Object.freeze({ family, id, label, property, type, defaultValue, description, ...(bounds[id] ? { min: bounds[id][0], max: bounds[id][1] } : type === 'fraction' ? { min: 0, max: 1 } : {}) }));
|
|
24
33
|
|
|
25
34
|
export const defaultRecipe = () => ({ schemaVersion: 1, values: Object.fromEntries(themeFields.map(field => [field.id, field.defaultValue])) });
|
|
26
35
|
const starting = values => ({ schemaVersion: 1, values: { ...defaultRecipe().values, ...values } });
|
|
27
36
|
export const startingPoints = {
|
|
28
37
|
clear: defaultRecipe(),
|
|
29
|
-
|
|
38
|
+
bookish: starting({ action: '#2e1228', canvas: '#f7f7f5', surface: '#fffefd', text: '#0d0c0c', 'body-font': 'Arial, sans-serif', 'display-font': 'Georgia, serif', 'text-weight': '400', 'control-size': '42px', 'scale-contrast': '1.18', 'outer-depth': '1', 'inner-depth': '1', glassiness: '0.8', 'surface-separation': '0.65', translucency: '0.18', 'edge-light': '1', 'response-time': '150ms', 'response-curve': 'ease' }),
|
|
39
|
+
wiki: starting({ action: '#4f46e5', canvas: '#ffffff', surface: '#ffffff', text: '#1a1a1a', 'control-size': '34px', density: '0.85', 'scale-contrast': '1.15', 'corner-roundness': '6px', 'outer-depth': '0.35', 'inner-depth': '0', 'response-time': '110ms' }),
|
|
30
40
|
editorial: starting({ action: '#182c24', canvas: '#eee9dc', surface: '#fffdf5', text: '#182c24', 'corner-roundness': '0px', 'outer-depth': '0', 'inner-depth': '0', 'body-font': 'Georgia, serif', 'display-font': 'Georgia, serif', 'text-weight': '400', 'line-weight': '2px', 'response-time': '90ms', 'response-curve': 'linear' }),
|
|
31
41
|
night: starting({ action: '#a7b8ff', canvas: '#11141c', surface: '#202634', text: '#edf1fb', 'corner-roundness': '12px', 'outer-depth': '1.4', 'inner-depth': '0.45', translucency: '0.2', 'edge-light': '0.65', 'response-time': '210ms', 'response-curve': 'cubic-bezier(0.2,0.8,0.2,1)' }),
|
|
32
42
|
garden: starting({ action: '#256342', canvas: '#eef5e7', surface: '#fcfff6', text: '#173121', 'corner-roundness': '22px', 'outer-depth': '0.35', 'inner-depth': '0', 'edge-light': '0.2', 'control-size': '46px', 'scale-contrast': '1.24', density: '1.05', 'response-time': '180ms', 'response-curve': 'cubic-bezier(0.16,1,0.3,1)' }),
|
|
33
43
|
};
|
|
34
|
-
|
|
44
|
+
// Preserve the public historical startingPoints.library import without a duplicate UI preset.
|
|
45
|
+
Object.defineProperty(startingPoints, 'library', { value: startingPoints.bookish });
|
|
46
|
+
export const startingPointLabels = { clear: 'Clear', bookish: 'Bookish', wiki: 'Wiki', editorial: 'Editorial', night: 'Night', garden: 'Garden' };
|
|
47
|
+
const paletteKeys = ['action', 'canvas', 'surface', 'text'];
|
|
48
|
+
// Palette choices move only the shared color seeds; size, material and motion stay independent.
|
|
49
|
+
export const themePalettes = Object.fromEntries([
|
|
50
|
+
['clear', 'Indigo & mist'], ['bookish', 'Plum & paper'], ['wiki', 'Indigo & chalk'],
|
|
51
|
+
['editorial', 'Ink & parchment'], ['night', 'Midnight'], ['garden', 'Forest & cream'],
|
|
52
|
+
].map(([id, label]) => [id, { label, values: Object.fromEntries(paletteKeys.map(key => [key, startingPoints[id].values[key]])) }]));
|
|
53
|
+
|
|
54
|
+
/** Identify a coordinated palette without coupling it to the rest of a recipe. */
|
|
55
|
+
export function paletteFor(input) {
|
|
56
|
+
const { values } = validateRecipe(input);
|
|
57
|
+
return Object.entries(themePalettes).find(([, palette]) => paletteKeys.every(key => values[key].toLowerCase() === palette.values[key].toLowerCase()))?.[0] ?? 'custom';
|
|
58
|
+
}
|
|
59
|
+
|
|
35
60
|
const scalar = /^[+]?(?:\d+(?:\.\d*)?|\.\d+)$/;
|
|
36
61
|
const length = /^(?:0|[+]?(?:\d+(?:\.\d*)?|\.\d+)(?:px|rem|em|ex|ch|cap|ic|lh|rlh|vw|vh|vi|vb|vmin|vmax|svw|svh|lvw|lvh|dvw|dvh|cm|mm|Q|in|pt|pc))$/;
|
|
37
62
|
const font = /^(?:"[^"\\;{}<>@\x00-\x1f]+"|'[^'\\;{}<>@\x00-\x1f]+'|[\p{L}_-][\p{L}\p{N}_ -]*)(?:\s*,\s*(?:"[^"\\;{}<>@\x00-\x1f]+"|'[^'\\;{}<>@\x00-\x1f]+'|[\p{L}_-][\p{L}\p{N}_ -]*))*$/u;
|
|
@@ -39,7 +64,21 @@ const time = /^[+]?(?:\d+(?:\.\d*)?|\.\d+)(?:ms|s)$/;
|
|
|
39
64
|
function easing(value) {
|
|
40
65
|
if (/^(ease|linear|ease-in|ease-out|ease-in-out|step-start|step-end)$/.test(value)) return true;
|
|
41
66
|
const match = /^cubic-bezier\(\s*([\d.+-]+)\s*,\s*([\d.+-]+)\s*,\s*([\d.+-]+)\s*,\s*([\d.+-]+)\s*\)$/.exec(value);
|
|
42
|
-
return Boolean(match && match.slice(1).every(n => Number.isFinite(Number(n))) && [1, 3].every(i => Number(match[i]) >= 0 && Number(match[i]) <= 1));
|
|
67
|
+
return Boolean(match && match.slice(1).every(n => Number.isFinite(Number(n))) && [1, 3].every(i => Number(match[i]) >= 0 && Number(match[i]) <= 1) && [2, 4].every(i => Number(match[i]) >= -1 && Number(match[i]) <= 2));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Authoring bounds are shared by every entry path. Relative lengths keep their units;
|
|
71
|
+
// viewport/font-metric lengths are constrained at use time rather than guessed in JS.
|
|
72
|
+
|
|
73
|
+
const pixels = { px: 1, cm: 96 / 2.54, mm: 96 / 25.4, Q: 96 / 101.6, in: 96, pt: 96 / 72, pc: 16 };
|
|
74
|
+
function boundedNumber(field, value) {
|
|
75
|
+
if (field.type === 'time') return parseFloat(value) * (value.endsWith('ms') ? 1 : 1000);
|
|
76
|
+
if (field.type.includes('length')) {
|
|
77
|
+
if (value === '0') return 0;
|
|
78
|
+
const unit = value.match(/[a-z]+$/i)?.[0];
|
|
79
|
+
return Object.hasOwn(pixels, unit) ? parseFloat(value) * pixels[unit] : undefined;
|
|
80
|
+
}
|
|
81
|
+
return Number(value);
|
|
43
82
|
}
|
|
44
83
|
|
|
45
84
|
/** Validate the whole recipe before previewing or writing any output. */
|
|
@@ -67,6 +106,10 @@ export function validateRecipe(input) {
|
|
|
67
106
|
}
|
|
68
107
|
}
|
|
69
108
|
if (!valid) throw new Error(`${field.label}: enter a valid ${field.type.replaceAll('-', ' ')} value.`);
|
|
109
|
+
if (Object.hasOwn(bounds, field.id)) {
|
|
110
|
+
const number = boundedNumber(field, value), [min, max] = bounds[field.id];
|
|
111
|
+
if (number !== undefined && (number < min || number > max)) throw new Error(`${field.label}: choose a value between ${min} and ${max}${field.type.includes('length') ? 'px (or equivalent)' : field.type === 'time' ? 'ms' : ''}.`);
|
|
112
|
+
}
|
|
70
113
|
}
|
|
71
114
|
return { schemaVersion: 1, values };
|
|
72
115
|
}
|
|
@@ -83,18 +126,94 @@ const luminance = hex => rgb(hex).map(n => { const s = n / 255; return s <= .040
|
|
|
83
126
|
const contrast = (a, b) => (Math.max(luminance(a), luminance(b)) + .05) / (Math.min(luminance(a), luminance(b)) + .05);
|
|
84
127
|
const foreground = (fill, light, dark) => [light, dark, '#ffffff', '#000000'].sort((a, b) => contrast(fill, b) - contrast(fill, a))[0];
|
|
85
128
|
|
|
86
|
-
/**
|
|
87
|
-
|
|
129
|
+
/** Find the nearest mixture with a known readable endpoint, including every supplied surface. */
|
|
130
|
+
function readableColour(seed, surfaces, minimum, endpoint) {
|
|
131
|
+
if (surfaces.every(surface => contrast(seed, surface) >= minimum)) return seed;
|
|
132
|
+
for (let step = 1; step <= 256; step++) {
|
|
133
|
+
const next = mix(seed, endpoint, 1 - step / 256);
|
|
134
|
+
if (surfaces.every(surface => contrast(next, surface) >= minimum)) return next;
|
|
135
|
+
}
|
|
136
|
+
return endpoint;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Resolve editable colour seeds into a coordinated set of readable semantic roles. */
|
|
140
|
+
export function deriveTheme(input, { mode } = {}) {
|
|
141
|
+
if (mode !== undefined && !['light', 'dark'].includes(mode)) throw new Error('Unknown theme color mode.');
|
|
88
142
|
const { values } = validateRecipe(input);
|
|
89
143
|
const tokens = Object.fromEntries(themeFields.map(field => [field.property, values[field.id]]));
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
144
|
+
for (const field of themeFields.filter(field => field.type.includes('length') && Object.hasOwn(bounds, field.id))) {
|
|
145
|
+
if (boundedNumber(field, values[field.id]) === undefined) {
|
|
146
|
+
const [min, max] = bounds[field.id]; tokens[field.property] = `clamp(${min}px, ${values[field.id]}, ${max}px)`;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const adjustments = [];
|
|
150
|
+
const nativeInk = foreground(values.canvas, '#ffffff', '#000000');
|
|
151
|
+
const ink = mode === 'light' ? '#000000' : mode === 'dark' ? '#ffffff' : nativeInk;
|
|
152
|
+
const paper = ink === '#000000' ? '#ffffff' : '#000000';
|
|
153
|
+
// Preserve the requested palette in its native mode. The companion keeps a restrained
|
|
154
|
+
// tint of the same surface seeds; contrast resolution below governs all usable roles.
|
|
155
|
+
const companionSurface = mix(values.text, paper, .8);
|
|
156
|
+
const surface = seed => readableColour(nativeInk === ink ? seed : mix(seed, companionSurface, .02), [ink], 7, paper);
|
|
157
|
+
tokens['--ui-surface'] = surface(values.canvas);
|
|
158
|
+
tokens['--ui-surface-raised'] = surface(values.surface);
|
|
159
|
+
tokens['--ui-surface-sunken'] = surface(mix(tokens['--ui-surface-raised'], tokens['--ui-surface'], .92));
|
|
160
|
+
const surfaces = ['--ui-surface', '--ui-surface-raised', '--ui-surface-sunken'].map(name => tokens[name]);
|
|
161
|
+
const tones = { accent: values.action, danger: '#c92d1e', success: '#007a33', warning: '#ba5b0b', info: '#0066cc' };
|
|
162
|
+
for (const [tone, seed] of Object.entries(tones)) {
|
|
163
|
+
const fill = readableColour(seed, surfaces, 5, ink);
|
|
164
|
+
tokens[`--ui-${tone}`] = fill;
|
|
165
|
+
tokens[`--ui-on-${tone}`] = foreground(fill, '#ffffff', '#000000');
|
|
166
|
+
tokens[`--ui-${tone}-on-strong`] = tokens[`--ui-on-${tone}`];
|
|
167
|
+
tokens[`--ui-${tone}-hover`] = mix(fill, ink, .86);
|
|
168
|
+
tokens[`--ui-${tone}-active`] = mix(fill, ink, .74);
|
|
169
|
+
tokens[`--ui-${tone}-soft`] = mix(fill, tokens['--ui-surface-raised'], .08);
|
|
170
|
+
}
|
|
171
|
+
// Readable roles cover the quiet tinted surfaces as well as the neutral family.
|
|
172
|
+
const readingSurfaces = [...surfaces, ...Object.keys(tones).flatMap(tone => surfaces.map(background => mix(tokens[`--ui-${tone}`], background, .12)))];
|
|
173
|
+
for (const tone of Object.keys(tones)) {
|
|
174
|
+
tokens[`--ui-${tone}-subtle-text`] = readableColour(tokens[`--ui-${tone}`], readingSurfaces, 4.5, ink);
|
|
175
|
+
}
|
|
176
|
+
const textSeed = nativeInk === ink ? values.text : mix(values.text, ink, .08);
|
|
177
|
+
tokens['--ui-text'] = readableColour(textSeed, readingSurfaces, 4.5, ink);
|
|
178
|
+
tokens['--ui-text-secondary'] = readableColour(mix(tokens['--ui-text'], tokens['--ui-surface-raised'], .72), readingSurfaces, 4.5, ink);
|
|
179
|
+
tokens['--ui-text-muted'] = tokens['--ui-text-secondary'];
|
|
180
|
+
tokens['--ui-selection-surface'] = mix(tokens['--ui-accent'], tokens['--ui-surface-raised'], .1);
|
|
181
|
+
tokens['--ui-selection-text'] = tokens['--ui-accent-subtle-text'];
|
|
182
|
+
tokens['--ui-selection-border'] = mix(tokens['--ui-accent'], tokens['--ui-surface-raised'], .2);
|
|
183
|
+
tokens['--ui-control-surface'] = tokens['--ui-surface-raised'];
|
|
184
|
+
tokens['--ui-control-border'] = readableColour(mix(tokens['--ui-text'], tokens['--ui-surface-raised'], .43), readingSurfaces, 3, ink);
|
|
185
|
+
tokens['--ui-control-border-hover'] = tokens['--ui-control-border'];
|
|
186
|
+
tokens['--ui-border-strong'] = tokens['--ui-control-border'];
|
|
187
|
+
tokens['--ui-disabled-text'] = tokens['--ui-text-secondary'];
|
|
188
|
+
tokens['--ui-disabled-surface'] = tokens['--ui-surface-sunken'];
|
|
189
|
+
tokens['--ui-disabled-border'] = tokens['--ui-control-border'];
|
|
190
|
+
tokens['--ui-focus-ring'] = readableColour(tokens['--ui-accent'], readingSurfaces, 3, ink);
|
|
98
191
|
tokens['--ui-focus-width'] = 'max(2px, calc(var(--ui-border-width) * 2))';
|
|
99
|
-
|
|
192
|
+
tokens['color-scheme'] = ink === '#000000' ? 'light' : 'dark';
|
|
193
|
+
for (const [id, property] of [['action', '--ui-accent'], ['canvas', '--ui-surface'], ['surface', '--ui-surface-raised'], ['text', '--ui-text']]) {
|
|
194
|
+
if (tokens[property].toLowerCase() !== values[id].toLowerCase()) adjustments.push({ id, requested: values[id], resolved: tokens[property], reason: 'Adjusted to keep the shared palette readable.' });
|
|
195
|
+
}
|
|
196
|
+
return { tokens, adjustments };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Shared public seeds; combine with theme-system.css for a standalone exported theme. */
|
|
200
|
+
export function themeCSS(input, systemCSS = '', { scope = 'portable' } = {}) {
|
|
201
|
+
if (!['portable', 'documentation'].includes(scope)) throw new Error('Unknown theme scope.');
|
|
202
|
+
// The early documentation stylesheet wins over the baseline loaded later without !important.
|
|
203
|
+
const selector = scope === 'documentation'
|
|
204
|
+
? ':root[data-looma-documentation-theme], :root[data-looma-documentation-theme] [data-theme], :root[data-looma-documentation-theme] [data-contrast]'
|
|
205
|
+
: ':root, [data-theme], [data-contrast]';
|
|
206
|
+
const light = deriveTheme(input, { mode: 'light' }).tokens;
|
|
207
|
+
const dark = deriveTheme(input, { mode: 'dark' }).tokens;
|
|
208
|
+
const palette = [], geometry = [];
|
|
209
|
+
for (const [name, value] of Object.entries(light)) {
|
|
210
|
+
if (name === 'color-scheme') continue;
|
|
211
|
+
if (/^#[\da-f]{6}$/i.test(value)) palette.push(` ${name}: light-dark(${value}, ${dark[name]});`);
|
|
212
|
+
else geometry.push(` ${name}: ${value};`);
|
|
213
|
+
}
|
|
214
|
+
const geometrySelector = scope === 'documentation' ? ':root[data-looma-documentation-theme]' : ':root';
|
|
215
|
+
// Resolve colors at their point of use, so a nearer mode boundary wins independently
|
|
216
|
+
// of the selector specificity needed to restore an early documentation theme.
|
|
217
|
+
// Keep the first comment stable: the local writer recognizes its generated files by this marker.
|
|
218
|
+
return `/* Looma shared semantic theme. */\n/* Always includes light and dark.\n Set data-theme="light" or "dark" on any element to fix that subtree's mode.\n Leave the root attribute unset to follow the system; a switch can set/remove it. */\n${systemCSS}\n${geometrySelector} {\n${geometry.join('\n')}\n}\n${selector} {\n${palette.join('\n')}\n}\n:root:not([data-theme]) { color-scheme: light dark; }\n[data-theme="light"] { color-scheme: light; }\n[data-theme="dark"] { color-scheme: dark; }\n@media (prefers-reduced-motion: reduce) {\n ${selector} { --ui-motion-fast: 0ms; --ui-motion-base: 0ms; --ui-motion-press: 0ms; --ui-press-scale: 1; }\n}\n`;
|
|
100
219
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Translate canonical recipe values for the reusable value editors; never duplicate design bounds here. */
|
|
2
|
+
const lengthUnits = { px: 1, rem: 16, em: 16, cm: 96 / 2.54, mm: 96 / 25.4, Q: 96 / 101.6, in: 96, pt: 96 / 72, pc: 16 };
|
|
3
|
+
const timeUnits = { ms: 1, s: 1000 };
|
|
4
|
+
const rounded = value => String(Number(Number(value).toFixed(6)));
|
|
5
|
+
|
|
6
|
+
export function valueEditor(field, value, { remPixels = 16 } = {}) {
|
|
7
|
+
if (field.type === 'easing') return { kind: /^step-/.test(value) ? 'text' : 'curve', value };
|
|
8
|
+
if (['colour', 'font'].includes(field.type)) return { kind: 'text', value };
|
|
9
|
+
if (field.type.includes('length') || field.type === 'time') {
|
|
10
|
+
const unit = value.match(/[a-z]+$/i)?.[0] ?? 'px';
|
|
11
|
+
const units = field.type === 'time' ? timeUnits : { ...lengthUnits, rem: remPixels, em: remPixels };
|
|
12
|
+
const factor = units[unit];
|
|
13
|
+
return { kind: 'number', value: value === '0' ? '0px' : value, unit,
|
|
14
|
+
min: factor ? field.min / factor : .001, max: factor ? field.max / factor : 1000000,
|
|
15
|
+
step: (field.type === 'time' ? 10 : field.id === 'line-weight' ? .25 : 1) / (factor ?? 1),
|
|
16
|
+
units: field.type === 'time' ? ['ms', 's'] : ['px', 'rem'], convertible: Boolean(factor) };
|
|
17
|
+
}
|
|
18
|
+
const scale = field.type === 'fraction' || field.id === 'density' ? 100 : 1;
|
|
19
|
+
return { kind: field.type === 'weight' ? 'number' : 'slider', value: field.type === 'weight' ? value : Number(value) * scale,
|
|
20
|
+
min: field.min * scale, max: field.max * scale, step: field.type === 'weight' ? 50 : scale === 100 ? 1 : .01,
|
|
21
|
+
unit: scale === 100 ? '%' : '', scale };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function editorValue(field, value) {
|
|
25
|
+
return field.type === 'fraction' || field.id === 'density' ? rounded(Number(value) / 100) : String(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function convertValueUnit(value, nextUnit, { remPixels = 16 } = {}) {
|
|
29
|
+
const unit = value.match(/[a-z]+$/i)?.[0] ?? 'px';
|
|
30
|
+
const units = Object.hasOwn(timeUnits, unit) ? timeUnits : { ...lengthUnits, rem: remPixels, em: remPixels };
|
|
31
|
+
if (!Object.hasOwn(units, unit) || !Object.hasOwn(units, nextUnit)) throw new Error('Choose compatible units.');
|
|
32
|
+
return rounded(parseFloat(value) * units[unit] / units[nextUnit]) + nextUnit;
|
|
33
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@threadlabs/looma",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
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.
|
|
130
|
+
"markupress": "^0.8.3",
|
|
131
131
|
"@nextwebwg/htmlkit": "1.0.0-alpha.69",
|
|
132
132
|
"vite": "^8.3.4"
|
|
133
133
|
},
|
package/styles/ui-button.css
CHANGED
|
@@ -167,7 +167,7 @@
|
|
|
167
167
|
|
|
168
168
|
/* A count is an adornment, not another line of label text. Keep the standard control height
|
|
169
169
|
with enough inset for a 24px mark; large content can still grow the button naturally. */
|
|
170
|
-
:scope:where([data-component~="ui-button"]):not([
|
|
170
|
+
:scope:where([data-component~="ui-button"]):not([data-appearance="link"]):has(.badge:not(:empty)) {
|
|
171
171
|
padding-block: var(--ui-space-0-5) !important;
|
|
172
172
|
}
|
|
173
173
|
|
|
@@ -212,7 +212,7 @@
|
|
|
212
212
|
|
|
213
213
|
/* Card actions inherit Button's palette and behavior; only their shared layout changes.
|
|
214
214
|
The icon may contain any decorative composition. Empty icons remove their column. */
|
|
215
|
-
:scope:where([data-component~="ui-button"])[data-
|
|
215
|
+
:scope:where([data-component~="ui-button"])[data-appearance="card"] {
|
|
216
216
|
display: grid !important;
|
|
217
217
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
218
218
|
align-content: center !important;
|
|
@@ -225,10 +225,10 @@
|
|
|
225
225
|
box-shadow: none;
|
|
226
226
|
background-image: none;
|
|
227
227
|
}
|
|
228
|
-
:scope:where([data-component~="ui-button"])[data-
|
|
228
|
+
:scope:where([data-component~="ui-button"])[data-appearance="card"][size="lg"] {
|
|
229
229
|
min-block-size: max(var(--ui-control-min-block-size), var(--ui-control-size-lg));
|
|
230
230
|
}
|
|
231
|
-
:scope:where([data-component~="ui-button"])[data-
|
|
231
|
+
:scope:where([data-component~="ui-button"])[data-appearance="card"]:has(.card-icon:empty) {
|
|
232
232
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
233
233
|
}
|
|
234
234
|
.card-icon {
|
|
@@ -255,11 +255,11 @@
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
/* A neutral card encloses content quietly; ordinary outlines keep their action-tone edge. */
|
|
258
|
-
:scope:where([data-component~="ui-button"])[data-
|
|
258
|
+
:scope:where([data-component~="ui-button"])[data-appearance="card"][data-tone="neutral"] {
|
|
259
259
|
border-color: var(--ui-button-border, var(--ui-border));
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
:scope:where([data-component~="ui-button"])[data-
|
|
262
|
+
:scope:where([data-component~="ui-button"])[data-appearance="card"][data-tone="neutral"]:hover:not(:disabled, [aria-disabled="true"]) {
|
|
263
263
|
border-color: var(--ui-button-hover-border, var(--ui-border-strong));
|
|
264
264
|
}
|
|
265
265
|
|
|
@@ -271,19 +271,19 @@
|
|
|
271
271
|
box-shadow: var(--ui-pressed);
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
:scope:where([data-component~="ui-button"])[data-
|
|
274
|
+
:scope:where([data-component~="ui-button"])[data-appearance="solid"], :scope:where([data-component~="ui-button"])[data-appearance="danger"] {
|
|
275
275
|
border-color: var(--_tone);
|
|
276
276
|
background-color: var(--_tone);
|
|
277
277
|
color: var(--_on-tone);
|
|
278
278
|
box-shadow: var(--ui-button-solid-shadow, var(--ui-action-shadow, var(--_ui-action-shadow)));
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
:is(:scope:where([data-component~="ui-button"])[data-
|
|
281
|
+
:is(:scope:where([data-component~="ui-button"])[data-appearance="solid"], :scope:where([data-component~="ui-button"])[data-appearance="danger"]):hover:not(:disabled, [aria-disabled="true"]) {
|
|
282
282
|
background-color: var(--_tone-hover);
|
|
283
283
|
border-color: var(--_tone-hover);
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
-
:is(:scope:where([data-component~="ui-button"])[data-
|
|
286
|
+
:is(:scope:where([data-component~="ui-button"])[data-appearance="solid"], :scope:where([data-component~="ui-button"])[data-appearance="danger"]):active:not(:disabled, [aria-disabled="true"]) {
|
|
287
287
|
background-color: var(--_tone-active);
|
|
288
288
|
border-color: var(--_tone-active);
|
|
289
289
|
background-image: none;
|
|
@@ -292,7 +292,7 @@
|
|
|
292
292
|
|
|
293
293
|
/* Tone is the colour, variant is the volume. Every pairing works: a danger outline is a
|
|
294
294
|
destructive secondary action, not a special case. */
|
|
295
|
-
:scope:where([data-component~="ui-button"])[data-
|
|
295
|
+
:scope:where([data-component~="ui-button"])[data-tone="neutral"] {
|
|
296
296
|
--_hue: var(--ui-text);
|
|
297
297
|
--_hue-hover: var(--ui-text);
|
|
298
298
|
--_hue-active: var(--ui-text);
|
|
@@ -300,40 +300,40 @@
|
|
|
300
300
|
--_on-hue: var(--ui-surface);
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
-
:scope:where([data-component~="ui-button"])[data-
|
|
303
|
+
:scope:where([data-component~="ui-button"])[data-tone="danger"], :scope:where([data-component~="ui-button"])[data-appearance="danger"] {
|
|
304
304
|
--_hue: var(--ui-danger);
|
|
305
305
|
--_hue-hover: var(--ui-danger-hover);
|
|
306
|
-
--_hue-active: var(--ui-danger-hover);
|
|
307
|
-
--_hue-text: var(--ui-danger-
|
|
306
|
+
--_hue-active: var(--ui-danger-active, var(--ui-danger-hover));
|
|
307
|
+
--_hue-text: var(--ui-danger-subtle-text);
|
|
308
308
|
--_on-hue: var(--ui-on-danger);
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
-
:scope:where([data-component~="ui-button"])[data-
|
|
311
|
+
:scope:where([data-component~="ui-button"])[data-tone="success"] {
|
|
312
312
|
--_hue: var(--ui-success);
|
|
313
|
-
--_hue-hover:
|
|
314
|
-
--_hue-active:
|
|
315
|
-
--_hue-text:
|
|
316
|
-
--_on-hue: var(--ui-
|
|
313
|
+
--_hue-hover: var(--ui-success-hover);
|
|
314
|
+
--_hue-active: var(--ui-success-active);
|
|
315
|
+
--_hue-text: var(--ui-success-subtle-text);
|
|
316
|
+
--_on-hue: var(--ui-success-on-strong);
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
-
:scope:where([data-component~="ui-button"])[data-
|
|
319
|
+
:scope:where([data-component~="ui-button"])[data-tone="warning"] {
|
|
320
320
|
--_hue: var(--ui-warning);
|
|
321
|
-
--_hue-hover:
|
|
322
|
-
--_hue-active:
|
|
323
|
-
--_hue-text:
|
|
324
|
-
--_on-hue: var(--ui-
|
|
321
|
+
--_hue-hover: var(--ui-warning-hover);
|
|
322
|
+
--_hue-active: var(--ui-warning-active);
|
|
323
|
+
--_hue-text: var(--ui-warning-subtle-text);
|
|
324
|
+
--_on-hue: var(--ui-warning-on-strong);
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
-
:scope:where([data-component~="ui-button"])[data-
|
|
327
|
+
:scope:where([data-component~="ui-button"])[data-tone="info"] {
|
|
328
328
|
--_hue: var(--ui-info);
|
|
329
|
-
--_hue-hover:
|
|
330
|
-
--_hue-active:
|
|
331
|
-
--_hue-text:
|
|
332
|
-
--_on-hue: var(--ui-
|
|
329
|
+
--_hue-hover: var(--ui-info-hover);
|
|
330
|
+
--_hue-active: var(--ui-info-active);
|
|
331
|
+
--_hue-text: var(--ui-info-subtle-text);
|
|
332
|
+
--_on-hue: var(--ui-info-on-strong);
|
|
333
333
|
}
|
|
334
334
|
|
|
335
335
|
/* A value picker is a field, with Select's quiet edge and resting lift. */
|
|
336
|
-
:scope:where([data-component~="ui-button"])[data-
|
|
336
|
+
:scope:where([data-component~="ui-button"])[data-appearance="field"] {
|
|
337
337
|
--_ui-field-shadow: var(--ui-shadow-sm);
|
|
338
338
|
border-color: var(--ui-button-border, var(--ui-control-border, var(--ui-border-strong)));
|
|
339
339
|
border-radius: var(--ui-button-radius, var(--ui-field-radius, var(--ui-radius-md)));
|
|
@@ -343,23 +343,23 @@
|
|
|
343
343
|
box-shadow: var(--ui-button-shadow, var(--_ui-field-shadow));
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
-
:scope:where([data-component~="ui-button"])[data-
|
|
346
|
+
:scope:where([data-component~="ui-button"])[data-appearance="field"]:hover:not(:disabled, [aria-disabled="true"]) {
|
|
347
347
|
border-color: var(--ui-button-hover-border, var(--ui-control-border-hover, var(--ui-text-secondary)));
|
|
348
348
|
background-color: var(--ui-button-hover-surface, var(--ui-control-surface, var(--ui-surface-elevated)));
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
-
:scope:where([data-component~="ui-button"])[data-
|
|
351
|
+
:scope:where([data-component~="ui-button"])[data-appearance="field"]:is(:active, [aria-expanded="true"]):not(:disabled, [aria-disabled="true"]) {
|
|
352
352
|
--_ui-field-shadow: var(--ui-pressed);
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
-
:scope:where([data-component~="ui-button"])[data-
|
|
355
|
+
:scope:where([data-component~="ui-button"])[data-appearance="field"]:focus-visible {
|
|
356
356
|
border-color: var(--ui-control-focus, var(--ui-accent-solid));
|
|
357
357
|
box-shadow:
|
|
358
358
|
var(--ui-button-shadow, var(--_ui-field-shadow)),
|
|
359
359
|
var(--ui-control-focus-halo-shadow);
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
:scope:where([data-component~="ui-button"])[data-
|
|
362
|
+
:scope:where([data-component~="ui-button"])[data-appearance="ghost"] {
|
|
363
363
|
border-color: transparent;
|
|
364
364
|
background: transparent;
|
|
365
365
|
box-shadow: none;
|
|
@@ -367,14 +367,14 @@
|
|
|
367
367
|
}
|
|
368
368
|
|
|
369
369
|
/* Tints derive from the text colour, not a surface token, so hover and press show on any background. */
|
|
370
|
-
:scope:where([data-component~="ui-button"])[data-
|
|
370
|
+
:scope:where([data-component~="ui-button"])[data-appearance="ghost"]:hover:not(:disabled, [aria-disabled="true"]) {
|
|
371
371
|
background-color: var(--ui-button-ghost-hover-surface, oklch(from var(--_tone) l c h / 10%));
|
|
372
372
|
border-color: transparent;
|
|
373
373
|
}
|
|
374
374
|
|
|
375
375
|
/* Pressed, or holding its menu open, a ghost button sinks slightly: a light surface with a faint
|
|
376
376
|
edge and a soft inner shadow. Lighter than hover, so an open menu's trigger never reads as heavy. */
|
|
377
|
-
:scope:where([data-component~="ui-button"])[data-
|
|
377
|
+
:scope:where([data-component~="ui-button"])[data-appearance="ghost"]:is(:active, [aria-expanded="true"]):not(:disabled, [aria-disabled="true"]) {
|
|
378
378
|
background-color: var(--ui-button-ghost-active-surface, color-mix(in oklch, var(--ui-surface), var(--_tone) 3%));
|
|
379
379
|
border-color: transparent;
|
|
380
380
|
box-shadow: inset 0 0 0 1px oklch(from var(--_tone) l c h / 10%), inset 0 1px 2px oklch(from var(--_tone) l c h / 8%);
|
|
@@ -388,13 +388,13 @@
|
|
|
388
388
|
var(--ui-focus-halo);
|
|
389
389
|
}
|
|
390
390
|
|
|
391
|
-
:scope:where([data-component~="ui-button"])[data-
|
|
391
|
+
:scope:where([data-component~="ui-button"])[data-appearance="solid"]:focus-visible {
|
|
392
392
|
box-shadow:
|
|
393
393
|
var(--ui-button-solid-shadow, var(--ui-action-shadow, var(--_ui-action-shadow))),
|
|
394
394
|
var(--ui-focus-halo);
|
|
395
395
|
}
|
|
396
396
|
|
|
397
|
-
:is(:scope:where([data-component~="ui-button"])[data-
|
|
397
|
+
:is(:scope:where([data-component~="ui-button"])[data-appearance="ghost"], :scope:where([data-component~="ui-button"])[data-appearance="link"], :scope:where([data-component~="ui-button"])[data-appearance="card"]):focus-visible {
|
|
398
398
|
box-shadow: var(--ui-focus-halo);
|
|
399
399
|
}
|
|
400
400
|
|
|
@@ -416,7 +416,7 @@
|
|
|
416
416
|
}
|
|
417
417
|
|
|
418
418
|
/* Ghost keeps no border, but gains the same neutral surface as other disabled buttons. */
|
|
419
|
-
:scope:where([data-component~="ui-button"])[data-
|
|
419
|
+
:scope:where([data-component~="ui-button"])[data-appearance="ghost"]:is(:disabled, [aria-disabled="true"]:not([aria-busy="true"])) {
|
|
420
420
|
background-color: var(--ui-disabled-surface);
|
|
421
421
|
border-color: transparent;
|
|
422
422
|
}
|
|
@@ -433,12 +433,12 @@
|
|
|
433
433
|
a link-style one keeps its text size, so a standalone one ("See all" under a list) needs the
|
|
434
434
|
area. xs sits inside a line of text the same way, so it keeps its size and gains the area too.
|
|
435
435
|
Only these need it: a hit area on every button would reach over its neighbours. */
|
|
436
|
-
html[data-ui-input-modality="touch"] :scope:where([data-component~="ui-button"])[data-
|
|
436
|
+
html[data-ui-input-modality="touch"] :scope:where([data-component~="ui-button"])[data-appearance="link"],
|
|
437
437
|
html[data-ui-input-modality="touch"] :scope:where([data-component~="ui-button"])[data-ui-button-state~="size=xs"] {
|
|
438
438
|
position: relative;
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
-
html[data-ui-input-modality="touch"] :scope:where([data-component~="ui-button"])[data-
|
|
441
|
+
html[data-ui-input-modality="touch"] :scope:where([data-component~="ui-button"])[data-appearance="link"]::after,
|
|
442
442
|
html[data-ui-input-modality="touch"] :scope:where([data-component~="ui-button"])[data-ui-button-state~="size=xs"]::after {
|
|
443
443
|
position: absolute;
|
|
444
444
|
inset: 50% auto auto 50%;
|
|
@@ -450,7 +450,7 @@
|
|
|
450
450
|
|
|
451
451
|
/* Link: an inline text action. No box, padding, or minimum height (inline targets are exempt under
|
|
452
452
|
WCAG 2.5.8); the inherited font; an underline that strengthens on hover; a visible focus ring. */
|
|
453
|
-
:scope:where([data-component~="ui-button"])[data-
|
|
453
|
+
:scope:where([data-component~="ui-button"])[data-appearance="link"] {
|
|
454
454
|
display: inline !important;
|
|
455
455
|
min-block-size: 0;
|
|
456
456
|
padding: 0 !important;
|
|
@@ -467,24 +467,24 @@
|
|
|
467
467
|
transition: color var(--ui-motion-fast) var(--ui-motion-ease), text-decoration-color var(--ui-motion-fast) var(--ui-motion-ease);
|
|
468
468
|
}
|
|
469
469
|
|
|
470
|
-
:scope:where([data-component~="ui-button"])[data-
|
|
470
|
+
:scope:where([data-component~="ui-button"])[data-appearance="link"]:hover:not(:disabled, [aria-disabled="true"]) {
|
|
471
471
|
color: var(--ui-button-link-hover-text, var(--ui-text-primary));
|
|
472
472
|
text-decoration-color: currentColor;
|
|
473
473
|
background: none;
|
|
474
474
|
transform: none;
|
|
475
475
|
}
|
|
476
476
|
|
|
477
|
-
:scope:where([data-component~="ui-button"])[data-
|
|
477
|
+
:scope:where([data-component~="ui-button"])[data-appearance="link"]:active:not(:disabled, [aria-disabled="true"]) {
|
|
478
478
|
background: none;
|
|
479
479
|
transform: none;
|
|
480
480
|
}
|
|
481
481
|
|
|
482
|
-
:scope:where([data-component~="ui-button"])[data-
|
|
482
|
+
:scope:where([data-component~="ui-button"])[data-appearance="link"]:is(:disabled, [aria-disabled="true"]:not([aria-busy="true"])) {
|
|
483
483
|
background: none;
|
|
484
484
|
border-color: transparent;
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
-
:scope:where([data-component~="ui-button"])[data-
|
|
487
|
+
:scope:where([data-component~="ui-button"])[data-appearance="link"]:focus-visible {
|
|
488
488
|
box-shadow: none;
|
|
489
489
|
/* Fallback keeps the ring if the token is missing: an unresolved var() would void the outline. */
|
|
490
490
|
outline: var(--ui-focus-width) solid var(--ui-focus-ring);
|
package/styles/ui-card.css
CHANGED
|
@@ -49,6 +49,47 @@
|
|
|
49
49
|
box-shadow: var(--ui-shadow-lg);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
:scope:where([data-component~="ui-card"])[data-ui-card-state~="variant=glass"] {
|
|
53
|
+
--_ui-panel-glass: clamp(0, var(--ui-glassiness, 0), 1);
|
|
54
|
+
--_ui-panel-separation: clamp(0, var(--ui-surface-separation, 0), 1);
|
|
55
|
+
--_ui-panel-rim: calc((var(--_ui-panel-separation) + var(--_ui-panel-glass) * 0.25) * 0.65 * 8px);
|
|
56
|
+
border-width: calc(var(--ui-border-width) + var(--_ui-panel-rim));
|
|
57
|
+
border-color: transparent;
|
|
58
|
+
background:
|
|
59
|
+
linear-gradient(var(--_ui-card-surface), var(--_ui-card-surface)) padding-box,
|
|
60
|
+
linear-gradient(var(--ui-material-glass, var(--ui-surface-raised)), var(--ui-material-glass, var(--ui-surface-raised))) border-box,
|
|
61
|
+
var(--_ui-card-surface) padding-box;
|
|
62
|
+
box-shadow:
|
|
63
|
+
var(--ui-shadow-lg),
|
|
64
|
+
inset 0 0 0 var(--ui-border-width) var(--ui-material-edge, var(--ui-border-strong));
|
|
65
|
+
backdrop-filter: blur(calc(var(--_ui-panel-glass) * 0.65 * 18px)) saturate(calc(1 + var(--_ui-panel-glass) * 0.65 * 0.18));
|
|
66
|
+
-webkit-backdrop-filter: blur(calc(var(--_ui-panel-glass) * 0.65 * 18px)) saturate(calc(1 + var(--_ui-panel-glass) * 0.65 * 0.18));
|
|
67
|
+
}
|
|
68
|
+
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
|
69
|
+
:scope:where([data-component~="ui-card"])[data-ui-card-state~="variant=glass"] {
|
|
70
|
+
background: var(--_ui-card-surface);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
74
|
+
:scope:where([data-component~="ui-card"])[data-ui-card-state~="variant=glass"] {
|
|
75
|
+
background: var(--_ui-card-surface);
|
|
76
|
+
border-width: var(--ui-border-width);
|
|
77
|
+
border-color: var(--ui-border-strong);
|
|
78
|
+
backdrop-filter: none;
|
|
79
|
+
-webkit-backdrop-filter: none;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
@media (forced-colors: active) {
|
|
83
|
+
:scope:where([data-component~="ui-card"])[data-ui-card-state~="variant=glass"] {
|
|
84
|
+
background: Canvas;
|
|
85
|
+
color: CanvasText;
|
|
86
|
+
border: var(--ui-border-width) solid CanvasText;
|
|
87
|
+
box-shadow: none;
|
|
88
|
+
backdrop-filter: none;
|
|
89
|
+
-webkit-backdrop-filter: none;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
52
93
|
:scope:where([data-component~="ui-card"])[data-ui-card-state~="tone=accent"] {
|
|
53
94
|
border-color: color-mix(in srgb, var(--ui-accent) 25%, var(--ui-border-default));
|
|
54
95
|
--_ui-card-surface: color-mix(in srgb, var(--ui-accent) 6%, var(--ui-surface-default));
|
|
@@ -116,6 +157,12 @@
|
|
|
116
157
|
@scope ([data-component~="ui-card"]) to ([data-component]) {
|
|
117
158
|
/* A surface that groups content. Its children stack with even space between them, so a card
|
|
118
159
|
needs no layout of its own. */
|
|
160
|
+
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
|
161
|
+
}
|
|
162
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
163
|
+
}
|
|
164
|
+
@media (forced-colors: active) {
|
|
165
|
+
}
|
|
119
166
|
|
|
120
167
|
/* A colored panel shades rows in its own hue, without changing button intent. */
|
|
121
168
|
|