@threadlabs/looma 0.34.0 → 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 (88) hide show
  1. package/components/ui-badge/ui-badge.html +11 -3
  2. package/components/ui-button/ui-button.html +29 -4
  3. package/components/ui-card/ui-card.html +15 -13
  4. package/components/ui-checkbox/ui-checkbox.html +4 -2
  5. package/components/ui-dialog/ui-dialog.html +2 -0
  6. package/components/ui-icon-button/ui-icon-button.html +22 -8
  7. package/components/ui-input/ui-input.html +2 -1
  8. package/components/ui-input-group/ui-input-group.html +4 -1
  9. package/components/ui-light-editor/ui-light-editor.html +45 -0
  10. package/components/ui-light-editor/ui-light-editor.js +53 -0
  11. package/components/ui-menu/ui-menu.html +2 -0
  12. package/components/ui-number-input/ui-number-input.html +2 -1
  13. package/components/ui-number-input/ui-number-input.js +6 -3
  14. package/components/ui-popover/ui-popover.html +2 -0
  15. package/components/ui-select/ui-select.html +9 -7
  16. package/components/ui-switch/ui-switch.html +3 -0
  17. package/components/ui-textarea/ui-textarea.html +2 -1
  18. package/dist/index.js +24 -23
  19. package/docs/README.md +26 -2
  20. package/docs/catalog.json +566 -44
  21. package/docs/customizer-render.mjs +24 -11
  22. package/docs/customizer.js +67 -17
  23. package/docs/index.d.ts +4 -1
  24. package/docs/index.mjs +1 -1
  25. package/docs/render.mjs +1 -1
  26. package/docs/site.mjs +30 -20
  27. package/docs/theme-session.mjs +4 -2
  28. package/docs/theme.mjs +59 -7
  29. package/package.json +2 -2
  30. package/styles/ui-badge.css +11 -6
  31. package/styles/ui-button.css +29 -4
  32. package/styles/ui-card.css +9 -9
  33. package/styles/ui-checkbox.css +4 -2
  34. package/styles/ui-dialog.css +4 -0
  35. package/styles/ui-icon-button.css +22 -8
  36. package/styles/ui-input-group.css +4 -1
  37. package/styles/ui-input.css +2 -1
  38. package/styles/ui-light-editor.css +28 -0
  39. package/styles/ui-menu.css +2 -0
  40. package/styles/ui-popover.css +2 -0
  41. package/styles/ui-select.css +9 -7
  42. package/styles/ui-switch.css +3 -0
  43. package/styles/ui-textarea.css +2 -1
  44. package/theme-light.css +1 -0
  45. package/theme-system.css +43 -7
  46. package/tokens.css +4 -0
  47. package/vanilla/UiCard.d.ts +2 -1
  48. package/vanilla/UiCard.js +12 -12
  49. package/vanilla/UiLightEditor.d.ts +29 -0
  50. package/vanilla/UiLightEditor.js +102 -0
  51. package/vanilla/UiNumberInput.js +27 -20
  52. package/vanilla/index.js +1 -0
  53. package/vue/UiBadge.vue +29 -4
  54. package/vue/UiButton.vue +33 -4
  55. package/vue/UiCard.d.ts +8 -2
  56. package/vue/UiCard.vue +41 -17
  57. package/vue/UiCheckbox.vue +4 -2
  58. package/vue/UiDialog.vue +4 -0
  59. package/vue/UiIconButton.vue +26 -11
  60. package/vue/UiInput.vue +2 -1
  61. package/vue/UiInputGroup.vue +4 -1
  62. package/vue/UiLightEditor.d.ts +75 -0
  63. package/vue/UiLightEditor.js +2 -0
  64. package/vue/UiLightEditor.vue +402 -0
  65. package/vue/UiMenu.vue +2 -0
  66. package/vue/UiNumberInput.vue +32 -3
  67. package/vue/UiPopover.vue +2 -0
  68. package/vue/UiSelect.vue +16 -10
  69. package/vue/UiSwitch.vue +3 -0
  70. package/vue/UiTextarea.vue +2 -1
  71. package/vue/chunks/UiBadge.js +1 -1
  72. package/vue/chunks/UiButton.js +1 -1
  73. package/vue/chunks/UiCard.js +33 -5
  74. package/vue/chunks/UiCheckbox.js +1 -1
  75. package/vue/chunks/UiDialog.js +1 -1
  76. package/vue/chunks/UiIconButton.js +1 -1
  77. package/vue/chunks/UiInput.js +1 -1
  78. package/vue/chunks/UiInputGroup.js +1 -1
  79. package/vue/chunks/UiLightEditor.js +347 -0
  80. package/vue/chunks/UiMenu.js +1 -1
  81. package/vue/chunks/UiNumberInput.js +21 -8
  82. package/vue/chunks/UiPopover.js +1 -1
  83. package/vue/chunks/UiSelect.js +1 -1
  84. package/vue/chunks/UiSwitch.js +1 -1
  85. package/vue/chunks/UiTextarea.js +1 -1
  86. package/vue/components.css +306 -66
  87. package/vue/index.d.ts +1 -0
  88. package/vue/index.js +2 -1
@@ -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"]) {
@@ -188,7 +188,7 @@
188
188
  :host {
189
189
  /* Optional inherited action-group values keep text and icon actions under the same light.
190
190
  One fine top/start highlight follows the bottom/end lowlight at a joined action seam.
191
- 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. */
192
192
  --_ui-action-start: 1px;
193
193
  --_ui-action-edge: var(
194
194
  --ui-action-edge,
@@ -196,6 +196,8 @@
196
196
  inset calc(-1 * var(--_ui-action-start)) -1px 0 light-dark(rgb(0 0 0 / 0.08), rgb(0 0 0 / 0.08))
197
197
  );
198
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);
199
201
  --_hue: var(--ui-accent);
200
202
  --_hue-hover: var(--ui-accent-hover);
201
203
  --_hue-active: var(--ui-accent-active);
@@ -246,8 +248,8 @@
246
248
  white-space: var(--ui-button-white-space, inherit);
247
249
  line-height: var(--ui-line-height-tight);
248
250
  padding: var(--ui-control-inset-block-md, var(--ui-space-2)) var(--ui-action-inset-md, var(--ui-space-3)) !important;
249
- background-image: none;
250
- 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);
251
253
  cursor: pointer;
252
254
  transition:
253
255
  scale var(--ui-motion-press, var(--ui-motion-fast)) var(--ui-motion-ease),
@@ -379,7 +381,8 @@
379
381
  border-color: var(--_tone);
380
382
  background-color: var(--_tone);
381
383
  color: var(--_on-tone);
382
- 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);
383
386
  }
384
387
 
385
388
  :is(:host[data-appearance="solid"], :host[data-appearance="danger"]):hover:not(:disabled, [aria-disabled="true"]) {
@@ -397,6 +400,8 @@
397
400
  /* Tone is the colour, variant is the volume. Every pairing works: a danger outline is a
398
401
  destructive secondary action, not a special case. */
399
402
  :host[data-tone="neutral"] {
403
+ --_tone-highlight: var(--ui-neutral-highlight, none);
404
+ --_tone-glow: var(--ui-neutral-glow, 0 0 transparent);
400
405
  --_hue: var(--ui-text);
401
406
  --_hue-hover: var(--ui-text);
402
407
  --_hue-active: var(--ui-text);
@@ -404,7 +409,21 @@
404
409
  --_on-hue: var(--ui-surface);
405
410
  }
406
411
 
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
+
407
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);
408
427
  --_hue: var(--ui-danger);
409
428
  --_hue-hover: var(--ui-danger-hover);
410
429
  --_hue-active: var(--ui-danger-active, var(--ui-danger-hover));
@@ -413,6 +432,8 @@
413
432
  }
414
433
 
415
434
  :host[data-tone="success"] {
435
+ --_tone-highlight: var(--ui-success-highlight, none);
436
+ --_tone-glow: var(--ui-success-glow, 0 0 transparent);
416
437
  --_hue: var(--ui-success);
417
438
  --_hue-hover: var(--ui-success-hover);
418
439
  --_hue-active: var(--ui-success-active);
@@ -421,6 +442,8 @@
421
442
  }
422
443
 
423
444
  :host[data-tone="warning"] {
445
+ --_tone-highlight: var(--ui-warning-highlight, none);
446
+ --_tone-glow: var(--ui-warning-glow, 0 0 transparent);
424
447
  --_hue: var(--ui-warning);
425
448
  --_hue-hover: var(--ui-warning-hover);
426
449
  --_hue-active: var(--ui-warning-active);
@@ -429,6 +452,8 @@
429
452
  }
430
453
 
431
454
  :host[data-tone="info"] {
455
+ --_tone-highlight: var(--ui-info-highlight, none);
456
+ --_tone-glow: var(--ui-info-glow, 0 0 transparent);
432
457
  --_hue: var(--ui-info);
433
458
  --_hue-hover: var(--ui-info-hover);
434
459
  --_hue-active: var(--ui-info-active);
@@ -2,14 +2,16 @@
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, glass" default="outlined"
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. glass frames an opaque readable content plane with frosted material;
8
- shared glassiness and surface separation govern its treatment.</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>
9
11
  <prop name="padding" type="keyword" values="xs, sm, md, lg" default="md"
10
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>
11
13
  <prop name="tone" type="keyword" values="neutral, accent, info, success, warning, danger" default="neutral"
12
- >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
13
15
  surface hue; action controls retain their own intent.</prop>
14
16
  <prop name="as" type="keyword" values="div, section, article, aside" default="div"
15
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>
@@ -25,6 +27,7 @@
25
27
  needs no layout of its own. */
26
28
  :host {
27
29
  --_ui-card-surface: var(--ui-surface-raised);
30
+ --_ui-card-elevation: 0 0 0 transparent;
28
31
  --_ui-selection-context-surface: initial;
29
32
  --_ui-selection-context-border: initial;
30
33
  --_ui-selection-context-text: initial;
@@ -59,33 +62,36 @@
59
62
  }
60
63
 
61
64
  :host([variant="elevated"]) {
65
+ --_ui-card-elevation: var(--ui-shadow-lg);
62
66
  border-color: var(--ui-border-subtle);
63
67
  box-shadow: var(--ui-shadow-lg);
64
68
  }
65
69
 
66
- :host([variant="glass"]) {
70
+ :host([treatment="expressive"]) {
67
71
  --_ui-panel-glass: clamp(0, var(--ui-glassiness, 0), 1);
68
72
  --_ui-panel-separation: clamp(0, var(--ui-surface-separation, 0), 1);
69
73
  --_ui-panel-rim: calc((var(--_ui-panel-separation) + var(--_ui-panel-glass) * 0.25) * 0.65 * 8px);
70
74
  border-width: calc(var(--ui-border-width) + var(--_ui-panel-rim));
71
75
  border-color: transparent;
72
76
  background:
77
+ var(--ui-surface-highlight, linear-gradient(transparent, transparent)) padding-box,
73
78
  linear-gradient(var(--_ui-card-surface), var(--_ui-card-surface)) padding-box,
74
79
  linear-gradient(var(--ui-material-glass, var(--ui-surface-raised)), var(--ui-material-glass, var(--ui-surface-raised))) border-box,
75
80
  var(--_ui-card-surface) padding-box;
76
81
  box-shadow:
77
- var(--ui-shadow-lg),
82
+ var(--_ui-card-elevation),
83
+ var(--ui-material-glow, 0 0 transparent),
78
84
  inset 0 0 0 var(--ui-border-width) var(--ui-material-edge, var(--ui-border-strong));
79
85
  backdrop-filter: blur(calc(var(--_ui-panel-glass) * 0.65 * 18px)) saturate(calc(1 + var(--_ui-panel-glass) * 0.65 * 0.18));
80
86
  -webkit-backdrop-filter: blur(calc(var(--_ui-panel-glass) * 0.65 * 18px)) saturate(calc(1 + var(--_ui-panel-glass) * 0.65 * 0.18));
81
87
  }
82
88
  @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
83
- :host([variant="glass"]) {
89
+ :host([treatment="expressive"]) {
84
90
  background: var(--_ui-card-surface);
85
91
  }
86
92
  }
87
93
  @media (prefers-reduced-transparency: reduce) {
88
- :host([variant="glass"]) {
94
+ :host([treatment="expressive"]) {
89
95
  background: var(--_ui-card-surface);
90
96
  border-width: var(--ui-border-width);
91
97
  border-color: var(--ui-border-strong);
@@ -94,7 +100,7 @@
94
100
  }
95
101
  }
96
102
  @media (forced-colors: active) {
97
- :host([variant="glass"]) {
103
+ :host([treatment="expressive"]) {
98
104
  background: Canvas;
99
105
  color: CanvasText;
100
106
  border: var(--ui-border-width) solid CanvasText;
@@ -130,10 +136,6 @@
130
136
 
131
137
  :host([tone="danger"]) {
132
138
  border-color: color-mix(in srgb, var(--ui-danger) 45%, var(--ui-border-default));
133
- border-inline-start: var(--ui-accent-line-width) solid var(--ui-danger);
134
- /* An accent edge keeps the corners on its side straight. */
135
- border-start-start-radius: 0;
136
- border-end-start-radius: 0;
137
139
  --_ui-card-surface: var(--ui-danger-soft);
138
140
  }
139
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
 
@@ -119,6 +119,7 @@
119
119
  --_ui-material-content-color: var(--ui-dialog-surface, var(--ui-material-content, var(--ui-overlay-surface, var(--ui-surface-elevated))));
120
120
  --_ui-material-rim-color: color-mix(in srgb, var(--ui-material-glass, var(--ui-surface-elevated)) calc(var(--_ui-material-active) * 100%), transparent);
121
121
  background:
122
+ var(--ui-surface-highlight, linear-gradient(transparent, transparent)) padding-box,
122
123
  linear-gradient(var(--_ui-material-content-color), var(--_ui-material-content-color)) padding-box,
123
124
  linear-gradient(var(--_ui-material-rim-color), var(--_ui-material-rim-color)) border-box,
124
125
  var(--_ui-material-content-color) padding-box;
@@ -134,6 +135,7 @@
134
135
  0 calc(var(--_ui-material-separation) * var(--_ui-material-scale, 1) * 8px) calc(
135
136
  var(--_ui-material-separation) * var(--_ui-material-scale, 1) * 24px
136
137
  ) color-mix(in oklab, var(--ui-text-primary) 15%, transparent),
138
+ var(--ui-material-glow, 0 0 transparent),
137
139
  inset 0 0 0 calc(var(--_ui-material-active) * var(--ui-border-width)) var(--ui-material-edge, var(--ui-border-strong)),
138
140
  var(--ui-elevation-dialog, var(--ui-shadow-xl))
139
141
  )
@@ -133,7 +133,9 @@
133
133
  :host {
134
134
  /* Optional inherited action-group values keep text and icon actions under the same light.
135
135
  One fine top/start highlight follows the bottom/end lowlight at a joined action seam.
136
- A single ambient lift avoids stacking a face gradient or another inset bevel. */
136
+ The shared theme supplies reflections and glow independently of the resting lift. */
137
+ --_tone-highlight: var(--ui-accent-highlight, none);
138
+ --_tone-glow: var(--ui-accent-glow, 0 0 transparent);
137
139
  --_ui-action-start: 1px;
138
140
  --_ui-action-edge: var(
139
141
  --ui-action-edge,
@@ -154,6 +156,8 @@
154
156
  }
155
157
 
156
158
  :host[data-tone="accent"] {
159
+ --_tone-highlight: var(--ui-accent-highlight, none);
160
+ --_tone-glow: var(--ui-accent-glow, 0 0 transparent);
157
161
  --_icon-button-border: var(--ui-accent-solid);
158
162
  --_icon-button-hover-border: var(--ui-accent-hover);
159
163
  --_icon-button-text: var(--ui-accent-subtle-text, var(--ui-accent-solid));
@@ -162,6 +166,8 @@
162
166
  }
163
167
 
164
168
  :host[data-tone="danger"] {
169
+ --_tone-highlight: var(--ui-danger-highlight, none);
170
+ --_tone-glow: var(--ui-danger-glow, 0 0 transparent);
165
171
  --_icon-button-border: var(--ui-danger);
166
172
  --_icon-button-hover-border: var(--ui-danger-hover);
167
173
  --_icon-button-text: var(--ui-danger-subtle-text);
@@ -173,6 +179,8 @@
173
179
  }
174
180
 
175
181
  :host[data-tone="success"] {
182
+ --_tone-highlight: var(--ui-success-highlight, none);
183
+ --_tone-glow: var(--ui-success-glow, 0 0 transparent);
176
184
  --_icon-button-border: var(--ui-success);
177
185
  --_icon-button-hover-border: var(--ui-success-hover);
178
186
  --_icon-button-text: var(--ui-success-subtle-text);
@@ -184,6 +192,8 @@
184
192
  }
185
193
 
186
194
  :host[data-tone="warning"] {
195
+ --_tone-highlight: var(--ui-warning-highlight, none);
196
+ --_tone-glow: var(--ui-warning-glow, 0 0 transparent);
187
197
  --_icon-button-border: var(--ui-warning);
188
198
  --_icon-button-hover-border: var(--ui-warning-hover);
189
199
  --_icon-button-text: var(--ui-warning-subtle-text);
@@ -195,6 +205,8 @@
195
205
  }
196
206
 
197
207
  :host[data-tone="info"] {
208
+ --_tone-highlight: var(--ui-info-highlight, none);
209
+ --_tone-glow: var(--ui-info-glow, 0 0 transparent);
198
210
  --_icon-button-border: var(--ui-info);
199
211
  --_icon-button-hover-border: var(--ui-info-hover);
200
212
  --_icon-button-text: var(--ui-info-subtle-text);
@@ -207,9 +219,11 @@
207
219
 
208
220
  /* Omitted tone retains the existing accent solid; explicit neutral is additive. */
209
221
  :host[data-tone="neutral"] {
210
- --_icon-button-solid-surface: var(--ui-text-primary);
211
- --_icon-button-solid-hover-surface: var(--ui-text-primary);
212
- --_icon-button-solid-text: var(--ui-surface-default);
222
+ --_tone-highlight: var(--ui-neutral-highlight, none);
223
+ --_tone-glow: var(--ui-neutral-glow, 0 0 transparent);
224
+ --_icon-button-solid-surface: var(--ui-neutral-surface);
225
+ --_icon-button-solid-hover-surface: var(--ui-neutral-hover);
226
+ --_icon-button-solid-text: var(--ui-text);
213
227
  }
214
228
 
215
229
  :host([anticipatory]) {
@@ -377,16 +391,16 @@
377
391
  :host[data-appearance="outline"] {
378
392
  border-color: var(--ui-icon-button-border, var(--_icon-button-border, var(--ui-control-border, var(--ui-border-strong))));
379
393
  background-color: var(--ui-icon-button-outline-surface, var(--_icon-button-outline-surface, var(--ui-surface-default)));
380
- background-image: none;
394
+ background-image: var(--ui-surface-highlight, none);
381
395
  color: var(--ui-icon-button-outline-text, var(--_icon-button-text, var(--ui-text-primary)));
382
- box-shadow: var(--ui-action-shadow, var(--_ui-action-shadow));
396
+ box-shadow: var(--ui-action-shadow, var(--_ui-action-shadow)), var(--_tone-glow);
383
397
  }
384
398
 
385
399
  :host[data-appearance="solid"] {
386
400
  background-color: var(--ui-icon-button-solid-surface, var(--_icon-button-solid-surface, var(--ui-accent-solid)));
387
- background-image: none;
401
+ background-image: var(--_tone-highlight);
388
402
  color: var(--ui-icon-button-solid-text, var(--_icon-button-solid-text, var(--ui-text-on-accent)));
389
- box-shadow: var(--ui-icon-button-solid-shadow, var(--ui-action-shadow, var(--_ui-action-shadow)));
403
+ box-shadow: var(--ui-icon-button-solid-shadow, var(--ui-action-shadow, var(--_ui-action-shadow))), var(--_tone-glow);
390
404
  }
391
405
 
392
406
  :host:hover:enabled:not([aria-disabled="true"]) {
@@ -108,7 +108,8 @@
108
108
  border: var(--ui-input-border-width, var(--_ui-default-input-border-width, var(--ui-border-width))) solid
109
109
  var(--ui-input-border, var(--_ui-default-input-border, var(--ui-control-border, var(--ui-border-strong))));
110
110
  border-radius: var(--ui-input-radius, var(--_ui-default-input-radius, var(--ui-field-radius, var(--ui-radius-md))));
111
- background: var(--ui-input-surface, var(--_ui-default-input-surface, var(--ui-control-surface, var(--ui-surface-elevated))));
111
+ background-color: var(--ui-input-surface, var(--_ui-default-input-surface, var(--ui-control-surface, var(--ui-surface-elevated))));
112
+ background-image: var(--_ui-default-input-highlight, var(--ui-surface-highlight, none));
112
113
  color: var(--ui-text-primary);
113
114
  font: inherit;
114
115
  line-height: var(--ui-line-height-tight);
@@ -53,6 +53,7 @@
53
53
  --_ui-default-input-focus-outline: none;
54
54
  --_ui-default-input-radius: 0;
55
55
  --_ui-default-input-surface: transparent;
56
+ --_ui-default-input-highlight: none;
56
57
  --_ui-default-textarea-border-width: 0;
57
58
  --_ui-default-textarea-border: transparent;
58
59
  --_ui-default-textarea-border-hover: transparent;
@@ -64,6 +65,7 @@
64
65
  --_ui-default-textarea-focus-outline: none;
65
66
  --_ui-default-textarea-radius: 0;
66
67
  --_ui-default-textarea-surface: transparent;
68
+ --_ui-default-textarea-highlight: none;
67
69
 
68
70
  display: flex !important;
69
71
  align-items: stretch !important;
@@ -74,7 +76,8 @@
74
76
  /* The frame reads the shared control tokens with a lone Input's fallbacks, so the two match. */
75
77
  border: var(--ui-border-width) solid var(--ui-control-border, var(--ui-border-strong));
76
78
  border-radius: var(--ui-input-group-radius, var(--ui-field-radius, var(--ui-radius-md)));
77
- background: var(--ui-control-surface, var(--ui-surface-elevated));
79
+ background-color: var(--ui-control-surface, var(--ui-surface-elevated));
80
+ background-image: var(--ui-surface-highlight, none);
78
81
  box-shadow: var(--ui-control-inset);
79
82
  overflow: hidden;
80
83
  /* A press anywhere in the frame focuses the input. */
@@ -0,0 +1,45 @@
1
+ <link rel="component" href="../ui-number-input/ui-number-input.html">
2
+ <template component="ui-light-editor" module="./ui-light-editor.js">
3
+ <defs>
4
+ <state name="horizontal" type="string" value="50"></state>
5
+ <state name="vertical" type="string" value="0"></state>
6
+ <prop name="x" type="number" default="0.5">Horizontal source position from 0 (left) to 1 (right), relative to every surface.</prop>
7
+ <prop name="y" type="number" default="0">Vertical source position from 0 (top) to 1 (bottom), relative to every surface.</prop>
8
+ <prop name="label" type="string" default="Light position">Accessible name for the position editor.</prop>
9
+ <prop name="size" type="keyword" values="sm, md, lg" default="md">Sets both coordinate fields to the shared small, medium or large control height.</prop>
10
+ <prop name="disabled" type="boolean" default="false">Prevents pointer, keyboard and exact edits.</prop>
11
+ <event name="input" type="object"><prop name="x" type="number" required></prop><prop name="y" type="number" required></prop></event>
12
+ <event name="change" type="object"><prop name="x" type="number" required></prop><prop name="y" type="number" required></prop></event>
13
+ </defs>
14
+ <span>
15
+ <span class="stage" from:aria-label="$label" role="group">
16
+ <span class="plane" $ref="plane">
17
+ <svg viewBox="0 0 100 60" preserveAspectRatio="none" aria-hidden="true"><path d="M0 30 H100 M50 0 V60"></path></svg>
18
+ <span class="specimen"><slot></slot></span>
19
+ <button type="button" class="source" $ref="source" from:disabled="$disabled" from:aria-label="concat($label, '. Arrow keys move the source; Shift moves faster.')"></button>
20
+ </span>
21
+ </span>
22
+ <span class="coordinates">
23
+ <ui-number-input data-coordinate="x" from:value="$horizontal" min="0" max="100" step="1" unit="%" label="Light horizontal position" from:size="$size" from:disabled="$disabled"></ui-number-input>
24
+ <ui-number-input data-coordinate="y" from:value="$vertical" min="0" max="100" step="1" unit="%" label="Light vertical position" from:size="$size" from:disabled="$disabled"></ui-number-input>
25
+ </span>
26
+ <span class="hint">Drag the light, use arrow keys, or enter its position. Top centre is 50%, 0%.</span>
27
+ </span>
28
+ <style>
29
+ :host { display: grid !important; gap: var(--ui-space-3) !important; min-inline-size: 0; padding: 0 !important; margin: 0 !important; }
30
+ .stage { box-sizing: border-box !important; display: block !important; block-size: 13rem; padding: calc(var(--ui-control-min-block-size) / 2) !important; border: var(--ui-border-width) solid var(--ui-control-border); border-radius: var(--ui-field-radius, var(--ui-radius-md)); background: var(--ui-neutral-surface, var(--ui-surface)); }
31
+ .plane { position: relative; display: block !important; inline-size: 100%; block-size: 100%; }
32
+ svg { inline-size: 100%; block-size: 100%; overflow: visible; }
33
+ path { fill: none; stroke: var(--ui-border); stroke-width: 1; vector-effect: non-scaling-stroke; }
34
+ .specimen { position: absolute; inset: 0; display: grid !important; place-items: center !important; }
35
+ .source { position: absolute; left: 50%; top: 0; translate: -50% -50%; display: grid !important; place-items: center !important; inline-size: max(2rem, var(--ui-control-min-block-size)); block-size: max(2rem, var(--ui-control-min-block-size)); padding: 0 !important; margin: 0 !important; border: 0; border-radius: 50%; background: transparent; cursor: grab; touch-action: none; }
36
+ .source::after { content: ""; inline-size: 1rem; block-size: 1rem; border: var(--ui-border-width) solid var(--ui-accent); border-radius: 50%; background: var(--ui-surface-raised); box-shadow: 0 0 0 var(--ui-space-2) color-mix(in srgb, var(--ui-accent) 12%, transparent), var(--ui-shadow-sm); }
37
+ .source:active { cursor: grabbing; }
38
+ .source:focus-visible { outline: var(--ui-focus-width) solid var(--ui-focus-ring); outline-offset: var(--ui-space-1); }
39
+ .source:disabled { cursor: not-allowed; }
40
+ .source:disabled::after { background: var(--ui-disabled-surface); border-color: var(--ui-disabled-border); box-shadow: none; }
41
+ .coordinates { display: grid !important; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--ui-space-2) !important; }
42
+ .hint { font-size: var(--ui-font-size-sm); color: var(--ui-text-secondary); }
43
+ @media (forced-colors: active) { .source::after { background: Canvas; border-color: CanvasText; } .source:focus-visible { outline-color: Highlight; } }
44
+ </style>
45
+ </template>
@@ -0,0 +1,53 @@
1
+ const coordinate = value => Number.isFinite(Number(value)) ? Math.round(Math.max(0, Math.min(1, Number(value))) * 100) / 100 : 0.5;
2
+
3
+ export function connect(host) {
4
+ const { plane, source } = host.refs;
5
+ let x = coordinate(host.props.x.value), y = coordinate(host.props.y.value), drag;
6
+ const listeners = [];
7
+ const listen = (element, type, fn) => { element.addEventListener(type, fn); listeners.push(() => element.removeEventListener(type, fn)); };
8
+ const paint = () => {
9
+ x = coordinate(x); y = coordinate(y);
10
+ source.style.left = `${x * 100}%`; source.style.top = `${y * 100}%`;
11
+ source.setAttribute('aria-description', `Horizontal ${Math.round(x * 100)} percent; vertical ${Math.round(y * 100)} percent.`);
12
+ host.state.horizontal = String(Math.round(x * 100)); host.state.vertical = String(Math.round(y * 100));
13
+ };
14
+ const emit = type => host.dispatch(type, { x, y });
15
+ listen(source, 'pointerdown', event => {
16
+ if (host.props.disabled.value || event.button !== 0) return;
17
+ event.preventDefault(); source.focus(); drag = event.pointerId; source.setPointerCapture(drag);
18
+ });
19
+ listen(source, 'pointermove', event => {
20
+ if (drag !== event.pointerId || host.props.disabled.value) return;
21
+ const rect = plane.getBoundingClientRect(); if (!rect.width || !rect.height) return;
22
+ x = (event.clientX - rect.left) / rect.width; y = (event.clientY - rect.top) / rect.height;
23
+ paint(); emit('input');
24
+ });
25
+ const end = event => {
26
+ if (drag !== event.pointerId) return;
27
+ drag = undefined;
28
+ if (source.hasPointerCapture(event.pointerId)) source.releasePointerCapture(event.pointerId);
29
+ if (!host.props.disabled.value) emit('change');
30
+ };
31
+ listen(source, 'pointerup', end); listen(source, 'pointercancel', end); listen(source, 'lostpointercapture', end);
32
+ listen(source, 'keydown', event => {
33
+ if (host.props.disabled.value || !['ArrowLeft','ArrowRight','ArrowUp','ArrowDown','Home','End'].includes(event.key)) return;
34
+ event.preventDefault(); const step = event.shiftKey ? .1 : .01;
35
+ if (event.key === 'Home') { x = .5; y = 0; }
36
+ else if (event.key === 'End') { x = .5; y = 1; }
37
+ else if (event.key === 'ArrowLeft') x -= step;
38
+ else if (event.key === 'ArrowRight') x += step;
39
+ else if (event.key === 'ArrowUp') y -= step;
40
+ else y += step;
41
+ paint(); emit('input'); emit('change');
42
+ });
43
+ for (const field of host.element.querySelectorAll('[data-coordinate]')) for (const type of ['input','change']) {
44
+ listen(field, type, event => {
45
+ event.stopPropagation(); if (host.props.disabled.value) return;
46
+ if (field.dataset.coordinate === 'x') x = parseFloat(event.detail.value) / 100;
47
+ else y = parseFloat(event.detail.value) / 100;
48
+ paint(); emit(type);
49
+ });
50
+ }
51
+ const stop = host.effect(() => { x = coordinate(host.props.x.value); y = coordinate(host.props.y.value); paint(); });
52
+ return () => { stop(); listeners.forEach(cleanup => cleanup()); if (drag !== undefined && source.hasPointerCapture(drag)) source.releasePointerCapture(drag); };
53
+ }
@@ -136,6 +136,7 @@
136
136
  --_ui-material-content-color: var(--_ui-menu-surface, var(--ui-material-content, var(--ui-overlay-surface, var(--ui-surface-elevated))));
137
137
  --_ui-material-rim-color: color-mix(in srgb, var(--ui-material-glass, var(--ui-surface-elevated)) calc(var(--_ui-material-active) * 100%), transparent);
138
138
  background:
139
+ var(--ui-surface-highlight, linear-gradient(transparent, transparent)) padding-box,
139
140
  linear-gradient(var(--_ui-material-content-color), var(--_ui-material-content-color)) padding-box,
140
141
  linear-gradient(var(--_ui-material-rim-color), var(--_ui-material-rim-color)) border-box,
141
142
  var(--_ui-material-content-color) padding-box;
@@ -151,6 +152,7 @@
151
152
  0 calc(var(--_ui-material-separation) * var(--_ui-material-scale, 0.28) * 8px) calc(
152
153
  var(--_ui-material-separation) * var(--_ui-material-scale, 0.28) * 24px
153
154
  ) color-mix(in oklab, var(--ui-text-primary) 15%, transparent),
155
+ var(--ui-material-glow, 0 0 transparent),
154
156
  inset 0 0 0 calc(var(--_ui-material-active) * var(--ui-border-width)) var(--ui-material-edge, var(--ui-border-strong)),
155
157
  var(--ui-elevation-overlay, var(--ui-shadow-lg))
156
158
  )
@@ -3,6 +3,7 @@
3
3
  <link rel="component" href="../ui-icon/ui-icon.html">
4
4
  <template component="ui-number-input" module="./ui-number-input.js">
5
5
  <defs>
6
+ <state name="display" type="string" from="$value"></state>
6
7
  <prop name="value" type="string" default="0"
7
8
  >Exact value. Arithmetic is calculated on Enter or blur; the resulting value includes unit. Invalid expressions stay visible and expose invalid
8
9
  state.</prop>
@@ -30,7 +31,7 @@
30
31
  data-ui-field-control
31
32
  from:size="$size"
32
33
  from:disabled="$disabled"
33
- from:value="$value"
34
+ from:value="$display"
34
35
  from:aria-label="$label"
35
36
  from:name="$name"
36
37
  inputmode="decimal"
@@ -7,9 +7,12 @@ export function connect(host) {
7
7
  const increase = host.element.querySelector('[data-number-step="increase"]');
8
8
  let current = 0;
9
9
  let external;
10
+ let resetValue;
10
11
  const unit = () => host.props.unit.value || "";
11
12
  const normalize = value => boundedNumber(value, host.props.min.value, host.props.max.value);
12
- const paint = value => { current = normalize(value); input.value = numberText(current, unit()); input.removeAttribute("aria-invalid"); input.setCustomValidity(""); input.setAttribute("role", "spinbutton"); input.setAttribute("aria-valuenow", String(current)); input.setAttribute("aria-valuetext", input.value); input.setAttribute("aria-valuemin", String(host.props.min.value)); input.setAttribute("aria-valuemax", String(host.props.max.value)); };
13
+ // Bind the normalized display, so a later child binding flush cannot restore the
14
+ // raw prop (for example "51") over its calculated value with units ("51%").
15
+ const paint = value => { current = normalize(value); input.value = numberText(current, unit()); host.state.display = input.value; input.removeAttribute("aria-invalid"); input.setCustomValidity(""); input.setAttribute("role", "spinbutton"); input.setAttribute("aria-valuenow", String(current)); input.setAttribute("aria-valuetext", input.value); input.setAttribute("aria-valuemin", String(host.props.min.value)); input.setAttribute("aria-valuemax", String(host.props.max.value)); };
13
16
  const emit = () => { const value = numberText(current, unit()); host.dispatch("input", { value }); host.dispatch("change", { value }); };
14
17
  const commit = () => {
15
18
  if (host.props.disabled.value) return;
@@ -37,8 +40,8 @@ export function connect(host) {
37
40
  const stop = host.effect(() => {
38
41
  const signature = JSON.stringify([host.props.value.value, unit(), host.props.min.value, host.props.max.value]);
39
42
  if (signature === external) return; external = signature;
40
- try { paint(calculateValue(host.props.value.value, unit())); input.defaultValue = input.value; } catch { paint(current); }
43
+ try { paint(calculateValue(host.props.value.value, unit())); resetValue = input.value; input.defaultValue = resetValue; } catch { paint(current); }
41
44
  });
42
- const reset = afterFormReset(input, () => { try { paint(calculateValue(input.defaultValue, unit())); } catch { paint(0); } });
45
+ const reset = afterFormReset(input, () => { try { paint(calculateValue(resetValue, unit())); } catch { paint(0); } });
43
46
  return () => { stop(); reset(); input.removeEventListener("input", stopInput); input.removeEventListener("change", stopInput); input.removeEventListener("keydown", key); input.removeEventListener("blur", commit); decrease.removeEventListener("click", down); increase.removeEventListener("click", up); };
44
47
  }
@@ -107,6 +107,7 @@
107
107
  --_ui-material-content-color: var(--_ui-popover-surface, var(--ui-material-content, var(--ui-overlay-surface, var(--ui-surface-elevated))));
108
108
  --_ui-material-rim-color: color-mix(in srgb, var(--ui-material-glass, var(--ui-surface-elevated)) calc(var(--_ui-material-active) * 100%), transparent);
109
109
  background:
110
+ var(--ui-surface-highlight, linear-gradient(transparent, transparent)) padding-box,
110
111
  linear-gradient(var(--_ui-material-content-color), var(--_ui-material-content-color)) padding-box,
111
112
  linear-gradient(var(--_ui-material-rim-color), var(--_ui-material-rim-color)) border-box,
112
113
  var(--_ui-material-content-color) padding-box;
@@ -122,6 +123,7 @@
122
123
  0 calc(var(--_ui-material-separation) * var(--_ui-material-scale, 0.55) * 8px) calc(
123
124
  var(--_ui-material-separation) * var(--_ui-material-scale, 0.55) * 24px
124
125
  ) color-mix(in oklab, var(--ui-text-primary) 15%, transparent),
126
+ var(--ui-material-glow, 0 0 transparent),
125
127
  inset 0 0 0 calc(var(--_ui-material-active) * var(--ui-border-width)) var(--ui-material-edge, var(--ui-border-strong)),
126
128
  var(--ui-elevation-overlay, var(--ui-shadow-lg))
127
129
  )
@@ -68,7 +68,7 @@
68
68
  border-radius: var(--ui-select-radius, var(--ui-field-radius, var(--ui-radius-md)));
69
69
  background-color: var(--ui-select-surface, var(--ui-control-surface, var(--ui-surface-elevated)));
70
70
  color: var(--ui-text-primary);
71
- color-scheme: light dark;
71
+ color-scheme: inherit;
72
72
  font: inherit;
73
73
  line-height: var(--ui-line-height-tight);
74
74
  box-shadow: var(--ui-select-shadow, var(--ui-shadow-sm));
@@ -81,15 +81,16 @@
81
81
  /* The select draws its own chevron: the native caret cannot be inset or sized consistently.
82
82
  * The root stays a native <select>, so the icon is a background image rather than an element. */
83
83
  :host {
84
+ --_ui-select-highlight: var(--ui-surface-highlight, linear-gradient(transparent, transparent));
84
85
  appearance: none;
85
86
  padding-inline-end: calc(var(--ui-space-3) + 1.5rem) !important;
86
87
  background-image: var(
87
88
  --ui-select-chevron,
88
89
  url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235c5c58' stroke-width='2.25' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E")
89
- );
90
+ ), var(--_ui-select-highlight);
90
91
  background-repeat: no-repeat;
91
- background-position: right var(--ui-space-3) center;
92
- background-size: 1.125rem 1.125rem;
92
+ background-position: right var(--ui-space-3) center, center;
93
+ background-size: 1.125rem 1.125rem, 100% 100%;
93
94
  }
94
95
 
95
96
  :host:hover:not(:focus):not(:disabled):not([aria-invalid="true"]):not(:user-invalid) {
@@ -121,6 +122,7 @@
121
122
  }
122
123
 
123
124
  :host:disabled {
125
+ --_ui-select-highlight: none;
124
126
  background-color: var(--ui-control-surface-disabled, var(--ui-surface-muted));
125
127
  color: var(--ui-disabled-text);
126
128
  box-shadow: none;
@@ -131,8 +133,8 @@
131
133
  min-block-size: var(--ui-control-size-sm);
132
134
  padding: var(--ui-control-inset-block-sm, var(--ui-space-1)) var(--ui-space-2) !important;
133
135
  padding-inline-end: calc(var(--ui-space-2) + var(--ui-icon-size-sm) + var(--ui-space-1)) !important;
134
- background-position: right var(--ui-space-2) center;
135
- background-size: var(--ui-icon-size-sm) var(--ui-icon-size-sm);
136
+ background-position: right var(--ui-space-2) center, center;
137
+ background-size: var(--ui-icon-size-sm) var(--ui-icon-size-sm), 100% 100%;
136
138
  font-size: var(--ui-font-size-sm);
137
139
  }
138
140
 
@@ -140,7 +142,7 @@
140
142
  min-block-size: var(--ui-control-size-lg);
141
143
  padding: var(--ui-control-inset-block-lg, var(--ui-space-3)) var(--ui-space-4) !important;
142
144
  padding-inline-end: calc(var(--ui-space-4) + 1.5rem) !important;
143
- background-position: right var(--ui-space-4) center;
145
+ background-position: right var(--ui-space-4) center, center;
144
146
  font-size: var(--ui-font-size-md);
145
147
  }
146
148
 
@@ -114,6 +114,7 @@
114
114
  border: var(--ui-border-width) solid var(--ui-control-border, var(--ui-border-strong));
115
115
  border-radius: var(--ui-radius-round);
116
116
  background-color: var(--ui-control-surface, var(--ui-surface-default));
117
+ background-image: var(--ui-surface-highlight, none);
117
118
  box-shadow: var(--ui-control-inset);
118
119
  cursor: pointer;
119
120
  transition: background-color var(--ui-motion-base) var(--ui-motion-ease), border-color var(--ui-motion-base) var(--ui-motion-ease);
@@ -135,6 +136,7 @@
135
136
 
136
137
  input:checked {
137
138
  background-color: var(--ui-accent-solid);
139
+ background-image: var(--ui-accent-highlight, none);
138
140
  border-color: var(--ui-accent-solid);
139
141
  }
140
142
 
@@ -158,6 +160,7 @@
158
160
  :host([disabled]) input {
159
161
  border-color: var(--ui-disabled-border);
160
162
  background-color: var(--ui-disabled-surface);
163
+ background-image: none;
161
164
  box-shadow: none;
162
165
  cursor: not-allowed;
163
166
  }