@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
@@ -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
+ }
@@ -84,6 +84,7 @@
84
84
  }
85
85
 
86
86
  :host {
87
+ --_ui-material-scale: 0.28;
87
88
  --_ui-menu-surface: var(--ui-menu-surface);
88
89
  --_ui-menu-border: var(--ui-menu-border);
89
90
  --_ui-menu-radius: var(--ui-menu-radius);
@@ -125,6 +126,67 @@
125
126
  overflow: auto;
126
127
  }
127
128
 
129
+ /* Purpose and measured size shape one shared separation lever. The content plane stays opaque;
130
+ transparency, saturation and edge light belong to the surrounding decorative material. */
131
+ :host:not([inline]) .surface {
132
+ --_ui-material-glassiness: clamp(0, var(--ui-glassiness, 0), 1);
133
+ --_ui-material-separation: clamp(0, var(--ui-surface-separation, 0), 1);
134
+ --_ui-material-active: clamp(0, calc((var(--_ui-material-glassiness) + var(--_ui-material-separation)) * 100000), 1);
135
+ --_ui-material-rim: calc((var(--_ui-material-separation) + var(--_ui-material-glassiness) * 0.25) * var(--_ui-material-scale, 0.28) * 8px);
136
+ --_ui-material-content-color: var(--_ui-menu-surface, var(--ui-material-content, var(--ui-overlay-surface, var(--ui-surface-elevated))));
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
+ background:
139
+ var(--ui-surface-highlight, linear-gradient(transparent, transparent)) padding-box,
140
+ linear-gradient(var(--_ui-material-content-color), var(--_ui-material-content-color)) padding-box,
141
+ linear-gradient(var(--_ui-material-rim-color), var(--_ui-material-rim-color)) border-box,
142
+ var(--_ui-material-content-color) padding-box;
143
+ border-width: calc(var(--ui-border-width) + var(--_ui-material-rim));
144
+ border-color: var(
145
+ --_ui-menu-border,
146
+ color-mix(in oklab, var(--ui-overlay-border, var(--ui-border-strong)) calc((1 - var(--_ui-material-active)) * 100%), transparent)
147
+ );
148
+ box-shadow: var(
149
+ --_ui-menu-shadow,
150
+ var(
151
+ --ui-overlay-shadow,
152
+ 0 calc(var(--_ui-material-separation) * var(--_ui-material-scale, 0.28) * 8px) calc(
153
+ var(--_ui-material-separation) * var(--_ui-material-scale, 0.28) * 24px
154
+ ) color-mix(in oklab, var(--ui-text-primary) 15%, transparent),
155
+ var(--ui-material-glow, 0 0 transparent),
156
+ inset 0 0 0 calc(var(--_ui-material-active) * var(--ui-border-width)) var(--ui-material-edge, var(--ui-border-strong)),
157
+ var(--ui-elevation-overlay, var(--ui-shadow-lg))
158
+ )
159
+ );
160
+ backdrop-filter: blur(calc(var(--_ui-material-glassiness) * var(--_ui-material-scale, 0.28) * 18px))
161
+ saturate(calc(1 + var(--_ui-material-glassiness) * var(--_ui-material-scale, 0.28) * 0.18));
162
+ -webkit-backdrop-filter: blur(calc(var(--_ui-material-glassiness) * var(--_ui-material-scale, 0.28) * 18px))
163
+ saturate(calc(1 + var(--_ui-material-glassiness) * var(--_ui-material-scale, 0.28) * 0.18));
164
+ }
165
+
166
+ @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
167
+ :host:not([inline]) .surface {
168
+ background: var(--_ui-material-content-color);
169
+ }
170
+ }
171
+
172
+ @media (prefers-reduced-transparency: reduce) {
173
+ :host:not([inline]) .surface {
174
+ background: var(--_ui-material-content-color);
175
+ backdrop-filter: none;
176
+ -webkit-backdrop-filter: none;
177
+ }
178
+ }
179
+
180
+ @media (forced-colors: active) {
181
+ :host:not([inline]) .surface {
182
+ background: Canvas;
183
+ border-color: CanvasText;
184
+ box-shadow: none;
185
+ backdrop-filter: none;
186
+ -webkit-backdrop-filter: none;
187
+ }
188
+ }
189
+
128
190
  .surface hr {
129
191
  align-self: stretch;
130
192
  block-size: 0;
@@ -63,7 +63,7 @@ export function connect(host) {
63
63
  anchor = ids.get(nextFor);
64
64
  if (anchor) anchorAria = { "aria-haspopup": anchor.getAttribute("aria-haspopup"), "aria-expanded": anchor.getAttribute("aria-expanded") };
65
65
  anchor?.addEventListener("click", onAnchorClick);
66
- surface = host.props.inline.value ? null : createAnchoredSurface(element, { anchor, placement: nextPlacement });
66
+ surface = host.props.inline.value ? null : createAnchoredSurface(element, { anchor, placement: nextPlacement, materialRole: "menu" });
67
67
  };
68
68
  const apply = () => {
69
69
  const inline = Boolean(host.props.inline.value);
@@ -0,0 +1,47 @@
1
+ // Restricted dimensional arithmetic: unitless factors and one matching unit, without code evaluation.
2
+ export function calculateValue(source, unit = "") {
3
+ const text = String(source).trim();
4
+ if (!text || text.length > 256) throw new Error("Enter a number or arithmetic expression.");
5
+ const tokens = text.match(/(?:\d*\.?\d+(?:e[+-]?\d+)?)(?:[a-z%]+)?|[()+*/-]/gi) ?? [];
6
+ if (tokens.join("") !== text.replace(/\s+/g, "")) throw new Error("Unsupported expression.");
7
+ let index = 0;
8
+ const add = () => {
9
+ let left = multiply();
10
+ while (["+", "-"].includes(tokens[index])) {
11
+ const operator = tokens[index++]; const right = multiply();
12
+ // A bare number in addition uses the field's unit.
13
+ if (left.unit && right.unit && left.unit !== right.unit) throw new Error("Use matching units.");
14
+ left = { number: left.number + (operator === "+" ? 1 : -1) * right.number, unit: left.unit || right.unit };
15
+ }
16
+ return left;
17
+ };
18
+ const multiply = () => {
19
+ let left = atom();
20
+ while (["*", "/"].includes(tokens[index])) {
21
+ const operator = tokens[index++]; const right = atom();
22
+ if (operator === "*" && left.unit && right.unit) throw new Error("Multiply by a unitless number.");
23
+ if (operator === "/" && right.unit) throw new Error("Divide by a unitless number.");
24
+ left = { number: operator === "*" ? left.number * right.number : left.number / right.number, unit: left.unit || right.unit };
25
+ }
26
+ return left;
27
+ };
28
+ const atom = () => {
29
+ const token = tokens[index++];
30
+ if (token === "+" || token === "-") { const value = atom(); return { ...value, number: (token === "-" ? -1 : 1) * value.number }; }
31
+ if (token === "(") { const value = add(); if (tokens[index++] !== ")") throw new Error("Close the parentheses."); return value; }
32
+ const match = /^(\d*\.?\d+(?:e[+-]?\d+)?)([a-z%]*)$/i.exec(token ?? "");
33
+ if (!match) throw new Error("Enter a number.");
34
+ if (match[2] && match[2] !== unit) throw new Error(`Use ${unit || "unitless values"}.`);
35
+ return { number: Number(match[1]), unit: match[2] };
36
+ };
37
+ const result = add();
38
+ if (index !== tokens.length || !Number.isFinite(result.number)) throw new Error("Enter a finite result.");
39
+ return result.number;
40
+ }
41
+
42
+ export function boundedNumber(value, min, max) {
43
+ const lower = Number.isFinite(Number(min)) ? Number(min) : -Number.MAX_VALUE;
44
+ const upper = Number.isFinite(Number(max)) ? Number(max) : Number.MAX_VALUE;
45
+ return Math.min(Math.max(value, Math.min(lower, upper)), Math.max(lower, upper));
46
+ }
47
+ export function numberText(value, unit = "") { return `${Number(value.toFixed(6))}${unit}`; }
@@ -0,0 +1,63 @@
1
+ <link rel="component" href="../ui-input/ui-input.html">
2
+ <link rel="component" href="../ui-icon-button/ui-icon-button.html">
3
+ <link rel="component" href="../ui-icon/ui-icon.html">
4
+ <template component="ui-number-input" module="./ui-number-input.js">
5
+ <defs>
6
+ <state name="display" type="string" from="$value"></state>
7
+ <prop name="value" type="string" default="0"
8
+ >Exact value. Arithmetic is calculated on Enter or blur; the resulting value includes unit. Invalid expressions stay visible and expose invalid
9
+ state.</prop>
10
+ <prop name="unit" type="string" default="">One accepted unit, such as px, rem, ms or %. Bare numbers use this unit. No automatic unit conversion.</prop>
11
+ <prop name="min" type="number" default="-1000000">Inclusive lower bound applied to typing, arithmetic, stepping and external values.</prop>
12
+ <prop name="max" type="number" default="1000000">Inclusive upper bound applied to every editing path.</prop>
13
+ <prop name="step" type="number" default="1">Positive increment used by arrows and step buttons; Shift multiplies it by ten.</prop>
14
+ <prop name="name" type="string" default="">Optional form field name.</prop>
15
+ <prop name="label" type="string" default="Value">Accessible name for the editing control.</prop>
16
+ <prop name="size" type="keyword" values="sm, md, lg" default="md">Shared field and button height.</prop>
17
+ <prop name="disabled" type="boolean" default="false">Prevents typing, stepping and pointer or keyboard changes while keeping the value visible.</prop>
18
+ <event name="input" type="object"><prop name="value" type="string" required></prop></event>
19
+ <event name="change" type="object"><prop name="value" type="string" required></prop></event>
20
+ </defs>
21
+ <span>
22
+ <ui-icon-button
23
+ data-number-step="decrease"
24
+ match-button
25
+ type="button"
26
+ from:size="$size"
27
+ from:disabled="$disabled"
28
+ from:label="concat('Decrease ', $label)"
29
+ ><ui-icon name="minus"></ui-icon></ui-icon-button>
30
+ <ui-input
31
+ data-ui-field-control
32
+ from:size="$size"
33
+ from:disabled="$disabled"
34
+ from:value="$display"
35
+ from:aria-label="$label"
36
+ from:name="$name"
37
+ inputmode="decimal"
38
+ spellcheck="false"
39
+ aria-description="Enter calculates arithmetic. Arrow keys step; Shift changes ten steps."
40
+ ></ui-input>
41
+ <ui-icon-button
42
+ data-number-step="increase"
43
+ match-button
44
+ type="button"
45
+ from:size="$size"
46
+ from:disabled="$disabled"
47
+ from:label="concat('Increase ', $label)"
48
+ ><ui-icon name="plus"></ui-icon></ui-icon-button>
49
+ </span>
50
+ <style>
51
+ :host {
52
+ display: flex !important;
53
+ align-items: center !important;
54
+ gap: var(--ui-space-1) !important;
55
+ margin: 0 !important;
56
+ padding: 0 !important;
57
+ min-inline-size: 0;
58
+ }
59
+ :host[hidden] {
60
+ display: none !important;
61
+ }
62
+ </style>
63
+ </template>
@@ -0,0 +1,47 @@
1
+ import { calculateValue, boundedNumber, numberText } from "./arithmetic.js";
2
+ import { afterFormReset } from "../shared/form-reset.js";
3
+
4
+ export function connect(host) {
5
+ const input = host.element.querySelector('input');
6
+ const decrease = host.element.querySelector('[data-number-step="decrease"]');
7
+ const increase = host.element.querySelector('[data-number-step="increase"]');
8
+ let current = 0;
9
+ let external;
10
+ let resetValue;
11
+ const unit = () => host.props.unit.value || "";
12
+ const normalize = value => boundedNumber(value, host.props.min.value, 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)); };
16
+ const emit = () => { const value = numberText(current, unit()); host.dispatch("input", { value }); host.dispatch("change", { value }); };
17
+ const commit = () => {
18
+ if (host.props.disabled.value) return;
19
+ try { paint(calculateValue(input.value, unit())); emit(); }
20
+ catch (error) { input.setAttribute("aria-invalid", "true"); input.setCustomValidity(error.message); }
21
+ };
22
+ const step = (direction, multiplier = 1) => {
23
+ if (host.props.disabled.value) return;
24
+ let value = current;
25
+ try { value = calculateValue(input.value, unit()); } catch { /* Recover from an unfinished expression using the last result. */ }
26
+ const increment = Math.abs(Number(host.props.step.value)) || 1;
27
+ paint(value + direction * increment * multiplier); emit();
28
+ };
29
+ const key = event => {
30
+ if (event.key === "Enter") { event.preventDefault(); commit(); }
31
+ if (event.key === "ArrowUp" || event.key === "ArrowDown") { event.preventDefault(); step(event.key === "ArrowUp" ? 1 : -1, event.shiftKey ? 10 : 1); }
32
+ if (event.key === "Escape") { paint(current); }
33
+ };
34
+ // Do not leak unfinished expressions as accepted component values.
35
+ const stopInput = event => event.stopPropagation();
36
+ const down = () => step(-1); const up = () => step(1);
37
+ input.addEventListener("input", stopInput); input.addEventListener("change", stopInput);
38
+ input.addEventListener("keydown", key); input.addEventListener("blur", commit);
39
+ decrease.addEventListener("click", down); increase.addEventListener("click", up);
40
+ const stop = host.effect(() => {
41
+ const signature = JSON.stringify([host.props.value.value, unit(), host.props.min.value, host.props.max.value]);
42
+ if (signature === external) return; external = signature;
43
+ try { paint(calculateValue(host.props.value.value, unit())); resetValue = input.value; input.defaultValue = resetValue; } catch { paint(current); }
44
+ });
45
+ const reset = afterFormReset(input, () => { try { paint(calculateValue(resetValue, unit())); } catch { paint(0); } });
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); };
47
+ }
@@ -3,7 +3,8 @@
3
3
  <prop name="bare" type="boolean" default="false"
4
4
  >Delegates the surface to its content, with no padding, border, background, shadow, or overflow clipping.</prop>
5
5
  <prop name="size" type="keyword" values="content, sm, md, lg" default="content"
6
- >Optional size preset. content keeps the existing intrinsic sizing; the other sizes constrain the surface for compact, medium, or large content.</prop>
6
+ >Optional size preset. content keeps the existing intrinsic sizing; the other sizes constrain the surface for compact, medium, or large content. Shared
7
+ surface separation adapts to popover purpose and rendered size.</prop>
7
8
  <prop name="open" type="boolean" default="false">Whether the popover is open initially or when set externally.</prop>
8
9
  <prop name="for" type="string" default="">ID of the control that toggles and anchors the popover.</prop>
9
10
  <prop
@@ -63,6 +64,7 @@
63
64
  }
64
65
 
65
66
  :host {
67
+ --_ui-material-scale: 0.55;
66
68
  --_ui-popover-surface: var(--ui-popover-surface);
67
69
  --_ui-popover-border: var(--ui-popover-border);
68
70
  --_ui-popover-radius: var(--ui-popover-radius);
@@ -95,6 +97,67 @@
95
97
  box-shadow: var(--_ui-popover-shadow, var(--ui-overlay-shadow, var(--ui-elevation-overlay, var(--ui-shadow-lg))));
96
98
  }
97
99
 
100
+ /* Purpose and measured size shape one shared separation lever. The content plane stays opaque;
101
+ transparency, saturation and edge light belong to the surrounding decorative material. */
102
+ .surface {
103
+ --_ui-material-glassiness: clamp(0, var(--ui-glassiness, 0), 1);
104
+ --_ui-material-separation: clamp(0, var(--ui-surface-separation, 0), 1);
105
+ --_ui-material-active: clamp(0, calc((var(--_ui-material-glassiness) + var(--_ui-material-separation)) * 100000), 1);
106
+ --_ui-material-rim: calc((var(--_ui-material-separation) + var(--_ui-material-glassiness) * 0.25) * var(--_ui-material-scale, 0.55) * 8px);
107
+ --_ui-material-content-color: var(--_ui-popover-surface, var(--ui-material-content, var(--ui-overlay-surface, var(--ui-surface-elevated))));
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
+ background:
110
+ var(--ui-surface-highlight, linear-gradient(transparent, transparent)) padding-box,
111
+ linear-gradient(var(--_ui-material-content-color), var(--_ui-material-content-color)) padding-box,
112
+ linear-gradient(var(--_ui-material-rim-color), var(--_ui-material-rim-color)) border-box,
113
+ var(--_ui-material-content-color) padding-box;
114
+ border-width: calc(var(--ui-border-width) + var(--_ui-material-rim));
115
+ border-color: var(
116
+ --_ui-popover-border,
117
+ color-mix(in oklab, var(--ui-overlay-border, var(--ui-border-strong)) calc((1 - var(--_ui-material-active)) * 100%), transparent)
118
+ );
119
+ box-shadow: var(
120
+ --_ui-popover-shadow,
121
+ var(
122
+ --ui-overlay-shadow,
123
+ 0 calc(var(--_ui-material-separation) * var(--_ui-material-scale, 0.55) * 8px) calc(
124
+ var(--_ui-material-separation) * var(--_ui-material-scale, 0.55) * 24px
125
+ ) color-mix(in oklab, var(--ui-text-primary) 15%, transparent),
126
+ var(--ui-material-glow, 0 0 transparent),
127
+ inset 0 0 0 calc(var(--_ui-material-active) * var(--ui-border-width)) var(--ui-material-edge, var(--ui-border-strong)),
128
+ var(--ui-elevation-overlay, var(--ui-shadow-lg))
129
+ )
130
+ );
131
+ backdrop-filter: blur(calc(var(--_ui-material-glassiness) * var(--_ui-material-scale, 0.55) * 18px))
132
+ saturate(calc(1 + var(--_ui-material-glassiness) * var(--_ui-material-scale, 0.55) * 0.18));
133
+ -webkit-backdrop-filter: blur(calc(var(--_ui-material-glassiness) * var(--_ui-material-scale, 0.55) * 18px))
134
+ saturate(calc(1 + var(--_ui-material-glassiness) * var(--_ui-material-scale, 0.55) * 0.18));
135
+ }
136
+
137
+ @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
138
+ .surface {
139
+ background: var(--_ui-material-content-color);
140
+ }
141
+ }
142
+
143
+ @media (prefers-reduced-transparency: reduce) {
144
+ .surface {
145
+ background: var(--_ui-material-content-color);
146
+ backdrop-filter: none;
147
+ -webkit-backdrop-filter: none;
148
+ }
149
+ }
150
+
151
+ @media (forced-colors: active) {
152
+ .surface {
153
+ background: Canvas;
154
+ border-color: CanvasText;
155
+ box-shadow: none;
156
+ backdrop-filter: none;
157
+ -webkit-backdrop-filter: none;
158
+ }
159
+ }
160
+
98
161
  /* A toolbar or card can supply its own surface, including an unclipped shadow. */
99
162
  :host([bare]) .surface {
100
163
  overflow: visible;
@@ -102,6 +165,8 @@
102
165
  border: 0;
103
166
  background: transparent;
104
167
  box-shadow: none;
168
+ backdrop-filter: none;
169
+ -webkit-backdrop-filter: none;
105
170
  }
106
171
 
107
172
  :host(:not(:state(internalOpen))) {
@@ -59,7 +59,7 @@ export function connect(host) {
59
59
  anchor = ids.get(nextFor);
60
60
  if (anchor) anchorExpanded = anchor.getAttribute("aria-expanded");
61
61
  anchor?.addEventListener("click", onAnchorClick);
62
- surface = createAnchoredSurface(element, { anchor, placement: nextPlacement, gap: offset });
62
+ surface = createAnchoredSurface(element, { anchor, placement: nextPlacement, gap: offset, materialRole: "popover" });
63
63
  };
64
64
  const apply = () => {
65
65
  const externalOpen = Boolean(host.props.open.value);
@@ -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
 
@@ -0,0 +1,84 @@
1
+ <link rel="component" href="../ui-number-input/ui-number-input.html">
2
+ <template component="ui-slider" module="./ui-slider.js">
3
+ <defs>
4
+ <state name="current" type="number" from="$value"></state>
5
+ <prop name="value" type="number" default="0">Current numeric value. Native range and exact entry share bounds.</prop>
6
+ <prop name="min" type="number" default="0">Inclusive lower bound for dragging, keyboard steps and exact entry.</prop>
7
+ <prop name="max" type="number" default="100">Inclusive upper bound for dragging, keyboard steps and exact entry.</prop>
8
+ <prop name="step" type="number" default="1">Positive native range and exact entry increment.</prop>
9
+ <prop name="unit" type="string" default="">Unit shown by the exact editor.</prop>
10
+ <prop name="name" type="string" default="">Optional numeric form field name.</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="number" required></prop></event>
15
+ <event name="change" type="object"><prop name="value" type="number" required></prop></event>
16
+ </defs>
17
+ <span>
18
+ <input
19
+ $ref="range"
20
+ type="range"
21
+ data-ui-field-control
22
+ from:aria-label="$label"
23
+ from:aria-valuetext="concat($current, $unit)"
24
+ from:value="$value"
25
+ from:min="$min"
26
+ from:max="$max"
27
+ from:step="$step"
28
+ from:disabled="$disabled"
29
+ from:name="$name"
30
+ >
31
+ <ui-number-input
32
+ data-slider-exact
33
+ from:value="concat($current, $unit)"
34
+ from:unit="$unit"
35
+ from:min="$min"
36
+ from:max="$max"
37
+ from:step="$step"
38
+ from:size="$size"
39
+ from:disabled="$disabled"
40
+ from:label="$label"
41
+ ></ui-number-input>
42
+ </span>
43
+ <style>
44
+ :host {
45
+ display: grid !important;
46
+ grid-template-columns: minmax(4rem, 1fr) minmax(8rem, 1fr) !important;
47
+ align-items: center !important;
48
+ gap: var(--ui-space-2) !important;
49
+ margin: 0 !important;
50
+ padding: 0 !important;
51
+ min-inline-size: 0;
52
+ }
53
+ input {
54
+ inline-size: 100%;
55
+ min-inline-size: 0;
56
+ min-block-size: var(--ui-control-size-md);
57
+ margin: 0 !important;
58
+ accent-color: var(--ui-accent-solid);
59
+ cursor: pointer;
60
+ }
61
+ :host([size="sm"]) input {
62
+ min-block-size: var(--ui-control-size-sm);
63
+ }
64
+ :host([size="lg"]) input {
65
+ min-block-size: var(--ui-control-size-lg);
66
+ }
67
+ input:focus-visible {
68
+ outline: var(--ui-focus-width) solid var(--ui-control-focus, var(--ui-accent-solid));
69
+ outline-offset: var(--ui-space-1);
70
+ border-radius: var(--ui-radius-sm);
71
+ }
72
+ input:disabled {
73
+ cursor: not-allowed;
74
+ }
75
+ @media (pointer: coarse) {
76
+ input {
77
+ min-block-size: max(var(--ui-control-size-md), var(--ui-control-min-block-size));
78
+ }
79
+ }
80
+ :host[hidden] {
81
+ display: none !important;
82
+ }
83
+ </style>
84
+ </template>
@@ -0,0 +1,29 @@
1
+ import { boundedNumber, calculateValue } from "../ui-number-input/arithmetic.js";
2
+ import { afterFormReset } from "../shared/form-reset.js";
3
+
4
+ export function connect(host) {
5
+ const { range } = host.refs;
6
+ const exact = host.element.querySelector("[data-slider-exact]");
7
+ let external;
8
+ const paint = value => {
9
+ range.value = String(boundedNumber(value, host.props.min.value, host.props.max.value));
10
+ host.state.current = Number(range.value);
11
+ };
12
+ const edit = event => {
13
+ event.stopPropagation();
14
+ if (host.props.disabled.value) return;
15
+ const raw = event.currentTarget === range ? Number(range.value) : calculateValue(event.detail.value, host.props.unit.value || "");
16
+ paint(raw); host.dispatch(event.type, { value: Number(range.value) });
17
+ };
18
+ for (const field of [range, exact]) for (const type of ["input", "change"]) field.addEventListener(type, edit);
19
+ const stop = host.effect(() => {
20
+ const signature = JSON.stringify([host.props.value.value, host.props.min.value, host.props.max.value, host.props.step.value]);
21
+ if (external === signature) return; external = signature;
22
+ range.min = String(Math.min(Number(host.props.min.value), Number(host.props.max.value)));
23
+ range.max = String(Math.max(Number(host.props.min.value), Number(host.props.max.value)));
24
+ range.step = String(Math.abs(Number(host.props.step.value)) || 1);
25
+ range.defaultValue = String(host.props.value.value); paint(Number(host.props.value.value));
26
+ });
27
+ const reset = afterFormReset(range, () => paint(Number(range.defaultValue)));
28
+ return () => { stop(); reset(); for (const field of [range, exact]) for (const type of ["input", "change"]) field.removeEventListener(type, edit); };
29
+ }
@@ -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
  }