@threadlabs/looma 0.35.0 → 0.36.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/ui-button/ui-button.html +11 -4
- package/components/ui-color-picker/ui-color-picker.html +1 -1
- package/components/ui-combobox/ui-combobox.html +1 -1
- package/components/ui-curve-editor/ui-curve-editor.html +1 -0
- package/components/ui-curve-editor/ui-curve-editor.js +15 -6
- package/components/ui-input/ui-input.html +4 -1
- package/components/ui-light-editor/ui-light-editor.html +143 -19
- package/components/ui-light-editor/ui-light-editor.js +26 -9
- package/components/ui-reel/ui-reel.html +2 -1
- package/components/ui-select/ui-select.html +10 -6
- package/components/ui-tabs/ui-tabs.html +2 -3
- package/components/ui-tabs/ui-tabs.js +0 -9
- package/components/ui-tooltip/ui-tooltip.html +2 -1
- package/dist/index.js +1 -1
- package/docs/README.md +36 -8
- package/docs/card.html +7 -6
- package/docs/card.js +1 -0
- package/docs/catalog-view.mjs +2 -1
- package/docs/catalog.json +46 -12
- package/docs/customizer-render.mjs +38 -18
- package/docs/customizer.js +128 -21
- package/docs/fonts.mjs +45 -0
- package/docs/index.d.ts +2 -2
- package/docs/palette.mjs +67 -0
- package/docs/site.mjs +5 -4
- package/docs/theme-session.mjs +28 -9
- package/docs/theme.mjs +69 -27
- package/docs/value-editors.mjs +6 -4
- package/package.json +2 -2
- package/styles/ui-button.css +11 -4
- package/styles/ui-color-picker.css +1 -1
- package/styles/ui-combobox.css +2 -2
- package/styles/ui-input.css +4 -1
- package/styles/ui-light-editor.css +111 -16
- package/styles/ui-select.css +10 -6
- package/styles/ui-tabs.css +2 -3
- package/styles/ui-tooltip.css +2 -1
- package/theme-system.css +14 -2
- package/tokens.css +11 -1
- package/vanilla/UiCurveEditor.d.ts +1 -0
- package/vanilla/UiCurveEditor.js +1 -1
- package/vanilla/UiLightEditor.d.ts +1 -0
- package/vanilla/UiLightEditor.js +2 -2
- package/vue/UiButton.vue +4 -2
- package/vue/UiColorPicker.vue +1 -1
- package/vue/UiCombobox.vue +2 -2
- package/vue/UiCurveEditor.d.ts +6 -0
- package/vue/UiCurveEditor.vue +2 -0
- package/vue/UiInput.vue +4 -1
- package/vue/UiLightEditor.d.ts +8 -0
- package/vue/UiLightEditor.vue +22 -3
- package/vue/UiReel.vue +1 -1
- package/vue/UiSelect.vue +1 -1
- package/vue/UiTabs.vue +2 -3
- package/vue/UiTooltip.vue +2 -1
- package/vue/chunks/UiButton.js +1 -1
- package/vue/chunks/UiColorPicker.js +1 -1
- package/vue/chunks/UiCombobox.js +1 -1
- package/vue/chunks/UiCurveEditor.js +5 -3
- package/vue/chunks/UiInput.js +1 -1
- package/vue/chunks/UiLightEditor.js +16 -6
- package/vue/chunks/UiReel.js +2 -2
- package/vue/chunks/UiSelect.js +1 -1
- package/vue/chunks/UiTabs.js +1 -1
- package/vue/chunks/UiTooltip.js +1 -1
- package/vue/components.css +47 -19
package/docs/theme.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { googleFonts, googleFontStack, googleFontURL } from './fonts.mjs';
|
|
2
|
+
import { mixColour as mix, paletteSeeds, companionSurface, toLch, fromLch } from './palette.mjs';
|
|
3
|
+
|
|
1
4
|
const bounds = {
|
|
2
|
-
'text-size': [14, 24], 'control-
|
|
3
|
-
'line-weight': [.5, 3],
|
|
5
|
+
'text-size': [14, 24], 'control-roominess': [2, 4], 'corner-roundness': [0, 32],
|
|
6
|
+
'line-weight': [.5, 3], spacing: [.75, 1.5], 'scale-contrast': [1, 1.5],
|
|
4
7
|
'space-curve': [.5, 2], 'text-weight': [400, 700], 'outer-depth': [0, 2], 'inner-depth': [0, 2],
|
|
5
8
|
'response-time': [0, 600],
|
|
6
9
|
};
|
|
@@ -11,13 +14,16 @@ export const themeFields = [
|
|
|
11
14
|
['Colour', 'canvas', 'Canvas', '--ui-surface', 'colour', '#f6f6f8', 'The ground behind surfaces.'],
|
|
12
15
|
['Colour', 'surface', 'Surface', '--ui-surface-raised', 'colour', '#ffffff', 'Fields, cards and floating surfaces.'],
|
|
13
16
|
['Colour', 'text', 'Text', '--ui-text', 'colour', '#1a1a1a', 'Readable content; supporting text follows this and Surface.'],
|
|
17
|
+
['Colour', 'warmth', 'Surface warmth', null, 'fraction', '0.5', '0% cool, 50% original, 100% warm paper and ink; your brand hue stays independent.'],
|
|
18
|
+
['Colour', 'color-intensity', 'Color intensity', null, 'fraction', '0.5', '0% quiet, 50% original, 100% vivid, coordinated across surfaces, actions and status colors.'],
|
|
19
|
+
['Colour', 'palette-relationship', 'Palette relationship', null, 'choice', 'preserve', 'Keep seed hues, or relate quiet surfaces to the brand through neighboring or complementary hues.'],
|
|
14
20
|
['Type', 'body-font', 'Body font', '--ui-font-sans', 'font', 'system-ui, sans-serif', 'The shared interface typeface.'],
|
|
15
21
|
['Type', 'display-font', 'Display font', '--ui-font-display', 'font', 'system-ui, sans-serif', 'The heading typeface.'],
|
|
16
|
-
['Type', 'text-size', 'Text size', '--ui-font-size', 'positive-length', '
|
|
22
|
+
['Type', 'text-size', 'Text size', '--ui-font-size', 'positive-length', '1rem', 'Base type and choice-mark size. Accepts native CSS lengths, such as 1rem.'],
|
|
17
23
|
['Type', 'text-weight', 'Emphasis weight', '--ui-font-medium', 'weight', '500', 'Control-label emphasis; body and headings keep their roles.'],
|
|
18
|
-
['Size & spacing', 'control-
|
|
19
|
-
['Size & spacing', '
|
|
20
|
-
['Size & spacing', 'scale-contrast', 'Size ratio', '--ui-scale-ratio', 'ratio', '1.2', 'The multiplier between small, medium and large controls.'],
|
|
24
|
+
['Size & spacing', 'control-roominess', 'Control roominess', '--ui-control-roominess', 'positive', '2.5', 'Shared control height as a multiple of base text size; buttons, fields and swatches stay aligned.'],
|
|
25
|
+
['Size & spacing', 'spacing', 'Spacing', '--ui-spacing', 'positive', '1', 'More or less breathing room throughout the interface.'],
|
|
26
|
+
['Size & spacing', 'scale-contrast', 'Size ratio', '--ui-scale-ratio', 'ratio', '1.2', 'The multiplier between small, medium and large controls. At 1 the sizes are equal; higher values separate them.'],
|
|
21
27
|
['Size & spacing', 'space-curve', 'Spacing curve', '--ui-space-curve', 'nonnegative', '1', 'Spacing growth: 1 is linear, 2 is quadratic. Independent of control sizes.'],
|
|
22
28
|
['Size & spacing', 'corner-roundness', 'Corner roundness', '--ui-radius-md', 'length', '8px', 'One corner family across controls and surfaces.'],
|
|
23
29
|
['Size & spacing', 'line-weight', 'Line weight', '--ui-border-width', 'length', '1px', 'Shared edge weight; focus stays independently visible.'],
|
|
@@ -35,20 +41,20 @@ export const themeFields = [
|
|
|
35
41
|
['Material', 'light-strength', 'Light strength', '--ui-light-strength', 'fraction', '0.65', 'Shared illumination; each material and component role determines its response.'],
|
|
36
42
|
['Motion', 'response-time', 'Response time', '--ui-motion-fast', 'time', '120ms', 'Shared feedback pace; floating transitions take proportionally longer.'],
|
|
37
43
|
['Motion', 'response-curve', 'Response curve', '--ui-motion-ease', 'easing', 'cubic-bezier(0.2,0,0.2,1)', 'How movement accelerates and settles.'],
|
|
38
|
-
].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 } : {}) }));
|
|
44
|
+
].map(([family, id, label, property, type, defaultValue, description]) => Object.freeze({ family, id, label, property, type, defaultValue, description, ...(type === 'choice' ? { choices: { preserve: 'Preserve seed hues', neighboring: 'Neighboring hues', complementary: 'Complementary hues' } } : {}), ...(bounds[id] ? { min: bounds[id][0], max: bounds[id][1] } : type === 'fraction' ? { min: 0, max: 1 } : {}) }));
|
|
39
45
|
|
|
40
46
|
export const defaultRecipe = () => ({ schemaVersion: 1, values: Object.fromEntries(themeFields.map(field => [field.id, field.defaultValue])) });
|
|
41
47
|
const starting = values => ({ schemaVersion: 1, values: { ...defaultRecipe().values, ...values } });
|
|
42
48
|
export const startingPoints = {
|
|
43
49
|
clear: defaultRecipe(),
|
|
44
|
-
bookish: starting({ action: '#2e1228', canvas: '#f7f7f5', surface: '#fffefd', text: '#0d0c0c', 'body-font': 'Arial, sans-serif', 'display-font': 'Georgia, serif', 'text-weight': '400', 'control-
|
|
45
|
-
wiki: starting({ action: '#4f46e5', canvas: '#ffffff', surface: '#ffffff', text: '#1a1a1a', 'control-
|
|
50
|
+
bookish: starting({ action: '#2e1228', canvas: '#f7f7f5', surface: '#fffefd', text: '#0d0c0c', 'body-font': 'Arial, sans-serif', 'display-font': 'Georgia, serif', 'text-weight': '400', 'control-roominess': '2.625', 'scale-contrast': '1.18', 'outer-depth': '1', 'inner-depth': '1', glassiness: '0.8', 'surface-separation': '0.65', translucency: '0.18', 'edge-light': '1', finish: '0.85', glow: '0.3', 'light-spread': '0.3', 'light-strength': '0.9', 'response-time': '150ms', 'response-curve': 'ease' }),
|
|
51
|
+
wiki: starting({ action: '#4f46e5', canvas: '#ffffff', surface: '#ffffff', text: '#1a1a1a', 'control-roominess': '2.125', spacing: '0.85', 'scale-contrast': '1.15', 'corner-roundness': '6px', 'outer-depth': '0.35', 'inner-depth': '0', 'response-time': '110ms' }),
|
|
46
52
|
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' }),
|
|
47
53
|
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)' }),
|
|
48
|
-
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-
|
|
54
|
+
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-roominess': '2.875', 'scale-contrast': '1.24', spacing: '1.05', 'response-time': '180ms', 'response-curve': 'cubic-bezier(0.16,1,0.3,1)' }),
|
|
49
55
|
};
|
|
50
56
|
export const startingPointLabels = { clear: 'Clear', bookish: 'Bookish', wiki: 'Wiki', editorial: 'Editorial', night: 'Night', garden: 'Garden' };
|
|
51
|
-
const paletteKeys =
|
|
57
|
+
const paletteKeys = themeFields.filter(field => field.family === 'Colour').map(field => field.id);
|
|
52
58
|
// Palette choices move only the shared color seeds; size, material and motion stay independent.
|
|
53
59
|
export const themePalettes = Object.fromEntries([
|
|
54
60
|
['clear', 'Indigo & mist'], ['bookish', 'Plum & paper'], ['wiki', 'Indigo & chalk'],
|
|
@@ -88,6 +94,24 @@ export function paletteFor(input) {
|
|
|
88
94
|
return Object.entries(themePalettes).find(([, palette]) => paletteKeys.every(key => values[key].toLowerCase() === palette.values[key].toLowerCase()))?.[0] ?? 'custom';
|
|
89
95
|
}
|
|
90
96
|
|
|
97
|
+
export const themeVariations = { warmer: 'Warmer', quieter: 'Quieter', expressive: 'More expressive', compact: 'More compact' };
|
|
98
|
+
/** Nearby choices reuse the same bounded semantics; locks protect whole decision families. */
|
|
99
|
+
export function nearbyVariation(input, kind, locks = []) {
|
|
100
|
+
const recipe = validateRecipe(input);
|
|
101
|
+
if (!Object.hasOwn(themeVariations, kind)) throw new Error('Unknown theme variation.');
|
|
102
|
+
const deltas = {
|
|
103
|
+
warmer: { warmth: .15 }, quieter: { 'color-intensity': -.12, glow: -.1, 'outer-depth': -.15 },
|
|
104
|
+
expressive: { finish: .15, 'edge-light': .15, glow: .1 }, compact: { 'control-roominess': -.15, spacing: -.1 },
|
|
105
|
+
}[kind];
|
|
106
|
+
const values = { ...recipe.values };
|
|
107
|
+
for (const [id, delta] of Object.entries(deltas)) {
|
|
108
|
+
const field = themeFields.find(item => item.id === id);
|
|
109
|
+
const family = field.family === 'Colour' ? 'palette' : field.family === 'Material' ? 'material' : 'type';
|
|
110
|
+
if (!locks.includes(family)) values[id] = String(Number(Math.max(field.min, Math.min(field.max, Number(values[id]) + delta)).toFixed(3)));
|
|
111
|
+
}
|
|
112
|
+
return validateRecipe({ ...recipe, values });
|
|
113
|
+
}
|
|
114
|
+
|
|
91
115
|
const scalar = /^[+]?(?:\d+(?:\.\d*)?|\.\d+)$/;
|
|
92
116
|
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))$/;
|
|
93
117
|
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;
|
|
@@ -122,6 +146,7 @@ export function validateRecipe(input) {
|
|
|
122
146
|
const value = values[field.id];
|
|
123
147
|
let valid = typeof value === 'string' && value.length > 0;
|
|
124
148
|
if (valid) switch (field.type) {
|
|
149
|
+
case 'choice': valid = Object.hasOwn(field.choices, value); break;
|
|
125
150
|
case 'colour': valid = /^#[\da-f]{6}$/i.test(value); break;
|
|
126
151
|
case 'font': valid = font.test(value); break;
|
|
127
152
|
case 'length': valid = length.test(value) && Number.isFinite(parseFloat(value)); break;
|
|
@@ -143,7 +168,9 @@ export function validateRecipe(input) {
|
|
|
143
168
|
}
|
|
144
169
|
}
|
|
145
170
|
if (input.presets !== undefined && (!input.presets || typeof input.presets !== 'object' || Array.isArray(input.presets) || Object.entries(input.presets).some(([family, id]) => !Object.hasOwn(refinementPresets(family) ?? {}, id)))) throw new Error('Unknown refinement preset.');
|
|
146
|
-
|
|
171
|
+
if (input.fonts !== undefined && (!input.fonts || typeof input.fonts !== 'object' || Array.isArray(input.fonts) || Object.entries(input.fonts).some(([key, family]) => !['body-font', 'display-font'].includes(key) || !Object.hasOwn(googleFonts, family) || values[key] !== googleFontStack(family)))) throw new Error('Choose a Google font with its matching font stack.');
|
|
172
|
+
if (input.origin !== undefined && !Object.hasOwn(startingPoints, input.origin)) throw new Error('Unknown theme origin.');
|
|
173
|
+
return { schemaVersion: 1, values, ...(input.fonts === undefined ? {} : { fonts: { ...input.fonts } }), ...(input.origin === undefined ? {} : { origin: input.origin }), ...(input.presets === undefined ? {} : { presets: { ...input.presets } }) };
|
|
147
174
|
}
|
|
148
175
|
|
|
149
176
|
/** Match presets by their complete semantic recipe, including locally saved themes. */
|
|
@@ -153,7 +180,6 @@ export function startingPointFor(input) {
|
|
|
153
180
|
}
|
|
154
181
|
|
|
155
182
|
const rgb = hex => [1, 3, 5].map(i => parseInt(hex.slice(i, i + 2), 16));
|
|
156
|
-
const mix = (a, b, weight) => '#' + rgb(a).map((n, i) => Math.round(n * weight + rgb(b)[i] * (1 - weight)).toString(16).padStart(2, '0')).join('');
|
|
157
183
|
const luminance = hex => rgb(hex).map(n => { const s = n / 255; return s <= .04045 ? s / 12.92 : ((s + .055) / 1.055) ** 2.4; }).reduce((sum, n, i) => sum + n * [.2126, .7152, .0722][i], 0);
|
|
158
184
|
const contrast = (a, b) => (Math.max(luminance(a), luminance(b)) + .05) / (Math.min(luminance(a), luminance(b)) + .05);
|
|
159
185
|
// The brightest reflection stop is itself readable. Interpolating it over a readable
|
|
@@ -177,29 +203,38 @@ function readableColour(seed, surfaces, minimum, endpoint) {
|
|
|
177
203
|
}
|
|
178
204
|
|
|
179
205
|
/** Resolve editable colour seeds into a coordinated set of readable semantic roles. */
|
|
206
|
+
const paletteCache = new Map();
|
|
180
207
|
export function deriveTheme(input, { mode } = {}) {
|
|
181
208
|
if (mode !== undefined && !['light', 'dark'].includes(mode)) throw new Error('Unknown theme color mode.');
|
|
182
209
|
const { values } = validateRecipe(input);
|
|
183
|
-
const tokens = Object.fromEntries(themeFields.map(field => [field.property, values[field.id]]));
|
|
210
|
+
const tokens = Object.fromEntries(themeFields.filter(field => field.property).map(field => [field.property, values[field.id]]));
|
|
184
211
|
for (const field of themeFields.filter(field => field.type.includes('length') && Object.hasOwn(bounds, field.id))) {
|
|
185
212
|
if (boundedNumber(field, values[field.id]) === undefined) {
|
|
186
|
-
const [min, max] = bounds[field.id]; tokens[field.property] = `clamp(${min}
|
|
213
|
+
const [min, max] = bounds[field.id]; tokens[field.property] = `clamp(${min / 16}rem, ${values[field.id]}, ${max / 16}rem)`;
|
|
187
214
|
}
|
|
188
215
|
}
|
|
216
|
+
const cacheKey = JSON.stringify([mode, ...paletteKeys.map(key => values[key])]);
|
|
217
|
+
if (paletteCache.has(cacheKey)) {
|
|
218
|
+
const cached = paletteCache.get(cacheKey);
|
|
219
|
+
return { tokens: { ...tokens, ...cached.tokens }, adjustments: cached.adjustments.map(item => ({ ...item })) };
|
|
220
|
+
}
|
|
221
|
+
const geometry = new Set(Object.keys(tokens));
|
|
222
|
+
const seeds = paletteSeeds(values);
|
|
189
223
|
const adjustments = [];
|
|
190
|
-
const nativeInk = foreground(
|
|
224
|
+
const nativeInk = foreground(seeds.canvas, '#ffffff', '#000000');
|
|
191
225
|
const ink = mode === 'light' ? '#000000' : mode === 'dark' ? '#ffffff' : nativeInk;
|
|
192
226
|
const paper = ink === '#000000' ? '#ffffff' : '#000000';
|
|
193
227
|
// Preserve the requested palette in its native mode. The companion keeps a restrained
|
|
194
228
|
// tint of the same surface seeds; contrast resolution below governs all usable roles.
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
tokens['--ui-surface'] = surface(
|
|
198
|
-
tokens['--ui-surface-raised'] = surface(values.surface);
|
|
229
|
+
const surface = seed => readableColour(nativeInk === ink ? seed : companionSurface(seed, mode ?? (ink === '#000000' ? 'light' : 'dark')), [ink], 7, paper);
|
|
230
|
+
tokens['--ui-surface'] = surface(seeds.canvas);
|
|
231
|
+
tokens['--ui-surface-raised'] = surface(seeds.surface);
|
|
199
232
|
tokens['--ui-surface-sunken'] = surface(mix(tokens['--ui-surface-raised'], tokens['--ui-surface'], .92));
|
|
200
233
|
const surfaces = ['--ui-surface', '--ui-surface-raised', '--ui-surface-sunken'].map(name => tokens[name]);
|
|
201
|
-
const tones = { accent:
|
|
202
|
-
for (const [tone,
|
|
234
|
+
const tones = { accent: seeds.action, danger: '#c92d1e', success: '#007a33', warning: '#ba5b0b', info: '#0066cc' };
|
|
235
|
+
for (const [tone, original] of Object.entries(tones)) {
|
|
236
|
+
const [l, c, h] = toLch(original);
|
|
237
|
+
const seed = tone === 'accent' ? original : fromLch([l, c * (.6 + Number(values['color-intensity']) * .8), h]);
|
|
203
238
|
const fill = readableColour(seed, surfaces, 5, ink);
|
|
204
239
|
tokens[`--ui-${tone}`] = fill;
|
|
205
240
|
tokens[`--ui-on-${tone}`] = foreground(fill, '#ffffff', '#000000');
|
|
@@ -214,7 +249,7 @@ export function deriveTheme(input, { mode } = {}) {
|
|
|
214
249
|
for (const tone of Object.keys(tones)) {
|
|
215
250
|
tokens[`--ui-${tone}-subtle-text`] = readableColour(tokens[`--ui-${tone}`], readingSurfaces, 4.5, ink);
|
|
216
251
|
}
|
|
217
|
-
const textSeed = nativeInk === ink ?
|
|
252
|
+
const textSeed = nativeInk === ink ? seeds.text : mix(seeds.text, ink, .08);
|
|
218
253
|
tokens['--ui-text'] = readableColour(textSeed, readingSurfaces, 4.5, ink);
|
|
219
254
|
tokens['--ui-text-secondary'] = readableColour(mix(tokens['--ui-text'], tokens['--ui-surface-raised'], .72), readingSurfaces, 4.5, ink);
|
|
220
255
|
tokens['--ui-text-muted'] = tokens['--ui-text-secondary'];
|
|
@@ -227,19 +262,24 @@ export function deriveTheme(input, { mode } = {}) {
|
|
|
227
262
|
tokens['--ui-selection-text'] = tokens['--ui-accent-subtle-text'];
|
|
228
263
|
tokens['--ui-selection-border'] = mix(tokens['--ui-accent'], tokens['--ui-surface-raised'], .2);
|
|
229
264
|
tokens['--ui-control-surface'] = tokens['--ui-surface-raised'];
|
|
265
|
+
// A hint is quieter than body copy; resolve it against its field, not every tinted content surface.
|
|
266
|
+
tokens['--ui-control-placeholder'] = readableColour(mix(tokens['--ui-text'], tokens['--ui-control-surface'], .5), [tokens['--ui-control-surface']], 4.5, ink);
|
|
230
267
|
tokens['--ui-control-border'] = readableColour(mix(tokens['--ui-text'], tokens['--ui-surface-raised'], .43), readingSurfaces, 3, ink);
|
|
231
268
|
tokens['--ui-control-border-hover'] = tokens['--ui-control-border'];
|
|
232
269
|
tokens['--ui-border-strong'] = tokens['--ui-control-border'];
|
|
233
|
-
tokens['--ui-disabled-text'] = tokens['--ui-text-
|
|
270
|
+
tokens['--ui-disabled-text'] = mix(tokens['--ui-text'], tokens['--ui-surface-raised'], .45);
|
|
234
271
|
tokens['--ui-disabled-surface'] = tokens['--ui-surface-sunken'];
|
|
235
|
-
tokens['--ui-disabled-border'] = tokens['--ui-
|
|
272
|
+
tokens['--ui-disabled-border'] = mix(tokens['--ui-text'], tokens['--ui-surface-raised'], .2);
|
|
236
273
|
tokens['--ui-focus-ring'] = readableColour(tokens['--ui-accent'], readingSurfaces, 3, ink);
|
|
237
274
|
tokens['--ui-focus-width'] = 'max(2px, calc(var(--ui-border-width) * 2))';
|
|
238
275
|
tokens['color-scheme'] = ink === '#000000' ? 'light' : 'dark';
|
|
239
276
|
for (const [id, property] of [['action', '--ui-accent'], ['canvas', '--ui-surface'], ['surface', '--ui-surface-raised'], ['text', '--ui-text']]) {
|
|
240
277
|
if (tokens[property].toLowerCase() !== values[id].toLowerCase()) adjustments.push({ id, requested: values[id], resolved: tokens[property], reason: 'Adjusted to keep the shared palette readable.' });
|
|
241
278
|
}
|
|
242
|
-
|
|
279
|
+
const colors = Object.fromEntries(Object.entries(tokens).filter(([key]) => !geometry.has(key) || /^#[\da-f]{6}$/i.test(tokens[key])));
|
|
280
|
+
paletteCache.set(cacheKey, { tokens: colors, adjustments });
|
|
281
|
+
if (paletteCache.size > 16) paletteCache.delete(paletteCache.keys().next().value);
|
|
282
|
+
return { tokens, adjustments: adjustments.map(item => ({ ...item })) };
|
|
243
283
|
}
|
|
244
284
|
|
|
245
285
|
/** Shared public seeds; combine with theme-system.css for a standalone exported theme. */
|
|
@@ -249,6 +289,8 @@ export function themeCSS(input, systemCSS = '', { scope = 'portable' } = {}) {
|
|
|
249
289
|
const selector = scope === 'documentation'
|
|
250
290
|
? ':root[data-looma-documentation-theme], :root[data-looma-documentation-theme] [data-theme], :root[data-looma-documentation-theme] [data-contrast]'
|
|
251
291
|
: ':root, [data-theme], [data-contrast]';
|
|
292
|
+
const fontURL = googleFontURL(validateRecipe(input).fonts);
|
|
293
|
+
const imports = fontURL ? `@import url("${fontURL}");\n` : '';
|
|
252
294
|
const light = deriveTheme(input, { mode: 'light' }).tokens;
|
|
253
295
|
const dark = deriveTheme(input, { mode: 'dark' }).tokens;
|
|
254
296
|
const palette = [], geometry = [];
|
|
@@ -267,5 +309,5 @@ export function themeCSS(input, systemCSS = '', { scope = 'portable' } = {}) {
|
|
|
267
309
|
// Resolve colors at their point of use, so a nearer mode boundary wins independently
|
|
268
310
|
// of the selector specificity needed to restore an early documentation theme.
|
|
269
311
|
// Keep the first comment stable: the local writer recognizes its generated files by this marker.
|
|
270
|
-
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${chevrons}\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@media (prefers-reduced-transparency: reduce), (forced-colors: active) {\n ${selector} { --ui-finish: 0; --ui-glow: 0; }\n}\n`;
|
|
312
|
+
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${imports}${systemCSS}\n${geometrySelector} {\n${geometry.join('\n')}\n}\n${selector} {\n${palette.join('\n')}\n}\n${chevrons}\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@media (prefers-reduced-transparency: reduce), (forced-colors: active) {\n ${selector} { --ui-finish: 0; --ui-glow: 0; }\n}\n`;
|
|
271
313
|
}
|
package/docs/value-editors.mjs
CHANGED
|
@@ -3,26 +3,28 @@ const lengthUnits = { px: 1, rem: 16, em: 16, cm: 96 / 2.54, mm: 96 / 25.4, Q: 9
|
|
|
3
3
|
const timeUnits = { ms: 1, s: 1000 };
|
|
4
4
|
const rounded = value => String(Number(Number(value).toFixed(6)));
|
|
5
5
|
|
|
6
|
-
export function valueEditor(field, value
|
|
6
|
+
export function valueEditor(field, value) {
|
|
7
|
+
if (field.type === 'choice') return { kind: 'choice', value };
|
|
7
8
|
if (field.type === 'easing') return { kind: /^step-/.test(value) ? 'text' : 'curve', value };
|
|
8
9
|
if (['colour', 'font'].includes(field.type)) return { kind: 'text', value };
|
|
9
10
|
if (field.type.includes('length') || field.type === 'time') {
|
|
10
11
|
const unit = value.match(/[a-z]+$/i)?.[0] ?? 'px';
|
|
11
|
-
|
|
12
|
+
// Relative limits stay root-scaled when the user enlarges their default font.
|
|
13
|
+
const units = field.type === 'time' ? timeUnits : lengthUnits;
|
|
12
14
|
const factor = units[unit];
|
|
13
15
|
return { kind: 'number', value: value === '0' ? '0px' : value, unit,
|
|
14
16
|
min: factor ? field.min / factor : .001, max: factor ? field.max / factor : 1000000,
|
|
15
17
|
step: (field.type === 'time' ? 10 : field.id === 'line-weight' ? .25 : 1) / (factor ?? 1),
|
|
16
18
|
units: field.type === 'time' ? ['ms', 's'] : ['px', 'rem'], convertible: Boolean(factor) };
|
|
17
19
|
}
|
|
18
|
-
const scale = field.type === 'fraction' || field.id === '
|
|
20
|
+
const scale = field.type === 'fraction' || field.id === 'spacing' ? 100 : 1;
|
|
19
21
|
return { kind: field.type === 'weight' ? 'number' : 'slider', value: field.type === 'weight' ? value : Number(value) * scale,
|
|
20
22
|
min: field.min * scale, max: field.max * scale, step: field.type === 'weight' ? 50 : scale === 100 ? 1 : .01,
|
|
21
23
|
unit: scale === 100 ? '%' : '', scale };
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
export function editorValue(field, value) {
|
|
25
|
-
return field.type === 'fraction' || field.id === '
|
|
27
|
+
return field.type === 'fraction' || field.id === 'spacing' ? rounded(Number(value) / 100) : String(value);
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export function convertValueUnit(value, nextUnit, { remPixels = 16 } = {}) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@threadlabs/looma",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.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.6",
|
|
131
131
|
"@nextwebwg/htmlkit": "1.0.0-alpha.69",
|
|
132
132
|
"vite": "^8.3.4"
|
|
133
133
|
},
|
package/styles/ui-button.css
CHANGED
|
@@ -145,7 +145,9 @@
|
|
|
145
145
|
line-height: var(--ui-line-height-tight);
|
|
146
146
|
padding: var(--ui-control-inset-block-md, var(--ui-space-2)) var(--ui-action-inset-md, var(--ui-space-3)) !important;
|
|
147
147
|
background-image: var(--ui-surface-highlight, none);
|
|
148
|
-
box-shadow:
|
|
148
|
+
box-shadow:
|
|
149
|
+
var(--ui-button-shadow, var(--ui-action-shadow, var(--_ui-action-shadow))),
|
|
150
|
+
var(--_tone-glow);
|
|
149
151
|
cursor: pointer;
|
|
150
152
|
transition:
|
|
151
153
|
scale var(--ui-motion-press, var(--ui-motion-fast)) var(--ui-motion-ease),
|
|
@@ -278,7 +280,9 @@
|
|
|
278
280
|
background-color: var(--_tone);
|
|
279
281
|
color: var(--_on-tone);
|
|
280
282
|
background-image: var(--_tone-highlight);
|
|
281
|
-
box-shadow:
|
|
283
|
+
box-shadow:
|
|
284
|
+
var(--ui-button-solid-shadow, var(--ui-action-shadow, var(--_ui-action-shadow))),
|
|
285
|
+
var(--_tone-glow);
|
|
282
286
|
}
|
|
283
287
|
|
|
284
288
|
: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"]) {
|
|
@@ -448,8 +452,11 @@
|
|
|
448
452
|
|
|
449
453
|
/* The size states outrank :host, so sm is named too, or a small button would stay small. */
|
|
450
454
|
@media (pointer: coarse) {
|
|
451
|
-
:scope:where([data-component~="ui-button"])
|
|
452
|
-
min-block-size: var(--ui-control-min-block-size);
|
|
455
|
+
:scope:where([data-component~="ui-button"]) {
|
|
456
|
+
min-block-size: max(var(--ui-control-size-md), var(--ui-control-min-block-size));
|
|
457
|
+
}
|
|
458
|
+
:scope:where([data-component~="ui-button"])[data-ui-button-state~="size=sm"] {
|
|
459
|
+
min-block-size: max(var(--ui-control-size-sm), var(--ui-control-min-block-size));
|
|
453
460
|
}
|
|
454
461
|
}
|
|
455
462
|
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
--_size: max(var(--ui-control-size-md), var(--ui-control-min-block-size));
|
|
110
110
|
}
|
|
111
111
|
:scope:where([data-component~="ui-color-picker"])[data-ui-color-picker-state~="size=sm"] {
|
|
112
|
-
--_size: var(--ui-control-min-block-size);
|
|
112
|
+
--_size: max(var(--ui-control-size-sm), var(--ui-control-min-block-size));
|
|
113
113
|
--_font: var(--ui-font-size-md);
|
|
114
114
|
}
|
|
115
115
|
}
|
package/styles/ui-combobox.css
CHANGED
|
@@ -320,7 +320,7 @@
|
|
|
320
320
|
/* Under touch a small field is still a touch-sized target, as a small button is, and keeps
|
|
321
321
|
body-size text: below 16px, iOS zooms the page into the focused field. */
|
|
322
322
|
:scope:where([data-component~="ui-combobox"])[data-ui-combobox-state~="size=sm"] {
|
|
323
|
-
--_row: var(--ui-control-min-block-size);
|
|
323
|
+
--_row: max(var(--ui-control-size-sm), var(--ui-control-min-block-size));
|
|
324
324
|
}
|
|
325
325
|
:scope:where([data-component~="ui-combobox"])[data-ui-combobox-state~="size=sm"] input {
|
|
326
326
|
font-size: inherit;
|
|
@@ -732,7 +732,7 @@
|
|
|
732
732
|
/* Under touch a small field is still a touch-sized target, as a small button is, and keeps
|
|
733
733
|
body-size text: below 16px, iOS zooms the page into the focused field. */
|
|
734
734
|
:where([data-component~="ui-combobox"])[data-ui-combobox-state~="size=sm"]:where(:scope, :not([data-component], [data-slotted]:not([data-slotted~="ui-combobox"]))) {
|
|
735
|
-
--_row: var(--ui-control-min-block-size);
|
|
735
|
+
--_row: max(var(--ui-control-size-sm), var(--ui-control-min-block-size));
|
|
736
736
|
}
|
|
737
737
|
:where([data-component~="ui-combobox"])[data-ui-combobox-state~="size=sm"] input:where(:scope, :not([data-component], [data-slotted]:not([data-slotted~="ui-combobox"]))) {
|
|
738
738
|
font-size: inherit;
|
package/styles/ui-input.css
CHANGED
|
@@ -117,7 +117,10 @@
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
:scope:where([data-component~="ui-input"])[data-ui-input-state~="size=sm"] {
|
|
120
|
-
min-block-size: var(
|
|
120
|
+
min-block-size: var(
|
|
121
|
+
--ui-input-min-block-size,
|
|
122
|
+
var(--_ui-default-input-min-block-size, max(var(--ui-control-size-sm), var(--ui-control-min-block-size)))
|
|
123
|
+
);
|
|
121
124
|
}
|
|
122
125
|
}
|
|
123
126
|
|
|
@@ -7,22 +7,117 @@
|
|
|
7
7
|
[data-component~="ui-light-editor"] { --html-next-scope-owner: "ui-light-editor" !important; }
|
|
8
8
|
}
|
|
9
9
|
@scope ([data-component~="ui-light-editor"]) to ([data-component], [data-slotted]) {
|
|
10
|
-
:scope:where([data-component~="ui-light-editor"]) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
10
|
+
:scope:where([data-component~="ui-light-editor"]) {
|
|
11
|
+
display: grid !important;
|
|
12
|
+
gap: var(--ui-space-3) !important;
|
|
13
|
+
min-inline-size: 0;
|
|
14
|
+
padding: 0 !important;
|
|
15
|
+
margin: 0 !important;
|
|
16
|
+
}
|
|
17
|
+
.stage {
|
|
18
|
+
box-sizing: border-box !important;
|
|
19
|
+
display: block !important;
|
|
20
|
+
block-size: 13rem;
|
|
21
|
+
padding: calc(var(--ui-control-min-block-size) / 2) !important;
|
|
22
|
+
border: var(--ui-border-width) solid var(--ui-control-border);
|
|
23
|
+
border-radius: var(--ui-field-radius, var(--ui-radius-md));
|
|
24
|
+
background: var(--ui-neutral-surface, var(--ui-surface));
|
|
25
|
+
}
|
|
26
|
+
.plane {
|
|
27
|
+
position: relative;
|
|
28
|
+
display: block !important;
|
|
29
|
+
inline-size: 100%;
|
|
30
|
+
block-size: 100%;
|
|
31
|
+
}
|
|
32
|
+
svg {
|
|
33
|
+
inline-size: 100%;
|
|
34
|
+
block-size: 100%;
|
|
35
|
+
overflow: visible;
|
|
36
|
+
}
|
|
37
|
+
path {
|
|
38
|
+
fill: none;
|
|
39
|
+
stroke: var(--ui-border);
|
|
40
|
+
stroke-width: 1;
|
|
41
|
+
vector-effect: non-scaling-stroke;
|
|
42
|
+
}
|
|
43
|
+
.specimen {
|
|
44
|
+
position: absolute;
|
|
45
|
+
inset: 0;
|
|
46
|
+
display: grid !important;
|
|
47
|
+
place-items: center !important;
|
|
48
|
+
}
|
|
49
|
+
.source {
|
|
50
|
+
position: absolute;
|
|
51
|
+
left: 50%;
|
|
52
|
+
top: 0;
|
|
53
|
+
translate: -50% -50%;
|
|
54
|
+
display: grid !important;
|
|
55
|
+
place-items: center !important;
|
|
56
|
+
inline-size: max(2rem, var(--ui-control-min-block-size));
|
|
57
|
+
block-size: max(2rem, var(--ui-control-min-block-size));
|
|
58
|
+
padding: 0 !important;
|
|
59
|
+
margin: 0 !important;
|
|
60
|
+
border: 0;
|
|
61
|
+
border-radius: 50%;
|
|
62
|
+
background: transparent;
|
|
63
|
+
cursor: grab;
|
|
64
|
+
touch-action: none;
|
|
65
|
+
}
|
|
66
|
+
.source-glyph {
|
|
67
|
+
position: relative;
|
|
68
|
+
z-index: 1;
|
|
69
|
+
display: inline-flex !important;
|
|
70
|
+
color: var(--ui-accent);
|
|
71
|
+
font-size: var(--ui-font-size-lg);
|
|
72
|
+
}
|
|
73
|
+
.source:where(:not([data-component], [data-slotted]))::after {
|
|
74
|
+
content: "";
|
|
75
|
+
position: absolute;
|
|
76
|
+
inline-size: 2rem;
|
|
77
|
+
block-size: 2rem;
|
|
78
|
+
border: var(--ui-border-width) solid var(--ui-accent);
|
|
79
|
+
border-radius: 50%;
|
|
80
|
+
background: var(--ui-surface-raised);
|
|
81
|
+
box-shadow: 0 0 0 var(--ui-space-2) color-mix(in srgb, var(--ui-accent) 12%, transparent), var(--ui-shadow-sm);
|
|
82
|
+
}
|
|
83
|
+
.source:active {
|
|
84
|
+
cursor: grabbing;
|
|
85
|
+
}
|
|
86
|
+
.source:focus-visible {
|
|
87
|
+
outline: var(--ui-focus-width) solid var(--ui-focus-ring);
|
|
88
|
+
outline-offset: var(--ui-space-1);
|
|
89
|
+
}
|
|
90
|
+
.source:disabled {
|
|
91
|
+
cursor: not-allowed;
|
|
92
|
+
}
|
|
93
|
+
.source:disabled:where(:not([data-component], [data-slotted]))::after {
|
|
94
|
+
background: var(--ui-disabled-surface);
|
|
95
|
+
border-color: var(--ui-disabled-border);
|
|
96
|
+
box-shadow: none;
|
|
97
|
+
}
|
|
98
|
+
.coordinates {
|
|
99
|
+
display: grid !important;
|
|
100
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
101
|
+
gap: var(--ui-space-2) !important;
|
|
102
|
+
}
|
|
103
|
+
.hint {
|
|
104
|
+
font-size: var(--ui-font-size-sm);
|
|
105
|
+
color: var(--ui-text-secondary);
|
|
106
|
+
}
|
|
107
|
+
@media (forced-colors: active) {
|
|
108
|
+
.source-glyph {
|
|
109
|
+
color: CanvasText;
|
|
110
|
+
}
|
|
111
|
+
.source:where(:not([data-component], [data-slotted]))::after {
|
|
112
|
+
background: Canvas;
|
|
113
|
+
border-color: CanvasText;
|
|
114
|
+
}
|
|
115
|
+
.source:focus-visible {
|
|
116
|
+
outline-color: Highlight;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
25
119
|
}
|
|
26
120
|
@scope ([data-component~="ui-light-editor"]) to ([data-component]) {
|
|
27
|
-
@media (forced-colors: active) {
|
|
121
|
+
@media (forced-colors: active) {
|
|
122
|
+
}
|
|
28
123
|
}
|
package/styles/ui-select.css
CHANGED
|
@@ -77,10 +77,12 @@
|
|
|
77
77
|
--_ui-select-highlight: var(--ui-surface-highlight, linear-gradient(transparent, transparent));
|
|
78
78
|
appearance: none;
|
|
79
79
|
padding-inline-end: calc(var(--ui-space-3) + 1.5rem) !important;
|
|
80
|
-
background-image:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
background-image:
|
|
81
|
+
var(
|
|
82
|
+
--ui-select-chevron,
|
|
83
|
+
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235c5c58' stroke-width='2.25' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E")
|
|
84
|
+
),
|
|
85
|
+
var(--_ui-select-highlight);
|
|
84
86
|
background-repeat: no-repeat;
|
|
85
87
|
background-position: right var(--ui-space-3) center, center;
|
|
86
88
|
background-size: 1.125rem 1.125rem, 100% 100%;
|
|
@@ -127,7 +129,9 @@
|
|
|
127
129
|
padding: var(--ui-control-inset-block-sm, var(--ui-space-1)) var(--ui-space-2) !important;
|
|
128
130
|
padding-inline-end: calc(var(--ui-space-2) + var(--ui-icon-size-sm) + var(--ui-space-1)) !important;
|
|
129
131
|
background-position: right var(--ui-space-2) center, center;
|
|
130
|
-
background-size:
|
|
132
|
+
background-size:
|
|
133
|
+
var(--ui-icon-size-sm) var(--ui-icon-size-sm),
|
|
134
|
+
100% 100%;
|
|
131
135
|
font-size: var(--ui-font-size-sm);
|
|
132
136
|
}
|
|
133
137
|
|
|
@@ -147,7 +151,7 @@
|
|
|
147
151
|
}
|
|
148
152
|
|
|
149
153
|
:scope:where([data-component~="ui-select"])[data-ui-select-state~="size=sm"] {
|
|
150
|
-
min-block-size: var(--ui-control-min-block-size);
|
|
154
|
+
min-block-size: max(var(--ui-control-size-sm), var(--ui-control-min-block-size));
|
|
151
155
|
font-size: inherit;
|
|
152
156
|
}
|
|
153
157
|
}
|
package/styles/ui-tabs.css
CHANGED
|
@@ -59,10 +59,9 @@
|
|
|
59
59
|
max-inline-size: 100%;
|
|
60
60
|
/* Tabs that do not fit scroll sideways rather than wrapping or squeezing. */
|
|
61
61
|
overflow-x: auto;
|
|
62
|
-
/*
|
|
63
|
-
strip or the page vertically. */
|
|
62
|
+
/* Contain sideways overscroll without trapping vertical page scrolling. */
|
|
64
63
|
overflow-y: hidden;
|
|
65
|
-
overscroll-behavior: contain;
|
|
64
|
+
overscroll-behavior-inline: contain;
|
|
66
65
|
scrollbar-width: thin;
|
|
67
66
|
border-block-end: var(--ui-border-width) solid var(--ui-border-divider);
|
|
68
67
|
}
|
package/styles/ui-tooltip.css
CHANGED
|
@@ -204,7 +204,8 @@
|
|
|
204
204
|
border-inline-end-width: var(--ui-border-width);
|
|
205
205
|
border-block-end-width: var(--ui-border-width);
|
|
206
206
|
border-end-end-radius: var(--ui-border-width);
|
|
207
|
-
|
|
207
|
+
/* Stretch in screen axes after rotation so both arrow shoulders stay level. */
|
|
208
|
+
transform: scale(1.25, 0.85) rotate(45deg);
|
|
208
209
|
}
|
|
209
210
|
|
|
210
211
|
:scope:where([data-component~="ui-tooltip"])[data-ui-actual-placement="bottom"] .surface:where(:not([data-component], [data-slotted]))::after {
|
package/theme-system.css
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
@layer theme {
|
|
14
14
|
:root {
|
|
15
|
-
--ui-
|
|
15
|
+
--ui-spacing: 1;
|
|
16
|
+
--ui-control-roominess: 2.5;
|
|
16
17
|
--ui-scale-ratio: 1.2;
|
|
17
18
|
--ui-space-curve: 1;
|
|
18
19
|
--ui-outer-depth: 0.6;
|
|
@@ -32,8 +33,9 @@
|
|
|
32
33
|
:where(:root, [data-theme], [data-contrast]) {
|
|
33
34
|
/* Controls use a ratio. Spacing uses an independent curve, so increasing the size
|
|
34
35
|
contrast cannot unexpectedly turn linear spacing into quadratic spacing. */
|
|
36
|
+
--ui-control-size: calc(var(--ui-font-size) * var(--ui-control-roominess));
|
|
35
37
|
--_ui-theme-ratio: max(1, var(--ui-scale-ratio));
|
|
36
|
-
--_ui-theme-unit: calc(var(--ui-
|
|
38
|
+
--_ui-theme-unit: calc(var(--ui-font-size) * 0.25 * max(0, var(--ui-spacing)));
|
|
37
39
|
--_ui-theme-curve: max(0, var(--ui-space-curve));
|
|
38
40
|
--ui-control-size-sm: calc(var(--ui-control-size) / var(--_ui-theme-ratio));
|
|
39
41
|
--ui-control-size-md: var(--ui-control-size);
|
|
@@ -116,6 +118,16 @@
|
|
|
116
118
|
--ui-press-scale: 0.98;
|
|
117
119
|
}
|
|
118
120
|
|
|
121
|
+
@media (pointer: coarse) {
|
|
122
|
+
:where(:root, [data-theme], [data-contrast]) {
|
|
123
|
+
--ui-control-size-sm: max(calc(var(--ui-control-size) / var(--_ui-theme-ratio)), var(--ui-control-min-block-size));
|
|
124
|
+
--ui-control-size-md: calc(var(--ui-control-size-sm) * var(--_ui-theme-ratio));
|
|
125
|
+
--ui-control-size-lg: calc(var(--ui-control-size-md) * var(--_ui-theme-ratio));
|
|
126
|
+
/* Small native fields use body-size text on touch to avoid focus zoom. */
|
|
127
|
+
--ui-control-inset-block-sm: max(0px, calc((var(--ui-control-size-sm) - var(--ui-font-size-md) * var(--ui-line-height-tight) - 2 * var(--ui-border-width)) / 2));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
119
131
|
@media (prefers-reduced-motion: reduce) {
|
|
120
132
|
:where(:root, [data-theme], [data-contrast]) {
|
|
121
133
|
--ui-motion-press: 0ms;
|
package/tokens.css
CHANGED
|
@@ -251,7 +251,7 @@
|
|
|
251
251
|
--ui-control-surface: var(--ui-surface);
|
|
252
252
|
--ui-control-surface-disabled: var(--ui-surface-muted);
|
|
253
253
|
--ui-control-border-hover: color-mix(in oklab, var(--ui-text) 50%, var(--ui-surface));
|
|
254
|
-
--ui-control-placeholder: var(--ui-text-
|
|
254
|
+
--ui-control-placeholder: color-mix(in oklab, var(--ui-text) 58%, var(--ui-control-surface));
|
|
255
255
|
--ui-control-focus: var(--ui-focus-ring);
|
|
256
256
|
/* Derived from the ring, not a near-white tint, so the halo shows on any surface. */
|
|
257
257
|
--ui-control-focus-halo: color-mix(in srgb, var(--ui-focus-ring) 24%, transparent);
|
|
@@ -283,6 +283,15 @@
|
|
|
283
283
|
--ui-motion-fast: 0ms;
|
|
284
284
|
}
|
|
285
285
|
}
|
|
286
|
+
/* Raise the family together on touch, so the minimum does not collapse sm and md. */
|
|
287
|
+
@media (pointer: coarse) {
|
|
288
|
+
:root, :where([data-theme], [data-contrast]) {
|
|
289
|
+
--ui-control-size-sm: max(calc(var(--ui-control-size) / 1.2), var(--ui-control-min-block-size));
|
|
290
|
+
--ui-control-size-md: calc(var(--ui-control-size-sm) * 1.2);
|
|
291
|
+
--ui-control-size-lg: calc(var(--ui-control-size-md) * 1.2);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
286
295
|
}
|
|
287
296
|
|
|
288
297
|
/* The scroll fade every Looma scroller uses (see ui-scroll-area). Scroll-driven animations grow each
|
|
@@ -409,4 +418,5 @@
|
|
|
409
418
|
color: var(--ui-disabled-text);
|
|
410
419
|
cursor: not-allowed;
|
|
411
420
|
}
|
|
421
|
+
|
|
412
422
|
}
|