@threadlabs/looma 0.39.2 → 0.39.4

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
@@ -256,7 +256,7 @@ Use the `@threadlabs/looma/docs` entrypoint for programmatic discovery, catalog
256
256
 
257
257
  ## Saving and recovery
258
258
 
259
- The local editor writes only the configured recipe and CSS inside the project. It rejects symlink paths, generated or dependency directories, cross-origin save requests and stale file revisions. A competing edit leaves your unsaved preview in place; reload and reconcile it before saving again. Examples run in isolated frames and receive theme CSS, never the local editing capability.
259
+ The local editor writes only the configured recipe and CSS inside the project. It rejects symlink paths, generated or dependency directories, cross-origin save requests and stale file revisions. A competing edit leaves your unsaved preview in place; reload and reconcile it before saving again. Examples run in isolated frames and receive theme CSS, never the local editing capability. Each preview's compiled runtime is embedded after its content and authorized by an exact content hash in the generated content policy. This keeps previews interactive in Safari while retaining their opaque origin and parent-page isolation.
260
260
 
261
261
  The writer stages complete files, serializes saves with a project lock, and rolls back the recipe if replacing the CSS fails. If a process stops during a save or build, close the old editor before removing its `.looma/theme-write.lock` or `.looma/docs-build.lock`; restart and save the loaded recipe to regenerate a consistent pair. Change the server base, output directory or theme-file paths by restarting `dev`.
262
262
 
package/docs/catalog.json CHANGED
@@ -458,7 +458,7 @@
458
458
  ],
459
459
  "schemaVersion": 1,
460
460
  "package": "@threadlabs/looma",
461
- "version": "0.39.2",
461
+ "version": "0.39.4",
462
462
  "groups": [
463
463
  {
464
464
  "id": "layout",
@@ -7430,24 +7430,12 @@
7430
7430
  {
7431
7431
  "name": "--ui-border-width"
7432
7432
  },
7433
- {
7434
- "name": "--ui-control-min-block-size"
7435
- },
7436
- {
7437
- "name": "--ui-control-size-sm"
7438
- },
7439
7433
  {
7440
7434
  "name": "--ui-elevation-dialog",
7441
7435
  "fallbacks": [
7442
7436
  "var(--ui-shadow-xl)"
7443
7437
  ]
7444
7438
  },
7445
- {
7446
- "name": "--ui-focus-ring"
7447
- },
7448
- {
7449
- "name": "--ui-focus-width"
7450
- },
7451
7439
  {
7452
7440
  "name": "--ui-font-semibold"
7453
7441
  },
@@ -7493,9 +7481,6 @@
7493
7481
  {
7494
7482
  "name": "--ui-motion-ease"
7495
7483
  },
7496
- {
7497
- "name": "--ui-motion-fast"
7498
- },
7499
7484
  {
7500
7485
  "name": "--ui-overlay-backdrop"
7501
7486
  },
@@ -7527,9 +7512,6 @@
7527
7512
  {
7528
7513
  "name": "--ui-radius-dialog"
7529
7514
  },
7530
- {
7531
- "name": "--ui-radius-round"
7532
- },
7533
7515
  {
7534
7516
  "name": "--ui-shadow-xl"
7535
7517
  },
@@ -7557,9 +7539,6 @@
7557
7539
  "linear-gradient(transparent, transparent)"
7558
7540
  ]
7559
7541
  },
7560
- {
7561
- "name": "--ui-surface-muted"
7562
- },
7563
7542
  {
7564
7543
  "name": "--ui-surface-separation",
7565
7544
  "fallbacks": [
@@ -7568,9 +7547,6 @@
7568
7547
  },
7569
7548
  {
7570
7549
  "name": "--ui-text-primary"
7571
- },
7572
- {
7573
- "name": "--ui-text-secondary"
7574
7550
  }
7575
7551
  ]
7576
7552
  },
@@ -66,11 +66,19 @@ export function connect(host) {
66
66
  };
67
67
  const header = document.querySelector('[data-component~="markupress-header"]');
68
68
  const previews = [...root.querySelectorAll('[data-editor-preview], .palette-preview')];
69
+ let layoutFrame = 0;
69
70
  const updatePreviewLayout = () => {
70
71
  const chrome = header?.getBoundingClientRect().height ?? 0;
71
72
  root.style.setProperty('--_editor-chrome-height', `${chrome}px`);
72
73
  for (const preview of previews) preview.toggleAttribute('data-page-flow', preview.offsetHeight > (view.innerHeight - chrome) / 2);
73
74
  };
75
+ // Layout writes must run outside ResizeObserver delivery, including when a preview appears.
76
+ const schedulePreviewLayout = () => {
77
+ if (!layoutFrame) layoutFrame = view.requestAnimationFrame(() => {
78
+ layoutFrame = 0;
79
+ updatePreviewLayout();
80
+ });
81
+ };
74
82
  const replayMotion = () => {
75
83
  const element = root.querySelector('[data-motion-object]');
76
84
  const duration = parseFloat(convertValueUnit(recipe.values['response-time'], 'ms', unitContext()));
@@ -405,9 +413,9 @@ export function connect(host) {
405
413
  filterExamples();
406
414
  };
407
415
  const motionPreferenceChanged = () => { motionAnimation?.cancel(); paint(); };
408
- const previewObserver = new view.ResizeObserver(updatePreviewLayout);
416
+ const previewObserver = new view.ResizeObserver(schedulePreviewLayout);
409
417
  for (const element of [...previews, ...(header ? [header] : [])]) previewObserver.observe(element);
410
- view.addEventListener('resize', updatePreviewLayout);
418
+ view.addEventListener('resize', schedulePreviewLayout);
411
419
  motionPreference.addEventListener('change', motionPreferenceChanged);
412
420
  root.addEventListener('toggle', updateSizes, true);
413
421
  root.addEventListener('looma-example-remove', remove);
@@ -438,5 +446,5 @@ export function connect(host) {
438
446
  root.querySelector('[data-action="save"]').hidden = false;
439
447
  }).catch(error => { if (alive && error.name !== 'AbortError') say(error.message, true); });
440
448
  }
441
- return () => { previewObserver.disconnect(); view.removeEventListener('resize', updatePreviewLayout); motionPreference.removeEventListener('change', motionPreferenceChanged); root.removeEventListener('toggle', updateSizes, true); motionAnimation?.cancel(); flush(); 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', choose); 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('popstate', renderView); view.removeEventListener('hashchange', renderView); view.removeEventListener(themeEvent, synchronize); };
449
+ return () => { previewObserver.disconnect(); if (layoutFrame) view.cancelAnimationFrame(layoutFrame); view.removeEventListener('resize', schedulePreviewLayout); motionPreference.removeEventListener('change', motionPreferenceChanged); root.removeEventListener('toggle', updateSizes, true); motionAnimation?.cancel(); flush(); 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', choose); 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('popstate', renderView); view.removeEventListener('hashchange', renderView); view.removeEventListener(themeEvent, synchronize); };
442
450
  }
package/docs/site.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { cp, mkdir, readFile, readdir, realpath, rename, rm, symlink, unlink, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { randomUUID } from 'node:crypto';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
4
  import { createRequire } from 'node:module';
5
5
  import { fileURLToPath, pathToFileURL } from 'node:url';
6
6
  import { discoverComponents, escapeHtml, mergeCatalogs, readCatalog, readJson } from './catalog.mjs';
@@ -220,7 +220,7 @@ export async function buildDocumentation({ root = process.cwd(), config = {}, ca
220
220
  });
221
221
  }
222
222
 
223
- async function isolatePreviewScripts(site, { outDir, writeFile: writeOutput }) {
223
+ async function isolatePreviewScripts(site, { outDir }) {
224
224
  const { build } = await tool('vite', site.root);
225
225
  // Preview entries and outputs are independent. Keep compiler memory bounded
226
226
  // and retain the build lock until every in-flight bundle has settled.
@@ -236,9 +236,13 @@ async function isolatePreviewScripts(site, { outDir, writeFile: writeOutput }) {
236
236
  const input = path.join(outDir, url.slice(site.base.length));
237
237
  const bundle = await build({ configFile: false, logLevel: 'silent', build: { write: false, minify: true, lib: { entry: input, name: 'LoomaPreview', formats: ['iife'] }, rollupOptions: { output: { codeSplitting: false } } } });
238
238
  const code = (Array.isArray(bundle) ? bundle[0] : bundle).output.find(output => output.type === 'chunk').code;
239
- const name = `${exampleKey(example.id)}.js`;
240
- await writeOutput('_looma/runtime/' + name, code);
241
- html = html.replace(tag, `<script src="${site.base}_looma/runtime/${name}" defer></script>`);
239
+ // A sandbox has an opaque origin: Safari cannot authorize this runtime via 'self'.
240
+ // Hash the exact inline bytes and run after the preview DOM, preserving isolation.
241
+ const script = code.replace(/<\/script/gi, '<\\/script');
242
+ const hash = createHash('sha256').update(script).digest('base64');
243
+ if (!/<meta\b[^>]*http-equiv="Content-Security-Policy"[^>]*content="[^"]*\bscript-src\b/.test(html)) throw new Error(`Missing script policy for isolated preview ${example.id}.`);
244
+ html = html.replace(/(<meta\b[^>]*http-equiv="Content-Security-Policy"[^>]*content="[^"]*\bscript-src)\b/, `$1 'sha256-${hash}'`);
245
+ html = html.replace(tag, '').replace('</body>', () => `<script>${script}</script></body>`);
242
246
  }
243
247
  html = html.replace(/<link\b[^>]*rel="modulepreload"[^>]*>/g, '');
244
248
  await writeFile(htmlFile, html);
package/docs/theme.mjs CHANGED
@@ -273,15 +273,21 @@ export function themeCSS(input, systemCSS = '', { scope = 'portable' } = {}) {
273
273
  const selector = scope === 'documentation'
274
274
  ? ':root[data-looma-documentation-theme], :root[data-looma-documentation-theme] [data-theme], :root[data-looma-documentation-theme] [data-contrast]'
275
275
  : ':root, [data-theme], [data-contrast]';
276
- const fontURL = googleFontURL(validateRecipe(input).fonts);
276
+ const recipe = validateRecipe(input);
277
+ const fontURL = googleFontURL(recipe.fonts);
277
278
  const imports = fontURL ? `@import url("${fontURL}");\n` : '';
278
279
  const light = deriveTheme(input, { mode: 'light' }).tokens;
279
280
  const dark = deriveTheme(input, { mode: 'dark' }).tokens;
280
281
  const palette = [], geometry = [];
282
+ const relativeLengths = new Map(themeFields.filter(field => field.property && field.type.includes('length') && recipe.values[field.id].endsWith('rem')).map(field => [field.property, recipe.values[field.id]]));
281
283
  for (const [name, value] of Object.entries(light)) {
282
284
  if (name === 'color-scheme') continue;
283
285
  if (/^#[\da-f]{6}$/i.test(value)) palette.push(` ${name}: light-dark(${value}, ${dark[name]});`);
284
- else geometry.push(` ${name}: ${value};`);
286
+ else {
287
+ const relative = relativeLengths.get(name);
288
+ const resolved = relative ? value.replace(relative, `calc(${parseFloat(relative)} * var(--_ui-root-font-unit, 1rem))`) : value;
289
+ geometry.push(` ${name}: ${resolved};`);
290
+ }
285
291
  }
286
292
  const geometrySelector = scope === 'documentation' ? ':root[data-looma-documentation-theme]' : ':root';
287
293
  const chevron = color => `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='${color.replace('#', '%23')}' stroke-width='2.25' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E")`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadlabs/looma",
3
- "version": "0.39.2",
3
+ "version": "0.39.4",
4
4
  "type": "module",
5
5
  "description": "Looma components as HTML, Vue, and plain DOM, with design tokens.",
6
6
  "license": "MIT",
@@ -198,10 +198,12 @@
198
198
  color: inherit;
199
199
  }
200
200
  input {
201
+ --_ui-combobox-inset-block: var(--ui-control-inset-block-md, var(--ui-space-2));
201
202
  appearance: none;
202
203
  width: 100%;
203
204
  min-inline-size: 0;
204
205
  min-block-size: calc(var(--_row) - 2 * var(--ui-border-width));
206
+ max-block-size: max(calc(var(--_row) - 2 * var(--ui-border-width)), calc(1em * var(--ui-line-height-tight) + 2 * var(--_ui-combobox-inset-block)));
205
207
  box-sizing: border-box !important;
206
208
  border: 0;
207
209
  border-radius: inherit;
@@ -209,7 +211,7 @@
209
211
  color: inherit;
210
212
  font: inherit;
211
213
  line-height: var(--ui-line-height-tight);
212
- padding: var(--ui-control-inset-block-md, var(--ui-space-2)) var(--ui-space-3) !important;
214
+ padding: var(--_ui-combobox-inset-block) var(--ui-space-3) !important;
213
215
  margin: 0 !important;
214
216
  outline: none;
215
217
  }
@@ -217,14 +219,16 @@
217
219
  --_row: var(--ui-control-size-sm);
218
220
  }
219
221
  :scope:where([data-component~="ui-combobox"])[data-ui-combobox-state~="size=sm"] input {
220
- padding: var(--ui-control-inset-block-sm, var(--ui-space-1)) var(--ui-space-2) !important;
222
+ --_ui-combobox-inset-block: var(--ui-control-inset-block-sm, var(--ui-space-1));
223
+ padding: var(--_ui-combobox-inset-block) var(--ui-space-2) !important;
221
224
  font-size: var(--ui-font-size-sm);
222
225
  }
223
226
  :scope:where([data-component~="ui-combobox"])[data-ui-combobox-state~="size=lg"] {
224
227
  --_row: var(--ui-control-size-lg);
225
228
  }
226
229
  :scope:where([data-component~="ui-combobox"])[data-ui-combobox-state~="size=lg"] input {
227
- padding: var(--ui-control-inset-block-lg, var(--ui-space-3)) var(--ui-space-4) !important;
230
+ --_ui-combobox-inset-block: var(--ui-control-inset-block-lg, var(--ui-space-3));
231
+ padding: var(--_ui-combobox-inset-block) var(--ui-space-4) !important;
228
232
  font-size: var(--ui-font-size-md);
229
233
  }
230
234
  button {
@@ -359,6 +363,7 @@
359
363
  display: none !important;
360
364
  }
361
365
  :scope:where([data-component~="ui-combobox"])[data-ui-combobox-state~="multiple"] input {
366
+ max-block-size: none;
362
367
  flex: 1 0 var(--_ui-combobox-item-input-min-inline-size, 5rem);
363
368
  width: auto;
364
369
  min-block-size: 1.75rem;
@@ -617,10 +622,12 @@
617
622
  color: inherit;
618
623
  }
619
624
  input:where(:scope, :scope *) {
625
+ --_ui-combobox-inset-block: var(--ui-control-inset-block-md, var(--ui-space-2));
620
626
  appearance: none;
621
627
  width: 100%;
622
628
  min-inline-size: 0;
623
629
  min-block-size: calc(var(--_row) - 2 * var(--ui-border-width));
630
+ max-block-size: max(calc(var(--_row) - 2 * var(--ui-border-width)), calc(1em * var(--ui-line-height-tight) + 2 * var(--_ui-combobox-inset-block)));
624
631
  box-sizing: border-box !important;
625
632
  border: 0;
626
633
  border-radius: inherit;
@@ -628,7 +635,7 @@
628
635
  color: inherit;
629
636
  font: inherit;
630
637
  line-height: var(--ui-line-height-tight);
631
- padding: var(--ui-control-inset-block-md, var(--ui-space-2)) var(--ui-space-3) !important;
638
+ padding: var(--_ui-combobox-inset-block) var(--ui-space-3) !important;
632
639
  margin: 0 !important;
633
640
  outline: none;
634
641
  }
@@ -636,14 +643,16 @@
636
643
  --_row: var(--ui-control-size-sm);
637
644
  }
638
645
  :where([data-component~="ui-combobox"])[data-ui-combobox-state~="size=sm"] input:where(:scope, :not([data-component], [data-slotted]:not([data-slotted~="ui-combobox"]))) {
639
- padding: var(--ui-control-inset-block-sm, var(--ui-space-1)) var(--ui-space-2) !important;
646
+ --_ui-combobox-inset-block: var(--ui-control-inset-block-sm, var(--ui-space-1));
647
+ padding: var(--_ui-combobox-inset-block) var(--ui-space-2) !important;
640
648
  font-size: var(--ui-font-size-sm);
641
649
  }
642
650
  :where([data-component~="ui-combobox"])[data-ui-combobox-state~="size=lg"]:where(:scope, :not([data-component], [data-slotted]:not([data-slotted~="ui-combobox"]))) {
643
651
  --_row: var(--ui-control-size-lg);
644
652
  }
645
653
  :where([data-component~="ui-combobox"])[data-ui-combobox-state~="size=lg"] input:where(:scope, :not([data-component], [data-slotted]:not([data-slotted~="ui-combobox"]))) {
646
- padding: var(--ui-control-inset-block-lg, var(--ui-space-3)) var(--ui-space-4) !important;
654
+ --_ui-combobox-inset-block: var(--ui-control-inset-block-lg, var(--ui-space-3));
655
+ padding: var(--_ui-combobox-inset-block) var(--ui-space-4) !important;
647
656
  font-size: var(--ui-font-size-md);
648
657
  }
649
658
  button:where(:scope, :scope *) {
@@ -776,6 +785,7 @@
776
785
  display: none !important;
777
786
  }
778
787
  :where([data-component~="ui-combobox"])[data-ui-combobox-state~="multiple"] input:where(:scope, :not([data-component], [data-slotted]:not([data-slotted~="ui-combobox"]))) {
788
+ max-block-size: none;
779
789
  flex: 1 0 var(--_ui-combobox-item-input-min-inline-size, 5rem);
780
790
  width: auto;
781
791
  min-block-size: 1.75rem;
@@ -193,58 +193,10 @@
193
193
  }
194
194
 
195
195
  .close {
196
- display: inline-grid !important;
197
- place-items: center;
198
- flex: none;
196
+ display: flex !important;
197
+ align-items: center !important;
198
+ flex: none !important;
199
199
  margin-inline-start: auto !important;
200
- inline-size: var(--ui-control-size-sm);
201
- block-size: var(--ui-control-size-sm);
202
- padding: 0 !important;
203
- border: 0;
204
- border-radius: var(--ui-radius-round);
205
- background: transparent;
206
- color: var(--ui-text-secondary);
207
- cursor: pointer;
208
- transition: background-color var(--ui-motion-fast) var(--ui-motion-ease), color var(--ui-motion-fast) var(--ui-motion-ease);
209
- }
210
-
211
- /* A CSS-drawn X. */
212
- .close:where(:not([data-component], [data-slotted]))::before,
213
- .close:where(:not([data-component], [data-slotted]))::after {
214
- content: "";
215
- grid-area: 1 / 1;
216
- inline-size: 0.875rem;
217
- block-size: 1.5px;
218
- border-radius: 1px;
219
- background: currentColor;
220
- transform: rotate(45deg);
221
- }
222
-
223
- .close:where(:not([data-component], [data-slotted]))::after {
224
- transform: rotate(-45deg);
225
- }
226
-
227
- .close:hover {
228
- background: var(--ui-surface-muted);
229
- color: var(--ui-text-primary);
230
- }
231
-
232
- .close:focus-visible {
233
- outline: var(--ui-focus-width) solid var(--ui-focus-ring);
234
- outline-offset: var(--ui-focus-width);
235
- }
236
-
237
- /* A touch target, like Button on coarse pointers. */
238
- @media (pointer: coarse) {
239
- .close {
240
- inline-size: var(--ui-control-min-block-size);
241
- block-size: var(--ui-control-min-block-size);
242
- }
243
- }
244
-
245
- html[data-ui-input-modality="touch"] :scope:where([data-component~="ui-dialog"]) .close {
246
- inline-size: var(--ui-control-min-block-size);
247
- block-size: var(--ui-control-min-block-size);
248
200
  }
249
201
 
250
202
  .body {
@@ -320,12 +272,6 @@
320
272
  /* Alerts have no close target to add height above the title. */
321
273
 
322
274
  /* :host-qualified so page heading rules that reach in cannot outrank the dialog chrome. */
323
-
324
- /* A CSS-drawn X. */
325
-
326
- /* A touch target, like Button on coarse pointers. */
327
- @media (pointer: coarse) {
328
- }
329
275
  }
330
276
  @scope ([data-slotted~="ui-dialog"]:not([data-component])) to ([data-component], [data-slotted]) {
331
277
  /* This component's hooks style the element they are set on, and never a nested one. */
@@ -514,57 +460,10 @@
514
460
  }
515
461
 
516
462
  .close:where(:scope, :scope *) {
517
- display: inline-grid !important;
518
- place-items: center;
519
- flex: none;
463
+ display: flex !important;
464
+ align-items: center !important;
465
+ flex: none !important;
520
466
  margin-inline-start: auto !important;
521
- inline-size: var(--ui-control-size-sm);
522
- block-size: var(--ui-control-size-sm);
523
- padding: 0 !important;
524
- border: 0;
525
- border-radius: var(--ui-radius-round);
526
- background: transparent;
527
- color: var(--ui-text-secondary);
528
- cursor: pointer;
529
- transition: background-color var(--ui-motion-fast) var(--ui-motion-ease), color var(--ui-motion-fast) var(--ui-motion-ease);
530
- }
531
-
532
- /* A CSS-drawn X. */
533
- .close:where(:not([data-component], [data-slotted]:not([data-slotted~="ui-dialog"]))):where(:scope, :scope *)::before, .close:where(:not([data-component], [data-slotted]:not([data-slotted~="ui-dialog"]))):where(:scope, :scope *)::after {
534
- content: "";
535
- grid-area: 1 / 1;
536
- inline-size: 0.875rem;
537
- block-size: 1.5px;
538
- border-radius: 1px;
539
- background: currentColor;
540
- transform: rotate(45deg);
541
- }
542
-
543
- .close:where(:not([data-component], [data-slotted]:not([data-slotted~="ui-dialog"]))):where(:scope, :scope *)::after {
544
- transform: rotate(-45deg);
545
- }
546
-
547
- .close:hover:where(:scope, :scope *) {
548
- background: var(--ui-surface-muted);
549
- color: var(--ui-text-primary);
550
- }
551
-
552
- .close:focus-visible:where(:scope, :scope *) {
553
- outline: var(--ui-focus-width) solid var(--ui-focus-ring);
554
- outline-offset: var(--ui-focus-width);
555
- }
556
-
557
- /* A touch target, like Button on coarse pointers. */
558
- @media (pointer: coarse) {
559
- .close:where(:scope, :scope *) {
560
- inline-size: var(--ui-control-min-block-size);
561
- block-size: var(--ui-control-min-block-size);
562
- }
563
- }
564
-
565
- html[data-ui-input-modality="touch"] :where([data-component~="ui-dialog"]) .close:where(:scope, :not([data-component], [data-slotted]:not([data-slotted~="ui-dialog"]))) {
566
- inline-size: var(--ui-control-min-block-size);
567
- block-size: var(--ui-control-min-block-size);
568
467
  }
569
468
 
570
469
  .body:where(:scope, :scope *) {
@@ -32,6 +32,7 @@
32
32
  Hooks do not inherit, so these are defaults for the input; its own hooks still win. */
33
33
  --_ui-default-input-border-width: 0;
34
34
  --_ui-default-input-min-block-size: 0;
35
+ --_ui-default-input-frame-width: calc(2 * var(--ui-border-width));
35
36
  --_ui-default-input-border: transparent;
36
37
  --_ui-default-input-border-hover: transparent;
37
38
  --_ui-default-input-focus-border: transparent;
@@ -61,15 +61,24 @@
61
61
  syntax: "*";
62
62
  inherits: false;
63
63
  }
64
+ /* Normalize a unitless zero border to a length before the box calculation. */
65
+ @property --_ui-input-stroke {
66
+ syntax: "<length>";
67
+ inherits: false;
68
+ initial-value: 1px;
69
+ }
64
70
 
65
71
  :scope:where([data-component~="ui-input"]) {
72
+ --_ui-input-size: var(--ui-control-size-md);
73
+ --_ui-input-inset-block: var(--ui-control-inset-block-md, var(--ui-space-2));
74
+ --_ui-input-stroke: var(--ui-input-border-width, var(--_ui-default-input-border-width, var(--ui-border-width)));
66
75
  box-sizing: border-box !important;
67
76
  display: inline-block !important;
68
77
  inline-size: var(--ui-input-inline-size, 100%);
69
78
  min-inline-size: 0;
70
79
  min-block-size: var(--ui-input-min-block-size, var(--_ui-default-input-min-block-size, var(--ui-control-size-md)));
71
80
  margin: 0 !important;
72
- padding: var(--ui-control-inset-block-md, var(--ui-space-2)) var(--ui-space-3) !important;
81
+ padding: var(--_ui-input-inset-block) var(--ui-space-3) !important;
73
82
  appearance: none;
74
83
  padding-inline-start: var(--_ui-default-input-padding-inline-start, var(--ui-space-3)) !important;
75
84
  border: var(--ui-input-border-width, var(--_ui-default-input-border-width, var(--ui-border-width))) solid
@@ -87,16 +96,25 @@
87
96
  background-color var(--ui-motion-fast) var(--ui-motion-ease);
88
97
  }
89
98
 
99
+ /* Cap Safari's excess text metrics without changing native date, file, color or range widgets. */
100
+ :scope:where([data-component~="ui-input"]):is([type="text"], [type="search"], [type="tel"], [type="url"], [type="email"], [type="password"], [type="number"]) {
101
+ max-block-size: max(calc(var(--_ui-input-size) - var(--_ui-default-input-frame-width, 0px)), calc(1em * var(--ui-line-height-tight) + 2 * var(--_ui-input-inset-block) + 2 * var(--_ui-input-stroke)));
102
+ }
103
+
90
104
  /* Each size reads the hook and then an Input Group's default first, so a group, which draws the frame, can zero it. */
91
105
  :scope:where([data-component~="ui-input"])[data-ui-input-state~="size=sm"] {
106
+ --_ui-input-size: var(--ui-control-size-sm);
107
+ --_ui-input-inset-block: var(--ui-control-inset-block-sm, var(--ui-space-1));
92
108
  min-block-size: var(--ui-input-min-block-size, var(--_ui-default-input-min-block-size, var(--ui-control-size-sm)));
93
- padding: var(--ui-control-inset-block-sm, var(--ui-space-1)) var(--ui-space-2) !important;
109
+ padding: var(--_ui-input-inset-block) var(--ui-space-2) !important;
94
110
  font-size: var(--ui-font-size-sm);
95
111
  }
96
112
 
97
113
  :scope:where([data-component~="ui-input"])[data-ui-input-state~="size=lg"] {
114
+ --_ui-input-size: var(--ui-control-size-lg);
115
+ --_ui-input-inset-block: var(--ui-control-inset-block-lg, var(--ui-space-3));
98
116
  min-block-size: var(--ui-input-min-block-size, var(--_ui-default-input-min-block-size, var(--ui-control-size-lg)));
99
- padding: var(--ui-control-inset-block-lg, var(--ui-space-3)) var(--ui-space-4) !important;
117
+ padding: var(--_ui-input-inset-block) var(--ui-space-4) !important;
100
118
  font-size: var(--ui-font-size-md);
101
119
  }
102
120
 
@@ -177,6 +195,9 @@
177
195
  @scope ([data-component~="ui-input"]) to ([data-component]) {
178
196
  /* This component's hooks style the element they are set on, and never a nested one. An Input
179
197
  Group sets --_ui-default-input-* for the input inside it, below the input's own hooks. */
198
+ /* Normalize a unitless zero border to a length before the box calculation. */
199
+
200
+ /* Cap Safari's excess text metrics without changing native date, file, color or range widgets. */
180
201
 
181
202
  /* Each size reads the hook and then an Input Group's default first, so a group, which draws the frame, can zero it. */
182
203
 
@@ -50,13 +50,16 @@
50
50
  }
51
51
 
52
52
  :scope:where([data-component~="ui-select"]) {
53
+ --_ui-select-size: var(--ui-control-size-md);
54
+ --_ui-select-inset-block: var(--ui-control-inset-block-md, var(--ui-space-2));
53
55
  box-sizing: border-box !important;
54
56
  display: inline-block !important;
55
57
  inline-size: var(--ui-select-inline-size, 100%);
56
58
  min-inline-size: 0;
59
+ max-block-size: max(var(--_ui-select-size), calc(1em * var(--ui-line-height-tight) + 2 * var(--_ui-select-inset-block) + 2 * var(--ui-border-width)));
57
60
  min-block-size: var(--ui-control-size-md);
58
61
  margin: 0 !important;
59
- padding: var(--ui-control-inset-block-md, var(--ui-space-2)) var(--ui-space-3) !important;
62
+ padding: var(--_ui-select-inset-block) var(--ui-space-3) !important;
60
63
  border: var(--ui-border-width) solid var(--ui-select-border, var(--ui-control-border, var(--ui-border-strong)));
61
64
  border-radius: var(--ui-select-radius, var(--ui-field-radius, var(--ui-radius-md)));
62
65
  background-color: var(--ui-select-surface, var(--ui-control-surface, var(--ui-surface-elevated)));
@@ -129,8 +132,10 @@
129
132
  }
130
133
 
131
134
  :scope:where([data-component~="ui-select"])[data-ui-select-state~="size=sm"] {
135
+ --_ui-select-size: var(--ui-control-size-sm);
136
+ --_ui-select-inset-block: var(--ui-control-inset-block-sm, var(--ui-space-1));
132
137
  min-block-size: var(--ui-control-size-sm);
133
- padding: var(--ui-control-inset-block-sm, var(--ui-space-1)) var(--ui-space-2) !important;
138
+ padding: var(--_ui-select-inset-block) var(--ui-space-2) !important;
134
139
  padding-inline-end: calc(var(--ui-space-2) + var(--ui-icon-size-sm) + var(--ui-space-1)) !important;
135
140
  background-position: right var(--ui-space-2) center, center;
136
141
  background-size:
@@ -140,8 +145,10 @@
140
145
  }
141
146
 
142
147
  :scope:where([data-component~="ui-select"])[data-ui-select-state~="size=lg"] {
148
+ --_ui-select-size: var(--ui-control-size-lg);
149
+ --_ui-select-inset-block: var(--ui-control-inset-block-lg, var(--ui-space-3));
143
150
  min-block-size: var(--ui-control-size-lg);
144
- padding: var(--ui-control-inset-block-lg, var(--ui-space-3)) var(--ui-space-4) !important;
151
+ padding: var(--_ui-select-inset-block) var(--ui-space-4) !important;
145
152
  padding-inline-end: calc(var(--ui-space-4) + 1.5rem) !important;
146
153
  background-position: right var(--ui-space-4) center, center;
147
154
  font-size: var(--ui-font-size-md);
package/theme-system.css CHANGED
@@ -8,11 +8,14 @@
8
8
  /* Keep the root font seed unregistered: a registered rem length used by root
9
9
  font-size creates a dependency cycle (CSS Properties and Values API 2.7.2). */
10
10
  @property --ui-control-size { syntax: "<length>"; inherits: true; initial-value: 40px; }
11
+ @property --_ui-root-font-unit { syntax: "<length>"; inherits: true; initial-value: 16px; }
11
12
  @property --ui-radius-md { syntax: "<length>"; inherits: true; initial-value: 8px; }
12
13
  @property --ui-border-width { syntax: "<length>"; inherits: true; initial-value: 1px; }
13
14
 
14
15
  @layer theme {
15
16
  :root {
17
+ /* Resolve once on the root: WebKit can resolve registered rem lengths against the initial font size. */
18
+ --_ui-root-font-unit: 1em;
16
19
  --ui-spacing: 1;
17
20
  --ui-rhythm: 1;
18
21
  --ui-control-roominess: 2.5;
@@ -35,7 +38,7 @@
35
38
  :where(:root, [data-theme], [data-contrast]) {
36
39
  /* Controls use a ratio. Spacing uses an independent curve, so increasing the size
37
40
  contrast cannot unexpectedly turn linear spacing into quadratic spacing. */
38
- --ui-control-size: calc(1rem * var(--ui-control-roominess));
41
+ --ui-control-size: calc(var(--_ui-root-font-unit) * var(--ui-control-roominess));
39
42
  --_ui-theme-ratio: max(1, var(--ui-scale-ratio));
40
43
  --_ui-theme-unit: calc(1rem * 0.25 * max(0, var(--ui-spacing)) * clamp(0.75, var(--ui-rhythm), 1.5));
41
44
  --ui-line-height-tight: clamp(1.1, calc(1.2 + (var(--ui-rhythm) - 1) * 0.2), 1.3);
@@ -3,7 +3,7 @@ import { attachGeneratedController, buildTemplate, clearRegion, toAttribute, toT
3
3
  import * as controller from "../components/ui-dialog/ui-dialog.js";
4
4
  import "../styles/ui-dialog.css";
5
5
 
6
- const T0 = ["",[],"\n ",["header",[],"\n ",1,"\n ",["button",["class","close","type","button","aria-label","Close"]],"\n "],"\n ",["div",["class","body"],["ui-scroll-area",[],4]],"\n ",["footer",[],4],"\n "];
6
+ const T0 = ["",[],"\n ",["header",[],"\n ",1,"\n ",["div",["class","close"],["ui-icon-button",["size","sm","round","","label","Close"],["ui-icon",["name","x"]]]],"\n "],"\n ",["div",["class","body"],["ui-scroll-area",[],4]],"\n ",["footer",[],4],"\n "];
7
7
  const T1 = ["h2",["class","title"],0];
8
8
  const T2 = ["",[]];
9
9
  const T3 = ["",[]];
@@ -82,7 +82,7 @@ export function createUiDialog(options = {}, html) {
82
82
  };
83
83
  const t0 = element.firstChild.nextSibling;
84
84
  const t1 = t0.firstChild.nextSibling;
85
- const t2 = t1.nextSibling.nextSibling.nextSibling;
85
+ const t2 = t1.nextSibling.nextSibling.nextSibling.firstChild;
86
86
  const t3 = t0.nextSibling.nextSibling;
87
87
  const t4 = t3.firstChild;
88
88
  const t5 = t4.firstChild;