@threadlabs/looma 0.33.1 → 0.35.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 (215) hide show
  1. package/components/shared/overlay.js +8 -2
  2. package/components/shared/surface-material.js +21 -0
  3. package/components/ui-badge/ui-badge.html +11 -3
  4. package/components/ui-button/ui-button.html +92 -51
  5. package/components/ui-card/ui-card.html +50 -6
  6. package/components/ui-checkbox/ui-checkbox.html +4 -2
  7. package/components/ui-curve-editor/curve.js +10 -0
  8. package/components/ui-curve-editor/ui-curve-editor.html +181 -0
  9. package/components/ui-curve-editor/ui-curve-editor.js +51 -0
  10. package/components/ui-dialog/ui-dialog.html +64 -1
  11. package/components/ui-dialog/ui-dialog.js +9 -1
  12. package/components/ui-icon-button/ui-icon-button.html +97 -31
  13. package/components/ui-input/ui-input.html +2 -1
  14. package/components/ui-input-group/ui-input-group.html +4 -1
  15. package/components/ui-light-editor/ui-light-editor.html +45 -0
  16. package/components/ui-light-editor/ui-light-editor.js +53 -0
  17. package/components/ui-menu/ui-menu.html +62 -0
  18. package/components/ui-menu/ui-menu.js +1 -1
  19. package/components/ui-number-input/arithmetic.js +47 -0
  20. package/components/ui-number-input/ui-number-input.html +63 -0
  21. package/components/ui-number-input/ui-number-input.js +47 -0
  22. package/components/ui-popover/ui-popover.html +66 -1
  23. package/components/ui-popover/ui-popover.js +1 -1
  24. package/components/ui-select/ui-select.html +9 -7
  25. package/components/ui-slider/ui-slider.html +84 -0
  26. package/components/ui-slider/ui-slider.js +29 -0
  27. package/components/ui-switch/ui-switch.html +3 -0
  28. package/components/ui-tabs/ui-tabs.html +6 -1
  29. package/components/ui-text/ui-text.html +31 -6
  30. package/components/ui-textarea/ui-textarea.html +2 -1
  31. package/components/ui-tooltip/ui-tooltip.html +60 -0
  32. package/components/ui-tooltip/ui-tooltip.js +1 -1
  33. package/dist/index.js +47 -43
  34. package/docs/README.md +59 -5
  35. package/docs/card.html +16 -2
  36. package/docs/card.js +17 -2
  37. package/docs/catalog.json +1640 -108
  38. package/docs/customizer-render.mjs +50 -11
  39. package/docs/customizer.js +137 -24
  40. package/docs/documentation.css +9 -1
  41. package/docs/index.d.ts +8 -3
  42. package/docs/index.mjs +1 -1
  43. package/docs/preview.js +2 -0
  44. package/docs/render.mjs +16 -7
  45. package/docs/site.mjs +78 -27
  46. package/docs/theme-session.mjs +53 -0
  47. package/docs/theme.mjs +189 -18
  48. package/docs/value-editors.mjs +33 -0
  49. package/package.json +2 -2
  50. package/styles/ui-badge.css +11 -6
  51. package/styles/ui-button.css +74 -49
  52. package/styles/ui-card.css +51 -4
  53. package/styles/ui-checkbox.css +4 -2
  54. package/styles/ui-curve-editor.css +109 -0
  55. package/styles/ui-dialog.css +136 -0
  56. package/styles/ui-icon-button.css +85 -28
  57. package/styles/ui-input-group.css +4 -1
  58. package/styles/ui-input.css +2 -1
  59. package/styles/ui-light-editor.css +28 -0
  60. package/styles/ui-menu.css +74 -0
  61. package/styles/ui-number-input.css +34 -0
  62. package/styles/ui-popover.css +76 -0
  63. package/styles/ui-select.css +9 -7
  64. package/styles/ui-slider.css +53 -0
  65. package/styles/ui-switch.css +3 -0
  66. package/styles/ui-tabs.css +6 -1
  67. package/styles/ui-text.css +23 -1
  68. package/styles/ui-textarea.css +2 -1
  69. package/styles/ui-tooltip.css +72 -0
  70. package/theme-light.css +1 -0
  71. package/theme-system.css +69 -17
  72. package/tokens.css +12 -2
  73. package/vanilla/UiButton.d.ts +1 -0
  74. package/vanilla/UiButton.js +195 -130
  75. package/vanilla/UiCard.d.ts +1 -0
  76. package/vanilla/UiCard.js +12 -12
  77. package/vanilla/UiCurveEditor.d.ts +28 -0
  78. package/vanilla/UiCurveEditor.js +134 -0
  79. package/vanilla/UiIconButton.d.ts +2 -1
  80. package/vanilla/UiIconButton.js +58 -31
  81. package/vanilla/UiLightEditor.d.ts +29 -0
  82. package/vanilla/UiLightEditor.js +102 -0
  83. package/vanilla/UiNumberInput.d.ts +33 -0
  84. package/vanilla/UiNumberInput.js +86 -0
  85. package/vanilla/UiSlider.d.ts +33 -0
  86. package/vanilla/UiSlider.js +125 -0
  87. package/vanilla/UiText.d.ts +2 -2
  88. package/vanilla/UiText.js +45 -11
  89. package/vanilla/index.js +4 -0
  90. package/vue/UiBadge.vue +29 -4
  91. package/vue/UiButton.d.ts +6 -0
  92. package/vue/UiButton.vue +188 -60
  93. package/vue/UiCard.d.ts +6 -0
  94. package/vue/UiCard.vue +80 -6
  95. package/vue/UiCheckbox.vue +4 -2
  96. package/vue/UiCurveEditor.d.ts +53 -0
  97. package/vue/UiCurveEditor.js +2 -0
  98. package/vue/UiCurveEditor.vue +427 -0
  99. package/vue/UiDialog.vue +181 -1
  100. package/vue/UiIconButton.d.ts +8 -2
  101. package/vue/UiIconButton.vue +190 -45
  102. package/vue/UiInput.vue +2 -1
  103. package/vue/UiInputGroup.vue +4 -1
  104. package/vue/UiLightEditor.d.ts +75 -0
  105. package/vue/UiLightEditor.js +2 -0
  106. package/vue/UiLightEditor.vue +402 -0
  107. package/vue/UiMenu.vue +91 -0
  108. package/vue/UiNumberInput.d.ts +83 -0
  109. package/vue/UiNumberInput.js +2 -0
  110. package/vue/UiNumberInput.vue +399 -0
  111. package/vue/UiPopover.vue +94 -1
  112. package/vue/UiSelect.vue +16 -10
  113. package/vue/UiSlider.d.ts +83 -0
  114. package/vue/UiSlider.js +2 -0
  115. package/vue/UiSlider.vue +412 -0
  116. package/vue/UiSwitch.vue +3 -0
  117. package/vue/UiTabs.vue +6 -1
  118. package/vue/UiText.d.ts +7 -5
  119. package/vue/UiText.vue +104 -22
  120. package/vue/UiTextarea.vue +2 -1
  121. package/vue/UiTooltip.vue +89 -0
  122. package/vue/chunks/UiActionBar.js +1 -1
  123. package/vue/chunks/UiAffordanceScope.js +2 -2
  124. package/vue/chunks/UiAvatar.js +2 -2
  125. package/vue/chunks/UiAvatarGroup.js +2 -2
  126. package/vue/chunks/UiBadge.js +3 -3
  127. package/vue/chunks/UiBreadcrumbItem.js +2 -2
  128. package/vue/chunks/UiBreadcrumbs.js +2 -2
  129. package/vue/chunks/UiButton.js +94 -11
  130. package/vue/chunks/UiCallout.js +2 -2
  131. package/vue/chunks/UiCard.js +34 -5
  132. package/vue/chunks/UiCheckbox.js +3 -3
  133. package/vue/chunks/UiCluster.js +2 -2
  134. package/vue/chunks/UiColorPicker.js +2 -2
  135. package/vue/chunks/UiCombobox.js +2 -2
  136. package/vue/chunks/UiContainer.js +2 -2
  137. package/vue/chunks/UiContextMenu.js +2 -2
  138. package/vue/chunks/UiCover.js +2 -2
  139. package/vue/chunks/UiCurveEditor.js +413 -0
  140. package/vue/chunks/UiDescriptionItem.js +2 -2
  141. package/vue/chunks/UiDescriptionList.js +2 -2
  142. package/vue/chunks/UiDialog.js +4 -4
  143. package/vue/chunks/UiDisclosure.js +2 -2
  144. package/vue/chunks/UiEditable.js +2 -2
  145. package/vue/chunks/UiEditorBlockActions.js +2 -2
  146. package/vue/chunks/UiEditorChipEditor.js +2 -2
  147. package/vue/chunks/UiEditorCodeLanguage.js +2 -2
  148. package/vue/chunks/UiEditorFootnoteEditor.js +2 -2
  149. package/vue/chunks/UiEditorImageToolbar.js +3 -3
  150. package/vue/chunks/UiEditorInsertTableGrid.js +2 -2
  151. package/vue/chunks/UiEditorLinkCard.js +3 -3
  152. package/vue/chunks/UiEditorLinkForm.js +3 -3
  153. package/vue/chunks/UiEditorMentionMenu.js +2 -2
  154. package/vue/chunks/UiEditorMentionMenuItem.js +2 -2
  155. package/vue/chunks/UiEditorSlashMenu.js +2 -2
  156. package/vue/chunks/UiEditorSlashMenuGroup.js +2 -2
  157. package/vue/chunks/UiEditorSlashMenuItem.js +2 -2
  158. package/vue/chunks/UiEditorSurface.js +2 -2
  159. package/vue/chunks/UiEditorTableContextMenu.js +2 -2
  160. package/vue/chunks/UiEditorTableOverlay.js +2 -2
  161. package/vue/chunks/UiEditorTableToolbar.js +2 -2
  162. package/vue/chunks/UiEditorToolbar.js +2 -2
  163. package/vue/chunks/UiFloatingActionButton.js +2 -2
  164. package/vue/chunks/UiFormField.js +2 -2
  165. package/vue/chunks/UiGrid.js +2 -2
  166. package/vue/chunks/UiIcon.js +2 -2
  167. package/vue/chunks/UiIconButton.js +77 -11
  168. package/vue/chunks/UiImage.js +2 -2
  169. package/vue/chunks/UiInput.js +3 -3
  170. package/vue/chunks/UiInputGroup.js +3 -3
  171. package/vue/chunks/UiLightEditor.js +347 -0
  172. package/vue/chunks/UiList.js +2 -2
  173. package/vue/chunks/UiListItem.js +2 -2
  174. package/vue/chunks/UiListbox.js +2 -2
  175. package/vue/chunks/UiMenu.js +3 -3
  176. package/vue/chunks/UiMenuGroup.js +2 -2
  177. package/vue/chunks/UiMenuItem.js +2 -2
  178. package/vue/chunks/UiMeter.js +2 -2
  179. package/vue/chunks/UiNavItem.js +2 -2
  180. package/vue/chunks/UiNumberInput.js +403 -0
  181. package/vue/chunks/UiPageHeader.js +2 -2
  182. package/vue/chunks/UiPopover.js +4 -4
  183. package/vue/chunks/UiRadio.js +2 -2
  184. package/vue/chunks/UiRadioGroup.js +2 -2
  185. package/vue/chunks/UiReel.js +2 -2
  186. package/vue/chunks/UiScrollArea.js +2 -2
  187. package/vue/chunks/UiSearchResultRow.js +2 -2
  188. package/vue/chunks/UiSearchShell.js +2 -2
  189. package/vue/chunks/UiSection.js +2 -2
  190. package/vue/chunks/UiSelect.js +3 -3
  191. package/vue/chunks/UiSeparator.js +2 -2
  192. package/vue/chunks/UiSidebar.js +2 -2
  193. package/vue/chunks/UiSlider.js +396 -0
  194. package/vue/chunks/UiSpinner.js +2 -2
  195. package/vue/chunks/UiStack.js +2 -2
  196. package/vue/chunks/UiStatusMessage.js +2 -2
  197. package/vue/chunks/UiSwatch.js +2 -2
  198. package/vue/chunks/UiSwitch.js +3 -3
  199. package/vue/chunks/UiSwitcher.js +2 -2
  200. package/vue/chunks/UiTable.js +2 -2
  201. package/vue/chunks/UiTabs.js +3 -3
  202. package/vue/chunks/UiText.js +53 -12
  203. package/vue/chunks/UiTextarea.js +3 -3
  204. package/vue/chunks/UiToast.js +2 -2
  205. package/vue/chunks/UiToastRegion.js +2 -2
  206. package/vue/chunks/UiTooltip.js +3 -3
  207. package/vue/chunks/UiTopBar.js +1 -1
  208. package/vue/chunks/UiTree.js +2 -2
  209. package/vue/chunks/UiTreeItem.js +2 -2
  210. package/vue/chunks/_plugin-vue_export-helper.js +68 -2
  211. package/vue/chunks/props.js +24 -1
  212. package/vue/components.css +3391 -2139
  213. package/vue/editor/index.js +623 -211
  214. package/vue/index.d.ts +4 -0
  215. package/vue/index.js +6 -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
+ }
@@ -325,10 +325,18 @@
325
325
  box-shadow: none;
326
326
  }
327
327
 
328
- /* Neutral reverses a subtle surface/ink pair rather than a strong tone. Keep its original
329
- surface ink: moving it toward dark-theme primary text would reduce overlay contrast. */
330
328
  :host([variant="inverse"][tone="neutral"]) {
331
- color: var(--ui-badge-text, var(--_badge-color-text, var(--_badge-surface)));
329
+ --_badge-outline-surface: var(--ui-badge-surface, var(--_badge-color-surface, color-mix(in srgb, var(--ui-surface-raised) 88%, transparent)));
330
+ color: var(--ui-badge-text, var(--_badge-color-text, var(--ui-text-primary)));
331
+ }
332
+ :host([variant="inverse"][tone="neutral"]:not([shape="circle"])) {
333
+ border-color: var(--ui-badge-border, var(--_badge-color-border, var(--_badge-outline-surface)));
334
+ }
335
+ :host([variant="inverse"][tone="neutral"][shape="circle"]) {
336
+ --_badge-outline-surface: var(--ui-badge-surface, var(--_badge-color-surface, light-dark(
337
+ color-mix(in srgb, var(--ui-surface-raised) var(--ui-badge-circle-opacity, 66%), transparent),
338
+ color-mix(in srgb, var(--ui-surface-raised) var(--ui-badge-circle-opacity, 56%), transparent)
339
+ )));
332
340
  }
333
341
 
334
342
  :host([variant="outline"]) {
@@ -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"
@@ -172,7 +188,7 @@
172
188
  :host {
173
189
  /* Optional inherited action-group values keep text and icon actions under the same light.
174
190
  One fine top/start highlight follows the bottom/end lowlight at a joined action seam.
175
- A single ambient lift avoids stacking a face gradient or another inset bevel. */
191
+ The shared theme supplies reflections and glow independently of the resting lift. */
176
192
  --_ui-action-start: 1px;
177
193
  --_ui-action-edge: var(
178
194
  --ui-action-edge,
@@ -180,6 +196,8 @@
180
196
  inset calc(-1 * var(--_ui-action-start)) -1px 0 light-dark(rgb(0 0 0 / 0.08), rgb(0 0 0 / 0.08))
181
197
  );
182
198
  --_ui-action-shadow: var(--_ui-action-edge), 0 1px 2px light-dark(rgb(0 0 0 / 0.10), rgb(0 0 0 / 0.20));
199
+ --_tone-highlight: var(--ui-accent-highlight, none);
200
+ --_tone-glow: var(--ui-accent-glow, 0 0 transparent);
183
201
  --_hue: var(--ui-accent);
184
202
  --_hue-hover: var(--ui-accent-hover);
185
203
  --_hue-active: var(--ui-accent-active);
@@ -230,8 +248,8 @@
230
248
  white-space: var(--ui-button-white-space, inherit);
231
249
  line-height: var(--ui-line-height-tight);
232
250
  padding: var(--ui-control-inset-block-md, var(--ui-space-2)) var(--ui-action-inset-md, var(--ui-space-3)) !important;
233
- background-image: none;
234
- box-shadow: var(--ui-button-shadow, var(--ui-action-shadow, var(--_ui-action-shadow)));
251
+ background-image: var(--ui-surface-highlight, none);
252
+ box-shadow: var(--ui-button-shadow, var(--ui-action-shadow, var(--_ui-action-shadow))), var(--_tone-glow);
235
253
  cursor: pointer;
236
254
  transition:
237
255
  scale var(--ui-motion-press, var(--ui-motion-fast)) var(--ui-motion-ease),
@@ -255,7 +273,7 @@
255
273
 
256
274
  /* A count is an adornment, not another line of label text. Keep the standard control height
257
275
  with enough inset for a 24px mark; large content can still grow the button naturally. */
258
- :host:not([variant="link"]):has(.badge:not(:empty)) {
276
+ :host:not([data-appearance="link"]):has(.badge:not(:empty)) {
259
277
  padding-block: var(--ui-space-0-5) !important;
260
278
  }
261
279
 
@@ -300,7 +318,7 @@
300
318
 
301
319
  /* Card actions inherit Button's palette and behavior; only their shared layout changes.
302
320
  The icon may contain any decorative composition. Empty icons remove their column. */
303
- :host([variant="card"]) {
321
+ :host[data-appearance="card"] {
304
322
  display: grid !important;
305
323
  grid-template-columns: auto minmax(0, 1fr) auto;
306
324
  align-content: center !important;
@@ -313,10 +331,10 @@
313
331
  box-shadow: none;
314
332
  background-image: none;
315
333
  }
316
- :host([variant="card"][size="lg"]) {
334
+ :host[data-appearance="card"][size="lg"] {
317
335
  min-block-size: max(var(--ui-control-min-block-size), var(--ui-control-size-lg));
318
336
  }
319
- :host([variant="card"]):has(.card-icon:empty) {
337
+ :host[data-appearance="card"]:has(.card-icon:empty) {
320
338
  grid-template-columns: minmax(0, 1fr) auto;
321
339
  }
322
340
  .card-icon {
@@ -343,11 +361,11 @@
343
361
  }
344
362
 
345
363
  /* A neutral card encloses content quietly; ordinary outlines keep their action-tone edge. */
346
- :host([variant="card"][tone="neutral"]) {
364
+ :host[data-appearance="card"][data-tone="neutral"] {
347
365
  border-color: var(--ui-button-border, var(--ui-border));
348
366
  }
349
367
 
350
- :host([variant="card"][tone="neutral"]):hover:not(:disabled, [aria-disabled="true"]) {
368
+ :host[data-appearance="card"][data-tone="neutral"]:hover:not(:disabled, [aria-disabled="true"]) {
351
369
  border-color: var(--ui-button-hover-border, var(--ui-border-strong));
352
370
  }
353
371
 
@@ -359,19 +377,20 @@
359
377
  box-shadow: var(--ui-pressed);
360
378
  }
361
379
 
362
- :host([variant="solid"]), :host([variant="danger"]) {
380
+ :host[data-appearance="solid"], :host[data-appearance="danger"] {
363
381
  border-color: var(--_tone);
364
382
  background-color: var(--_tone);
365
383
  color: var(--_on-tone);
366
- box-shadow: var(--ui-button-solid-shadow, var(--ui-action-shadow, var(--_ui-action-shadow)));
384
+ background-image: var(--_tone-highlight);
385
+ box-shadow: var(--ui-button-solid-shadow, var(--ui-action-shadow, var(--_ui-action-shadow))), var(--_tone-glow);
367
386
  }
368
387
 
369
- :is(:host([variant="solid"]), :host([variant="danger"])):hover:not(:disabled, [aria-disabled="true"]) {
388
+ :is(:host[data-appearance="solid"], :host[data-appearance="danger"]):hover:not(:disabled, [aria-disabled="true"]) {
370
389
  background-color: var(--_tone-hover);
371
390
  border-color: var(--_tone-hover);
372
391
  }
373
392
 
374
- :is(:host([variant="solid"]), :host([variant="danger"])):active:not(:disabled, [aria-disabled="true"]) {
393
+ :is(:host[data-appearance="solid"], :host[data-appearance="danger"]):active:not(:disabled, [aria-disabled="true"]) {
375
394
  background-color: var(--_tone-active);
376
395
  border-color: var(--_tone-active);
377
396
  background-image: none;
@@ -380,7 +399,9 @@
380
399
 
381
400
  /* Tone is the colour, variant is the volume. Every pairing works: a danger outline is a
382
401
  destructive secondary action, not a special case. */
383
- :host([tone="neutral"]) {
402
+ :host[data-tone="neutral"] {
403
+ --_tone-highlight: var(--ui-neutral-highlight, none);
404
+ --_tone-glow: var(--ui-neutral-glow, 0 0 transparent);
384
405
  --_hue: var(--ui-text);
385
406
  --_hue-hover: var(--ui-text);
386
407
  --_hue-active: var(--ui-text);
@@ -388,40 +409,60 @@
388
409
  --_on-hue: var(--ui-surface);
389
410
  }
390
411
 
391
- :host([tone="danger"]), :host([variant="danger"]) {
412
+ :host[data-tone="neutral"][data-appearance="solid"] {
413
+ border-color: var(--ui-button-border, var(--ui-control-border));
414
+ background-color: var(--ui-button-surface, var(--ui-neutral-surface));
415
+ color: var(--ui-button-text, var(--ui-text));
416
+ }
417
+ :host[data-tone="neutral"][data-appearance="solid"]:hover:not(:disabled, [aria-disabled="true"]) {
418
+ background-color: var(--ui-button-hover-surface, var(--ui-neutral-hover));
419
+ }
420
+ :host[data-tone="neutral"][data-appearance="solid"]:active:not(:disabled, [aria-disabled="true"]) {
421
+ background-color: var(--ui-button-active-surface, var(--ui-neutral-active));
422
+ }
423
+
424
+ :host[data-tone="danger"], :host[data-appearance="danger"] {
425
+ --_tone-highlight: var(--ui-danger-highlight, none);
426
+ --_tone-glow: var(--ui-danger-glow, 0 0 transparent);
392
427
  --_hue: var(--ui-danger);
393
428
  --_hue-hover: var(--ui-danger-hover);
394
- --_hue-active: var(--ui-danger-hover);
395
- --_hue-text: var(--ui-danger-hover);
429
+ --_hue-active: var(--ui-danger-active, var(--ui-danger-hover));
430
+ --_hue-text: var(--ui-danger-subtle-text);
396
431
  --_on-hue: var(--ui-on-danger);
397
432
  }
398
433
 
399
- :host([tone="success"]) {
434
+ :host[data-tone="success"] {
435
+ --_tone-highlight: var(--ui-success-highlight, none);
436
+ --_tone-glow: var(--ui-success-glow, 0 0 transparent);
400
437
  --_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);
438
+ --_hue-hover: var(--ui-success-hover);
439
+ --_hue-active: var(--ui-success-active);
440
+ --_hue-text: var(--ui-success-subtle-text);
441
+ --_on-hue: var(--ui-success-on-strong);
405
442
  }
406
443
 
407
- :host([tone="warning"]) {
444
+ :host[data-tone="warning"] {
445
+ --_tone-highlight: var(--ui-warning-highlight, none);
446
+ --_tone-glow: var(--ui-warning-glow, 0 0 transparent);
408
447
  --_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);
448
+ --_hue-hover: var(--ui-warning-hover);
449
+ --_hue-active: var(--ui-warning-active);
450
+ --_hue-text: var(--ui-warning-subtle-text);
451
+ --_on-hue: var(--ui-warning-on-strong);
413
452
  }
414
453
 
415
- :host([tone="info"]) {
454
+ :host[data-tone="info"] {
455
+ --_tone-highlight: var(--ui-info-highlight, none);
456
+ --_tone-glow: var(--ui-info-glow, 0 0 transparent);
416
457
  --_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);
458
+ --_hue-hover: var(--ui-info-hover);
459
+ --_hue-active: var(--ui-info-active);
460
+ --_hue-text: var(--ui-info-subtle-text);
461
+ --_on-hue: var(--ui-info-on-strong);
421
462
  }
422
463
 
423
464
  /* A value picker is a field, with Select's quiet edge and resting lift. */
424
- :host([variant="field"]) {
465
+ :host[data-appearance="field"] {
425
466
  --_ui-field-shadow: var(--ui-shadow-sm);
426
467
  border-color: var(--ui-button-border, var(--ui-control-border, var(--ui-border-strong)));
427
468
  border-radius: var(--ui-button-radius, var(--ui-field-radius, var(--ui-radius-md)));
@@ -431,23 +472,23 @@
431
472
  box-shadow: var(--ui-button-shadow, var(--_ui-field-shadow));
432
473
  }
433
474
 
434
- :host([variant="field"]):hover:not(:disabled, [aria-disabled="true"]) {
475
+ :host[data-appearance="field"]:hover:not(:disabled, [aria-disabled="true"]) {
435
476
  border-color: var(--ui-button-hover-border, var(--ui-control-border-hover, var(--ui-text-secondary)));
436
477
  background-color: var(--ui-button-hover-surface, var(--ui-control-surface, var(--ui-surface-elevated)));
437
478
  }
438
479
 
439
- :host([variant="field"]):is(:active, [aria-expanded="true"]):not(:disabled, [aria-disabled="true"]) {
480
+ :host[data-appearance="field"]:is(:active, [aria-expanded="true"]):not(:disabled, [aria-disabled="true"]) {
440
481
  --_ui-field-shadow: var(--ui-pressed);
441
482
  }
442
483
 
443
- :host([variant="field"]):focus-visible {
484
+ :host[data-appearance="field"]:focus-visible {
444
485
  border-color: var(--ui-control-focus, var(--ui-accent-solid));
445
486
  box-shadow:
446
487
  var(--ui-button-shadow, var(--_ui-field-shadow)),
447
488
  var(--ui-control-focus-halo-shadow);
448
489
  }
449
490
 
450
- :host([variant="ghost"]) {
491
+ :host[data-appearance="ghost"] {
451
492
  border-color: transparent;
452
493
  background: transparent;
453
494
  box-shadow: none;
@@ -455,14 +496,14 @@
455
496
  }
456
497
 
457
498
  /* 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"]) {
499
+ :host[data-appearance="ghost"]:hover:not(:disabled, [aria-disabled="true"]) {
459
500
  background-color: var(--ui-button-ghost-hover-surface, oklch(from var(--_tone) l c h / 10%));
460
501
  border-color: transparent;
461
502
  }
462
503
 
463
504
  /* Pressed, or holding its menu open, a ghost button sinks slightly: a light surface with a faint
464
505
  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"]) {
506
+ :host[data-appearance="ghost"]:is(:active, [aria-expanded="true"]):not(:disabled, [aria-disabled="true"]) {
466
507
  background-color: var(--ui-button-ghost-active-surface, color-mix(in oklch, var(--ui-surface), var(--_tone) 3%));
467
508
  border-color: transparent;
468
509
  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 +517,13 @@
476
517
  var(--ui-focus-halo);
477
518
  }
478
519
 
479
- :host([variant="solid"]):focus-visible {
520
+ :host[data-appearance="solid"]:focus-visible {
480
521
  box-shadow:
481
522
  var(--ui-button-solid-shadow, var(--ui-action-shadow, var(--_ui-action-shadow))),
482
523
  var(--ui-focus-halo);
483
524
  }
484
525
 
485
- :is(:host([variant="ghost"]), :host([variant="link"]), :host([variant="card"])):focus-visible {
526
+ :is(:host[data-appearance="ghost"], :host[data-appearance="link"], :host[data-appearance="card"]):focus-visible {
486
527
  box-shadow: var(--ui-focus-halo);
487
528
  }
488
529
 
@@ -504,7 +545,7 @@
504
545
  }
505
546
 
506
547
  /* 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"])) {
548
+ :host[data-appearance="ghost"]:is(:disabled, [aria-disabled="true"]:not([aria-busy="true"])) {
508
549
  background-color: var(--ui-disabled-surface);
509
550
  border-color: transparent;
510
551
  }
@@ -521,12 +562,12 @@
521
562
  a link-style one keeps its text size, so a standalone one ("See all" under a list) needs the
522
563
  area. xs sits inside a line of text the same way, so it keeps its size and gains the area too.
523
564
  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"]),
565
+ html[data-ui-input-modality="touch"] :host[data-appearance="link"],
525
566
  html[data-ui-input-modality="touch"] :host([size="xs"]) {
526
567
  position: relative;
527
568
  }
528
569
 
529
- html[data-ui-input-modality="touch"] :host([variant="link"])::after,
570
+ html[data-ui-input-modality="touch"] :host[data-appearance="link"]::after,
530
571
  html[data-ui-input-modality="touch"] :host([size="xs"])::after {
531
572
  position: absolute;
532
573
  inset: 50% auto auto 50%;
@@ -538,7 +579,7 @@
538
579
 
539
580
  /* Link: an inline text action. No box, padding, or minimum height (inline targets are exempt under
540
581
  WCAG 2.5.8); the inherited font; an underline that strengthens on hover; a visible focus ring. */
541
- :host([variant="link"]) {
582
+ :host[data-appearance="link"] {
542
583
  display: inline !important;
543
584
  min-block-size: 0;
544
585
  padding: 0 !important;
@@ -555,24 +596,24 @@
555
596
  transition: color var(--ui-motion-fast) var(--ui-motion-ease), text-decoration-color var(--ui-motion-fast) var(--ui-motion-ease);
556
597
  }
557
598
 
558
- :host([variant="link"]):hover:not(:disabled, [aria-disabled="true"]) {
599
+ :host[data-appearance="link"]:hover:not(:disabled, [aria-disabled="true"]) {
559
600
  color: var(--ui-button-link-hover-text, var(--ui-text-primary));
560
601
  text-decoration-color: currentColor;
561
602
  background: none;
562
603
  transform: none;
563
604
  }
564
605
 
565
- :host([variant="link"]):active:not(:disabled, [aria-disabled="true"]) {
606
+ :host[data-appearance="link"]:active:not(:disabled, [aria-disabled="true"]) {
566
607
  background: none;
567
608
  transform: none;
568
609
  }
569
610
 
570
- :host([variant="link"]):is(:disabled, [aria-disabled="true"]:not([aria-busy="true"])) {
611
+ :host[data-appearance="link"]:is(:disabled, [aria-disabled="true"]:not([aria-busy="true"])) {
571
612
  background: none;
572
613
  border-color: transparent;
573
614
  }
574
615
 
575
- :host([variant="link"]):focus-visible {
616
+ :host[data-appearance="link"]:focus-visible {
576
617
  box-shadow: none;
577
618
  /* Fallback keeps the ring if the token is missing: an unresolved var() would void the outline. */
578
619
  outline: var(--ui-focus-width) solid var(--ui-focus-ring);
@@ -4,11 +4,14 @@
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
5
  <prop name="variant" type="keyword" values="outlined, subtle, elevated" 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 adds a subtle lift above the page, for a view's one card, such as sign-in. Treatment independently controls decorative material.</prop>
8
+ <prop name="treatment" type="keyword" values="standard, expressive" default="standard"
9
+ >How much of the theme's decorative material the surface expresses. standard preserves its restrained treatment; expressive uses the full material treatment.
10
+ This does not add elevation or require glass.</prop>
8
11
  <prop name="padding" type="keyword" values="xs, sm, md, lg" default="md"
9
12
  >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
13
  <prop name="tone" type="keyword" values="neutral, accent, info, success, warning, danger" default="neutral"
11
- >accent, info, success and warning add a quiet semantic surface. danger retains its strong leading edge for consequential content. Row highlights follow the
14
+ >accent, info, success, warning and danger add a quiet semantic surface without changing card geometry. Row highlights follow the
12
15
  surface hue; action controls retain their own intent.</prop>
13
16
  <prop name="as" type="keyword" values="div, section, article, aside" default="div"
14
17
  >The native element the card renders. Choose the one that names the content: a section of a page, a self-contained article, or an aside.</prop>
@@ -24,6 +27,7 @@
24
27
  needs no layout of its own. */
25
28
  :host {
26
29
  --_ui-card-surface: var(--ui-surface-raised);
30
+ --_ui-card-elevation: 0 0 0 transparent;
27
31
  --_ui-selection-context-surface: initial;
28
32
  --_ui-selection-context-border: initial;
29
33
  --_ui-selection-context-text: initial;
@@ -58,10 +62,54 @@
58
62
  }
59
63
 
60
64
  :host([variant="elevated"]) {
65
+ --_ui-card-elevation: var(--ui-shadow-lg);
61
66
  border-color: var(--ui-border-subtle);
62
67
  box-shadow: var(--ui-shadow-lg);
63
68
  }
64
69
 
70
+ :host([treatment="expressive"]) {
71
+ --_ui-panel-glass: clamp(0, var(--ui-glassiness, 0), 1);
72
+ --_ui-panel-separation: clamp(0, var(--ui-surface-separation, 0), 1);
73
+ --_ui-panel-rim: calc((var(--_ui-panel-separation) + var(--_ui-panel-glass) * 0.25) * 0.65 * 8px);
74
+ border-width: calc(var(--ui-border-width) + var(--_ui-panel-rim));
75
+ border-color: transparent;
76
+ background:
77
+ var(--ui-surface-highlight, linear-gradient(transparent, transparent)) padding-box,
78
+ linear-gradient(var(--_ui-card-surface), var(--_ui-card-surface)) padding-box,
79
+ linear-gradient(var(--ui-material-glass, var(--ui-surface-raised)), var(--ui-material-glass, var(--ui-surface-raised))) border-box,
80
+ var(--_ui-card-surface) padding-box;
81
+ box-shadow:
82
+ var(--_ui-card-elevation),
83
+ var(--ui-material-glow, 0 0 transparent),
84
+ inset 0 0 0 var(--ui-border-width) var(--ui-material-edge, var(--ui-border-strong));
85
+ backdrop-filter: blur(calc(var(--_ui-panel-glass) * 0.65 * 18px)) saturate(calc(1 + var(--_ui-panel-glass) * 0.65 * 0.18));
86
+ -webkit-backdrop-filter: blur(calc(var(--_ui-panel-glass) * 0.65 * 18px)) saturate(calc(1 + var(--_ui-panel-glass) * 0.65 * 0.18));
87
+ }
88
+ @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
89
+ :host([treatment="expressive"]) {
90
+ background: var(--_ui-card-surface);
91
+ }
92
+ }
93
+ @media (prefers-reduced-transparency: reduce) {
94
+ :host([treatment="expressive"]) {
95
+ background: var(--_ui-card-surface);
96
+ border-width: var(--ui-border-width);
97
+ border-color: var(--ui-border-strong);
98
+ backdrop-filter: none;
99
+ -webkit-backdrop-filter: none;
100
+ }
101
+ }
102
+ @media (forced-colors: active) {
103
+ :host([treatment="expressive"]) {
104
+ background: Canvas;
105
+ color: CanvasText;
106
+ border: var(--ui-border-width) solid CanvasText;
107
+ box-shadow: none;
108
+ backdrop-filter: none;
109
+ -webkit-backdrop-filter: none;
110
+ }
111
+ }
112
+
65
113
  :host([tone="accent"]) {
66
114
  border-color: color-mix(in srgb, var(--ui-accent) 25%, var(--ui-border-default));
67
115
  --_ui-card-surface: color-mix(in srgb, var(--ui-accent) 6%, var(--ui-surface-default));
@@ -88,10 +136,6 @@
88
136
 
89
137
  :host([tone="danger"]) {
90
138
  border-color: color-mix(in srgb, var(--ui-danger) 45%, var(--ui-border-default));
91
- border-inline-start: var(--ui-accent-line-width) solid var(--ui-danger);
92
- /* An accent edge keeps the corners on its side straight. */
93
- border-start-start-radius: 0;
94
- border-end-start-radius: 0;
95
139
  --_ui-card-surface: var(--ui-danger-soft);
96
140
  }
97
141
 
@@ -130,7 +130,8 @@
130
130
  margin: max(0px, calc((1lh - var(--_ui-checkbox-size, 1.125rem)) / 2)) 0 0 !important;
131
131
  border: var(--ui-border-width) solid var(--ui-control-border, var(--ui-border-strong));
132
132
  border-radius: min(var(--ui-radius-sm), calc(var(--_ui-checkbox-size, 1.125rem) / 4));
133
- background: var(--ui-control-surface, var(--ui-surface-default));
133
+ background-color: var(--ui-control-surface, var(--ui-surface-default));
134
+ background-image: var(--ui-surface-highlight, none);
134
135
  box-shadow: var(--ui-control-inset);
135
136
  cursor: pointer;
136
137
  transition:
@@ -149,7 +150,8 @@
149
150
  input:checked,
150
151
  input:indeterminate {
151
152
  border-color: var(--ui-accent-solid);
152
- background: var(--ui-accent-solid);
153
+ background-color: var(--ui-accent-solid);
154
+ background-image: var(--ui-accent-highlight, none);
153
155
  box-shadow: var(--ui-control-inset);
154
156
  }
155
157
 
@@ -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(", ")})`; }