@threadlabs/looma 0.33.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/bin/looma-docs.mjs +3 -0
- package/components/shared/overlay.js +8 -2
- package/components/shared/surface-material.js +21 -0
- package/components/ui-button/ui-button.html +63 -47
- package/components/ui-card/ui-card.html +44 -2
- package/components/ui-curve-editor/curve.js +10 -0
- package/components/ui-curve-editor/ui-curve-editor.html +181 -0
- package/components/ui-curve-editor/ui-curve-editor.js +51 -0
- package/components/ui-dialog/ui-dialog.html +62 -1
- package/components/ui-dialog/ui-dialog.js +9 -1
- package/components/ui-icon-button/ui-icon-button.html +75 -23
- package/components/ui-menu/ui-menu.html +60 -0
- package/components/ui-menu/ui-menu.js +1 -1
- package/components/ui-number-input/arithmetic.js +47 -0
- package/components/ui-number-input/ui-number-input.html +62 -0
- package/components/ui-number-input/ui-number-input.js +44 -0
- package/components/ui-popover/ui-popover.html +64 -1
- package/components/ui-popover/ui-popover.js +1 -1
- package/components/ui-slider/ui-slider.html +84 -0
- package/components/ui-slider/ui-slider.js +29 -0
- package/components/ui-tabs/ui-tabs.html +6 -1
- package/components/ui-text/ui-text.html +31 -6
- package/components/ui-tooltip/ui-tooltip.html +60 -0
- package/components/ui-tooltip/ui-tooltip.js +1 -1
- package/dist/index.js +46 -43
- package/docs/README.md +146 -0
- package/docs/card.html +77 -0
- package/docs/card.js +47 -0
- package/docs/catalog-view.js +18 -0
- package/docs/catalog-view.mjs +11 -0
- package/docs/catalog.json +23984 -0
- package/docs/catalog.mjs +131 -0
- package/docs/cli.mjs +26 -0
- package/docs/commands.mjs +94 -0
- package/docs/customizer-render.mjs +66 -0
- package/docs/customizer.js +181 -0
- package/docs/documentation.css +16 -0
- package/docs/examples/ui-action-bar/examples/01-form-footer.behavior.js +7 -0
- package/docs/examples/ui-button/examples/08-loading.behavior.js +21 -0
- package/docs/examples/ui-checkbox/examples/02-checked-indeterminate-and-disabled.behavior.js +30 -0
- package/docs/examples/ui-combobox/examples/06-selected-values.behavior.js +11 -0
- package/docs/examples/ui-search-shell/examples/02-label-dismissible-status-and-footer.behavior.js +34 -0
- package/docs/examples/ui-toast/examples/01-authored.behavior.js +15 -0
- package/docs/examples/ui-toast/examples/02-danger.behavior.js +2 -0
- package/docs/examples/ui-tree/examples/03-max-depth-2-and-hover-expand-delay-300.behavior.js +30 -0
- package/docs/examples/ui-tree/examples/06-move-without-dragging.behavior.js +2 -0
- package/docs/examples/ui-tree/examples/07-multiple-selection.behavior.js +16 -0
- package/docs/examples/ui-tree-item/examples/02-container.behavior.js +1 -0
- package/docs/index.d.ts +56 -0
- package/docs/index.mjs +4 -0
- package/docs/local-server.mjs +68 -0
- package/docs/preview.js +22 -0
- package/docs/reference.html +1 -0
- package/docs/reference.js +36 -0
- package/docs/render.mjs +93 -0
- package/docs/site.mjs +239 -0
- package/docs/store.mjs +92 -0
- package/docs/theme-session.mjs +51 -0
- package/docs/theme.mjs +219 -0
- package/docs/value-editors.mjs +33 -0
- package/docs/workspace.mjs +32 -0
- package/package.json +28 -4
- package/styles/ui-button.css +45 -45
- package/styles/ui-card.css +47 -0
- package/styles/ui-curve-editor.css +109 -0
- package/styles/ui-dialog.css +132 -0
- package/styles/ui-icon-button.css +63 -20
- package/styles/ui-menu.css +72 -0
- package/styles/ui-number-input.css +34 -0
- package/styles/ui-popover.css +74 -0
- package/styles/ui-slider.css +53 -0
- package/styles/ui-tabs.css +6 -1
- package/styles/ui-text.css +23 -1
- package/styles/ui-tooltip.css +72 -0
- package/theme-system.css +26 -10
- package/tokens.css +8 -2
- package/vanilla/UiButton.d.ts +1 -0
- package/vanilla/UiButton.js +195 -130
- package/vanilla/UiCard.d.ts +1 -1
- package/vanilla/UiCard.js +1 -1
- package/vanilla/UiCurveEditor.d.ts +28 -0
- package/vanilla/UiCurveEditor.js +134 -0
- package/vanilla/UiIconButton.d.ts +2 -1
- package/vanilla/UiIconButton.js +58 -31
- package/vanilla/UiNumberInput.d.ts +33 -0
- package/vanilla/UiNumberInput.js +79 -0
- package/vanilla/UiSlider.d.ts +33 -0
- package/vanilla/UiSlider.js +125 -0
- package/vanilla/UiText.d.ts +2 -2
- package/vanilla/UiText.js +45 -11
- package/vanilla/index.js +3 -0
- package/vue/UiButton.d.ts +6 -0
- package/vue/UiButton.vue +155 -56
- package/vue/UiCard.d.ts +2 -2
- package/vue/UiCard.vue +55 -5
- package/vue/UiCurveEditor.d.ts +53 -0
- package/vue/UiCurveEditor.js +2 -0
- package/vue/UiCurveEditor.vue +427 -0
- package/vue/UiDialog.vue +177 -1
- package/vue/UiIconButton.d.ts +8 -2
- package/vue/UiIconButton.vue +164 -34
- package/vue/UiMenu.vue +89 -0
- package/vue/UiNumberInput.d.ts +83 -0
- package/vue/UiNumberInput.js +2 -0
- package/vue/UiNumberInput.vue +370 -0
- package/vue/UiPopover.vue +92 -1
- package/vue/UiSlider.d.ts +83 -0
- package/vue/UiSlider.js +2 -0
- package/vue/UiSlider.vue +412 -0
- package/vue/UiTabs.vue +6 -1
- package/vue/UiText.d.ts +7 -5
- package/vue/UiText.vue +104 -22
- package/vue/UiTooltip.vue +89 -0
- package/vue/chunks/UiActionBar.js +1 -1
- package/vue/chunks/UiAffordanceScope.js +2 -2
- package/vue/chunks/UiAvatar.js +2 -2
- package/vue/chunks/UiAvatarGroup.js +2 -2
- package/vue/chunks/UiBadge.js +2 -2
- package/vue/chunks/UiBreadcrumbItem.js +2 -2
- package/vue/chunks/UiBreadcrumbs.js +2 -2
- package/vue/chunks/UiButton.js +94 -11
- package/vue/chunks/UiCallout.js +2 -2
- package/vue/chunks/UiCard.js +6 -5
- package/vue/chunks/UiCheckbox.js +2 -2
- package/vue/chunks/UiCluster.js +2 -2
- package/vue/chunks/UiColorPicker.js +2 -2
- package/vue/chunks/UiCombobox.js +2 -2
- package/vue/chunks/UiContainer.js +2 -2
- package/vue/chunks/UiContextMenu.js +2 -2
- package/vue/chunks/UiCover.js +2 -2
- package/vue/chunks/UiCurveEditor.js +413 -0
- package/vue/chunks/UiDescriptionItem.js +2 -2
- package/vue/chunks/UiDescriptionList.js +2 -2
- package/vue/chunks/UiDialog.js +4 -4
- package/vue/chunks/UiDisclosure.js +2 -2
- package/vue/chunks/UiEditable.js +2 -2
- package/vue/chunks/UiEditorBlockActions.js +2 -2
- package/vue/chunks/UiEditorChipEditor.js +2 -2
- package/vue/chunks/UiEditorCodeLanguage.js +2 -2
- package/vue/chunks/UiEditorFootnoteEditor.js +2 -2
- package/vue/chunks/UiEditorImageToolbar.js +3 -3
- package/vue/chunks/UiEditorInsertTableGrid.js +2 -2
- package/vue/chunks/UiEditorLinkCard.js +3 -3
- package/vue/chunks/UiEditorLinkForm.js +3 -3
- package/vue/chunks/UiEditorMentionMenu.js +2 -2
- package/vue/chunks/UiEditorMentionMenuItem.js +2 -2
- package/vue/chunks/UiEditorSlashMenu.js +2 -2
- package/vue/chunks/UiEditorSlashMenuGroup.js +2 -2
- package/vue/chunks/UiEditorSlashMenuItem.js +2 -2
- package/vue/chunks/UiEditorSurface.js +2 -2
- package/vue/chunks/UiEditorTableContextMenu.js +2 -2
- package/vue/chunks/UiEditorTableOverlay.js +2 -2
- package/vue/chunks/UiEditorTableToolbar.js +2 -2
- package/vue/chunks/UiEditorToolbar.js +2 -2
- package/vue/chunks/UiFloatingActionButton.js +2 -2
- package/vue/chunks/UiFormField.js +2 -2
- package/vue/chunks/UiGrid.js +2 -2
- package/vue/chunks/UiIcon.js +2 -2
- package/vue/chunks/UiIconButton.js +77 -11
- package/vue/chunks/UiImage.js +2 -2
- package/vue/chunks/UiInput.js +2 -2
- package/vue/chunks/UiInputGroup.js +2 -2
- package/vue/chunks/UiList.js +2 -2
- package/vue/chunks/UiListItem.js +2 -2
- package/vue/chunks/UiListbox.js +2 -2
- package/vue/chunks/UiMenu.js +3 -3
- package/vue/chunks/UiMenuGroup.js +2 -2
- package/vue/chunks/UiMenuItem.js +2 -2
- package/vue/chunks/UiMeter.js +2 -2
- package/vue/chunks/UiNavItem.js +2 -2
- package/vue/chunks/UiNumberInput.js +390 -0
- package/vue/chunks/UiPageHeader.js +2 -2
- package/vue/chunks/UiPopover.js +4 -4
- package/vue/chunks/UiRadio.js +2 -2
- package/vue/chunks/UiRadioGroup.js +2 -2
- package/vue/chunks/UiReel.js +2 -2
- package/vue/chunks/UiScrollArea.js +2 -2
- package/vue/chunks/UiSearchResultRow.js +2 -2
- package/vue/chunks/UiSearchShell.js +2 -2
- package/vue/chunks/UiSection.js +2 -2
- package/vue/chunks/UiSelect.js +2 -2
- package/vue/chunks/UiSeparator.js +2 -2
- package/vue/chunks/UiSidebar.js +2 -2
- package/vue/chunks/UiSlider.js +396 -0
- package/vue/chunks/UiSpinner.js +2 -2
- package/vue/chunks/UiStack.js +2 -2
- package/vue/chunks/UiStatusMessage.js +2 -2
- package/vue/chunks/UiSwatch.js +2 -2
- package/vue/chunks/UiSwitch.js +2 -2
- package/vue/chunks/UiSwitcher.js +2 -2
- package/vue/chunks/UiTable.js +2 -2
- package/vue/chunks/UiTabs.js +3 -3
- package/vue/chunks/UiText.js +53 -12
- package/vue/chunks/UiTextarea.js +2 -2
- package/vue/chunks/UiToast.js +2 -2
- package/vue/chunks/UiToastRegion.js +2 -2
- package/vue/chunks/UiTooltip.js +3 -3
- package/vue/chunks/UiTopBar.js +1 -1
- package/vue/chunks/UiTree.js +2 -2
- package/vue/chunks/UiTreeItem.js +2 -2
- package/vue/chunks/_plugin-vue_export-helper.js +68 -2
- package/vue/chunks/props.js +24 -1
- package/vue/components.css +2215 -1203
- package/vue/editor/index.js +623 -211
- package/vue/index.d.ts +3 -0
- package/vue/index.js +5 -2
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { curvePoints, curveText } from "./curve.js";
|
|
2
|
+
|
|
3
|
+
export function connect(host) {
|
|
4
|
+
const { plot, first, second, firstLine, secondLine, curve, result } = host.refs;
|
|
5
|
+
const fields = [...host.element.querySelectorAll("[data-curve-coordinate]")];
|
|
6
|
+
let points = [.25, .1, .25, 1]; let external; let drag;
|
|
7
|
+
const limit = () => { points = curvePoints(curveText(points)); };
|
|
8
|
+
const paint = () => {
|
|
9
|
+
limit();
|
|
10
|
+
const x = index => points[index] * 100;
|
|
11
|
+
const y = index => (2 - points[index]) / 3 * 100;
|
|
12
|
+
for (const [index, handle] of [[0, first], [2, second]]) {
|
|
13
|
+
handle.style.left = `${x(index)}%`; handle.style.top = `${y(index + 1)}%`;
|
|
14
|
+
handle.setAttribute("aria-description", `X ${points[index]}, Y ${points[index + 1]}`);
|
|
15
|
+
}
|
|
16
|
+
firstLine.setAttribute("d", `M0 66.667 L${x(0)} ${y(1)}`);
|
|
17
|
+
secondLine.setAttribute("d", `M100 33.333 L${x(2)} ${y(3)}`);
|
|
18
|
+
curve.setAttribute("d", `M0 66.667 C${x(0)} ${y(1)} ${x(2)} ${y(3)} 100 33.333`);
|
|
19
|
+
fields.forEach((field, index) => { host.state[`point${index}`] = String(points[index]); });
|
|
20
|
+
result.value = curveText(points); result.textContent = curveText(points);
|
|
21
|
+
};
|
|
22
|
+
const emit = type => host.dispatch(type, { value: curveText(points) });
|
|
23
|
+
const move = event => {
|
|
24
|
+
if (!drag || event.pointerId !== drag.id || host.props.disabled.value) return;
|
|
25
|
+
const rect = plot.getBoundingClientRect();
|
|
26
|
+
if (!rect.width || !rect.height) return;
|
|
27
|
+
points[drag.index] = (event.clientX - rect.left) / rect.width;
|
|
28
|
+
points[drag.index + 1] = 2 - (event.clientY - rect.top) / rect.height * 3;
|
|
29
|
+
paint(); emit("input");
|
|
30
|
+
};
|
|
31
|
+
const finish = event => { if (drag?.id !== event.pointerId) return; drag = undefined; if (!host.props.disabled.value) emit("change"); };
|
|
32
|
+
const cleanups = [];
|
|
33
|
+
for (const [index, handle] of [[0, first], [2, second]]) {
|
|
34
|
+
const down = event => { if (host.props.disabled.value || event.button !== 0) return; event.preventDefault(); handle.focus(); drag = { id: event.pointerId, index }; handle.setPointerCapture(event.pointerId); };
|
|
35
|
+
const key = event => {
|
|
36
|
+
if (host.props.disabled.value || !["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key)) return;
|
|
37
|
+
event.preventDefault(); const step = event.shiftKey ? .1 : .01;
|
|
38
|
+
points[index + (event.key === "ArrowUp" || event.key === "ArrowDown" ? 1 : 0)] += (event.key === "ArrowLeft" || event.key === "ArrowDown" ? -step : step);
|
|
39
|
+
paint(); emit("input"); emit("change");
|
|
40
|
+
};
|
|
41
|
+
for (const [type, callback] of [["pointerdown", down], ["pointermove", move], ["pointerup", finish], ["pointercancel", finish], ["keydown", key]]) {
|
|
42
|
+
handle.addEventListener(type, callback); cleanups.push(() => handle.removeEventListener(type, callback));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
fields.forEach((field, index) => {
|
|
46
|
+
const edit = event => { event.stopPropagation(); if (host.props.disabled.value) return; points[index] = Number(event.detail.value); paint(); emit(event.type); };
|
|
47
|
+
for (const type of ["input", "change"]) { field.addEventListener(type, edit); cleanups.push(() => field.removeEventListener(type, edit)); }
|
|
48
|
+
});
|
|
49
|
+
const stop = host.effect(() => { const value = host.props.value.value; if (value === external) return; external = value; try { points = curvePoints(value); } catch { /* Keep the last valid curve. */ } paint(); });
|
|
50
|
+
return () => { stop(); cleanups.forEach(cleanup => cleanup()); };
|
|
51
|
+
}
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
closerequest when modal, none when not.</prop>
|
|
14
14
|
<prop name="alert" type="boolean" default="false"
|
|
15
15
|
>Use alert dialog semantics and require an explicit action instead of the header close button. An alert dialog is always modal.</prop>
|
|
16
|
-
<prop name="size" type="keyword" values="sm, md, lg, fullscreen" default="md"
|
|
16
|
+
<prop name="size" type="keyword" values="sm, md, lg, fullscreen" default="md"
|
|
17
|
+
>Choose a compact, standard, wide, or viewport-filling dialog. Shared surface separation adapts to dialog purpose and rendered size.</prop>
|
|
17
18
|
<prop name="label" type="string">Visible title in the dialog header; also its accessible name.</prop>
|
|
18
19
|
<prop name="for" type="string" default="">ID of the control that opens the dialog.</prop>
|
|
19
20
|
<state type="boolean" name="internalOpen" value="false"></state>
|
|
@@ -75,6 +76,7 @@
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
:host {
|
|
79
|
+
--_ui-material-scale: 1;
|
|
78
80
|
/* Hooks do not inherit, so the parts below read them from the root. */
|
|
79
81
|
--_ui-dialog-duration: var(--ui-dialog-duration);
|
|
80
82
|
|
|
@@ -107,6 +109,65 @@
|
|
|
107
109
|
overlay var(--ui-dialog-duration, var(--ui-motion-base)) var(--ui-motion-ease) allow-discrete;
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
/* Purpose and measured size shape one shared separation lever. The content plane stays opaque;
|
|
113
|
+
transparency, saturation and edge light belong to the surrounding decorative material. */
|
|
114
|
+
:host {
|
|
115
|
+
--_ui-material-glassiness: clamp(0, var(--ui-glassiness, 0), 1);
|
|
116
|
+
--_ui-material-separation: clamp(0, var(--ui-surface-separation, 0), 1);
|
|
117
|
+
--_ui-material-active: clamp(0, calc((var(--_ui-material-glassiness) + var(--_ui-material-separation)) * 100000), 1);
|
|
118
|
+
--_ui-material-rim: calc((var(--_ui-material-separation) + var(--_ui-material-glassiness) * 0.25) * var(--_ui-material-scale, 1) * 8px);
|
|
119
|
+
--_ui-material-content-color: var(--ui-dialog-surface, var(--ui-material-content, var(--ui-overlay-surface, var(--ui-surface-elevated))));
|
|
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
|
+
background:
|
|
122
|
+
linear-gradient(var(--_ui-material-content-color), var(--_ui-material-content-color)) padding-box,
|
|
123
|
+
linear-gradient(var(--_ui-material-rim-color), var(--_ui-material-rim-color)) border-box,
|
|
124
|
+
var(--_ui-material-content-color) padding-box;
|
|
125
|
+
border-width: calc(var(--ui-border-width) + var(--_ui-material-rim));
|
|
126
|
+
border-color: var(
|
|
127
|
+
--ui-dialog-border,
|
|
128
|
+
color-mix(in oklab, var(--ui-overlay-border, var(--ui-border-strong)) calc((1 - var(--_ui-material-active)) * 100%), transparent)
|
|
129
|
+
);
|
|
130
|
+
box-shadow: var(
|
|
131
|
+
--ui-dialog-shadow,
|
|
132
|
+
var(
|
|
133
|
+
--ui-overlay-shadow,
|
|
134
|
+
0 calc(var(--_ui-material-separation) * var(--_ui-material-scale, 1) * 8px) calc(
|
|
135
|
+
var(--_ui-material-separation) * var(--_ui-material-scale, 1) * 24px
|
|
136
|
+
) color-mix(in oklab, var(--ui-text-primary) 15%, transparent),
|
|
137
|
+
inset 0 0 0 calc(var(--_ui-material-active) * var(--ui-border-width)) var(--ui-material-edge, var(--ui-border-strong)),
|
|
138
|
+
var(--ui-elevation-dialog, var(--ui-shadow-xl))
|
|
139
|
+
)
|
|
140
|
+
);
|
|
141
|
+
backdrop-filter: blur(calc(var(--_ui-material-glassiness) * var(--_ui-material-scale, 1) * 18px))
|
|
142
|
+
saturate(calc(1 + var(--_ui-material-glassiness) * var(--_ui-material-scale, 1) * 0.18));
|
|
143
|
+
-webkit-backdrop-filter: blur(calc(var(--_ui-material-glassiness) * var(--_ui-material-scale, 1) * 18px))
|
|
144
|
+
saturate(calc(1 + var(--_ui-material-glassiness) * var(--_ui-material-scale, 1) * 0.18));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
|
148
|
+
:host {
|
|
149
|
+
background: var(--_ui-material-content-color);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
154
|
+
:host {
|
|
155
|
+
background: var(--_ui-material-content-color);
|
|
156
|
+
backdrop-filter: none;
|
|
157
|
+
-webkit-backdrop-filter: none;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@media (forced-colors: active) {
|
|
162
|
+
:host {
|
|
163
|
+
background: Canvas;
|
|
164
|
+
border-color: CanvasText;
|
|
165
|
+
box-shadow: none;
|
|
166
|
+
backdrop-filter: none;
|
|
167
|
+
-webkit-backdrop-filter: none;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
110
171
|
:host[open] {
|
|
111
172
|
display: flex !important;
|
|
112
173
|
visibility: visible;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { updateSurfaceMaterial } from "../shared/surface-material.js";
|
|
2
|
+
import { closeDialog, closeOverlay, createIdResolver, openOverlay, observeOverlayViewport, requestTopOverlayClose, showDialog } from "../shared/overlay.js";
|
|
2
3
|
|
|
3
4
|
function inferredLabel(element, explicit) {
|
|
4
5
|
if (String(explicit ?? "").trim()) return String(explicit).trim();
|
|
@@ -18,6 +19,7 @@ function observeContentHeight(dialog, refs) {
|
|
|
18
19
|
};
|
|
19
20
|
const observer = new owner.ResizeObserver(() => {
|
|
20
21
|
if (!dialog.open) { reset(); return; }
|
|
22
|
+
updateSurfaceMaterial(dialog, "dialog");
|
|
21
23
|
const previous = animation ? parseFloat(owner.getComputedStyle(dialog).height) : lastHeight;
|
|
22
24
|
animation?.cancel();
|
|
23
25
|
animation = null;
|
|
@@ -63,6 +65,7 @@ export function connect(host) {
|
|
|
63
65
|
let lastFor = "";
|
|
64
66
|
let lastExternalOpen = Boolean(host.props.open.value);
|
|
65
67
|
let lastOpen;
|
|
68
|
+
let stopMaterialViewport = null;
|
|
66
69
|
host.state.internalOpen = lastExternalOpen;
|
|
67
70
|
|
|
68
71
|
const requestClose = (reason, input) => {
|
|
@@ -126,9 +129,13 @@ export function connect(host) {
|
|
|
126
129
|
dialog.setAttribute("closedby", closedBy);
|
|
127
130
|
if (open) {
|
|
128
131
|
showDialog(dialog, modal);
|
|
132
|
+
updateSurfaceMaterial(dialog, "dialog");
|
|
133
|
+
stopMaterialViewport ??= observeOverlayViewport(document, () => updateSurfaceMaterial(dialog, "dialog"));
|
|
129
134
|
openOverlay({ id: overlayId, modal, element, modalElement: dialog, dismissible: true, canClose: (reason) => reason === "light-dismiss" ? closedBy === "any" : reason !== "escape" || closedBy !== "none", requestClose });
|
|
130
135
|
} else {
|
|
131
136
|
resize.reset();
|
|
137
|
+
stopMaterialViewport?.();
|
|
138
|
+
stopMaterialViewport = null;
|
|
132
139
|
closeDialog(dialog);
|
|
133
140
|
closeOverlay(document, overlayId);
|
|
134
141
|
}
|
|
@@ -176,6 +183,7 @@ export function connect(host) {
|
|
|
176
183
|
ids.stop();
|
|
177
184
|
observer.disconnect();
|
|
178
185
|
resize.destroy();
|
|
186
|
+
stopMaterialViewport?.();
|
|
179
187
|
dialog?.removeEventListener("close", onClose, true);
|
|
180
188
|
dialog?.removeEventListener("cancel", onCancel);
|
|
181
189
|
closeButton?.removeEventListener("click", onCloseClick);
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
<link rel="component" href="../ui-spinner/ui-spinner.html">
|
|
2
2
|
<template component="ui-icon-button" module="./ui-icon-button.js">
|
|
3
3
|
<defs>
|
|
4
|
+
<prop name="preset" type="keyword" values="none, primary, standard, quiet, destructive" default="none"
|
|
5
|
+
>The same shortcuts as Button: primary is solid accent, standard is solid neutral, quiet is ghost neutral, and destructive is solid danger. A named preset
|
|
6
|
+
wins over variant and tone; none preserves independent choices.</prop>
|
|
7
|
+
<!-- Computed maps read every member in HTML Next. auto keeps an omitted tone concrete so the preset map can resolve, while retaining the existing variant-dependent appearance. -->
|
|
8
|
+
<computed name="appearance" from="{ none: $variant, primary: 'solid', standard: 'solid', quiet: 'ghost', destructive: 'solid' }[$preset]"></computed>
|
|
9
|
+
<computed name="colorTone" from="{ none: $tone, primary: 'accent', standard: 'neutral', quiet: 'neutral', destructive: 'danger' }[$preset]"></computed>
|
|
4
10
|
<prop name="disabled" type="boolean" default="false"
|
|
5
11
|
>Disables the button, so it cannot be pressed or focused, and gives it a flat neutral surface and muted icon.</prop>
|
|
6
12
|
<prop name="loading" type="boolean" default="false"
|
|
@@ -17,9 +23,10 @@
|
|
|
17
23
|
<prop name="variant" type="keyword" values="ghost, outline, solid" default="ghost"
|
|
18
24
|
>Emphasis. ghost shows only the icon until it is hovered, for toolbars and dense rows; outline adds a border on the default surface; solid fills with the
|
|
19
25
|
accent colour, for the one primary action in a group.</prop>
|
|
20
|
-
<prop name="tone" type="keyword" values="accent, neutral"
|
|
21
|
-
>Colour for every variant. When omitted, ghost and outline keep their neutral appearance and solid
|
|
22
|
-
or neutral for a quiet one.
|
|
26
|
+
<prop name="tone" type="keyword" values="auto, accent, neutral, danger, success, warning, info" default="auto"
|
|
27
|
+
>Colour for every variant. auto preserves the existing variant-specific default. When omitted, ghost and outline keep their neutral appearance and solid
|
|
28
|
+
keeps its accent fill. Set accent for a coloured action or neutral for a quiet one. danger, success, warning and info share Button's intent colors. Native
|
|
29
|
+
aria-pressed="true" keeps an outline toggle visibly pressed; the consumer owns that state.</prop>
|
|
23
30
|
<prop name="anticipatory" type="boolean" default="false"
|
|
24
31
|
>Keeps the button out of sight until it is wanted: at rest a fine pointer sees only a small guide dot, and the button appears on hover, on focus, or when
|
|
25
32
|
the pointer comes near inside a ui-affordance-scope. Touch devices and the solid variant always show it.</prop>
|
|
@@ -32,6 +39,8 @@
|
|
|
32
39
|
<prop name="round" type="boolean" default="false">Use a fully circular shape.</prop>
|
|
33
40
|
</defs>
|
|
34
41
|
<button
|
|
42
|
+
from:data-appearance="$appearance"
|
|
43
|
+
from:data-tone="$colorTone"
|
|
35
44
|
from:disabled="$disabled"
|
|
36
45
|
type="button"
|
|
37
46
|
from:aria-label="$label"
|
|
@@ -144,7 +153,7 @@
|
|
|
144
153
|
--_ui-action-start: -1px;
|
|
145
154
|
}
|
|
146
155
|
|
|
147
|
-
:host
|
|
156
|
+
:host[data-tone="accent"] {
|
|
148
157
|
--_icon-button-border: var(--ui-accent-solid);
|
|
149
158
|
--_icon-button-hover-border: var(--ui-accent-hover);
|
|
150
159
|
--_icon-button-text: var(--ui-accent-subtle-text, var(--ui-accent-solid));
|
|
@@ -152,8 +161,52 @@
|
|
|
152
161
|
color: var(--_icon-button-text);
|
|
153
162
|
}
|
|
154
163
|
|
|
164
|
+
:host[data-tone="danger"] {
|
|
165
|
+
--_icon-button-border: var(--ui-danger);
|
|
166
|
+
--_icon-button-hover-border: var(--ui-danger-hover);
|
|
167
|
+
--_icon-button-text: var(--ui-danger-subtle-text);
|
|
168
|
+
--_icon-button-outline-surface: color-mix(in srgb, var(--ui-danger) 5%, var(--ui-surface-default));
|
|
169
|
+
--_icon-button-solid-surface: var(--ui-danger);
|
|
170
|
+
--_icon-button-solid-hover-surface: var(--ui-danger-hover);
|
|
171
|
+
--_icon-button-solid-text: var(--ui-danger-on-strong);
|
|
172
|
+
color: var(--_icon-button-text);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
:host[data-tone="success"] {
|
|
176
|
+
--_icon-button-border: var(--ui-success);
|
|
177
|
+
--_icon-button-hover-border: var(--ui-success-hover);
|
|
178
|
+
--_icon-button-text: var(--ui-success-subtle-text);
|
|
179
|
+
--_icon-button-outline-surface: color-mix(in srgb, var(--ui-success) 5%, var(--ui-surface-default));
|
|
180
|
+
--_icon-button-solid-surface: var(--ui-success);
|
|
181
|
+
--_icon-button-solid-hover-surface: var(--ui-success-hover);
|
|
182
|
+
--_icon-button-solid-text: var(--ui-success-on-strong);
|
|
183
|
+
color: var(--_icon-button-text);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
:host[data-tone="warning"] {
|
|
187
|
+
--_icon-button-border: var(--ui-warning);
|
|
188
|
+
--_icon-button-hover-border: var(--ui-warning-hover);
|
|
189
|
+
--_icon-button-text: var(--ui-warning-subtle-text);
|
|
190
|
+
--_icon-button-outline-surface: color-mix(in srgb, var(--ui-warning) 5%, var(--ui-surface-default));
|
|
191
|
+
--_icon-button-solid-surface: var(--ui-warning);
|
|
192
|
+
--_icon-button-solid-hover-surface: var(--ui-warning-hover);
|
|
193
|
+
--_icon-button-solid-text: var(--ui-warning-on-strong);
|
|
194
|
+
color: var(--_icon-button-text);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
:host[data-tone="info"] {
|
|
198
|
+
--_icon-button-border: var(--ui-info);
|
|
199
|
+
--_icon-button-hover-border: var(--ui-info-hover);
|
|
200
|
+
--_icon-button-text: var(--ui-info-subtle-text);
|
|
201
|
+
--_icon-button-outline-surface: color-mix(in srgb, var(--ui-info) 5%, var(--ui-surface-default));
|
|
202
|
+
--_icon-button-solid-surface: var(--ui-info);
|
|
203
|
+
--_icon-button-solid-hover-surface: var(--ui-info-hover);
|
|
204
|
+
--_icon-button-solid-text: var(--ui-info-on-strong);
|
|
205
|
+
color: var(--_icon-button-text);
|
|
206
|
+
}
|
|
207
|
+
|
|
155
208
|
/* Omitted tone retains the existing accent solid; explicit neutral is additive. */
|
|
156
|
-
:host
|
|
209
|
+
:host[data-tone="neutral"] {
|
|
157
210
|
--_icon-button-solid-surface: var(--ui-text-primary);
|
|
158
211
|
--_icon-button-solid-hover-surface: var(--ui-text-primary);
|
|
159
212
|
--_icon-button-solid-text: var(--ui-surface-default);
|
|
@@ -198,13 +251,13 @@
|
|
|
198
251
|
}
|
|
199
252
|
|
|
200
253
|
@media (hover: hover) and (pointer: fine) {
|
|
201
|
-
:host([anticipatory]):not(:host([revealed]), :host
|
|
254
|
+
:host([anticipatory]):not(:host([revealed]), :host[data-appearance="solid"], :host([loading]), :host([pending]), :disabled, [aria-pressed="true"], [data-ui-proximity="near"], :hover, :focus-visible) {
|
|
202
255
|
border-color: transparent;
|
|
203
256
|
background-color: transparent;
|
|
204
257
|
box-shadow: none;
|
|
205
258
|
}
|
|
206
259
|
|
|
207
|
-
:host([anticipatory]):not(:host([revealed]), :host
|
|
260
|
+
:host([anticipatory]):not(:host([revealed]), :host[data-appearance="solid"], :host([loading]), :host([pending]), :disabled, [aria-pressed="true"], [data-ui-proximity="near"], :hover, :focus-visible)
|
|
208
261
|
.content {
|
|
209
262
|
opacity: 0;
|
|
210
263
|
transform: scale(0.65);
|
|
@@ -212,7 +265,7 @@
|
|
|
212
265
|
}
|
|
213
266
|
}
|
|
214
267
|
|
|
215
|
-
:host([anticipatory]):is(:host([revealed]), :host
|
|
268
|
+
:host([anticipatory]):is(:host([revealed]), :host[data-appearance="solid"], [aria-pressed="true"], [data-ui-proximity="near"], :hover, :focus-visible)::before {
|
|
216
269
|
opacity: 0;
|
|
217
270
|
}
|
|
218
271
|
|
|
@@ -321,7 +374,7 @@
|
|
|
321
374
|
border-radius: var(--ui-radius-round);
|
|
322
375
|
}
|
|
323
376
|
|
|
324
|
-
:host
|
|
377
|
+
:host[data-appearance="outline"] {
|
|
325
378
|
border-color: var(--ui-icon-button-border, var(--_icon-button-border, var(--ui-control-border, var(--ui-border-strong))));
|
|
326
379
|
background-color: var(--ui-icon-button-outline-surface, var(--_icon-button-outline-surface, var(--ui-surface-default)));
|
|
327
380
|
background-image: none;
|
|
@@ -329,7 +382,7 @@
|
|
|
329
382
|
box-shadow: var(--ui-action-shadow, var(--_ui-action-shadow));
|
|
330
383
|
}
|
|
331
384
|
|
|
332
|
-
:host
|
|
385
|
+
:host[data-appearance="solid"] {
|
|
333
386
|
background-color: var(--ui-icon-button-solid-surface, var(--_icon-button-solid-surface, var(--ui-accent-solid)));
|
|
334
387
|
background-image: none;
|
|
335
388
|
color: var(--ui-icon-button-solid-text, var(--_icon-button-solid-text, var(--ui-text-on-accent)));
|
|
@@ -342,27 +395,26 @@
|
|
|
342
395
|
color: var(--ui-icon-button-hover-text, var(--_icon-button-text, var(--ui-text-primary)));
|
|
343
396
|
}
|
|
344
397
|
|
|
345
|
-
:host
|
|
398
|
+
:host[data-appearance="outline"]:hover:enabled:not([aria-disabled="true"]) {
|
|
346
399
|
border-color: var(--ui-icon-button-hover-border, var(--_icon-button-hover-border, var(--ui-control-border-hover, var(--ui-control-border))));
|
|
347
400
|
}
|
|
348
401
|
|
|
349
|
-
:host
|
|
402
|
+
:host[data-appearance="solid"]:hover:enabled:not([aria-disabled="true"]) {
|
|
350
403
|
background-color: var(--ui-icon-button-solid-hover-surface, var(--_icon-button-solid-hover-surface, var(--ui-accent-hover)));
|
|
351
404
|
}
|
|
352
405
|
|
|
353
|
-
:host
|
|
354
|
-
:host([variant="solid"][tone="neutral"]):hover:enabled:not([aria-disabled="true"]) {
|
|
406
|
+
:host[data-appearance="solid"]:hover:enabled:not([aria-disabled="true"]) {
|
|
355
407
|
color: var(--ui-icon-button-solid-text, var(--_icon-button-solid-text, var(--ui-text-on-accent)));
|
|
356
408
|
}
|
|
357
409
|
|
|
358
|
-
:host
|
|
410
|
+
:host[data-appearance="ghost"][aria-expanded="true"]:enabled:not([aria-disabled="true"]) {
|
|
359
411
|
background-color: var(--ui-icon-button-active-surface, color-mix(in srgb, currentColor 16%, transparent));
|
|
360
412
|
color: var(--ui-icon-button-hover-text, var(--_icon-button-text, var(--ui-text-primary)));
|
|
361
413
|
box-shadow: var(--ui-pressed);
|
|
362
414
|
}
|
|
363
415
|
|
|
364
416
|
/* Native toggle state persists after pointer release, including during loading. */
|
|
365
|
-
:host
|
|
417
|
+
:host[data-appearance="outline"][aria-pressed="true"]:enabled {
|
|
366
418
|
background-color: var(--ui-icon-button-active-surface, color-mix(in srgb, currentColor 12%, var(--ui-surface-default)));
|
|
367
419
|
background-image: none;
|
|
368
420
|
box-shadow: var(--ui-pressed);
|
|
@@ -397,35 +449,35 @@
|
|
|
397
449
|
box-shadow: var(--ui-focus-halo);
|
|
398
450
|
}
|
|
399
451
|
|
|
400
|
-
:host
|
|
452
|
+
:host[data-appearance="outline"]:focus-visible {
|
|
401
453
|
box-shadow:
|
|
402
454
|
var(--ui-action-shadow, var(--_ui-action-shadow)),
|
|
403
455
|
var(--ui-focus-halo);
|
|
404
456
|
}
|
|
405
457
|
|
|
406
|
-
:host
|
|
458
|
+
:host[data-appearance="solid"]:focus-visible {
|
|
407
459
|
box-shadow:
|
|
408
460
|
var(--ui-icon-button-solid-shadow, var(--ui-action-shadow, var(--_ui-action-shadow))),
|
|
409
461
|
var(--ui-focus-halo);
|
|
410
462
|
}
|
|
411
463
|
|
|
412
|
-
:host:is(:host
|
|
464
|
+
:host:is(:host[data-appearance="outline"], :host[data-appearance="solid"]):active:enabled:not([aria-disabled="true"]) {
|
|
413
465
|
background-image: none;
|
|
414
466
|
box-shadow: var(--ui-pressed);
|
|
415
467
|
}
|
|
416
468
|
|
|
417
|
-
:host
|
|
469
|
+
:host[data-appearance="outline"][aria-pressed="true"]:enabled:focus-visible {
|
|
418
470
|
box-shadow:
|
|
419
471
|
var(--ui-pressed),
|
|
420
472
|
var(--ui-focus-halo);
|
|
421
473
|
}
|
|
422
474
|
|
|
423
475
|
@media (forced-colors: active) {
|
|
424
|
-
:host
|
|
476
|
+
:host[data-appearance="outline"][aria-pressed="true"]:enabled {
|
|
425
477
|
outline: var(--ui-focus-width) solid Highlight;
|
|
426
478
|
outline-offset: calc(-2 * var(--ui-focus-width));
|
|
427
479
|
}
|
|
428
|
-
:host
|
|
480
|
+
:host[data-appearance="outline"][aria-pressed="true"]:enabled:focus-visible {
|
|
429
481
|
outline-offset: var(--ui-focus-width);
|
|
430
482
|
}
|
|
431
483
|
}
|
|
@@ -442,7 +494,7 @@
|
|
|
442
494
|
background-image: none;
|
|
443
495
|
}
|
|
444
496
|
|
|
445
|
-
:host
|
|
497
|
+
:host[data-appearance="ghost"]:disabled {
|
|
446
498
|
border-color: transparent;
|
|
447
499
|
}
|
|
448
500
|
|
|
@@ -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,65 @@
|
|
|
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
|
+
linear-gradient(var(--_ui-material-content-color), var(--_ui-material-content-color)) padding-box,
|
|
140
|
+
linear-gradient(var(--_ui-material-rim-color), var(--_ui-material-rim-color)) border-box,
|
|
141
|
+
var(--_ui-material-content-color) padding-box;
|
|
142
|
+
border-width: calc(var(--ui-border-width) + var(--_ui-material-rim));
|
|
143
|
+
border-color: var(
|
|
144
|
+
--_ui-menu-border,
|
|
145
|
+
color-mix(in oklab, var(--ui-overlay-border, var(--ui-border-strong)) calc((1 - var(--_ui-material-active)) * 100%), transparent)
|
|
146
|
+
);
|
|
147
|
+
box-shadow: var(
|
|
148
|
+
--_ui-menu-shadow,
|
|
149
|
+
var(
|
|
150
|
+
--ui-overlay-shadow,
|
|
151
|
+
0 calc(var(--_ui-material-separation) * var(--_ui-material-scale, 0.28) * 8px) calc(
|
|
152
|
+
var(--_ui-material-separation) * var(--_ui-material-scale, 0.28) * 24px
|
|
153
|
+
) color-mix(in oklab, var(--ui-text-primary) 15%, transparent),
|
|
154
|
+
inset 0 0 0 calc(var(--_ui-material-active) * var(--ui-border-width)) var(--ui-material-edge, var(--ui-border-strong)),
|
|
155
|
+
var(--ui-elevation-overlay, var(--ui-shadow-lg))
|
|
156
|
+
)
|
|
157
|
+
);
|
|
158
|
+
backdrop-filter: blur(calc(var(--_ui-material-glassiness) * var(--_ui-material-scale, 0.28) * 18px))
|
|
159
|
+
saturate(calc(1 + var(--_ui-material-glassiness) * var(--_ui-material-scale, 0.28) * 0.18));
|
|
160
|
+
-webkit-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
|
+
}
|
|
163
|
+
|
|
164
|
+
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
|
165
|
+
:host:not([inline]) .surface {
|
|
166
|
+
background: var(--_ui-material-content-color);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
171
|
+
:host:not([inline]) .surface {
|
|
172
|
+
background: var(--_ui-material-content-color);
|
|
173
|
+
backdrop-filter: none;
|
|
174
|
+
-webkit-backdrop-filter: none;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@media (forced-colors: active) {
|
|
179
|
+
:host:not([inline]) .surface {
|
|
180
|
+
background: Canvas;
|
|
181
|
+
border-color: CanvasText;
|
|
182
|
+
box-shadow: none;
|
|
183
|
+
backdrop-filter: none;
|
|
184
|
+
-webkit-backdrop-filter: none;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
128
188
|
.surface hr {
|
|
129
189
|
align-self: stretch;
|
|
130
190
|
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,62 @@
|
|
|
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
|
+
<prop name="value" type="string" default="0"
|
|
7
|
+
>Exact value. Arithmetic is calculated on Enter or blur; the resulting value includes unit. Invalid expressions stay visible and expose invalid
|
|
8
|
+
state.</prop>
|
|
9
|
+
<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>
|
|
10
|
+
<prop name="min" type="number" default="-1000000">Inclusive lower bound applied to typing, arithmetic, stepping and external values.</prop>
|
|
11
|
+
<prop name="max" type="number" default="1000000">Inclusive upper bound applied to every editing path.</prop>
|
|
12
|
+
<prop name="step" type="number" default="1">Positive increment used by arrows and step buttons; Shift multiplies it by ten.</prop>
|
|
13
|
+
<prop name="name" type="string" default="">Optional form field name.</prop>
|
|
14
|
+
<prop name="label" type="string" default="Value">Accessible name for the editing control.</prop>
|
|
15
|
+
<prop name="size" type="keyword" values="sm, md, lg" default="md">Shared field and button height.</prop>
|
|
16
|
+
<prop name="disabled" type="boolean" default="false">Prevents typing, stepping and pointer or keyboard changes while keeping the value visible.</prop>
|
|
17
|
+
<event name="input" type="object"><prop name="value" type="string" required></prop></event>
|
|
18
|
+
<event name="change" type="object"><prop name="value" type="string" required></prop></event>
|
|
19
|
+
</defs>
|
|
20
|
+
<span>
|
|
21
|
+
<ui-icon-button
|
|
22
|
+
data-number-step="decrease"
|
|
23
|
+
match-button
|
|
24
|
+
type="button"
|
|
25
|
+
from:size="$size"
|
|
26
|
+
from:disabled="$disabled"
|
|
27
|
+
from:label="concat('Decrease ', $label)"
|
|
28
|
+
><ui-icon name="minus"></ui-icon></ui-icon-button>
|
|
29
|
+
<ui-input
|
|
30
|
+
data-ui-field-control
|
|
31
|
+
from:size="$size"
|
|
32
|
+
from:disabled="$disabled"
|
|
33
|
+
from:value="$value"
|
|
34
|
+
from:aria-label="$label"
|
|
35
|
+
from:name="$name"
|
|
36
|
+
inputmode="decimal"
|
|
37
|
+
spellcheck="false"
|
|
38
|
+
aria-description="Enter calculates arithmetic. Arrow keys step; Shift changes ten steps."
|
|
39
|
+
></ui-input>
|
|
40
|
+
<ui-icon-button
|
|
41
|
+
data-number-step="increase"
|
|
42
|
+
match-button
|
|
43
|
+
type="button"
|
|
44
|
+
from:size="$size"
|
|
45
|
+
from:disabled="$disabled"
|
|
46
|
+
from:label="concat('Increase ', $label)"
|
|
47
|
+
><ui-icon name="plus"></ui-icon></ui-icon-button>
|
|
48
|
+
</span>
|
|
49
|
+
<style>
|
|
50
|
+
:host {
|
|
51
|
+
display: flex !important;
|
|
52
|
+
align-items: center !important;
|
|
53
|
+
gap: var(--ui-space-1) !important;
|
|
54
|
+
margin: 0 !important;
|
|
55
|
+
padding: 0 !important;
|
|
56
|
+
min-inline-size: 0;
|
|
57
|
+
}
|
|
58
|
+
:host[hidden] {
|
|
59
|
+
display: none !important;
|
|
60
|
+
}
|
|
61
|
+
</style>
|
|
62
|
+
</template>
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
const unit = () => host.props.unit.value || "";
|
|
11
|
+
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
|
+
const emit = () => { const value = numberText(current, unit()); host.dispatch("input", { value }); host.dispatch("change", { value }); };
|
|
14
|
+
const commit = () => {
|
|
15
|
+
if (host.props.disabled.value) return;
|
|
16
|
+
try { paint(calculateValue(input.value, unit())); emit(); }
|
|
17
|
+
catch (error) { input.setAttribute("aria-invalid", "true"); input.setCustomValidity(error.message); }
|
|
18
|
+
};
|
|
19
|
+
const step = (direction, multiplier = 1) => {
|
|
20
|
+
if (host.props.disabled.value) return;
|
|
21
|
+
let value = current;
|
|
22
|
+
try { value = calculateValue(input.value, unit()); } catch { /* Recover from an unfinished expression using the last result. */ }
|
|
23
|
+
const increment = Math.abs(Number(host.props.step.value)) || 1;
|
|
24
|
+
paint(value + direction * increment * multiplier); emit();
|
|
25
|
+
};
|
|
26
|
+
const key = event => {
|
|
27
|
+
if (event.key === "Enter") { event.preventDefault(); commit(); }
|
|
28
|
+
if (event.key === "ArrowUp" || event.key === "ArrowDown") { event.preventDefault(); step(event.key === "ArrowUp" ? 1 : -1, event.shiftKey ? 10 : 1); }
|
|
29
|
+
if (event.key === "Escape") { paint(current); }
|
|
30
|
+
};
|
|
31
|
+
// Do not leak unfinished expressions as accepted component values.
|
|
32
|
+
const stopInput = event => event.stopPropagation();
|
|
33
|
+
const down = () => step(-1); const up = () => step(1);
|
|
34
|
+
input.addEventListener("input", stopInput); input.addEventListener("change", stopInput);
|
|
35
|
+
input.addEventListener("keydown", key); input.addEventListener("blur", commit);
|
|
36
|
+
decrease.addEventListener("click", down); increase.addEventListener("click", up);
|
|
37
|
+
const stop = host.effect(() => {
|
|
38
|
+
const signature = JSON.stringify([host.props.value.value, unit(), host.props.min.value, host.props.max.value]);
|
|
39
|
+
if (signature === external) return; external = signature;
|
|
40
|
+
try { paint(calculateValue(host.props.value.value, unit())); input.defaultValue = input.value; } catch { paint(current); }
|
|
41
|
+
});
|
|
42
|
+
const reset = afterFormReset(input, () => { try { paint(calculateValue(input.defaultValue, unit())); } catch { paint(0); } });
|
|
43
|
+
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
|
+
}
|