@threadlabs/looma 0.36.0 → 0.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/components/shared/icons.js +3 -0
  2. package/components/ui-avatar/ui-avatar.html +1 -1
  3. package/components/ui-avatar-group/ui-avatar-group.html +1 -1
  4. package/components/ui-button/ui-button.html +3 -12
  5. package/components/ui-card/ui-card.html +5 -5
  6. package/components/ui-checkbox/ui-checkbox.html +0 -6
  7. package/components/ui-color-picker/ui-color-picker.html +0 -9
  8. package/components/ui-combobox/ui-combobox.html +1 -13
  9. package/components/ui-cycle-button/ui-cycle-button.html +41 -0
  10. package/components/ui-cycle-button/ui-cycle-button.js +19 -0
  11. package/components/ui-dialog/ui-dialog.html +1 -1
  12. package/components/ui-icon/ui-icon.html +3 -0
  13. package/components/ui-icon-button/ui-icon-button.html +10 -14
  14. package/components/ui-input/ui-input.html +1 -16
  15. package/components/ui-input-group/ui-input-group.html +0 -10
  16. package/components/ui-menu/ui-menu.html +1 -1
  17. package/components/ui-popover/ui-popover.html +1 -1
  18. package/components/ui-radio/ui-radio.html +0 -6
  19. package/components/ui-select/ui-select.html +2 -8
  20. package/components/ui-switch/ui-switch.html +0 -6
  21. package/components/ui-tooltip/ui-tooltip.html +1 -1
  22. package/dist/index.js +47 -46
  23. package/docs/README.md +47 -8
  24. package/docs/card.html +2 -1
  25. package/docs/catalog.json +214 -39
  26. package/docs/customizer-render.mjs +8 -7
  27. package/docs/customizer.js +39 -21
  28. package/docs/theme.mjs +6 -5
  29. package/docs/value-editors.mjs +4 -3
  30. package/editor/icons.d.ts +3 -0
  31. package/editor/index.js +4 -1
  32. package/package.json +2 -2
  33. package/styles/ui-avatar-group.css +1 -1
  34. package/styles/ui-avatar.css +1 -1
  35. package/styles/ui-button.css +2 -14
  36. package/styles/ui-card.css +1 -1
  37. package/styles/ui-checkbox.css +0 -9
  38. package/styles/ui-color-picker.css +0 -13
  39. package/styles/ui-combobox.css +2 -28
  40. package/styles/ui-cycle-button.css +36 -0
  41. package/styles/ui-dialog.css +2 -2
  42. package/styles/ui-icon-button.css +6 -13
  43. package/styles/ui-input-group.css +0 -13
  44. package/styles/ui-input.css +2 -18
  45. package/styles/ui-menu.css +1 -1
  46. package/styles/ui-popover.css +1 -1
  47. package/styles/ui-radio.css +0 -9
  48. package/styles/ui-select.css +3 -10
  49. package/styles/ui-switch.css +0 -9
  50. package/styles/ui-tooltip.css +1 -1
  51. package/theme-system.css +19 -18
  52. package/tokens.css +20 -25
  53. package/vanilla/UiCycleButton.d.ts +29 -0
  54. package/vanilla/UiCycleButton.js +85 -0
  55. package/vanilla/UiIcon.js +1 -1
  56. package/vanilla/index.js +1 -0
  57. package/vue/UiAvatar.vue +1 -1
  58. package/vue/UiAvatarGroup.vue +1 -1
  59. package/vue/UiButton.vue +2 -11
  60. package/vue/UiCard.vue +5 -3
  61. package/vue/UiCheckbox.vue +0 -6
  62. package/vue/UiColorPicker.vue +0 -9
  63. package/vue/UiCombobox.vue +2 -26
  64. package/vue/UiCycleButton.d.ts +69 -0
  65. package/vue/UiCycleButton.js +2 -0
  66. package/vue/UiCycleButton.vue +403 -0
  67. package/vue/UiDialog.vue +6 -2
  68. package/vue/UiIcon.vue +22 -0
  69. package/vue/UiIconButton.vue +2 -16
  70. package/vue/UiInput.vue +1 -22
  71. package/vue/UiInputGroup.vue +0 -10
  72. package/vue/UiMenu.vue +3 -1
  73. package/vue/UiPopover.vue +3 -1
  74. package/vue/UiRadio.vue +0 -6
  75. package/vue/UiSelect.vue +2 -8
  76. package/vue/UiSwitch.vue +0 -6
  77. package/vue/UiTooltip.vue +3 -1
  78. package/vue/chunks/UiAffordanceScope.js +1 -1
  79. package/vue/chunks/UiAvatar.js +2 -2
  80. package/vue/chunks/UiAvatarGroup.js +2 -2
  81. package/vue/chunks/UiBadge.js +1 -1
  82. package/vue/chunks/UiBreadcrumbItem.js +1 -1
  83. package/vue/chunks/UiBreadcrumbs.js +1 -1
  84. package/vue/chunks/UiButton.js +3 -3
  85. package/vue/chunks/UiCallout.js +1 -1
  86. package/vue/chunks/UiCard.js +4 -4
  87. package/vue/chunks/UiCheckbox.js +2 -2
  88. package/vue/chunks/UiCluster.js +1 -1
  89. package/vue/chunks/UiColorPicker.js +2 -2
  90. package/vue/chunks/UiCombobox.js +2 -2
  91. package/vue/chunks/UiContainer.js +1 -1
  92. package/vue/chunks/UiContextMenu.js +1 -1
  93. package/vue/chunks/UiCover.js +1 -1
  94. package/vue/chunks/UiCurveEditor.js +1 -1
  95. package/vue/chunks/UiCycleButton.js +352 -0
  96. package/vue/chunks/UiDescriptionItem.js +1 -1
  97. package/vue/chunks/UiDescriptionList.js +1 -1
  98. package/vue/chunks/UiDialog.js +2 -2
  99. package/vue/chunks/UiDisclosure.js +1 -1
  100. package/vue/chunks/UiEditable.js +1 -1
  101. package/vue/chunks/UiEditorBlockActions.js +1 -1
  102. package/vue/chunks/UiEditorChipEditor.js +1 -1
  103. package/vue/chunks/UiEditorCodeLanguage.js +1 -1
  104. package/vue/chunks/UiEditorFootnoteEditor.js +1 -1
  105. package/vue/chunks/UiEditorImageToolbar.js +1 -1
  106. package/vue/chunks/UiEditorInsertTableGrid.js +1 -1
  107. package/vue/chunks/UiEditorLinkCard.js +1 -1
  108. package/vue/chunks/UiEditorLinkForm.js +1 -1
  109. package/vue/chunks/UiEditorMentionMenu.js +1 -1
  110. package/vue/chunks/UiEditorMentionMenuItem.js +1 -1
  111. package/vue/chunks/UiEditorSlashMenu.js +1 -1
  112. package/vue/chunks/UiEditorSlashMenuGroup.js +1 -1
  113. package/vue/chunks/UiEditorSlashMenuItem.js +1 -1
  114. package/vue/chunks/UiEditorSurface.js +1 -1
  115. package/vue/chunks/UiEditorTableContextMenu.js +1 -1
  116. package/vue/chunks/UiEditorTableOverlay.js +1 -1
  117. package/vue/chunks/UiEditorTableToolbar.js +1 -1
  118. package/vue/chunks/UiEditorToolbar.js +1 -1
  119. package/vue/chunks/UiFloatingActionButton.js +1 -1
  120. package/vue/chunks/UiFormField.js +1 -1
  121. package/vue/chunks/UiGrid.js +1 -1
  122. package/vue/chunks/UiIcon.js +70 -2
  123. package/vue/chunks/UiIconButton.js +4 -4
  124. package/vue/chunks/UiImage.js +1 -1
  125. package/vue/chunks/UiInput.js +2 -2
  126. package/vue/chunks/UiInputGroup.js +2 -2
  127. package/vue/chunks/UiLightEditor.js +1 -1
  128. package/vue/chunks/UiList.js +1 -1
  129. package/vue/chunks/UiListItem.js +1 -1
  130. package/vue/chunks/UiListbox.js +1 -1
  131. package/vue/chunks/UiMenu.js +2 -2
  132. package/vue/chunks/UiMenuGroup.js +1 -1
  133. package/vue/chunks/UiMenuItem.js +1 -1
  134. package/vue/chunks/UiMeter.js +1 -1
  135. package/vue/chunks/UiNavItem.js +1 -1
  136. package/vue/chunks/UiNumberInput.js +1 -1
  137. package/vue/chunks/UiPageHeader.js +1 -1
  138. package/vue/chunks/UiPopover.js +2 -2
  139. package/vue/chunks/UiRadio.js +2 -2
  140. package/vue/chunks/UiRadioGroup.js +1 -1
  141. package/vue/chunks/UiReel.js +1 -1
  142. package/vue/chunks/UiScrollArea.js +1 -1
  143. package/vue/chunks/UiSearchResultRow.js +1 -1
  144. package/vue/chunks/UiSearchShell.js +1 -1
  145. package/vue/chunks/UiSection.js +1 -1
  146. package/vue/chunks/UiSelect.js +2 -2
  147. package/vue/chunks/UiSeparator.js +1 -1
  148. package/vue/chunks/UiSidebar.js +1 -1
  149. package/vue/chunks/UiSlider.js +1 -1
  150. package/vue/chunks/UiSpinner.js +1 -1
  151. package/vue/chunks/UiStack.js +1 -1
  152. package/vue/chunks/UiStatusMessage.js +1 -1
  153. package/vue/chunks/UiSwatch.js +1 -1
  154. package/vue/chunks/UiSwitch.js +2 -2
  155. package/vue/chunks/UiSwitcher.js +1 -1
  156. package/vue/chunks/UiTable.js +1 -1
  157. package/vue/chunks/UiTabs.js +1 -1
  158. package/vue/chunks/UiText.js +1 -1
  159. package/vue/chunks/UiTextarea.js +1 -1
  160. package/vue/chunks/UiToast.js +1 -1
  161. package/vue/chunks/UiToastRegion.js +1 -1
  162. package/vue/chunks/UiTooltip.js +2 -2
  163. package/vue/chunks/UiTree.js +1 -1
  164. package/vue/chunks/UiTreeItem.js +1 -1
  165. package/vue/chunks/props.js +12 -1
  166. package/vue/components.css +76 -181
  167. package/vue/editor/index.js +68 -0
  168. package/vue/index.d.ts +1 -0
  169. package/vue/index.js +2 -1
@@ -22,17 +22,18 @@ export function connect(host) {
22
22
  let alive = true;
23
23
  let selectedExample = '';
24
24
  let refinementTrigger;
25
- let previewFrame = 0, pending = false, comparing = false, candidate, gestureStart;
26
- const entryRecipe = structuredClone(recipe);
25
+ let previewFrame = 0, pending = false, candidate, gestureStart;
27
26
  const abort = new AbortController();
28
27
  const say = (text, error = false) => { message.textContent = text; message.setAttribute('role', error ? 'alert' : 'status'); };
29
28
  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
29
  const fields = new Map(themeFields.map(field => [field.id, field]));
31
30
  const unitContext = () => ({ remPixels: parseFloat(view.getComputedStyle(document.documentElement).fontSize) });
31
+ const editorFor = (field, value) => valueEditor(field, value, { remPixels: parseFloat(recipe.values['text-size']) });
32
+ let boundsBase;
32
33
  const updateControl = (element, value) => {
33
34
  const field = fields.get(element.dataset.token);
34
35
  if (!field || !element.dataset.editorKind) { componentRegistry.update(element, { value }); return; }
35
- const editor = valueEditor(field, value);
36
+ const editor = editorFor(field, value);
36
37
  if (element.dataset.editorKind === 'curve') {
37
38
  element.closest('[data-curve-for]').hidden = editor.kind !== 'curve';
38
39
  if (editor.kind === 'curve') componentRegistry.update(element, { value });
@@ -40,13 +41,33 @@ export function connect(host) {
40
41
  };
41
42
  const updateUnits = () => {
42
43
  for (const control of root.querySelectorAll('[data-unit-for]')) {
43
- const field = fields.get(control.dataset.unitFor), editor = valueEditor(field, recipe.values[field.id]);
44
+ const field = fields.get(control.dataset.unitFor), editor = editorFor(field, recipe.values[field.id]);
44
45
  if (![...control.options].some(option => option.value === editor.unit)) control.add(new view.Option(editor.unit, editor.unit));
45
46
  componentRegistry.update(control, { value: editor.unit });
46
47
  }
47
48
  };
49
+ const updateSizes = () => {
50
+ const samples = [...root.querySelectorAll('[data-size-sample]')].filter(sample => sample.getClientRects().length);
51
+ if (!samples.length) return;
52
+ const base = parseFloat(view.getComputedStyle(document.documentElement).fontSize);
53
+ const rounded = value => Number(value.toFixed(2));
54
+ for (const sample of samples) {
55
+ const button = sample.querySelector('button');
56
+ if (button) {
57
+ const height = button.getBoundingClientRect().height;
58
+ sample.querySelector('[data-size-readout]').textContent = ` · ${rounded(height / base)}rem (${rounded(height)}px)`;
59
+ }
60
+ }
61
+ };
48
62
  const paint = () => {
49
63
  const css = theme?.css || themeCSS(recipe);
64
+ if (boundsBase !== recipe.values['text-size']) {
65
+ boundsBase = recipe.values['text-size'];
66
+ for (const control of root.querySelectorAll('[data-token]')) {
67
+ const field = fields.get(control.dataset.token);
68
+ if (field?.type.includes('length') && field.id !== 'text-size') updateControl(control, recipe.values[field.id]);
69
+ }
70
+ }
50
71
  for (const family of ['material','palette']) {
51
72
  const state = refinementState(recipe, family), presets = family === 'material' ? materialPresets : themePalettes;
52
73
  for (const select of root.querySelectorAll(`[data-refinement-preset="${family}"]`)) {
@@ -73,6 +94,7 @@ export function connect(host) {
73
94
  : 'Finish catches the light. Spread shapes its reflection; strength affects reflection, edge light and glow together.';
74
95
  const gaps = root.querySelector('[data-spacing-samples]');
75
96
  gaps.setAttribute('aria-label', `Spacing progression with curve ${recipe.values['space-curve']}`);
97
+ updateSizes();
76
98
  };
77
99
  const show = (next, persist = true) => {
78
100
  recipe = validateRecipe(next);
@@ -83,16 +105,15 @@ export function connect(host) {
83
105
  changed = JSON.stringify(recipe) !== JSON.stringify(saved);
84
106
  paint();
85
107
  };
86
- const clearComparison = () => {
87
- if (!comparing && !candidate) return;
88
- comparing = false; candidate = undefined; theme?.compare();
89
- root.querySelector('[data-action="compare"]').setAttribute('aria-pressed', 'false');
108
+ const clearPreview = () => {
109
+ if (!candidate) return;
110
+ candidate = undefined; theme?.compare();
90
111
  root.querySelector('[data-variation-confirm]').hidden = true;
91
112
  };
92
113
  const preview = next => {
93
114
  const validated = validateRecipe(next);
94
115
  if (JSON.stringify(validated.values) === JSON.stringify(recipe.values)) return;
95
- clearComparison();
116
+ clearPreview();
96
117
  if (!pending) gestureStart = structuredClone(recipe);
97
118
  recipe = validated; pending = true;
98
119
  if (!previewFrame) previewFrame = view.requestAnimationFrame(() => {
@@ -166,17 +187,13 @@ export function connect(host) {
166
187
  if (!button) { view.requestAnimationFrame(paint); return; }
167
188
  if (button.dataset.action === 'save' && saving) return;
168
189
  flush();
169
- if (!['compare', 'preview-variation', 'apply-variation', 'cancel-variation'].includes(button.dataset.action)) clearComparison();
190
+ if (!['preview-variation', 'apply-variation', 'cancel-variation'].includes(button.dataset.action)) clearPreview();
170
191
  try {
171
192
  switch (button.dataset.action) {
172
193
  case 'undo': case 'redo':
173
194
  theme?.[button.dataset.action](); show(theme?.recipe ?? recipe, false); sayApplied('Previous theme decision restored.'); break;
174
- case 'compare':
175
- candidate = undefined; comparing = !comparing; theme?.compare(comparing ? entryRecipe : undefined);
176
- button.setAttribute('aria-pressed', String(comparing)); root.querySelector('[data-variation-confirm]').hidden = true;
177
- say(comparing ? 'Comparing with the theme when you entered. Your edits remain intact. Choose Compare again to return.' : 'Back to your edited theme.'); break;
178
195
  case 'preview-variation': {
179
- clearComparison();
196
+ clearPreview();
180
197
  const locks = [...root.querySelectorAll('[data-family-lock]')].filter(control => control.querySelector('input')?.checked).map(control => control.dataset.familyLock);
181
198
  candidate = nearbyVariation(recipe, root.querySelector('[data-variation-choice]').value, locks);
182
199
  theme?.compare(candidate); root.querySelector('[data-variation-confirm]').hidden = false;
@@ -185,10 +202,10 @@ export function connect(host) {
185
202
  break;
186
203
  }
187
204
  case 'apply-variation': {
188
- const selected = candidate; clearComparison();
205
+ const selected = candidate; clearPreview();
189
206
  if (selected) show(selected); sayApplied('Variation applied. Undo returns to your previous theme.'); break;
190
207
  }
191
- case 'cancel-variation': clearComparison(); paint(); say('Variation cancelled; your theme is unchanged.'); break;
208
+ case 'cancel-variation': clearPreview(); paint(); say('Variation cancelled; your theme is unchanged.'); break;
192
209
  case 'light-top': case 'light-left': case 'light-center': {
193
210
  const positions = { 'light-top': [.5, 0], 'light-left': [.15, .15], 'light-center': [.5, .5] };
194
211
  const [x, y] = positions[button.dataset.action];
@@ -252,7 +269,7 @@ export function connect(host) {
252
269
  if (event.target.matches('[data-light-editor]') || event.target.closest?.('[data-token]')) {
253
270
  input(event); flush(); sayApplied('Theme remembered in this browser. Undo restores the previous decision.'); return;
254
271
  }
255
- flush(); clearComparison();
272
+ flush(); clearPreview();
256
273
  if (event.target.matches('[data-unit-for]')) {
257
274
  const field = fields.get(event.target.dataset.unitFor);
258
275
  try {
@@ -280,7 +297,7 @@ export function connect(host) {
280
297
  const cancel = event => { if (event.target.matches('[data-light-editor], [data-editor-kind="curve"]')) { flush(false); say('Drag cancelled; the previous value is restored.'); } };
281
298
  const keyboard = event => {
282
299
  if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== 'z' || event.target.closest?.('input,textarea,select,[contenteditable="true"]')) return;
283
- event.preventDefault(); flush(); clearComparison(); theme?.[event.shiftKey ? 'redo' : 'undo'](); show(theme?.recipe ?? recipe, false);
300
+ event.preventDefault(); flush(); clearPreview(); theme?.[event.shiftKey ? 'redo' : 'undo'](); show(theme?.recipe ?? recipe, false);
284
301
  };
285
302
  const ready = event => {
286
303
  if (event.data?.type === 'looma-preview-ready' && [...root.querySelectorAll('iframe')].some(frame => frame.contentWindow === event.source)) paint();
@@ -289,7 +306,7 @@ export function connect(host) {
289
306
  let fontRequest = 0;
290
307
  const selectFont = async event => {
291
308
  const control = event.target.closest?.('[data-font-picker]'); if (!control) return;
292
- flush(); clearComparison();
309
+ flush(); clearPreview();
293
310
  const id = control.dataset.fontPicker, family = event.detail.value;
294
311
  const fonts = { ...recipe.fonts };
295
312
  if (family === 'system') delete fonts[id]; else fonts[id] = family;
@@ -310,6 +327,7 @@ export function connect(host) {
310
327
  selectedExample = event.detail.value ?? '';
311
328
  filterExamples();
312
329
  };
330
+ root.addEventListener('toggle', updateSizes, true);
313
331
  root.addEventListener('looma-example-remove', remove);
314
332
  root.addEventListener('input', input);
315
333
  root.addEventListener('cancel', cancel);
@@ -334,5 +352,5 @@ export function connect(host) {
334
352
  root.querySelector('[data-action="save"]').hidden = false;
335
353
  }).catch(error => { if (alive && error.name !== 'AbortError') say(error.message, true); });
336
354
  }
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); };
355
+ return () => { root.removeEventListener('toggle', updateSizes, true); flush(); clearPreview(); 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
356
  }
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.'],
@@ -29,12 +29,12 @@ export const themeFields = [
29
29
  ['Size & spacing', 'line-weight', 'Line weight', '--ui-border-width', 'length', '1px', 'Shared edge weight; focus stays independently visible.'],
30
30
  ['Material', 'outer-depth', 'Outer depth', '--ui-outer-depth', 'nonnegative', '0.6', 'How strongly controls and floating surfaces rise above the page.'],
31
31
  ['Material', 'inner-depth', 'Inner depth', '--ui-inner-depth', 'nonnegative', '0.3', 'How strongly fields and pressed controls sit into their surface.'],
32
- ['Material', 'glassiness', 'Glassiness', '--ui-glassiness', 'fraction', '0', 'Frosted blur, richer background colour and light-catching glass edges. Content stays readable.'],
32
+ ['Material', 'glassiness', 'Glassiness', '--ui-glassiness', 'fraction', '0', 'Clear through richly frosted: the upper range adds stronger blur, saturation and glass edges. Content stays readable.'],
33
33
  ['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.'],
34
34
  ['Material', 'translucency', 'Overlay translucency', '--ui-overlay-translucency', 'fraction', '0', 'How much background comes through floating surfaces.'],
35
35
  ['Material', 'edge-light', 'Edge light', '--ui-edge-light', 'fraction', '0', 'Light caught by surface edges, independent of depth.'],
36
36
  ['Material', 'finish', 'Finish', '--ui-finish', 'fraction', '0', 'Matte through polished: how strongly the material catches a reflection.'],
37
- ['Material', 'glow', 'Glow', '--ui-glow', 'fraction', '0', 'Soft reflected light beyond a surface, derived from its tone.'],
37
+ ['Material', 'glow', 'Glow', '--ui-glow', 'fraction', '0', 'Reflected light beyond a surface, from a quiet halo to a vivid bloom at the upper end.'],
38
38
  ['Material', 'light-x', 'Light horizontal position', '--ui-light-x', 'fraction', '0.5', 'The light source from left to right, relative to each surface.'],
39
39
  ['Material', 'light-y', 'Light vertical position', '--ui-light-y', 'fraction', '0', 'The light source from top to bottom, relative to each surface.'],
40
40
  ['Material', 'light-spread', 'Light spread', '--ui-light-spread', 'fraction', '0.5', 'A concentrated spot through broad illumination.'],
@@ -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];
@@ -186,7 +187,7 @@ const contrast = (a, b) => (Math.max(luminance(a), luminance(b)) + .05) / (Math.
186
187
  // fill cannot wash out the label, even at maximum finish and light strength.
187
188
  function reflection(fill, text) {
188
189
  const labels = Array.isArray(text) ? text : [text];
189
- let amount = .32;
190
+ let amount = .6;
190
191
  while (amount > 0 && labels.some(label => contrast(mix('#ffffff', fill, amount), label) < 4.5)) amount = Math.max(0, amount - .005);
191
192
  return mix('#ffffff', fill, amount);
192
193
  }
@@ -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/editor/icons.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import { type IconNode } from 'lucide';
2
2
  /** The opinionated Lucide set used by Looma's shipped interaction surfaces. */
3
3
  export declare const LOOMA_ICONS: {
4
+ monitor: IconNode;
5
+ sun: IconNode;
6
+ moon: IconNode;
4
7
  'align-center': IconNode;
5
8
  'align-left': IconNode;
6
9
  'align-right': IconNode;
package/editor/index.js CHANGED
@@ -2,10 +2,13 @@ import { n as TABLE_CELL_BACKGROUND_PRESETS, t as TABLE_CELL_BACKGROUND_OPTIONS
2
2
  import { n as resolveTableCellAt, t as measureTableOverlayGeometry } from "./chunks/ui.js";
3
3
  import { $ as LOOMA_ACTIVE_BLOCK_BLUR_GRACE_MS, A as filterLoomaMentionItems, B as LOOMA_FOOTNOTE_LABEL, C as loomaMarkdownToHtml, D as createLoomaMentionExtension, E as LoomaMentionSuggestionPluginKey, F as LoomaTableOfContents, G as LoomaFigcaption, H as LoomaFootnoteReference, I as getTableOfContentsEntries, J as revealLoomaExpandAt, K as LoomaFigure, L as normalizeTableOfContentsConfig, M as LoomaImage, N as normalizeImageDimension, O as DEFAULT_MENTION_RESULT_LIMIT, P as normalizeImagePlacement, Q as LoomaCallout, R as LoomaTab, S as loomaMarkdownFromDoc, T as siteRelativeHref, U as LoomaFootnotes, V as LoomaFootnote, W as findLoomaFootnote, X as LoomaChip, Y as LOOMA_CHIP_COLORS, Z as normalizeLoomaChipColor, _ as getActiveTableCellAlignment, a as getActiveTableUiState, b as setActiveTableCellBackground, c as insertTableAtRange, d as LoomaTableKit, et as LOOMA_ACTIVE_BLOCK_CLASS, f as getDefaultEditorExtensions, g as LoomaTableHeader, h as LoomaTableCell, i as getDefaultSlashCommands, j as normalizeMentionResultLimit, k as MAX_MENTION_RESULT_LIMIT, l as normalizeActiveTableColumnWidths, m as LoomaTable, n as createLoomaSlashCommandExtension, nt as LoomaActiveBlock, o as handleTableAction, p as getLoomaTableExtensions, q as LoomaExpand, r as filterLoomaSlashCommands, rt as LoomaActiveBlockPluginKey, s as handleTableOverlayAction, t as LoomaSlashCommand, tt as LOOMA_ACTIVE_BLOCK_TYPING_IDLE_MS, u as shouldShowTextFormattingToolbar, v as getActiveTableCellBackground, w as createSiteRelativeLinks, x as LoomaSmartPaste, y as setActiveTableCellAlignment, z as LoomaTabs } from "./chunks/extensions.js";
4
4
  import { a as setReferenceDisplay, i as referenceDisplay, n as LoomaReferencePresentation, o as LOOMA_CALLOUT_TONES, r as getReferenceDisplay, t as LoomaReference } from "./chunks/reference-display.js";
5
- import { AlignCenter, AlignLeft, AlignRight, ArrowLeft, Bell, Bold, BookUser, Braces, Calculator, Check, ChevronDown, ChevronLeft, ChevronRight, CircleCheck, CircleDot, CircleQuestionMark, CircleX, CodeXml, Columns3, Compass, CreditCard, Ellipsis, Eraser, ExternalLink, FileText, Folder, GripHorizontal, GripVertical, Heading1, Heading2, Heading3, Highlighter, History, Image, Info, Italic, LayoutDashboard, Lightbulb, Link, List, ListOrdered, ListTodo, LoaderCircle, Merge, MessageSquareWarning, Minus, NotebookPen, PaintBucket, PanelBottom, PanelLeft, PanelRight, PanelTop, Paperclip, Pencil, Pilcrow, Plus, Quote, Receipt, Redo2, Rows3, Search, Settings, Split, Strikethrough, Superscript, Table2, Tag, Trash2, TriangleAlert, Truck, Underline, Undo2, Unlink, UserRound, Users, X } from "lucide";
5
+ import { AlignCenter, AlignLeft, AlignRight, ArrowLeft, Bell, Bold, BookUser, Braces, Calculator, Check, ChevronDown, ChevronLeft, ChevronRight, CircleCheck, CircleDot, CircleQuestionMark, CircleX, CodeXml, Columns3, Compass, CreditCard, Ellipsis, Eraser, ExternalLink, FileText, Folder, GripHorizontal, GripVertical, Heading1, Heading2, Heading3, Highlighter, History, Image, Info, Italic, LayoutDashboard, Lightbulb, Link, List, ListOrdered, ListTodo, LoaderCircle, Merge, MessageSquareWarning, Minus, Monitor, Moon, NotebookPen, PaintBucket, PanelBottom, PanelLeft, PanelRight, PanelTop, Paperclip, Pencil, Pilcrow, Plus, Quote, Receipt, Redo2, Rows3, Search, Settings, Split, Strikethrough, Sun, Superscript, Table2, Tag, Trash2, TriangleAlert, Truck, Underline, Undo2, Unlink, UserRound, Users, X } from "lucide";
6
6
  //#region src/editor/icons.ts
7
7
  /** The opinionated Lucide set used by Looma's shipped interaction surfaces. */
8
8
  var LOOMA_ICONS = {
9
+ monitor: Monitor,
10
+ sun: Sun,
11
+ moon: Moon,
9
12
  "align-center": AlignCenter,
10
13
  "align-left": AlignLeft,
11
14
  "align-right": AlignRight,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadlabs/looma",
3
- "version": "0.36.0",
3
+ "version": "0.37.1",
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 || ^0.9.0",
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
 
@@ -451,18 +451,9 @@
451
451
  }
452
452
 
453
453
  /* The size states outrank :host, so sm is named too, or a small button would stay small. */
454
- @media (pointer: coarse) {
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));
460
- }
461
- }
462
454
 
463
455
  /* Under touch, a link-style button's press lands within the control minimum: an invisible hit
464
- area, centred on it, like IconButton's. A boxed button grows to the minimum itself (below);
465
- a link-style one keeps its text size, so a standalone one ("See all" under a list) needs the
456
+ area, centred on it, like IconButton's. A link-style action keeps its text size, so a standalone one ("See all" under a list) needs the
466
457
  area. xs sits inside a line of text the same way, so it keeps its size and gains the area too.
467
458
  Only these need it: a hit area on every button would reach over its neighbours. */
468
459
  html[data-ui-input-modality="touch"] :scope:where([data-component~="ui-button"])[data-appearance="link"],
@@ -567,12 +558,9 @@
567
558
  /* Ghost keeps no border, but gains the same neutral surface as other disabled buttons. */
568
559
 
569
560
  /* The size states outrank :host, so sm is named too, or a small button would stay small. */
570
- @media (pointer: coarse) {
571
- }
572
561
 
573
562
  /* Under touch, a link-style button's press lands within the control minimum: an invisible hit
574
- area, centred on it, like IconButton's. A boxed button grows to the minimum itself (below);
575
- a link-style one keeps its text size, so a standalone one ("See all" under a list) needs the
563
+ area, centred on it, like IconButton's. A link-style action keeps its text size, so a standalone one ("See all" under a list) needs the
576
564
  area. xs sits inside a line of text the same way, so it keeps its size and gains the area too.
577
565
  Only these need it: a hit area on every button would reach over its neighbours. */
578
566
 
@@ -52,7 +52,7 @@
52
52
  }
53
53
 
54
54
  :scope:where([data-component~="ui-card"])[data-ui-card-state~="treatment=expressive"] {
55
- --_ui-panel-glass: clamp(0, var(--ui-glassiness, 0), 1);
55
+ --_ui-panel-glass: calc(clamp(0, var(--ui-glassiness, 0), 1) + 2 * pow(clamp(0, var(--ui-glassiness, 0), 1), 4));
56
56
  --_ui-panel-separation: clamp(0, var(--ui-surface-separation, 0), 1);
57
57
  --_ui-panel-rim: calc((var(--_ui-panel-separation) + var(--_ui-panel-glass) * 0.25) * 0.65 * 8px);
58
58
  border-width: calc(var(--ui-border-width) + var(--_ui-panel-rim));
@@ -210,12 +210,6 @@
210
210
  padding-block: max(0px, calc((var(--_row) - max(var(--_line), var(--_ui-checkbox-size, 1.125rem))) / 2)) !important;
211
211
  }
212
212
 
213
- @media (pointer: coarse) {
214
- :scope:where([data-component~="ui-checkbox"])[data-ui-checkbox-state~="size=sm"] {
215
- --_row: var(--ui-control-min-block-size);
216
- }
217
- }
218
-
219
213
  /* A component that sets its display still honours the hidden attribute on its root. */
220
214
  :scope:where([data-component~="ui-checkbox"])[hidden] {
221
215
  display: none !important;
@@ -235,8 +229,5 @@
235
229
  and the first line centres in that control height, so the label shares a row's baseline.
236
230
  Multi-line labels and descriptions still grow downward from that first line. */
237
231
 
238
- @media (pointer: coarse) {
239
- }
240
-
241
232
  /* A component that sets its display still honours the hidden attribute on its root. */
242
233
  }
@@ -104,17 +104,4 @@
104
104
  padding: 0 !important;
105
105
  accent-color: var(--ui-accent-solid);
106
106
  }
107
- @media (pointer: coarse) {
108
- :scope:where([data-component~="ui-color-picker"]) {
109
- --_size: max(var(--ui-control-size-md), var(--ui-control-min-block-size));
110
- }
111
- :scope:where([data-component~="ui-color-picker"])[data-ui-color-picker-state~="size=sm"] {
112
- --_size: max(var(--ui-control-size-sm), var(--ui-control-min-block-size));
113
- --_font: var(--ui-font-size-md);
114
- }
115
- }
116
- }
117
- @scope ([data-component~="ui-color-picker"]) to ([data-component]) {
118
- @media (pointer: coarse) {
119
- }
120
107
  }
@@ -310,20 +310,8 @@
310
310
  white-space: nowrap;
311
311
  }
312
312
  @media (pointer: coarse) {
313
- :scope:where([data-component~="ui-combobox"]) {
314
- --_row: max(var(--ui-control-size-md), var(--ui-control-min-block-size));
315
- }
316
- button {
317
- min-inline-size: 2.75rem;
318
- min-block-size: calc(var(--_row) - 2 * var(--ui-border-width));
319
- }
320
- /* Under touch a small field is still a touch-sized target, as a small button is, and keeps
321
- body-size text: below 16px, iOS zooms the page into the focused field. */
322
- :scope:where([data-component~="ui-combobox"])[data-ui-combobox-state~="size=sm"] {
323
- --_row: max(var(--ui-control-size-sm), var(--ui-control-min-block-size));
324
- }
325
313
  :scope:where([data-component~="ui-combobox"])[data-ui-combobox-state~="size=sm"] input {
326
- font-size: inherit;
314
+ font-size: var(--ui-font-size-md);
327
315
  }
328
316
  }
329
317
  @media (forced-colors: active) {
@@ -408,8 +396,6 @@
408
396
  /* A ghost icon button: the affordance is the mark, not a box around it. */
409
397
  /* A row with a tag or a description: the label and its tag on one line, the description under them. */
410
398
  @media (pointer: coarse) {
411
- /* Under touch a small field is still a touch-sized target, as a small button is, and keeps
412
- body-size text: below 16px, iOS zooms the page into the focused field. */
413
399
  }
414
400
  @media (forced-colors: active) {
415
401
  }
@@ -722,20 +708,8 @@
722
708
  white-space: nowrap;
723
709
  }
724
710
  @media (pointer: coarse) {
725
- :where([data-component~="ui-combobox"]):where(:scope, :not([data-component], [data-slotted]:not([data-slotted~="ui-combobox"]))) {
726
- --_row: max(var(--ui-control-size-md), var(--ui-control-min-block-size));
727
- }
728
- button:where(:scope, :scope *) {
729
- min-inline-size: 2.75rem;
730
- min-block-size: calc(var(--_row) - 2 * var(--ui-border-width));
731
- }
732
- /* Under touch a small field is still a touch-sized target, as a small button is, and keeps
733
- body-size text: below 16px, iOS zooms the page into the focused field. */
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: max(var(--ui-control-size-sm), var(--ui-control-min-block-size));
736
- }
737
711
  :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
- font-size: inherit;
712
+ font-size: var(--ui-font-size-md);
739
713
  }
740
714
  }
741
715
  @media (forced-colors: active) {
@@ -0,0 +1,36 @@
1
+ /* Generated by HTML Next 1.0.0-alpha.69. Do not edit. */
2
+ @supports (-moz-appearance: none) {
3
+ :where([data-component], [data-slotted]) { --html-next-scope-owner: boundary; }
4
+ :where([data-slotted]:not([data-component])) { --html-next-scope-owner: projected !important; }
5
+ }
6
+ @supports (-moz-appearance: none) {
7
+ [data-component~="ui-cycle-button"] { --html-next-scope-owner: "ui-cycle-button" !important; }
8
+ }
9
+ @scope ([data-component~="ui-cycle-button"]) to ([data-component], [data-slotted]) {
10
+ :scope:where([data-component~="ui-cycle-button"]) {
11
+ display: inline-flex !important;
12
+ align-items: center !important;
13
+ justify-content: center !important;
14
+ gap: 0 !important;
15
+ margin: 0 !important;
16
+ padding: 0 !important;
17
+ vertical-align: middle;
18
+ }
19
+ :scope:where([data-component~="ui-cycle-button"])[hidden] {
20
+ display: none !important;
21
+ }
22
+ }
23
+ @scope ([data-slotted~="ui-cycle-button"]:not([data-component])) to ([data-component], [data-slotted]) {
24
+ :where([data-component~="ui-cycle-button"]):where(:scope, :not([data-component], [data-slotted]:not([data-slotted~="ui-cycle-button"]))) {
25
+ display: inline-flex !important;
26
+ align-items: center !important;
27
+ justify-content: center !important;
28
+ gap: 0 !important;
29
+ margin: 0 !important;
30
+ padding: 0 !important;
31
+ vertical-align: middle;
32
+ }
33
+ :where([data-component~="ui-cycle-button"])[hidden]:where(:scope, :not([data-component], [data-slotted]:not([data-slotted~="ui-cycle-button"]))) {
34
+ display: none !important;
35
+ }
36
+ }
@@ -74,7 +74,7 @@
74
74
  /* Purpose and measured size shape one shared separation lever. The content plane stays opaque;
75
75
  transparency, saturation and edge light belong to the surrounding decorative material. */
76
76
  :scope:where([data-component~="ui-dialog"]) {
77
- --_ui-material-glassiness: clamp(0, var(--ui-glassiness, 0), 1);
77
+ --_ui-material-glassiness: calc(clamp(0, var(--ui-glassiness, 0), 1) + 2 * pow(clamp(0, var(--ui-glassiness, 0), 1), 4));
78
78
  --_ui-material-separation: clamp(0, var(--ui-surface-separation, 0), 1);
79
79
  --_ui-material-active: clamp(0, calc((var(--_ui-material-glassiness) + var(--_ui-material-separation)) * 100000), 1);
80
80
  --_ui-material-rim: calc((var(--_ui-material-separation) + var(--_ui-material-glassiness) * 0.25) * var(--_ui-material-scale, 1) * 8px);
@@ -395,7 +395,7 @@
395
395
  /* Purpose and measured size shape one shared separation lever. The content plane stays opaque;
396
396
  transparency, saturation and edge light belong to the surrounding decorative material. */
397
397
  :where([data-component~="ui-dialog"]):where(:scope, :not([data-component], [data-slotted]:not([data-slotted~="ui-dialog"]))) {
398
- --_ui-material-glassiness: clamp(0, var(--ui-glassiness, 0), 1);
398
+ --_ui-material-glassiness: calc(clamp(0, var(--ui-glassiness, 0), 1) + 2 * pow(clamp(0, var(--ui-glassiness, 0), 1), 4));
399
399
  --_ui-material-separation: clamp(0, var(--ui-surface-separation, 0), 1);
400
400
  --_ui-material-active: clamp(0, calc((var(--_ui-material-glassiness) + var(--_ui-material-separation)) * 100000), 1);
401
401
  --_ui-material-rim: calc((var(--_ui-material-separation) + var(--_ui-material-glassiness) * 0.25) * var(--_ui-material-scale, 1) * 8px);
@@ -331,14 +331,6 @@
331
331
  --_size: var(--ui-control-size-lg);
332
332
  }
333
333
 
334
- @media (pointer: coarse) {
335
- :scope:where([data-component~="ui-icon-button"])[data-ui-icon-button-state~="matchButton"] {
336
- inline-size: max(var(--ui-icon-button-size, var(--_size)), var(--ui-control-min-block-size));
337
- min-inline-size: max(var(--ui-icon-button-size, var(--_size)), var(--ui-control-min-block-size));
338
- min-block-size: max(var(--ui-icon-button-size, var(--_size)), var(--ui-control-min-block-size));
339
- }
340
- }
341
-
342
334
  :scope:where([data-component~="ui-icon-button"])[data-ui-icon-button-state~="round"] {
343
335
  border-radius: var(--ui-radius-round);
344
336
  }
@@ -348,14 +340,18 @@
348
340
  background-color: var(--ui-icon-button-outline-surface, var(--_icon-button-outline-surface, var(--ui-surface-default)));
349
341
  background-image: var(--ui-surface-highlight, none);
350
342
  color: var(--ui-icon-button-outline-text, var(--_icon-button-text, var(--ui-text-primary)));
351
- box-shadow: var(--ui-action-shadow, var(--_ui-action-shadow)), var(--_tone-glow);
343
+ box-shadow:
344
+ var(--ui-action-shadow, var(--_ui-action-shadow)),
345
+ var(--_tone-glow);
352
346
  }
353
347
 
354
348
  :scope:where([data-component~="ui-icon-button"])[data-appearance="solid"] {
355
349
  background-color: var(--ui-icon-button-solid-surface, var(--_icon-button-solid-surface, var(--ui-accent-solid)));
356
350
  background-image: var(--_tone-highlight);
357
351
  color: var(--ui-icon-button-solid-text, var(--_icon-button-solid-text, var(--ui-text-on-accent)));
358
- box-shadow: var(--ui-icon-button-solid-shadow, var(--ui-action-shadow, var(--_ui-action-shadow))), var(--_tone-glow);
352
+ box-shadow:
353
+ var(--ui-icon-button-solid-shadow, var(--ui-action-shadow, var(--_ui-action-shadow))),
354
+ var(--_tone-glow);
359
355
  }
360
356
 
361
357
  :scope:where([data-component~="ui-icon-button"]):hover:enabled:not([aria-disabled="true"]) {
@@ -509,9 +505,6 @@
509
505
 
510
506
  /* A text button's companion uses control geometry; compact icon buttons keep their own density. */
511
507
 
512
- @media (pointer: coarse) {
513
- }
514
-
515
508
  /* Native toggle state persists after pointer release, including during loading. */
516
509
 
517
510
  @media (prefers-reduced-motion: reduce) {
@@ -89,16 +89,6 @@
89
89
  min-block-size: var(--ui-control-size-lg);
90
90
  }
91
91
 
92
- @media (pointer: coarse) {
93
- :scope:where([data-component~="ui-input-group"]) {
94
- min-block-size: max(var(--ui-control-size-md), var(--ui-control-min-block-size));
95
- }
96
-
97
- :scope:where([data-component~="ui-input-group"]):has(.field [data-ui-input-state~="size=sm"]) {
98
- min-block-size: var(--ui-control-min-block-size);
99
- }
100
- }
101
-
102
92
  :scope:where([data-component~="ui-input-group"]):hover:not(:has(:is(input, textarea):focus, :is(input, textarea):disabled, :is(input, textarea):read-only, [aria-invalid="true"], :is(:user-invalid, [data-user-invalid]))) {
103
93
  border-color: var(--ui-control-border-hover, var(--ui-text-secondary));
104
94
  }
@@ -249,9 +239,6 @@
249
239
 
250
240
  /* The frame takes its input's size, so a small group lines up with small buttons and fields. */
251
241
 
252
- @media (pointer: coarse) {
253
- }
254
-
255
242
  /* The ring shows when the field's input would show its own: when it is focus-visible, which a
256
243
  text input is for a pointer as well as a keyboard. A focused action button draws its own. */
257
244
 
@@ -100,28 +100,13 @@
100
100
  font-size: var(--ui-font-size-md);
101
101
  }
102
102
 
103
- /* Under touch a small field is still a touch-sized target, as a small button is, and keeps
104
- body-size text: below 16px, iOS zooms the page into the focused field. */
103
+ /* Touch may use body-size text in a small native field to avoid focus zoom; this does not impose a larger visible control height. */
105
104
  @media (pointer: coarse) {
106
- :scope:where([data-component~="ui-input"]) {
107
- min-block-size: var(
108
- --ui-input-min-block-size,
109
- var(--_ui-default-input-min-block-size, max(var(--ui-control-size-md), var(--ui-control-min-block-size)))
110
- );
111
- }
112
-
113
105
  :scope:where([data-component~="ui-input"]),
114
106
  :scope:where([data-component~="ui-input"])[data-ui-input-state~="size=sm"],
115
107
  :scope:where([data-component~="ui-input"])[data-ui-input-state~="size=lg"] {
116
108
  font-size: var(--ui-font-size-md);
117
109
  }
118
-
119
- :scope:where([data-component~="ui-input"])[data-ui-input-state~="size=sm"] {
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
- );
124
- }
125
110
  }
126
111
 
127
112
  /* Tabbing into a field selects its text; tint that selection softly instead of the page highlight. */
@@ -195,8 +180,7 @@
195
180
 
196
181
  /* Each size reads the hook and then an Input Group's default first, so a group, which draws the frame, can zero it. */
197
182
 
198
- /* Under touch a small field is still a touch-sized target, as a small button is, and keeps
199
- body-size text: below 16px, iOS zooms the page into the focused field. */
183
+ /* Touch may use body-size text in a small native field to avoid focus zoom; this does not impose a larger visible control height. */
200
184
  @media (pointer: coarse) {
201
185
  }
202
186
 
@@ -103,7 +103,7 @@
103
103
  /* Purpose and measured size shape one shared separation lever. The content plane stays opaque;
104
104
  transparency, saturation and edge light belong to the surrounding decorative material. */
105
105
  :scope:where([data-component~="ui-menu"]):not([inline]) .surface {
106
- --_ui-material-glassiness: clamp(0, var(--ui-glassiness, 0), 1);
106
+ --_ui-material-glassiness: calc(clamp(0, var(--ui-glassiness, 0), 1) + 2 * pow(clamp(0, var(--ui-glassiness, 0), 1), 4));
107
107
  --_ui-material-separation: clamp(0, var(--ui-surface-separation, 0), 1);
108
108
  --_ui-material-active: clamp(0, calc((var(--_ui-material-glassiness) + var(--_ui-material-separation)) * 100000), 1);
109
109
  --_ui-material-rim: calc((var(--_ui-material-separation) + var(--_ui-material-glassiness) * 0.25) * var(--_ui-material-scale, 0.28) * 8px);