@threadlabs/looma 0.36.0 → 0.37.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/docs/README.md CHANGED
@@ -76,9 +76,9 @@ The [public token reference](https://github.com/threadlabs-studio/looma/blob/mai
76
76
  | Finish and illumination | `finish` → `--ui-finish`; `edge-light` → `--ui-edge-light`; `glow` → `--ui-glow`; `light-x` → `--ui-light-x`; `light-y` → `--ui-light-y`; `light-spread` → `--ui-light-spread`; `light-strength` → `--ui-light-strength` |
77
77
  | Motion | `response-time` → `--ui-motion-fast`; `response-curve` → `--ui-motion-ease` |
78
78
 
79
- Palette generation decisions exist only in the JSON recipe; edit it and regenerate to change resolved colors. Geometry, material and motion properties feed the included CSS recipe. `--ui-control-size` remains a derived public height role: author numeric roominess (default 2.5, range 2–4) instead of an absolute control size. Spacing (default 1, range 0.75–1.5) changes space without changing heights. Base text defaults to `1rem`. Relative length bounds use `rem` endpoints in generated CSS.
79
+ Palette generation decisions exist only in the JSON recipe; edit it and regenerate to change resolved colors. Geometry, material and motion properties feed the included CSS recipe. `--ui-control-size` remains a derived public height role: author numeric roominess (default 2.5, range 2–4) instead of an absolute control size. Spacing (default 1, range 0.75–1.5) changes space without changing heights. Base text defaults to `16px` and sets the document root. Type and geometry derive from `1rem` once; the base editor accepts only px. Other relative length inputs preserve their units; editor ranges convert through the chosen root, and generated guards keep the documented pixel bounds. Visible size samples report actual rem/px heights. Set the base on `:root`; opposite-mode boundaries retain that same rem root.
80
80
 
81
- Before 1.0 there are no compatibility aliases. Migrate recipe `density` to `spacing` and CSS `--ui-density` to `--ui-spacing`. Replace length-valued recipe `control-size` with numeric `control-roominess` by dividing the desired medium height by base text size: `40px / 16px` becomes `2.5`.
81
+ Before 1.0 there are no compatibility aliases. Migrate recipe `density` to `spacing` and CSS `--ui-density` to `--ui-spacing`. Replace length-valued recipe `control-size` with numeric `control-roominess` by dividing the desired medium height by base text size: `40px / 16px` becomes `2.5`. Base `text-size` is now px-only and sets the document root. Replace older relative base values with an explicit px value using their original root; no compatibility conversion guesses it.
82
82
 
83
83
  ## Choose fonts
84
84
 
package/docs/catalog.json CHANGED
@@ -443,7 +443,7 @@
443
443
  ],
444
444
  "schemaVersion": 1,
445
445
  "package": "@threadlabs/looma",
446
- "version": "0.36.0",
446
+ "version": "0.37.0",
447
447
  "groups": [
448
448
  {
449
449
  "id": "layout",
@@ -729,7 +729,7 @@
729
729
  "name": "--ui-border-width"
730
730
  },
731
731
  {
732
- "name": "--ui-font-size"
732
+ "name": "--ui-font-size-md"
733
733
  },
734
734
  {
735
735
  "name": "--ui-font-size-sm"
@@ -975,7 +975,7 @@
975
975
  "name": "--ui-border-width"
976
976
  },
977
977
  {
978
- "name": "--ui-font-size"
978
+ "name": "--ui-font-size-md"
979
979
  },
980
980
  {
981
981
  "name": "--ui-font-size-xs"
@@ -16,7 +16,7 @@ export function customizerResource(site, input = defaultRecipe()) {
16
16
  return example ? `<looma-example-card ${cardFields.filter(key => key !== 'id').map(key => `${key.replace(/[A-Z]/g, letter => '-' + letter.toLowerCase())}="${escape(String(example[key]))}"`).join(' ')} identity="${escape(example.id)}" loading="lazy"></looma-example-card>` : '';
17
17
  };
18
18
  const renderField = field => {
19
- const value = recipe.values[field.id], editor = valueEditor(field, value);
19
+ const value = recipe.values[field.id], editor = valueEditor(field, value, { remPixels: parseFloat(recipe.values['text-size']) });
20
20
  const common = `size="sm" data-token="${field.id}" label="${escape(field.label)}"`;
21
21
  let control;
22
22
  if (field.type === 'font') {
@@ -45,7 +45,7 @@ export function customizerResource(site, input = defaultRecipe()) {
45
45
  '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>`,
46
46
  '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>`,
47
47
  'Motion': `<ui-text variant="caption">Open and close this real documentation example to feel the response pace and curve. Reduced motion follows your system preference.</ui-text>${documentationCard('ui-popover')}`,
48
- 'Size & spacing': `<ui-text variant="caption">Shared size ladder: buttons, fields and swatches align at each step. Touch raises the family together.</ui-text><ui-stack>${['sm','md','lg'].map((size,index) => `<ui-stack gap="xs" data-size-sample="${size}"><ui-text variant="caption">${['Small','Medium','Large'][index]}</ui-text><ui-cluster wrap="nowrap"><ui-button size="${size}">Action</ui-button><ui-input size="${size}" label="${size} size sample" value="Frodo"></ui-input><ui-color-picker size="${size}" label="${size} swatch sample"></ui-color-picker></ui-cluster></ui-stack>`).join('')}</ui-stack><div class="spacing-samples" data-spacing-samples role="img" aria-label="Spacing progression"><span></span><span></span><span></span><span></span></div><ui-disclosure summary="How the scale grows"><ui-stack><p>Control roominess relates every field and button to base text. Size ratio separates small from large. On touch, the whole family grows together to keep targets comfortable. Spacing and its curve shape page rhythm independently.</p>${themeFields.filter(item => ['scale-contrast','space-curve'].includes(item.id)).map(renderField).join('')}</ui-stack></ui-disclosure>`,
48
+ 'Size & spacing': `<ui-text variant="caption">Shared size ladder: buttons, fields and swatches align at each step. Touch raises the family together.</ui-text><ui-stack>${['sm','md','lg'].map((size,index) => `<ui-stack gap="xs" data-size-sample="${size}"><ui-text variant="caption">${['Small','Medium','Large'][index]}<span data-size-readout></span></ui-text><ui-cluster><ui-button size="${size}">Action</ui-button><ui-input size="${size}" style="--ui-input-inline-size: 10ch" label="${size} size sample" value="Frodo"></ui-input><ui-color-picker size="${size}" label="${size} swatch sample"></ui-color-picker></ui-cluster></ui-stack>`).join('')}</ui-stack><div class="spacing-samples" data-spacing-samples role="img" aria-label="Spacing progression"><span></span><span></span><span></span><span></span></div><ui-disclosure summary="How the scale grows"><ui-stack><p>Control roominess relates every field and button to base text. Size ratio separates small from large. On touch, the whole family grows together to keep targets comfortable. Spacing and its curve shape page rhythm independently.</p>${themeFields.filter(item => ['scale-contrast','space-curve'].includes(item.id)).map(renderField).join('')}</ui-stack></ui-disclosure>`,
49
49
  };
50
50
  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('');
51
51
  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>`;
@@ -29,10 +29,12 @@ export function connect(host) {
29
29
  const sayApplied = text => say(theme?.storageAvailable ? text : 'Theme applies to this page. Browser storage is unavailable; download CSS or a recipe to keep it.');
30
30
  const fields = new Map(themeFields.map(field => [field.id, field]));
31
31
  const unitContext = () => ({ remPixels: parseFloat(view.getComputedStyle(document.documentElement).fontSize) });
32
+ const editorFor = (field, value) => valueEditor(field, value, { remPixels: parseFloat(recipe.values['text-size']) });
33
+ let boundsBase;
32
34
  const updateControl = (element, value) => {
33
35
  const field = fields.get(element.dataset.token);
34
36
  if (!field || !element.dataset.editorKind) { componentRegistry.update(element, { value }); return; }
35
- const editor = valueEditor(field, value);
37
+ const editor = editorFor(field, value);
36
38
  if (element.dataset.editorKind === 'curve') {
37
39
  element.closest('[data-curve-for]').hidden = editor.kind !== 'curve';
38
40
  if (editor.kind === 'curve') componentRegistry.update(element, { value });
@@ -40,13 +42,33 @@ export function connect(host) {
40
42
  };
41
43
  const updateUnits = () => {
42
44
  for (const control of root.querySelectorAll('[data-unit-for]')) {
43
- const field = fields.get(control.dataset.unitFor), editor = valueEditor(field, recipe.values[field.id]);
45
+ const field = fields.get(control.dataset.unitFor), editor = editorFor(field, recipe.values[field.id]);
44
46
  if (![...control.options].some(option => option.value === editor.unit)) control.add(new view.Option(editor.unit, editor.unit));
45
47
  componentRegistry.update(control, { value: editor.unit });
46
48
  }
47
49
  };
50
+ const updateSizes = () => {
51
+ const samples = [...root.querySelectorAll('[data-size-sample]')].filter(sample => sample.getClientRects().length);
52
+ if (!samples.length) return;
53
+ const base = parseFloat(view.getComputedStyle(document.documentElement).fontSize);
54
+ const rounded = value => Number(value.toFixed(2));
55
+ for (const sample of samples) {
56
+ const button = sample.querySelector('button');
57
+ if (button) {
58
+ const height = button.getBoundingClientRect().height;
59
+ sample.querySelector('[data-size-readout]').textContent = ` · ${rounded(height / base)}rem (${rounded(height)}px)`;
60
+ }
61
+ }
62
+ };
48
63
  const paint = () => {
49
64
  const css = theme?.css || themeCSS(recipe);
65
+ if (boundsBase !== recipe.values['text-size']) {
66
+ boundsBase = recipe.values['text-size'];
67
+ for (const control of root.querySelectorAll('[data-token]')) {
68
+ const field = fields.get(control.dataset.token);
69
+ if (field?.type.includes('length') && field.id !== 'text-size') updateControl(control, recipe.values[field.id]);
70
+ }
71
+ }
50
72
  for (const family of ['material','palette']) {
51
73
  const state = refinementState(recipe, family), presets = family === 'material' ? materialPresets : themePalettes;
52
74
  for (const select of root.querySelectorAll(`[data-refinement-preset="${family}"]`)) {
@@ -73,6 +95,7 @@ export function connect(host) {
73
95
  : 'Finish catches the light. Spread shapes its reflection; strength affects reflection, edge light and glow together.';
74
96
  const gaps = root.querySelector('[data-spacing-samples]');
75
97
  gaps.setAttribute('aria-label', `Spacing progression with curve ${recipe.values['space-curve']}`);
98
+ updateSizes();
76
99
  };
77
100
  const show = (next, persist = true) => {
78
101
  recipe = validateRecipe(next);
@@ -310,6 +333,7 @@ export function connect(host) {
310
333
  selectedExample = event.detail.value ?? '';
311
334
  filterExamples();
312
335
  };
336
+ root.addEventListener('toggle', updateSizes, true);
313
337
  root.addEventListener('looma-example-remove', remove);
314
338
  root.addEventListener('input', input);
315
339
  root.addEventListener('cancel', cancel);
@@ -334,5 +358,5 @@ export function connect(host) {
334
358
  root.querySelector('[data-action="save"]').hidden = false;
335
359
  }).catch(error => { if (alive && error.name !== 'AbortError') say(error.message, true); });
336
360
  }
337
- return () => { flush(); clearComparison(); root.removeEventListener('cancel', cancel); root.removeEventListener('keydown', keyboard); alive = false; abort.abort(); root.removeEventListener('looma-example-remove', remove); root.removeEventListener('input', input); root.removeEventListener('value-change', selectExample); root.removeEventListener('value-change', selectFont); root.removeEventListener('blur', blur, true); root.removeEventListener('change', change); root.removeEventListener('click', click); view.removeEventListener('beforeunload', flush); view.removeEventListener('message', ready); view.removeEventListener(themeEvent, synchronize); };
361
+ return () => { root.removeEventListener('toggle', updateSizes, true); flush(); clearComparison(); root.removeEventListener('cancel', cancel); root.removeEventListener('keydown', keyboard); alive = false; abort.abort(); root.removeEventListener('looma-example-remove', remove); root.removeEventListener('input', input); root.removeEventListener('value-change', selectExample); root.removeEventListener('value-change', selectFont); root.removeEventListener('blur', blur, true); root.removeEventListener('change', change); root.removeEventListener('click', click); view.removeEventListener('beforeunload', flush); view.removeEventListener('message', ready); view.removeEventListener(themeEvent, synchronize); };
338
362
  }
package/docs/theme.mjs CHANGED
@@ -19,7 +19,7 @@ export const themeFields = [
19
19
  ['Colour', 'palette-relationship', 'Palette relationship', null, 'choice', 'preserve', 'Keep seed hues, or relate quiet surfaces to the brand through neighboring or complementary hues.'],
20
20
  ['Type', 'body-font', 'Body font', '--ui-font-sans', 'font', 'system-ui, sans-serif', 'The shared interface typeface.'],
21
21
  ['Type', 'display-font', 'Display font', '--ui-font-display', 'font', 'system-ui, sans-serif', 'The heading typeface.'],
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.'],
22
+ ['Type', 'text-size', 'Base text size', '--ui-font-size', 'positive-length', '16px', 'Sets the document root in px. Type, spacing and layout use rem relationships and scale from this base once.'],
23
23
  ['Type', 'text-weight', 'Emphasis weight', '--ui-font-medium', 'weight', '500', 'Control-label emphasis; body and headings keep their roles.'],
24
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
25
  ['Size & spacing', 'spacing', 'Spacing', '--ui-spacing', 'positive', '1', 'More or less breathing room throughout the interface.'],
@@ -161,6 +161,7 @@ export function validateRecipe(input) {
161
161
  if (field.type === 'weight') valid &&= number >= 1 && number <= 1000;
162
162
  }
163
163
  }
164
+ if (field.id === 'text-size' && valid && !value.endsWith('px')) throw new Error('Base text size: enter a px value for the document root.');
164
165
  if (!valid) throw new Error(`${field.label}: enter a valid ${field.type.replaceAll('-', ' ')} value.`);
165
166
  if (Object.hasOwn(bounds, field.id)) {
166
167
  const number = boundedNumber(field, value), [min, max] = bounds[field.id];
@@ -210,7 +211,7 @@ export function deriveTheme(input, { mode } = {}) {
210
211
  const tokens = Object.fromEntries(themeFields.filter(field => field.property).map(field => [field.property, values[field.id]]));
211
212
  for (const field of themeFields.filter(field => field.type.includes('length') && Object.hasOwn(bounds, field.id))) {
212
213
  if (boundedNumber(field, values[field.id]) === undefined) {
213
- const [min, max] = bounds[field.id]; tokens[field.property] = `clamp(${min / 16}rem, ${values[field.id]}, ${max / 16}rem)`;
214
+ const [min, max] = bounds[field.id]; tokens[field.property] = `clamp(${min}px, ${values[field.id]}, ${max}px)`;
214
215
  }
215
216
  }
216
217
  const cacheKey = JSON.stringify([mode, ...paletteKeys.map(key => values[key])]);
@@ -3,14 +3,15 @@ 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, { remPixels = 16 } = {}) {
7
+ if (field.id === 'text-size') return { kind: 'number', value, unit: 'px', min: field.min, max: field.max, step: 1 };
7
8
  if (field.type === 'choice') return { kind: 'choice', value };
8
9
  if (field.type === 'easing') return { kind: /^step-/.test(value) ? 'text' : 'curve', value };
9
10
  if (['colour', 'font'].includes(field.type)) return { kind: 'text', value };
10
11
  if (field.type.includes('length') || field.type === 'time') {
11
12
  const unit = value.match(/[a-z]+$/i)?.[0] ?? 'px';
12
- // Relative limits stay root-scaled when the user enlarges their default font.
13
- const units = field.type === 'time' ? timeUnits : lengthUnits;
13
+ // Convert authoring limits against the same root as the displayed quantity.
14
+ const units = field.type === 'time' ? timeUnits : { ...lengthUnits, rem: remPixels };
14
15
  const factor = units[unit];
15
16
  return { kind: 'number', value: value === '0' ? '0px' : value, unit,
16
17
  min: factor ? field.min / factor : .001, max: factor ? field.max / factor : 1000000,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadlabs/looma",
3
- "version": "0.36.0",
3
+ "version": "0.37.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.6",
130
+ "markupress": "^0.8.7",
131
131
  "@nextwebwg/htmlkit": "1.0.0-alpha.69",
132
132
  "vite": "^8.3.4"
133
133
  },
@@ -70,7 +70,7 @@
70
70
  inline-size: 1.25rem;
71
71
  block-size: 1.25rem;
72
72
  /* Matches the xs Avatar's initials. */
73
- font-size: calc(var(--ui-font-size) * 0.5625);
73
+ font-size: calc(var(--ui-font-size-md) * 0.5625);
74
74
  line-height: 1;
75
75
  }
76
76
 
@@ -60,7 +60,7 @@
60
60
  inline-size: 1.25rem;
61
61
  block-size: 1.25rem;
62
62
  /* Below the type scale on purpose: two initials need room inside a 1.25rem circle. */
63
- font-size: calc(var(--ui-font-size) * 0.5625);
63
+ font-size: calc(var(--ui-font-size-md) * 0.5625);
64
64
  letter-spacing: 0.02em;
65
65
  }
66
66
 
package/theme-system.css CHANGED
@@ -5,7 +5,8 @@
5
5
  /* Reserve the same order when the remembered theme arrives before baseline CSS. */
6
6
  @layer tokens, base, theme, components;
7
7
 
8
- @property --ui-font-size { syntax: "<length>"; inherits: true; initial-value: 16px; }
8
+ /* Keep the root font seed unregistered: a registered rem length used by root
9
+ font-size creates a dependency cycle (CSS Properties and Values API 2.7.2). */
9
10
  @property --ui-control-size { syntax: "<length>"; inherits: true; initial-value: 40px; }
10
11
  @property --ui-radius-md { syntax: "<length>"; inherits: true; initial-value: 8px; }
11
12
  @property --ui-border-width { syntax: "<length>"; inherits: true; initial-value: 1px; }
@@ -33,9 +34,9 @@
33
34
  :where(:root, [data-theme], [data-contrast]) {
34
35
  /* Controls use a ratio. Spacing uses an independent curve, so increasing the size
35
36
  contrast cannot unexpectedly turn linear spacing into quadratic spacing. */
36
- --ui-control-size: calc(var(--ui-font-size) * var(--ui-control-roominess));
37
+ --ui-control-size: calc(1rem * var(--ui-control-roominess));
37
38
  --_ui-theme-ratio: max(1, var(--ui-scale-ratio));
38
- --_ui-theme-unit: calc(var(--ui-font-size) * 0.25 * max(0, var(--ui-spacing)));
39
+ --_ui-theme-unit: calc(1rem * 0.25 * max(0, var(--ui-spacing)));
39
40
  --_ui-theme-curve: max(0, var(--ui-space-curve));
40
41
  --ui-control-size-sm: calc(var(--ui-control-size) / var(--_ui-theme-ratio));
41
42
  --ui-control-size-md: var(--ui-control-size);
@@ -53,7 +54,7 @@
53
54
  --ui-control-inset-block-sm: max(0px, calc((var(--ui-control-size-sm) - var(--ui-font-size-sm) * var(--ui-line-height-tight) - 2 * var(--ui-border-width)) / 2));
54
55
  --ui-control-inset-block-md: max(0px, calc((var(--ui-control-size-md) - var(--ui-font-size-md) * var(--ui-line-height-tight) - 2 * var(--ui-border-width)) / 2));
55
56
  --ui-control-inset-block-lg: max(0px, calc((var(--ui-control-size-lg) - var(--ui-font-size-md) * var(--ui-line-height-tight) - 2 * var(--ui-border-width)) / 2));
56
- --ui-font-size-xs: max(12px, calc(var(--ui-font-size) * 0.75));
57
+ --ui-font-size-xs: max(12px, calc(1rem * 0.75));
57
58
  --ui-action-inset-sm: var(--ui-space-2);
58
59
  --ui-action-inset-md: var(--ui-space-3);
59
60
  --ui-action-inset-lg: var(--ui-space-4);
package/tokens.css CHANGED
@@ -25,6 +25,9 @@
25
25
  --ui-text: #1a1a1a;
26
26
  --ui-focus-halo: 0 0 0 calc(var(--ui-focus-width) * 1.5) color-mix(in srgb, var(--ui-focus-ring) 40%, transparent);
27
27
 
28
+ /* The base type token owns the document root; rem-based roles scale from it once. */
29
+ font-size: var(--ui-font-size);
30
+
28
31
  /* Type. */
29
32
  --ui-font-sans:
30
33
  system-ui,
@@ -38,7 +41,7 @@
38
41
  "Apple Color Emoji",
39
42
  "Segoe UI Emoji";
40
43
  --ui-font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
41
- --ui-font-size: 1rem;
44
+ --ui-font-size: 16px;
42
45
  --ui-font-medium: 500;
43
46
  --ui-line-height: 1.4;
44
47
 
@@ -92,14 +95,14 @@
92
95
  --ui-accent-line-width: var(--ui-border-width);
93
96
 
94
97
  /* Type scale, from the base size. */
95
- --ui-font-size-2xs: calc(var(--ui-font-size) * 0.6875);
96
- --ui-font-size-xs: calc(var(--ui-font-size) * 0.75);
98
+ --ui-font-size-2xs: calc(1rem * 0.6875);
99
+ --ui-font-size-xs: calc(1rem * 0.75);
97
100
  /* Dense text: controls, labels, and metadata. Two steps below body copy, never one pixel. */
98
- --ui-font-size-sm: calc(var(--ui-font-size) * 0.875);
99
- --ui-font-size-md: var(--ui-font-size);
100
- --ui-font-size-lg: calc(var(--ui-font-size) * 1.125);
101
- --ui-font-size-xl: calc(var(--ui-font-size) * 1.25);
102
- --ui-font-size-2xl: calc(var(--ui-font-size) * 1.5);
101
+ --ui-font-size-sm: calc(1rem * 0.875);
102
+ --ui-font-size-md: 1rem;
103
+ --ui-font-size-lg: calc(1rem * 1.125);
104
+ --ui-font-size-xl: calc(1rem * 1.25);
105
+ --ui-font-size-2xl: calc(1rem * 1.5);
103
106
  --ui-line-height-tight: 1.2;
104
107
  --ui-line-height-md: var(--ui-line-height);
105
108
  /* Prose, not controls: the editor's document body reads at a looser line height. */
@@ -260,14 +263,14 @@
260
263
  --ui-control-size-lg: calc(var(--ui-control-size) * 1.2);
261
264
 
262
265
  /* Choice marks follow typography; row heights follow the shared control scale. */
263
- --ui-choice-mark-size: calc(var(--ui-font-size) * 1.125);
264
- --ui-choice-track-height: calc(var(--ui-font-size) * 1.5);
265
- --ui-choice-track-width: calc(var(--ui-font-size) * 2.5);
266
+ --ui-choice-mark-size: calc(1rem * 1.125);
267
+ --ui-choice-track-height: calc(1rem * 1.5);
268
+ --ui-choice-track-width: calc(1rem * 2.5);
266
269
 
267
270
  /* Icon sizes, used by icon buttons and field affordances. */
268
- --ui-icon-size-sm: var(--ui-font-size);
269
- --ui-icon-size-md: calc(var(--ui-font-size) * 1.25);
270
- --ui-icon-size-lg: calc(var(--ui-font-size) * 1.5);
271
+ --ui-icon-size-sm: 1rem;
272
+ --ui-icon-size-md: calc(1rem * 1.25);
273
+ --ui-icon-size-lg: calc(1rem * 1.5);
271
274
 
272
275
  /* Anticipatory interaction: guide → near → direct. */
273
276
  --ui-affordance-guide-size: var(--ui-space-1);
@@ -392,7 +395,7 @@
392
395
  background-color: var(--ui-control-surface);
393
396
  background-repeat: no-repeat;
394
397
  background-position: center;
395
- background-size: calc(var(--ui-font-size) * 0.7);
398
+ background-size: calc(1rem * 0.7);
396
399
  }
397
400
 
398
401
  [data-ui-choice-option][data-multiple][aria-selected="true"]::before {
package/vue/UiAvatar.vue CHANGED
@@ -366,7 +366,7 @@ useComponentHost(() => import('../components/ui-avatar/ui-avatar.js'), {
366
366
  inline-size: 1.25rem;
367
367
  block-size: 1.25rem;
368
368
  /* Below the type scale on purpose: two initials need room inside a 1.25rem circle. */
369
- font-size: calc(var(--ui-font-size) * 0.5625);
369
+ font-size: calc(var(--ui-font-size-md) * 0.5625);
370
370
  letter-spacing: 0.02em;
371
371
  }
372
372
 
@@ -301,7 +301,7 @@ useComponentHost(() => import('../components/ui-avatar-group/ui-avatar-group.js'
301
301
  inline-size: 1.25rem;
302
302
  block-size: 1.25rem;
303
303
  /* Matches the xs Avatar's initials. */
304
- font-size: calc(var(--ui-font-size) * 0.5625);
304
+ font-size: calc(var(--ui-font-size-md) * 0.5625);
305
305
  line-height: 1;
306
306
  }
307
307
 
@@ -336,6 +336,6 @@ var UiAvatar_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PUR
336
336
  }]]);
337
337
  };
338
338
  }
339
- }), [["__scopeId", "data-v-befec7d5"]]);
339
+ }), [["__scopeId", "data-v-d03c9917"]]);
340
340
  //#endregion
341
341
  export { UiAvatar_default as t };
@@ -229,6 +229,6 @@ var UiAvatarGroup_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @
229
229
  }]]);
230
230
  };
231
231
  }
232
- }), [["__scopeId", "data-v-2446552a"]]);
232
+ }), [["__scopeId", "data-v-a5b0148e"]]);
233
233
  //#endregion
234
234
  export { UiAvatarGroup_default as t };
@@ -180,7 +180,7 @@ svg {
180
180
  inline-size: 1.25rem;
181
181
  block-size: 1.25rem;
182
182
  /* Below the type scale on purpose: two initials need room inside a 1.25rem circle. */
183
- font-size: calc(var(--ui-font-size) * 0.5625);
183
+ font-size: calc(var(--ui-font-size-md) * 0.5625);
184
184
  letter-spacing: 0.02em;
185
185
  }
186
186
  :scope[data-ui-avatar-state~="hasImage"]:where([data-hn-ui-avatar]) {
@@ -297,7 +297,7 @@ svg {
297
297
  inline-size: 1.25rem;
298
298
  block-size: 1.25rem;
299
299
  /* Matches the xs Avatar's initials. */
300
- font-size: calc(var(--ui-font-size) * 0.5625);
300
+ font-size: calc(var(--ui-font-size-md) * 0.5625);
301
301
  line-height: 1;
302
302
  }
303
303
  .overflow[hidden]:where([data-hn-ui-avatar-group]) {