@threadlabs/looma 0.33.1 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/components/shared/overlay.js +8 -2
  2. package/components/shared/surface-material.js +21 -0
  3. package/components/ui-button/ui-button.html +63 -47
  4. package/components/ui-card/ui-card.html +44 -2
  5. package/components/ui-curve-editor/curve.js +10 -0
  6. package/components/ui-curve-editor/ui-curve-editor.html +181 -0
  7. package/components/ui-curve-editor/ui-curve-editor.js +51 -0
  8. package/components/ui-dialog/ui-dialog.html +62 -1
  9. package/components/ui-dialog/ui-dialog.js +9 -1
  10. package/components/ui-icon-button/ui-icon-button.html +75 -23
  11. package/components/ui-menu/ui-menu.html +60 -0
  12. package/components/ui-menu/ui-menu.js +1 -1
  13. package/components/ui-number-input/arithmetic.js +47 -0
  14. package/components/ui-number-input/ui-number-input.html +62 -0
  15. package/components/ui-number-input/ui-number-input.js +44 -0
  16. package/components/ui-popover/ui-popover.html +64 -1
  17. package/components/ui-popover/ui-popover.js +1 -1
  18. package/components/ui-slider/ui-slider.html +84 -0
  19. package/components/ui-slider/ui-slider.js +29 -0
  20. package/components/ui-tabs/ui-tabs.html +6 -1
  21. package/components/ui-text/ui-text.html +31 -6
  22. package/components/ui-tooltip/ui-tooltip.html +60 -0
  23. package/components/ui-tooltip/ui-tooltip.js +1 -1
  24. package/dist/index.js +46 -43
  25. package/docs/README.md +34 -4
  26. package/docs/card.html +16 -2
  27. package/docs/card.js +17 -2
  28. package/docs/catalog.json +1154 -144
  29. package/docs/customizer-render.mjs +35 -9
  30. package/docs/customizer.js +83 -20
  31. package/docs/documentation.css +9 -1
  32. package/docs/index.d.ts +4 -2
  33. package/docs/index.mjs +1 -1
  34. package/docs/preview.js +2 -0
  35. package/docs/render.mjs +15 -6
  36. package/docs/site.mjs +50 -9
  37. package/docs/theme-session.mjs +51 -0
  38. package/docs/theme.mjs +134 -15
  39. package/docs/value-editors.mjs +33 -0
  40. package/package.json +2 -2
  41. package/styles/ui-button.css +45 -45
  42. package/styles/ui-card.css +47 -0
  43. package/styles/ui-curve-editor.css +109 -0
  44. package/styles/ui-dialog.css +132 -0
  45. package/styles/ui-icon-button.css +63 -20
  46. package/styles/ui-menu.css +72 -0
  47. package/styles/ui-number-input.css +34 -0
  48. package/styles/ui-popover.css +74 -0
  49. package/styles/ui-slider.css +53 -0
  50. package/styles/ui-tabs.css +6 -1
  51. package/styles/ui-text.css +23 -1
  52. package/styles/ui-tooltip.css +72 -0
  53. package/theme-system.css +26 -10
  54. package/tokens.css +8 -2
  55. package/vanilla/UiButton.d.ts +1 -0
  56. package/vanilla/UiButton.js +195 -130
  57. package/vanilla/UiCard.d.ts +1 -1
  58. package/vanilla/UiCard.js +1 -1
  59. package/vanilla/UiCurveEditor.d.ts +28 -0
  60. package/vanilla/UiCurveEditor.js +134 -0
  61. package/vanilla/UiIconButton.d.ts +2 -1
  62. package/vanilla/UiIconButton.js +58 -31
  63. package/vanilla/UiNumberInput.d.ts +33 -0
  64. package/vanilla/UiNumberInput.js +79 -0
  65. package/vanilla/UiSlider.d.ts +33 -0
  66. package/vanilla/UiSlider.js +125 -0
  67. package/vanilla/UiText.d.ts +2 -2
  68. package/vanilla/UiText.js +45 -11
  69. package/vanilla/index.js +3 -0
  70. package/vue/UiButton.d.ts +6 -0
  71. package/vue/UiButton.vue +155 -56
  72. package/vue/UiCard.d.ts +2 -2
  73. package/vue/UiCard.vue +55 -5
  74. package/vue/UiCurveEditor.d.ts +53 -0
  75. package/vue/UiCurveEditor.js +2 -0
  76. package/vue/UiCurveEditor.vue +427 -0
  77. package/vue/UiDialog.vue +177 -1
  78. package/vue/UiIconButton.d.ts +8 -2
  79. package/vue/UiIconButton.vue +164 -34
  80. package/vue/UiMenu.vue +89 -0
  81. package/vue/UiNumberInput.d.ts +83 -0
  82. package/vue/UiNumberInput.js +2 -0
  83. package/vue/UiNumberInput.vue +370 -0
  84. package/vue/UiPopover.vue +92 -1
  85. package/vue/UiSlider.d.ts +83 -0
  86. package/vue/UiSlider.js +2 -0
  87. package/vue/UiSlider.vue +412 -0
  88. package/vue/UiTabs.vue +6 -1
  89. package/vue/UiText.d.ts +7 -5
  90. package/vue/UiText.vue +104 -22
  91. package/vue/UiTooltip.vue +89 -0
  92. package/vue/chunks/UiActionBar.js +1 -1
  93. package/vue/chunks/UiAffordanceScope.js +2 -2
  94. package/vue/chunks/UiAvatar.js +2 -2
  95. package/vue/chunks/UiAvatarGroup.js +2 -2
  96. package/vue/chunks/UiBadge.js +2 -2
  97. package/vue/chunks/UiBreadcrumbItem.js +2 -2
  98. package/vue/chunks/UiBreadcrumbs.js +2 -2
  99. package/vue/chunks/UiButton.js +94 -11
  100. package/vue/chunks/UiCallout.js +2 -2
  101. package/vue/chunks/UiCard.js +6 -5
  102. package/vue/chunks/UiCheckbox.js +2 -2
  103. package/vue/chunks/UiCluster.js +2 -2
  104. package/vue/chunks/UiColorPicker.js +2 -2
  105. package/vue/chunks/UiCombobox.js +2 -2
  106. package/vue/chunks/UiContainer.js +2 -2
  107. package/vue/chunks/UiContextMenu.js +2 -2
  108. package/vue/chunks/UiCover.js +2 -2
  109. package/vue/chunks/UiCurveEditor.js +413 -0
  110. package/vue/chunks/UiDescriptionItem.js +2 -2
  111. package/vue/chunks/UiDescriptionList.js +2 -2
  112. package/vue/chunks/UiDialog.js +4 -4
  113. package/vue/chunks/UiDisclosure.js +2 -2
  114. package/vue/chunks/UiEditable.js +2 -2
  115. package/vue/chunks/UiEditorBlockActions.js +2 -2
  116. package/vue/chunks/UiEditorChipEditor.js +2 -2
  117. package/vue/chunks/UiEditorCodeLanguage.js +2 -2
  118. package/vue/chunks/UiEditorFootnoteEditor.js +2 -2
  119. package/vue/chunks/UiEditorImageToolbar.js +3 -3
  120. package/vue/chunks/UiEditorInsertTableGrid.js +2 -2
  121. package/vue/chunks/UiEditorLinkCard.js +3 -3
  122. package/vue/chunks/UiEditorLinkForm.js +3 -3
  123. package/vue/chunks/UiEditorMentionMenu.js +2 -2
  124. package/vue/chunks/UiEditorMentionMenuItem.js +2 -2
  125. package/vue/chunks/UiEditorSlashMenu.js +2 -2
  126. package/vue/chunks/UiEditorSlashMenuGroup.js +2 -2
  127. package/vue/chunks/UiEditorSlashMenuItem.js +2 -2
  128. package/vue/chunks/UiEditorSurface.js +2 -2
  129. package/vue/chunks/UiEditorTableContextMenu.js +2 -2
  130. package/vue/chunks/UiEditorTableOverlay.js +2 -2
  131. package/vue/chunks/UiEditorTableToolbar.js +2 -2
  132. package/vue/chunks/UiEditorToolbar.js +2 -2
  133. package/vue/chunks/UiFloatingActionButton.js +2 -2
  134. package/vue/chunks/UiFormField.js +2 -2
  135. package/vue/chunks/UiGrid.js +2 -2
  136. package/vue/chunks/UiIcon.js +2 -2
  137. package/vue/chunks/UiIconButton.js +77 -11
  138. package/vue/chunks/UiImage.js +2 -2
  139. package/vue/chunks/UiInput.js +2 -2
  140. package/vue/chunks/UiInputGroup.js +2 -2
  141. package/vue/chunks/UiList.js +2 -2
  142. package/vue/chunks/UiListItem.js +2 -2
  143. package/vue/chunks/UiListbox.js +2 -2
  144. package/vue/chunks/UiMenu.js +3 -3
  145. package/vue/chunks/UiMenuGroup.js +2 -2
  146. package/vue/chunks/UiMenuItem.js +2 -2
  147. package/vue/chunks/UiMeter.js +2 -2
  148. package/vue/chunks/UiNavItem.js +2 -2
  149. package/vue/chunks/UiNumberInput.js +390 -0
  150. package/vue/chunks/UiPageHeader.js +2 -2
  151. package/vue/chunks/UiPopover.js +4 -4
  152. package/vue/chunks/UiRadio.js +2 -2
  153. package/vue/chunks/UiRadioGroup.js +2 -2
  154. package/vue/chunks/UiReel.js +2 -2
  155. package/vue/chunks/UiScrollArea.js +2 -2
  156. package/vue/chunks/UiSearchResultRow.js +2 -2
  157. package/vue/chunks/UiSearchShell.js +2 -2
  158. package/vue/chunks/UiSection.js +2 -2
  159. package/vue/chunks/UiSelect.js +2 -2
  160. package/vue/chunks/UiSeparator.js +2 -2
  161. package/vue/chunks/UiSidebar.js +2 -2
  162. package/vue/chunks/UiSlider.js +396 -0
  163. package/vue/chunks/UiSpinner.js +2 -2
  164. package/vue/chunks/UiStack.js +2 -2
  165. package/vue/chunks/UiStatusMessage.js +2 -2
  166. package/vue/chunks/UiSwatch.js +2 -2
  167. package/vue/chunks/UiSwitch.js +2 -2
  168. package/vue/chunks/UiSwitcher.js +2 -2
  169. package/vue/chunks/UiTable.js +2 -2
  170. package/vue/chunks/UiTabs.js +3 -3
  171. package/vue/chunks/UiText.js +53 -12
  172. package/vue/chunks/UiTextarea.js +2 -2
  173. package/vue/chunks/UiToast.js +2 -2
  174. package/vue/chunks/UiToastRegion.js +2 -2
  175. package/vue/chunks/UiTooltip.js +3 -3
  176. package/vue/chunks/UiTopBar.js +1 -1
  177. package/vue/chunks/UiTree.js +2 -2
  178. package/vue/chunks/UiTreeItem.js +2 -2
  179. package/vue/chunks/_plugin-vue_export-helper.js +68 -2
  180. package/vue/chunks/props.js +24 -1
  181. package/vue/components.css +2215 -1203
  182. package/vue/editor/index.js +623 -211
  183. package/vue/index.d.ts +3 -0
  184. package/vue/index.js +5 -2
@@ -1,3 +1,5 @@
1
+ import { updateSurfaceMaterial } from "./surface-material.js";
2
+
1
3
  const dialogInvokers = new WeakMap();
2
4
 
3
5
  function stateFor(document) {
@@ -345,7 +347,10 @@ export function createViewportSurface(surface, options = {}) {
345
347
  let frame = null;
346
348
  const position = () => {
347
349
  frame = null;
348
- if (stop) options.position?.();
350
+ if (stop) {
351
+ if (options.materialRole) updateSurfaceMaterial(surface, options.materialRole);
352
+ options.position?.();
353
+ }
349
354
  };
350
355
  const refresh = () => {
351
356
  if (stop && frame === null) frame = owner.requestAnimationFrame(position);
@@ -378,6 +383,7 @@ export function createViewportSurface(surface, options = {}) {
378
383
  show() {
379
384
  if (destroyed) return;
380
385
  show(surface);
386
+ if (options.materialRole) updateSurfaceMaterial(surface, options.materialRole);
381
387
  stop ??= observeOverlayViewport(surface.ownerDocument, position);
382
388
  options.position?.();
383
389
  if (!sizeObserver && owner.ResizeObserver) {
@@ -435,7 +441,7 @@ export function createAnchoredSurface(surface, options = {}) {
435
441
  surface.style.position = "fixed";
436
442
  surface.style.setProperty("margin", "0", "important");
437
443
  // `bare` resets the browser's popover chrome on a surface that has none of its own.
438
- const presentation = createViewportSurface(surface, { position, bare: options.bare });
444
+ const presentation = createViewportSurface(surface, { position, bare: options.bare, materialRole: options.materialRole });
439
445
  // Resize and scroll events do not report an ancestor's transition or layout shift.
440
446
  // While visible, sample the live anchor and only re-position when its bounds change.
441
447
  const trackAnchor = () => {
@@ -0,0 +1,21 @@
1
+ // Purpose has greater influence than size: even a large menu stays lighter than a dialog.
2
+ const roleWeight = Object.freeze({ tooltip: 0.08, menu: 0.28, popover: 0.55, dialog: 1, panel: 0.65 });
3
+
4
+ /** Derive a bounded material scale from a surface's purpose and rendered border box. */
5
+ export function surfaceMaterialScale(role, width, height) {
6
+ if (!Object.hasOwn(roleWeight, role)) throw new Error(`Unknown surface material role: ${role}`);
7
+ const measured = [width, height].every(value => Number.isFinite(value) && value > 0);
8
+ const size = measured ? Math.max(0.8, Math.min(1.35, Math.sqrt(width * height / (320 * 240)))) : 1;
9
+ return Math.round(roleWeight[role] * size * 1000) / 1000;
10
+ }
11
+
12
+ /** Update component-owned material geometry; a tolerance prevents its rim feeding tiny resize changes back into itself. */
13
+ export function updateSurfaceMaterial(element, role) {
14
+ const width = element.offsetWidth;
15
+ const height = element.offsetHeight;
16
+ const next = surfaceMaterialScale(role, width, height);
17
+ const previous = Number.parseFloat(element.style.getPropertyValue("--_ui-material-scale"));
18
+ if (!Number.isFinite(previous) || Math.abs(next - previous) >= Math.max(0.002, Math.abs(previous) * 0.04)) {
19
+ element.style.setProperty("--_ui-material-scale", String(next));
20
+ }
21
+ }
@@ -2,6 +2,14 @@
2
2
  <link rel="component" href="../ui-icon/ui-icon.html">
3
3
  <template component="ui-button" module="./ui-button.js">
4
4
  <defs>
5
+ <prop name="preset" type="keyword" values="none, primary, standard, quiet, destructive" default="none"
6
+ >A shortcut that sets appearance and tone together: primary is solid accent, standard is solid neutral, quiet is ghost neutral, and destructive is solid
7
+ danger. A named preset takes precedence over variant and tone. Leave it at none to choose variant and tone independently.</prop>
8
+ <computed name="appearance" from="{ none: $variant, primary: 'solid', standard: 'solid', quiet: 'ghost', destructive: 'solid' }[$preset]"></computed>
9
+ <computed
10
+ name="colorTone"
11
+ from="{ none: { true: 'danger', false: $tone }[concat($variant = 'danger')], primary: 'accent', standard: 'neutral', quiet: 'neutral', destructive: 'danger' }[$preset]"
12
+ ></computed>
5
13
  <prop name="variant" type="keyword" values="outline, solid, danger, ghost, link, card, field" default="outline"
6
14
  >Emphasis: how loudly the button asks. solid fills with its tone, outline draws its tone at the edge over a wash of it, ghost carries only text until it is
7
15
  hovered, and link is an inline text action that reads as a link. danger is solid in the danger tone, kept for the call sites that name it. card uses the
@@ -42,7 +50,9 @@
42
50
  </defs>
43
51
  <template $match>
44
52
  <a
45
- $when="$variant = 'card' and $as = 'a'"
53
+ from:data-appearance="$appearance"
54
+ from:data-tone="$colorTone"
55
+ $when="$appearance = 'card' and $as = 'a'"
46
56
  from:href="{ true: null, false: $href }[concat($disabled or $loading or $pending)]"
47
57
  from:target="$target"
48
58
  from:rel="$rel"
@@ -57,7 +67,9 @@
57
67
  <span class="card-action"><slot name="action"><ui-icon name="chevron-right" aria-hidden="true"></ui-icon></slot></span>
58
68
  </a>
59
69
  <button
60
- $when="$variant = 'card'"
70
+ from:data-appearance="$appearance"
71
+ from:data-tone="$colorTone"
72
+ $when="$appearance = 'card'"
61
73
  from:type="$type"
62
74
  from:disabled="$disabled"
63
75
  from:aria-disabled="{ true: 'true', false: null }[concat($loading or $pending)]"
@@ -70,6 +82,8 @@
70
82
  <span class="card-action"><slot name="action"><ui-icon name="chevron-right" aria-hidden="true"></ui-icon></slot></span>
71
83
  </button>
72
84
  <a
85
+ from:data-appearance="$appearance"
86
+ from:data-tone="$colorTone"
73
87
  $when="$as = 'a'"
74
88
  from:href="{ true: null, false: $href }[concat($disabled or $loading or $pending)]"
75
89
  from:target="$target"
@@ -83,6 +97,8 @@
83
97
  <span class="badge"><slot name="badge"></slot></span>
84
98
  </a>
85
99
  <button
100
+ from:data-appearance="$appearance"
101
+ from:data-tone="$colorTone"
86
102
  $else
87
103
  from:type="$type"
88
104
  from:disabled="$disabled"
@@ -255,7 +271,7 @@
255
271
 
256
272
  /* A count is an adornment, not another line of label text. Keep the standard control height
257
273
  with enough inset for a 24px mark; large content can still grow the button naturally. */
258
- :host:not([variant="link"]):has(.badge:not(:empty)) {
274
+ :host:not([data-appearance="link"]):has(.badge:not(:empty)) {
259
275
  padding-block: var(--ui-space-0-5) !important;
260
276
  }
261
277
 
@@ -300,7 +316,7 @@
300
316
 
301
317
  /* Card actions inherit Button's palette and behavior; only their shared layout changes.
302
318
  The icon may contain any decorative composition. Empty icons remove their column. */
303
- :host([variant="card"]) {
319
+ :host[data-appearance="card"] {
304
320
  display: grid !important;
305
321
  grid-template-columns: auto minmax(0, 1fr) auto;
306
322
  align-content: center !important;
@@ -313,10 +329,10 @@
313
329
  box-shadow: none;
314
330
  background-image: none;
315
331
  }
316
- :host([variant="card"][size="lg"]) {
332
+ :host[data-appearance="card"][size="lg"] {
317
333
  min-block-size: max(var(--ui-control-min-block-size), var(--ui-control-size-lg));
318
334
  }
319
- :host([variant="card"]):has(.card-icon:empty) {
335
+ :host[data-appearance="card"]:has(.card-icon:empty) {
320
336
  grid-template-columns: minmax(0, 1fr) auto;
321
337
  }
322
338
  .card-icon {
@@ -343,11 +359,11 @@
343
359
  }
344
360
 
345
361
  /* A neutral card encloses content quietly; ordinary outlines keep their action-tone edge. */
346
- :host([variant="card"][tone="neutral"]) {
362
+ :host[data-appearance="card"][data-tone="neutral"] {
347
363
  border-color: var(--ui-button-border, var(--ui-border));
348
364
  }
349
365
 
350
- :host([variant="card"][tone="neutral"]):hover:not(:disabled, [aria-disabled="true"]) {
366
+ :host[data-appearance="card"][data-tone="neutral"]:hover:not(:disabled, [aria-disabled="true"]) {
351
367
  border-color: var(--ui-button-hover-border, var(--ui-border-strong));
352
368
  }
353
369
 
@@ -359,19 +375,19 @@
359
375
  box-shadow: var(--ui-pressed);
360
376
  }
361
377
 
362
- :host([variant="solid"]), :host([variant="danger"]) {
378
+ :host[data-appearance="solid"], :host[data-appearance="danger"] {
363
379
  border-color: var(--_tone);
364
380
  background-color: var(--_tone);
365
381
  color: var(--_on-tone);
366
382
  box-shadow: var(--ui-button-solid-shadow, var(--ui-action-shadow, var(--_ui-action-shadow)));
367
383
  }
368
384
 
369
- :is(:host([variant="solid"]), :host([variant="danger"])):hover:not(:disabled, [aria-disabled="true"]) {
385
+ :is(:host[data-appearance="solid"], :host[data-appearance="danger"]):hover:not(:disabled, [aria-disabled="true"]) {
370
386
  background-color: var(--_tone-hover);
371
387
  border-color: var(--_tone-hover);
372
388
  }
373
389
 
374
- :is(:host([variant="solid"]), :host([variant="danger"])):active:not(:disabled, [aria-disabled="true"]) {
390
+ :is(:host[data-appearance="solid"], :host[data-appearance="danger"]):active:not(:disabled, [aria-disabled="true"]) {
375
391
  background-color: var(--_tone-active);
376
392
  border-color: var(--_tone-active);
377
393
  background-image: none;
@@ -380,7 +396,7 @@
380
396
 
381
397
  /* Tone is the colour, variant is the volume. Every pairing works: a danger outline is a
382
398
  destructive secondary action, not a special case. */
383
- :host([tone="neutral"]) {
399
+ :host[data-tone="neutral"] {
384
400
  --_hue: var(--ui-text);
385
401
  --_hue-hover: var(--ui-text);
386
402
  --_hue-active: var(--ui-text);
@@ -388,40 +404,40 @@
388
404
  --_on-hue: var(--ui-surface);
389
405
  }
390
406
 
391
- :host([tone="danger"]), :host([variant="danger"]) {
407
+ :host[data-tone="danger"], :host[data-appearance="danger"] {
392
408
  --_hue: var(--ui-danger);
393
409
  --_hue-hover: var(--ui-danger-hover);
394
- --_hue-active: var(--ui-danger-hover);
395
- --_hue-text: var(--ui-danger-hover);
410
+ --_hue-active: var(--ui-danger-active, var(--ui-danger-hover));
411
+ --_hue-text: var(--ui-danger-subtle-text);
396
412
  --_on-hue: var(--ui-on-danger);
397
413
  }
398
414
 
399
- :host([tone="success"]) {
415
+ :host[data-tone="success"] {
400
416
  --_hue: var(--ui-success);
401
- --_hue-hover: color-mix(in srgb, var(--ui-success) 85%, #000);
402
- --_hue-active: color-mix(in srgb, var(--ui-success) 75%, #000);
403
- --_hue-text: color-mix(in srgb, var(--ui-success) 85%, #000);
404
- --_on-hue: var(--ui-surface);
417
+ --_hue-hover: var(--ui-success-hover);
418
+ --_hue-active: var(--ui-success-active);
419
+ --_hue-text: var(--ui-success-subtle-text);
420
+ --_on-hue: var(--ui-success-on-strong);
405
421
  }
406
422
 
407
- :host([tone="warning"]) {
423
+ :host[data-tone="warning"] {
408
424
  --_hue: var(--ui-warning);
409
- --_hue-hover: color-mix(in srgb, var(--ui-warning) 85%, #000);
410
- --_hue-active: color-mix(in srgb, var(--ui-warning) 75%, #000);
411
- --_hue-text: color-mix(in srgb, var(--ui-warning) 85%, #000);
412
- --_on-hue: var(--ui-surface);
425
+ --_hue-hover: var(--ui-warning-hover);
426
+ --_hue-active: var(--ui-warning-active);
427
+ --_hue-text: var(--ui-warning-subtle-text);
428
+ --_on-hue: var(--ui-warning-on-strong);
413
429
  }
414
430
 
415
- :host([tone="info"]) {
431
+ :host[data-tone="info"] {
416
432
  --_hue: var(--ui-info);
417
- --_hue-hover: color-mix(in srgb, var(--ui-info) 85%, #000);
418
- --_hue-active: color-mix(in srgb, var(--ui-info) 75%, #000);
419
- --_hue-text: color-mix(in srgb, var(--ui-info) 85%, #000);
420
- --_on-hue: var(--ui-surface);
433
+ --_hue-hover: var(--ui-info-hover);
434
+ --_hue-active: var(--ui-info-active);
435
+ --_hue-text: var(--ui-info-subtle-text);
436
+ --_on-hue: var(--ui-info-on-strong);
421
437
  }
422
438
 
423
439
  /* A value picker is a field, with Select's quiet edge and resting lift. */
424
- :host([variant="field"]) {
440
+ :host[data-appearance="field"] {
425
441
  --_ui-field-shadow: var(--ui-shadow-sm);
426
442
  border-color: var(--ui-button-border, var(--ui-control-border, var(--ui-border-strong)));
427
443
  border-radius: var(--ui-button-radius, var(--ui-field-radius, var(--ui-radius-md)));
@@ -431,23 +447,23 @@
431
447
  box-shadow: var(--ui-button-shadow, var(--_ui-field-shadow));
432
448
  }
433
449
 
434
- :host([variant="field"]):hover:not(:disabled, [aria-disabled="true"]) {
450
+ :host[data-appearance="field"]:hover:not(:disabled, [aria-disabled="true"]) {
435
451
  border-color: var(--ui-button-hover-border, var(--ui-control-border-hover, var(--ui-text-secondary)));
436
452
  background-color: var(--ui-button-hover-surface, var(--ui-control-surface, var(--ui-surface-elevated)));
437
453
  }
438
454
 
439
- :host([variant="field"]):is(:active, [aria-expanded="true"]):not(:disabled, [aria-disabled="true"]) {
455
+ :host[data-appearance="field"]:is(:active, [aria-expanded="true"]):not(:disabled, [aria-disabled="true"]) {
440
456
  --_ui-field-shadow: var(--ui-pressed);
441
457
  }
442
458
 
443
- :host([variant="field"]):focus-visible {
459
+ :host[data-appearance="field"]:focus-visible {
444
460
  border-color: var(--ui-control-focus, var(--ui-accent-solid));
445
461
  box-shadow:
446
462
  var(--ui-button-shadow, var(--_ui-field-shadow)),
447
463
  var(--ui-control-focus-halo-shadow);
448
464
  }
449
465
 
450
- :host([variant="ghost"]) {
466
+ :host[data-appearance="ghost"] {
451
467
  border-color: transparent;
452
468
  background: transparent;
453
469
  box-shadow: none;
@@ -455,14 +471,14 @@
455
471
  }
456
472
 
457
473
  /* Tints derive from the text colour, not a surface token, so hover and press show on any background. */
458
- :host([variant="ghost"]):hover:not(:disabled, [aria-disabled="true"]) {
474
+ :host[data-appearance="ghost"]:hover:not(:disabled, [aria-disabled="true"]) {
459
475
  background-color: var(--ui-button-ghost-hover-surface, oklch(from var(--_tone) l c h / 10%));
460
476
  border-color: transparent;
461
477
  }
462
478
 
463
479
  /* Pressed, or holding its menu open, a ghost button sinks slightly: a light surface with a faint
464
480
  edge and a soft inner shadow. Lighter than hover, so an open menu's trigger never reads as heavy. */
465
- :host([variant="ghost"]):is(:active, [aria-expanded="true"]):not(:disabled, [aria-disabled="true"]) {
481
+ :host[data-appearance="ghost"]:is(:active, [aria-expanded="true"]):not(:disabled, [aria-disabled="true"]) {
466
482
  background-color: var(--ui-button-ghost-active-surface, color-mix(in oklch, var(--ui-surface), var(--_tone) 3%));
467
483
  border-color: transparent;
468
484
  box-shadow: inset 0 0 0 1px oklch(from var(--_tone) l c h / 10%), inset 0 1px 2px oklch(from var(--_tone) l c h / 8%);
@@ -476,13 +492,13 @@
476
492
  var(--ui-focus-halo);
477
493
  }
478
494
 
479
- :host([variant="solid"]):focus-visible {
495
+ :host[data-appearance="solid"]:focus-visible {
480
496
  box-shadow:
481
497
  var(--ui-button-solid-shadow, var(--ui-action-shadow, var(--_ui-action-shadow))),
482
498
  var(--ui-focus-halo);
483
499
  }
484
500
 
485
- :is(:host([variant="ghost"]), :host([variant="link"]), :host([variant="card"])):focus-visible {
501
+ :is(:host[data-appearance="ghost"], :host[data-appearance="link"], :host[data-appearance="card"]):focus-visible {
486
502
  box-shadow: var(--ui-focus-halo);
487
503
  }
488
504
 
@@ -504,7 +520,7 @@
504
520
  }
505
521
 
506
522
  /* Ghost keeps no border, but gains the same neutral surface as other disabled buttons. */
507
- :host([variant="ghost"]):is(:disabled, [aria-disabled="true"]:not([aria-busy="true"])) {
523
+ :host[data-appearance="ghost"]:is(:disabled, [aria-disabled="true"]:not([aria-busy="true"])) {
508
524
  background-color: var(--ui-disabled-surface);
509
525
  border-color: transparent;
510
526
  }
@@ -521,12 +537,12 @@
521
537
  a link-style one keeps its text size, so a standalone one ("See all" under a list) needs the
522
538
  area. xs sits inside a line of text the same way, so it keeps its size and gains the area too.
523
539
  Only these need it: a hit area on every button would reach over its neighbours. */
524
- html[data-ui-input-modality="touch"] :host([variant="link"]),
540
+ html[data-ui-input-modality="touch"] :host[data-appearance="link"],
525
541
  html[data-ui-input-modality="touch"] :host([size="xs"]) {
526
542
  position: relative;
527
543
  }
528
544
 
529
- html[data-ui-input-modality="touch"] :host([variant="link"])::after,
545
+ html[data-ui-input-modality="touch"] :host[data-appearance="link"]::after,
530
546
  html[data-ui-input-modality="touch"] :host([size="xs"])::after {
531
547
  position: absolute;
532
548
  inset: 50% auto auto 50%;
@@ -538,7 +554,7 @@
538
554
 
539
555
  /* Link: an inline text action. No box, padding, or minimum height (inline targets are exempt under
540
556
  WCAG 2.5.8); the inherited font; an underline that strengthens on hover; a visible focus ring. */
541
- :host([variant="link"]) {
557
+ :host[data-appearance="link"] {
542
558
  display: inline !important;
543
559
  min-block-size: 0;
544
560
  padding: 0 !important;
@@ -555,24 +571,24 @@
555
571
  transition: color var(--ui-motion-fast) var(--ui-motion-ease), text-decoration-color var(--ui-motion-fast) var(--ui-motion-ease);
556
572
  }
557
573
 
558
- :host([variant="link"]):hover:not(:disabled, [aria-disabled="true"]) {
574
+ :host[data-appearance="link"]:hover:not(:disabled, [aria-disabled="true"]) {
559
575
  color: var(--ui-button-link-hover-text, var(--ui-text-primary));
560
576
  text-decoration-color: currentColor;
561
577
  background: none;
562
578
  transform: none;
563
579
  }
564
580
 
565
- :host([variant="link"]):active:not(:disabled, [aria-disabled="true"]) {
581
+ :host[data-appearance="link"]:active:not(:disabled, [aria-disabled="true"]) {
566
582
  background: none;
567
583
  transform: none;
568
584
  }
569
585
 
570
- :host([variant="link"]):is(:disabled, [aria-disabled="true"]:not([aria-busy="true"])) {
586
+ :host[data-appearance="link"]:is(:disabled, [aria-disabled="true"]:not([aria-busy="true"])) {
571
587
  background: none;
572
588
  border-color: transparent;
573
589
  }
574
590
 
575
- :host([variant="link"]):focus-visible {
591
+ :host[data-appearance="link"]:focus-visible {
576
592
  box-shadow: none;
577
593
  /* Fallback keeps the ring if the token is missing: an unresolved var() would void the outline. */
578
594
  outline: var(--ui-focus-width) solid var(--ui-focus-ring);
@@ -2,9 +2,10 @@
2
2
  <defs>
3
3
  <prop name="size" type="keyword" values="content, sm, md, lg" default="content"
4
4
  >Optional size preset. content keeps the existing intrinsic sizing; the other sizes constrain the surface for compact, medium, or large content.</prop>
5
- <prop name="variant" type="keyword" values="outlined, subtle, elevated" default="outlined"
5
+ <prop name="variant" type="keyword" values="outlined, subtle, elevated, glass" default="outlined"
6
6
  >The surface. outlined is a bordered panel on the page; subtle is a quiet filled one with no border, for supporting details set apart from the flow;
7
- elevated floats above the page with a shadow, for a view's one card, such as sign-in.</prop>
7
+ elevated floats above the page with a shadow, for a view's one card, such as sign-in. glass frames an opaque readable content plane with frosted material;
8
+ shared glassiness and surface separation govern its treatment.</prop>
8
9
  <prop name="padding" type="keyword" values="xs, sm, md, lg" default="md"
9
10
  >The space inside the card: xs is compact (0.5rem), sm is snug (0.75rem), md is the default (1.5rem), and lg is roomy (2rem).</prop>
10
11
  <prop name="tone" type="keyword" values="neutral, accent, info, success, warning, danger" default="neutral"
@@ -62,6 +63,47 @@
62
63
  box-shadow: var(--ui-shadow-lg);
63
64
  }
64
65
 
66
+ :host([variant="glass"]) {
67
+ --_ui-panel-glass: clamp(0, var(--ui-glassiness, 0), 1);
68
+ --_ui-panel-separation: clamp(0, var(--ui-surface-separation, 0), 1);
69
+ --_ui-panel-rim: calc((var(--_ui-panel-separation) + var(--_ui-panel-glass) * 0.25) * 0.65 * 8px);
70
+ border-width: calc(var(--ui-border-width) + var(--_ui-panel-rim));
71
+ border-color: transparent;
72
+ background:
73
+ linear-gradient(var(--_ui-card-surface), var(--_ui-card-surface)) padding-box,
74
+ linear-gradient(var(--ui-material-glass, var(--ui-surface-raised)), var(--ui-material-glass, var(--ui-surface-raised))) border-box,
75
+ var(--_ui-card-surface) padding-box;
76
+ box-shadow:
77
+ var(--ui-shadow-lg),
78
+ inset 0 0 0 var(--ui-border-width) var(--ui-material-edge, var(--ui-border-strong));
79
+ backdrop-filter: blur(calc(var(--_ui-panel-glass) * 0.65 * 18px)) saturate(calc(1 + var(--_ui-panel-glass) * 0.65 * 0.18));
80
+ -webkit-backdrop-filter: blur(calc(var(--_ui-panel-glass) * 0.65 * 18px)) saturate(calc(1 + var(--_ui-panel-glass) * 0.65 * 0.18));
81
+ }
82
+ @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
83
+ :host([variant="glass"]) {
84
+ background: var(--_ui-card-surface);
85
+ }
86
+ }
87
+ @media (prefers-reduced-transparency: reduce) {
88
+ :host([variant="glass"]) {
89
+ background: var(--_ui-card-surface);
90
+ border-width: var(--ui-border-width);
91
+ border-color: var(--ui-border-strong);
92
+ backdrop-filter: none;
93
+ -webkit-backdrop-filter: none;
94
+ }
95
+ }
96
+ @media (forced-colors: active) {
97
+ :host([variant="glass"]) {
98
+ background: Canvas;
99
+ color: CanvasText;
100
+ border: var(--ui-border-width) solid CanvasText;
101
+ box-shadow: none;
102
+ backdrop-filter: none;
103
+ -webkit-backdrop-filter: none;
104
+ }
105
+ }
106
+
65
107
  :host([tone="accent"]) {
66
108
  border-color: color-mix(in srgb, var(--ui-accent) 25%, var(--ui-border-default));
67
109
  --_ui-card-surface: color-mix(in srgb, var(--ui-accent) 6%, var(--ui-surface-default));
@@ -0,0 +1,10 @@
1
+ export function curvePoints(value) {
2
+ const presets = { linear: [0, 0, 1, 1], ease: [.25, .1, .25, 1], "ease-in": [.42, 0, 1, 1], "ease-out": [0, 0, .58, 1], "ease-in-out": [.42, 0, .58, 1] };
3
+ if (presets[value]) return [...presets[value]];
4
+ const match = /^cubic-bezier\(\s*([^)]*)\s*\)$/.exec(String(value));
5
+ const parts = match?.[1].split(",");
6
+ const points = parts?.map(part => part.trim() === "" ? NaN : Number(part));
7
+ if (!points || points.length !== 4 || points.some(number => !Number.isFinite(number))) throw new Error("Use a cubic-bezier curve.");
8
+ return points.map((number, index) => Math.min(index % 2 ? 2 : 1, Math.max(index % 2 ? -1 : 0, number)));
9
+ }
10
+ export function curveText(points) { return `cubic-bezier(${points.map(number => Number(number.toFixed(3))).join(", ")})`; }
@@ -0,0 +1,181 @@
1
+ <link rel="component" href="../ui-number-input/ui-number-input.html">
2
+ <template component="ui-curve-editor" module="./ui-curve-editor.js">
3
+ <defs>
4
+ <state name="point0" type="string" value="0.25"></state>
5
+ <state name="point1" type="string" value="0.1"></state>
6
+ <state name="point2" type="string" value="0.25"></state>
7
+ <state name="point3" type="string" value="1"></state>
8
+
9
+ <prop name="value" type="string" default="cubic-bezier(0.25, 0.1, 0.25, 1)"
10
+ >CSS cubic-bezier or a standard easing keyword. X stays within 0–1; Y is bounded to -1–2 for a limited overshoot.</prop>
11
+ <prop name="label" type="string" default="Value">Accessible name for the editing control.</prop>
12
+ <prop name="size" type="keyword" values="sm, md, lg" default="md">Shared field and button height.</prop>
13
+ <prop name="disabled" type="boolean" default="false">Prevents typing, stepping and pointer or keyboard changes while keeping the value visible.</prop>
14
+ <event name="input" type="object"><prop name="value" type="string" required></prop></event>
15
+ <event name="change" type="object"><prop name="value" type="string" required></prop></event>
16
+ </defs>
17
+ <span>
18
+ <span class="plot-frame" from:aria-label="$label" role="group"><span class="plot" $ref="plot">
19
+ <svg viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
20
+ <path class="guide" d="M0 66.667 H100 M0 33.333 H100"></path>
21
+ <path $ref="firstLine" class="guide" d="M0 66.667 L25 63.333"></path>
22
+ <path $ref="secondLine" class="guide" d="M100 33.333 L25 33.333"></path>
23
+ <path $ref="curve" d="M0 66.667 C25 63.333 25 33.333 100 33.333"></path>
24
+ </svg>
25
+ <button $ref="first" class="handle" type="button" from:disabled="$disabled" aria-label="First curve handle. Arrow keys adjust; Shift moves faster.">
26
+ </button>
27
+ <button
28
+ $ref="second"
29
+ class="handle second"
30
+ type="button"
31
+ from:disabled="$disabled"
32
+ aria-label="Second curve handle. Arrow keys adjust; Shift moves faster."
33
+ >
34
+ </button>
35
+ </span></span>
36
+ <span class="coordinates">
37
+ <span class="coordinate"><span>X1</span><ui-number-input
38
+ data-curve-coordinate="0"
39
+ from:value="$point0"
40
+ min="0"
41
+ max="1"
42
+ step="0.01"
43
+ label="First handle X"
44
+ from:size="$size"
45
+ from:disabled="$disabled"
46
+ ></ui-number-input></span>
47
+ <span class="coordinate"><span>Y1</span><ui-number-input
48
+ data-curve-coordinate="1"
49
+ from:value="$point1"
50
+ min="-1"
51
+ max="2"
52
+ step="0.01"
53
+ label="First handle Y"
54
+ from:size="$size"
55
+ from:disabled="$disabled"
56
+ ></ui-number-input></span>
57
+ <span class="coordinate"><span>X2</span><ui-number-input
58
+ data-curve-coordinate="2"
59
+ from:value="$point2"
60
+ min="0"
61
+ max="1"
62
+ step="0.01"
63
+ label="Second handle X"
64
+ from:size="$size"
65
+ from:disabled="$disabled"
66
+ ></ui-number-input></span>
67
+ <span class="coordinate"><span>Y2</span><ui-number-input
68
+ data-curve-coordinate="3"
69
+ from:value="$point3"
70
+ min="-1"
71
+ max="2"
72
+ step="0.01"
73
+ label="Second handle Y"
74
+ from:size="$size"
75
+ from:disabled="$disabled"
76
+ ></ui-number-input></span>
77
+ </span>
78
+ <output $ref="result">{$value}</output>
79
+ </span>
80
+ <style>
81
+ :host {
82
+ display: grid !important;
83
+ gap: var(--ui-space-3) !important;
84
+ margin: 0 !important;
85
+ padding: 0 !important;
86
+ min-inline-size: 0;
87
+ }
88
+ .plot-frame {
89
+ box-sizing: border-box !important;
90
+ display: block !important;
91
+ block-size: 12rem;
92
+ padding: max(var(--ui-space-4), 0.75rem, calc(var(--ui-control-min-block-size) / 2)) !important;
93
+ margin: 0 !important;
94
+ border: var(--ui-border-width) solid var(--ui-control-border);
95
+ border-radius: var(--ui-field-radius, var(--ui-radius-md));
96
+ background: var(--ui-control-surface, var(--ui-surface));
97
+ }
98
+ .plot {
99
+ position: relative;
100
+ display: block !important;
101
+ inline-size: 100%;
102
+ block-size: 100%;
103
+ }
104
+ svg {
105
+ inline-size: 100%;
106
+ block-size: 100%;
107
+ overflow: visible;
108
+ }
109
+ path {
110
+ fill: none;
111
+ stroke: var(--ui-accent-solid);
112
+ stroke-width: 2;
113
+ vector-effect: non-scaling-stroke;
114
+ }
115
+ .guide {
116
+ stroke: var(--ui-control-border);
117
+ stroke-width: 1;
118
+ }
119
+ .handle {
120
+ position: absolute;
121
+ left: 25%;
122
+ top: 63.333%;
123
+ translate: -50% -50%;
124
+ inline-size: max(1.5rem, var(--ui-control-min-block-size));
125
+ block-size: max(1.5rem, var(--ui-control-min-block-size));
126
+ border: 0;
127
+ border-radius: 50%;
128
+ background: transparent;
129
+ color: var(--ui-text-primary);
130
+ touch-action: none;
131
+ cursor: grab;
132
+ padding: 0 !important;
133
+ margin: 0 !important;
134
+ }
135
+ .second {
136
+ top: 33.333%;
137
+ }
138
+ .handle::after {
139
+ content: "";
140
+ position: absolute;
141
+ inset: 25%;
142
+ border: var(--ui-border-width) solid var(--ui-accent-solid);
143
+ background: var(--ui-surface-raised);
144
+ border-radius: 50%;
145
+ box-shadow: var(--ui-shadow-xs);
146
+ }
147
+ .handle::before {
148
+ content: "";
149
+ position: absolute;
150
+ inset: 40%;
151
+ background: var(--ui-accent-solid);
152
+ border-radius: 50%;
153
+ z-index: 1;
154
+ }
155
+ .handle:focus-visible {
156
+ outline: var(--ui-focus-width) solid var(--ui-control-focus, var(--ui-accent-solid));
157
+ outline-offset: var(--ui-space-1);
158
+ }
159
+ .handle:disabled {
160
+ cursor: not-allowed;
161
+ }
162
+ .coordinates {
163
+ display: grid !important;
164
+ grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
165
+ gap: var(--ui-space-2) !important;
166
+ }
167
+ .coordinate {
168
+ display: grid !important;
169
+ gap: var(--ui-space-1) !important;
170
+ font-size: var(--ui-font-size-sm);
171
+ }
172
+ output {
173
+ overflow-wrap: anywhere;
174
+ font-family: var(--ui-font-mono, monospace);
175
+ font-size: var(--ui-font-size-sm);
176
+ }
177
+ :host[hidden] {
178
+ display: none !important;
179
+ }
180
+ </style>
181
+ </template>