@alacris/ui 0.0.0 → 0.1.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 (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -1
  3. package/package.json +75 -3
  4. package/src/components/base.js +46 -0
  5. package/src/components/ui-accordion-item.js +157 -0
  6. package/src/components/ui-accordion.js +58 -0
  7. package/src/components/ui-alert.js +168 -0
  8. package/src/components/ui-app-bar.js +119 -0
  9. package/src/components/ui-autocomplete.js +371 -0
  10. package/src/components/ui-avatar.js +104 -0
  11. package/src/components/ui-backdrop.js +66 -0
  12. package/src/components/ui-badge.js +95 -0
  13. package/src/components/ui-bottom-app-bar.js +78 -0
  14. package/src/components/ui-bottom-nav.js +83 -0
  15. package/src/components/ui-breadcrumbs.js +100 -0
  16. package/src/components/ui-button-group.js +49 -0
  17. package/src/components/ui-button.js +143 -0
  18. package/src/components/ui-card.js +86 -0
  19. package/src/components/ui-carousel-item.js +53 -0
  20. package/src/components/ui-carousel.js +301 -0
  21. package/src/components/ui-checkbox.js +165 -0
  22. package/src/components/ui-chip-set.js +90 -0
  23. package/src/components/ui-chip.js +226 -0
  24. package/src/components/ui-container.js +59 -0
  25. package/src/components/ui-date-picker.js +774 -0
  26. package/src/components/ui-dialog.js +179 -0
  27. package/src/components/ui-divider.js +58 -0
  28. package/src/components/ui-drawer.js +169 -0
  29. package/src/components/ui-fab-menu.js +122 -0
  30. package/src/components/ui-fab.js +117 -0
  31. package/src/components/ui-icon-button.js +112 -0
  32. package/src/components/ui-icon.js +64 -0
  33. package/src/components/ui-list-item.js +166 -0
  34. package/src/components/ui-list.js +38 -0
  35. package/src/components/ui-loading-indicator.js +74 -0
  36. package/src/components/ui-menu-item.js +117 -0
  37. package/src/components/ui-menu.js +192 -0
  38. package/src/components/ui-nav-item.js +138 -0
  39. package/src/components/ui-nav-rail.js +111 -0
  40. package/src/components/ui-option.js +85 -0
  41. package/src/components/ui-pagination.js +173 -0
  42. package/src/components/ui-progress.js +100 -0
  43. package/src/components/ui-radio-group.js +104 -0
  44. package/src/components/ui-radio.js +143 -0
  45. package/src/components/ui-rating.js +126 -0
  46. package/src/components/ui-search.js +316 -0
  47. package/src/components/ui-select.js +418 -0
  48. package/src/components/ui-sheet.js +205 -0
  49. package/src/components/ui-side-sheet.js +221 -0
  50. package/src/components/ui-skeleton.js +89 -0
  51. package/src/components/ui-slider.js +266 -0
  52. package/src/components/ui-snackbar.js +233 -0
  53. package/src/components/ui-spinner.js +101 -0
  54. package/src/components/ui-split-button.js +158 -0
  55. package/src/components/ui-stack.js +37 -0
  56. package/src/components/ui-step.js +91 -0
  57. package/src/components/ui-stepper.js +86 -0
  58. package/src/components/ui-surface.js +51 -0
  59. package/src/components/ui-switch.js +153 -0
  60. package/src/components/ui-tab-panel.js +54 -0
  61. package/src/components/ui-tab.js +116 -0
  62. package/src/components/ui-table-footer.js +141 -0
  63. package/src/components/ui-table-toolbar.js +219 -0
  64. package/src/components/ui-table.js +735 -0
  65. package/src/components/ui-tabs.js +178 -0
  66. package/src/components/ui-text-field.js +319 -0
  67. package/src/components/ui-text.js +36 -0
  68. package/src/components/ui-time-picker.js +753 -0
  69. package/src/components/ui-toggle-button.js +170 -0
  70. package/src/components/ui-toggle-group.js +111 -0
  71. package/src/components/ui-toolbar.js +64 -0
  72. package/src/components/ui-tooltip.js +148 -0
  73. package/src/index.js +121 -0
  74. package/src/motion/animate.js +145 -0
  75. package/src/motion/flip.js +46 -0
  76. package/src/motion/index.js +4 -0
  77. package/src/motion/presence.js +104 -0
  78. package/src/motion/ripple.js +108 -0
  79. package/src/theme/apply-theme.js +142 -0
  80. package/src/theme/create-theme.js +70 -0
  81. package/src/theme/index.js +5 -0
  82. package/src/tokens/color.js +237 -0
  83. package/src/tokens/index.js +14 -0
  84. package/src/tokens/sys.js +76 -0
  85. package/src/tokens/system.js +114 -0
  86. package/src/tokens/typography.js +189 -0
  87. package/src/util/focus.js +89 -0
  88. package/src/util/form.js +69 -0
  89. package/src/util/icons.js +161 -0
  90. package/src/util/keys.js +110 -0
  91. package/src/util/position.js +102 -0
  92. package/src/util/table.js +230 -0
  93. package/types/elements.d.ts +78 -0
  94. package/types/index.d.ts +168 -0
@@ -0,0 +1,221 @@
1
+ // <ui-side-sheet> — a Material side sheet for complementary content.
2
+ //
3
+ // <ui-side-sheet open=${open} @close=${() => open(false)}>
4
+ // <span slot="headline">Filters</span>
5
+ // Sheet body
6
+ // <ui-button slot="actions" variant="text">Apply</ui-button>
7
+ // </ui-side-sheet>
8
+ //
9
+ // Distinct from <ui-drawer> (navigation) and <ui-sheet> (bottom). Modal
10
+ // (default): a scrim plus a panel that slides in from the end edge. Focus is
11
+ // trapped and page scroll locked while open. The PARENT owns `open`. Standard:
12
+ // an in-flow panel that animates its inline size — no scrim, no trap.
13
+ //
14
+ // @prop {boolean} open=false
15
+ // @prop {string} variant='modal' — modal | standard
16
+ // @prop {string} anchor='end' — start | end
17
+ // @prop {boolean} persistent=false — Escape/scrim do not request closing
18
+ // @prop {string} label='' — accessible name if no headline slot
19
+ // @event close — detail: { reason: 'esc' | 'scrim' | 'method' }
20
+ // @event opened — enter animation finished
21
+ // @event closed — exit animation finished, DOM removed
22
+ // @slot (default) — body content
23
+ // @slot headline
24
+ // @slot actions
25
+ // @part scrim, surface, headline, body, actions
26
+ // @vars see `t` below (`themeVars.names`)
27
+
28
+ import { define, html, css, vars, effect, onCleanup, signal } from '@alacris/core';
29
+ import { sys } from '../tokens/sys.js';
30
+ import { base } from './base.js';
31
+ import { presence } from '../motion/presence.js';
32
+ import { animate, fx } from '../motion/animate.js';
33
+ import { focusTrap, scrollLock } from '../util/focus.js';
34
+ import './ui-icon-button.js';
35
+
36
+ const t = vars('ui-side-sheet', {
37
+ bg: sys.color.surfaceContainerLow,
38
+ fg: sys.color.onSurface,
39
+ width: 'min(400px, 90vw)',
40
+ radius: sys.radius.lg,
41
+ scrim: `color-mix(in srgb, ${sys.color.scrim} 32%, transparent)`,
42
+ });
43
+
44
+ const styles = css`
45
+ :host { display: contents; }
46
+ .overlay {
47
+ position: fixed;
48
+ inset: 0;
49
+ z-index: ${sys.z.modal};
50
+ }
51
+ .scrim { position: absolute; inset: 0; background: ${t.scrim}; }
52
+ .surface {
53
+ position: absolute;
54
+ inset-block: 0;
55
+ display: flex;
56
+ flex-direction: column;
57
+ inline-size: ${t.width};
58
+ background: ${t.bg};
59
+ color: ${t.fg};
60
+ box-shadow: ${sys.elevation[1]};
61
+ }
62
+ .surface.start {
63
+ inset-inline-start: 0;
64
+ border-start-end-radius: ${t.radius};
65
+ border-end-end-radius: ${t.radius};
66
+ }
67
+ .surface.end {
68
+ inset-inline-end: 0;
69
+ border-start-start-radius: ${t.radius};
70
+ border-end-start-radius: ${t.radius};
71
+ }
72
+ .header {
73
+ display: flex;
74
+ align-items: center;
75
+ gap: ${sys.space(1)};
76
+ min-block-size: 64px;
77
+ padding-inline: ${sys.space(4)} ${sys.space(2)};
78
+ }
79
+ .headline {
80
+ flex: 1;
81
+ font: ${sys.type.titleLg};
82
+ letter-spacing: ${sys.tracking.titleLg};
83
+ }
84
+ .body {
85
+ flex: 1;
86
+ overflow: auto;
87
+ padding: ${sys.space(2)} ${sys.space(6)} ${sys.space(6)};
88
+ font: ${sys.type.bodyMd};
89
+ letter-spacing: ${sys.tracking.bodyMd};
90
+ color: ${sys.color.onSurfaceVariant};
91
+ }
92
+ .actions {
93
+ display: flex;
94
+ justify-content: flex-end;
95
+ gap: ${sys.space(2)};
96
+ padding: ${sys.space(2)} ${sys.space(4)} ${sys.space(4)};
97
+ }
98
+ .actions:not(.has) { display: none; }
99
+
100
+ .std {
101
+ display: block;
102
+ inline-size: 0;
103
+ overflow: hidden;
104
+ background: ${t.bg};
105
+ color: ${t.fg};
106
+ transition: inline-size ${sys.duration.medium2} ${sys.easing.emphasized};
107
+ }
108
+ .std.open { inline-size: ${t.width}; }
109
+ .std-inner { inline-size: ${t.width}; }
110
+ `;
111
+
112
+ define('ui-side-sheet', {
113
+ props: { open: false, variant: 'modal', anchor: 'end', persistent: false, label: '' },
114
+ styles: [base, styles],
115
+ setup({ open, variant, anchor, persistent, label }, host) {
116
+ let releaseTrap = null;
117
+ let unlock = null;
118
+ let surfaceEl = null;
119
+ const hasHeadline = signal(false);
120
+
121
+ const requestClose = (reason) => {
122
+ if (reason !== 'method' && persistent()) return;
123
+ host.emit('close', { reason });
124
+ };
125
+
126
+ const slideIn = () => (anchor.peek() === 'start' ? fx.slideInLeft : fx.slideInRight);
127
+ const slideOut = () => (anchor.peek() === 'start' ? fx.slideOutLeft : fx.slideOutRight);
128
+
129
+ const onDocKeydown = (e) => {
130
+ if (e.key === 'Escape') requestClose('esc');
131
+ };
132
+
133
+ effect(() => {
134
+ if (open() && variant() !== 'standard') {
135
+ document.addEventListener('keydown', onDocKeydown, true);
136
+ unlock = scrollLock();
137
+ queueMicrotask(() => {
138
+ if (open() && !releaseTrap) releaseTrap = focusTrap(host);
139
+ });
140
+ } else {
141
+ document.removeEventListener('keydown', onDocKeydown, true);
142
+ releaseTrap?.();
143
+ releaseTrap = null;
144
+ unlock?.();
145
+ unlock = null;
146
+ }
147
+ });
148
+ effect(() => {
149
+ if (!open() && surfaceEl?.isConnected) {
150
+ animate(surfaceEl, slideOut(), { duration: 'short4', easing: 'emphasizedAccelerate' });
151
+ }
152
+ });
153
+ onCleanup(() => {
154
+ document.removeEventListener('keydown', onDocKeydown, true);
155
+ releaseTrap?.();
156
+ unlock?.();
157
+ });
158
+
159
+ const surfaceRef = (el) => {
160
+ surfaceEl = el;
161
+ animate(el, slideIn(), { duration: 'medium2', easing: 'emphasizedDecelerate' });
162
+ };
163
+
164
+ const hasSlot = (el, set) => {
165
+ const sync = () => set(el.assignedElements().length > 0);
166
+ el.addEventListener('slotchange', sync);
167
+ sync();
168
+ };
169
+
170
+ const modalView = () => html`
171
+ <div class="overlay">
172
+ <div class="scrim" part="scrim" aria-hidden="true" @click=${() => requestClose('scrim')}></div>
173
+ <aside class=${() => `surface ${anchor()}`} part="surface" role="dialog" aria-modal="true"
174
+ aria-labelledby=${() => (hasHeadline() ? 'headline' : null)}
175
+ aria-label=${() => (hasHeadline() ? null : (label() || 'Side sheet'))}
176
+ tabindex="-1" ref=${surfaceRef}>
177
+ <div class="header">
178
+ <div class="headline" part="headline" id="headline"
179
+ ref=${(el) => hasSlot(el.querySelector('slot'), (has) => hasHeadline.set(has))}>
180
+ <slot name="headline"></slot>
181
+ </div>
182
+ <ui-icon-button icon="close" label="Close" @click=${() => requestClose('method')}></ui-icon-button>
183
+ </div>
184
+ <div class="body" part="body"><slot></slot></div>
185
+ <div class="actions" part="actions"
186
+ ref=${(el) => hasSlot(el.querySelector('slot'), (has) => el.classList.toggle('has', has))}>
187
+ <slot name="actions"></slot>
188
+ </div>
189
+ </aside>
190
+ </div>`;
191
+
192
+ return html`
193
+ ${() =>
194
+ variant() === 'standard'
195
+ ? html`<aside class=${() => `std ${anchor()}${open() ? ' open' : ''}`} part="surface"
196
+ role="region"
197
+ aria-labelledby=${() => (hasHeadline() ? 'headline' : null)}
198
+ aria-label=${() => (hasHeadline() ? null : (label() || 'Side sheet'))}>
199
+ <div class="std-inner">
200
+ <div class="header">
201
+ <div class="headline" part="headline" id="headline"
202
+ ref=${(el) => hasSlot(el.querySelector('slot'), (has) => hasHeadline.set(has))}>
203
+ <slot name="headline"></slot>
204
+ </div>
205
+ </div>
206
+ <div class="body" part="body"><slot></slot></div>
207
+ </div>
208
+ </aside>`
209
+ : null}
210
+ ${presence(() => open() && variant() !== 'standard', modalView, {
211
+ enter: fx.fadeIn,
212
+ exit: fx.fadeOut,
213
+ exitDuration: 'short4',
214
+ onEntered: () => host.emit('opened'),
215
+ onExited: () => host.emit('closed'),
216
+ })}`;
217
+ },
218
+ });
219
+
220
+ export const tag = 'ui-side-sheet';
221
+ export const themeVars = t;
@@ -0,0 +1,89 @@
1
+ // <ui-skeleton> — a loading placeholder shape.
2
+ //
3
+ // <ui-skeleton variant="circular" width="40px" height="40px"></ui-skeleton>
4
+ // <ui-skeleton width="60%"></ui-skeleton>
5
+ // <ui-skeleton variant="rectangular" height="120px" animation="wave"></ui-skeleton>
6
+ //
7
+ // Always aria-hidden: a skeleton is decorative. Announce loading state on the
8
+ // region that will receive the content (aria-busy), not on the placeholder.
9
+ //
10
+ // @prop {string} variant='text' — text | circular | rectangular
11
+ // @prop {string} width='' — CSS length; defaults to 100%
12
+ // @prop {string} height='' — CSS length; text defaults to 1em
13
+ // @prop {string} animation='pulse' — pulse | wave | none
14
+ // @part shape — the placeholder element
15
+ // @vars see `t` below (`themeVars.names`)
16
+
17
+ import { define, html, css, vars } from '@alacris/core';
18
+ import { sys } from '../tokens/sys.js';
19
+ import { base } from './base.js';
20
+
21
+ const t = vars('ui-skeleton', {
22
+ bg: `color-mix(in srgb, ${sys.color.onSurface} 11%, transparent)`,
23
+ wave: `color-mix(in srgb, ${sys.color.onSurface} 7%, transparent)`,
24
+ radius: sys.radius.xs,
25
+ });
26
+
27
+ const styles = css`
28
+ :host { display: block; }
29
+ .shape {
30
+ position: relative;
31
+ overflow: hidden;
32
+ inline-size: 100%;
33
+ background: ${t.bg};
34
+ }
35
+ .text {
36
+ block-size: 1em;
37
+ border-radius: ${t.radius};
38
+ transform: scale(1, 0.6); /* mimic a line of text within its line-height */
39
+ transform-origin: 0 60%;
40
+ }
41
+ .circular {
42
+ border-radius: ${sys.radius.full};
43
+ inline-size: 40px;
44
+ block-size: 40px;
45
+ }
46
+ .rectangular { block-size: 1.2em; }
47
+
48
+ @keyframes ui-skeleton-pulse {
49
+ 0% { opacity: 1; }
50
+ 50% { opacity: 0.5; }
51
+ 100% { opacity: 1; }
52
+ }
53
+ .pulse {
54
+ animation: ui-skeleton-pulse calc(${sys.duration.extraLong4} * 2) ${sys.easing.standard} infinite;
55
+ }
56
+
57
+ @keyframes ui-skeleton-wave {
58
+ 0% { transform: translateX(-100%); }
59
+ 100% { transform: translateX(100%); }
60
+ }
61
+ .wave::after {
62
+ content: '';
63
+ position: absolute;
64
+ inset: 0;
65
+ transform: translateX(-100%);
66
+ background: linear-gradient(90deg, transparent, ${t.wave}, transparent);
67
+ animation: ui-skeleton-wave calc(${sys.duration.extraLong4} * 1.6) ${sys.easing.linear} infinite;
68
+ }
69
+ `;
70
+
71
+ define('ui-skeleton', {
72
+ props: { variant: 'text', width: '', height: '', animation: 'pulse' },
73
+ styles: [base, styles],
74
+ setup({ variant, width, height, animation }, host) {
75
+ host.setAttribute('aria-hidden', 'true');
76
+
77
+ const cls = () =>
78
+ `shape ${variant()}${animation() !== 'none' ? ` ${animation()}` : ''}`;
79
+ const dims = () => ({
80
+ width: width() || null,
81
+ height: height() || null,
82
+ });
83
+
84
+ return html`<div part="shape" class=${cls} style=${dims}></div>`;
85
+ },
86
+ });
87
+
88
+ export const tag = 'ui-skeleton';
89
+ export const themeVars = t;
@@ -0,0 +1,266 @@
1
+ // <ui-slider> — a Material slider on native <input type="range">s for
2
+ // keyboard and screen-reader behavior.
3
+ //
4
+ // The active track portion is painted with `--ui-slider-fill` (or start/end
5
+ // when `range`) bound from the template into a gradient; the thumb's
6
+ // hover/focus halo is a box-shadow state layer.
7
+ //
8
+ // @prop {number} value=0
9
+ // @prop {number} min=0
10
+ // @prop {number} max=100
11
+ // @prop {number} step=1
12
+ // @prop {boolean} range=false — two thumbs; uses valueStart / valueEnd
13
+ // @prop {number} valueStart=0
14
+ // @prop {number} valueEnd=100
15
+ // @prop {string} label='' — REQUIRED accessible name (aria-label)
16
+ // @prop {boolean} disabled=false
17
+ // @prop {boolean} showValue=false — value bubble above the thumb while
18
+ // focused/dragging (animates in and out)
19
+ // @prop {string} name='' — form participation
20
+ // @event input — every drag/keystroke; detail: { value } or { start, end }
21
+ // @event change — committed value; detail: { value } or { start, end }
22
+ // @part input — the native <input type="range"> (both, when range)
23
+ // @vars see `t` below (`themeVars.names`)
24
+
25
+ import { define, html, css, vars, computed, signal, effect } from '@alacris/core';
26
+ import { sys } from '../tokens/sys.js';
27
+ import { base } from './base.js';
28
+ import { formBind } from '../util/form.js';
29
+ import { presence } from '../motion/presence.js';
30
+ import { fx } from '../motion/animate.js';
31
+
32
+ const t = vars('ui-slider', {
33
+ trackHeight: '4px',
34
+ thumbSize: '20px',
35
+ track: sys.color.surfaceContainerHighest,
36
+ active: sys.color.primary,
37
+ thumb: sys.color.primary,
38
+ bubbleBg: sys.color.primary,
39
+ bubbleFg: sys.color.onPrimary,
40
+ });
41
+
42
+ const styles = css`
43
+ :host { display: block; inline-size: 240px; }
44
+ .root { position: relative; display: flex; align-items: center; }
45
+ input {
46
+ appearance: none;
47
+ -webkit-appearance: none;
48
+ inline-size: 100%;
49
+ /* density never shrinks the touch target below 44px */
50
+ block-size: max(44px, calc(48px + var(--ui-density, 0) * 4px));
51
+ margin: 0;
52
+ padding: 0;
53
+ background: transparent;
54
+ cursor: pointer;
55
+ }
56
+ /* The thumb halo below replaces the native outline as the focus indicator. */
57
+ input:focus-visible { outline: none; }
58
+
59
+ input::-webkit-slider-runnable-track {
60
+ block-size: ${t.trackHeight};
61
+ border-radius: ${sys.radius.full};
62
+ background: linear-gradient(to right,
63
+ ${t.track} 0%,
64
+ ${t.track} var(--ui-slider-start, 0%),
65
+ ${t.active} var(--ui-slider-start, 0%),
66
+ ${t.active} var(--ui-slider-end, var(--ui-slider-fill, 0%)),
67
+ ${t.track} var(--ui-slider-end, var(--ui-slider-fill, 0%)),
68
+ ${t.track} 100%);
69
+ }
70
+ input::-webkit-slider-thumb {
71
+ appearance: none;
72
+ -webkit-appearance: none;
73
+ inline-size: ${t.thumbSize};
74
+ block-size: ${t.thumbSize};
75
+ margin-block-start: calc((${t.trackHeight} - ${t.thumbSize}) / 2);
76
+ border: none;
77
+ border-radius: ${sys.radius.full};
78
+ background: ${t.thumb};
79
+ transition: box-shadow ${sys.duration.short2} ${sys.easing.standard};
80
+ }
81
+ input:hover::-webkit-slider-thumb {
82
+ box-shadow: 0 0 0 10px color-mix(in srgb, ${t.thumb} calc(${sys.state.hover} * 100%), transparent);
83
+ }
84
+ input:focus-visible::-webkit-slider-thumb,
85
+ input:active::-webkit-slider-thumb {
86
+ box-shadow: 0 0 0 10px color-mix(in srgb, ${t.thumb} calc(${sys.state.focus} * 100%), transparent);
87
+ }
88
+
89
+ input::-moz-range-track {
90
+ block-size: ${t.trackHeight};
91
+ border-radius: ${sys.radius.full};
92
+ background: ${t.track};
93
+ }
94
+ input::-moz-range-progress {
95
+ block-size: ${t.trackHeight};
96
+ border-radius: ${sys.radius.full};
97
+ background: ${t.active};
98
+ }
99
+ input::-moz-range-thumb {
100
+ inline-size: ${t.thumbSize};
101
+ block-size: ${t.thumbSize};
102
+ border: none;
103
+ border-radius: ${sys.radius.full};
104
+ background: ${t.thumb};
105
+ transition: box-shadow ${sys.duration.short2} ${sys.easing.standard};
106
+ }
107
+ input:hover::-moz-range-thumb {
108
+ box-shadow: 0 0 0 10px color-mix(in srgb, ${t.thumb} calc(${sys.state.hover} * 100%), transparent);
109
+ }
110
+ input:focus-visible::-moz-range-thumb {
111
+ box-shadow: 0 0 0 10px color-mix(in srgb, ${t.thumb} calc(${sys.state.focus} * 100%), transparent);
112
+ }
113
+
114
+ input:disabled { cursor: default; pointer-events: none; opacity: ${sys.state.disabledContent}; }
115
+
116
+ .dual {
117
+ position: relative;
118
+ inline-size: 100%;
119
+ /* Abs-pos thumbs are out of flow; without this the track collapses to 0. */
120
+ block-size: max(44px, calc(48px + var(--ui-density, 0) * 4px));
121
+ }
122
+ .dual input { position: absolute; inset: 0; pointer-events: none; }
123
+ .dual input::-webkit-slider-thumb { pointer-events: auto; }
124
+ .dual input::-moz-range-thumb { pointer-events: auto; }
125
+ .dual input:last-child::-webkit-slider-runnable-track { background: transparent; }
126
+ .dual input:last-child::-moz-range-track { background: transparent; }
127
+ .dual input:last-child::-moz-range-progress { background: transparent; }
128
+
129
+ .bubble {
130
+ position: absolute;
131
+ inset-inline-start: var(--ui-slider-fill, 0%);
132
+ inset-block-start: 0;
133
+ translate: -50% -100%;
134
+ padding: ${sys.space(1)} ${sys.space(2)};
135
+ border-radius: ${sys.radius.full};
136
+ background: ${t.bubbleBg};
137
+ color: ${t.bubbleFg};
138
+ font: ${sys.type.labelMd};
139
+ letter-spacing: ${sys.tracking.labelMd};
140
+ white-space: nowrap;
141
+ pointer-events: none;
142
+ }
143
+ `;
144
+
145
+ define('ui-slider', {
146
+ formAssociated: true,
147
+ props: {
148
+ value: 0, min: 0, max: 100, step: 1,
149
+ range: false, valueStart: 0, valueEnd: 100,
150
+ label: '', disabled: false, showValue: false, name: '',
151
+ },
152
+ styles: [base, styles],
153
+ setup({ value, min, max, step, range, valueStart, valueEnd, label, disabled, showValue, name }, host) {
154
+ const submitted = signal(String(value.peek()));
155
+ effect(() => {
156
+ submitted.set(range() ? `${valueStart()},${valueEnd()}` : String(value()));
157
+ });
158
+ formBind(host, { name, value: submitted, disabled });
159
+
160
+ const active = signal(false);
161
+ const activeThumb = signal('end');
162
+
163
+ const pct = (n) => {
164
+ const lo = min();
165
+ const span = max() - lo || 1;
166
+ return Math.min(100, Math.max(0, ((n - lo) / span) * 100));
167
+ };
168
+ const fill = computed(() => pct(value()));
169
+ const startPct = computed(() => (range() ? pct(valueStart()) : 0));
170
+ const endPct = computed(() => (range() ? pct(valueEnd()) : fill()));
171
+ const bubblePct = computed(() =>
172
+ range() ? (activeThumb() === 'start' ? startPct() : endPct()) : fill());
173
+ const bubbleText = computed(() =>
174
+ range() ? (activeThumb() === 'start' ? valueStart() : valueEnd()) : value());
175
+
176
+ const read = (el) => {
177
+ const n = Number(el.value);
178
+ return Number.isNaN(n) ? min() : n;
179
+ };
180
+ const emit = (type) => {
181
+ host.emit(type, range()
182
+ ? { start: valueStart(), end: valueEnd(), value: `${valueStart()},${valueEnd()}` }
183
+ : { value: value() });
184
+ };
185
+ const onInput = (e) => {
186
+ e.stopPropagation();
187
+ value.set(read(e.target));
188
+ emit('input');
189
+ };
190
+ const onChange = (e) => {
191
+ e.stopPropagation();
192
+ value.set(read(e.target));
193
+ emit('change');
194
+ };
195
+ const onStartInput = (e) => {
196
+ e.stopPropagation();
197
+ valueStart.set(Math.min(read(e.target), valueEnd()));
198
+ activeThumb.set('start');
199
+ emit('input');
200
+ };
201
+ const onEndInput = (e) => {
202
+ e.stopPropagation();
203
+ valueEnd.set(Math.max(read(e.target), valueStart()));
204
+ activeThumb.set('end');
205
+ emit('input');
206
+ };
207
+ const onStartChange = (e) => {
208
+ e.stopPropagation();
209
+ valueStart.set(Math.min(read(e.target), valueEnd()));
210
+ emit('change');
211
+ };
212
+ const onEndChange = (e) => {
213
+ e.stopPropagation();
214
+ valueEnd.set(Math.max(read(e.target), valueStart()));
215
+ emit('change');
216
+ };
217
+
218
+ const styleVars = computed(() => ({
219
+ '--ui-slider-fill': fill() + '%',
220
+ '--ui-slider-start': startPct() + '%',
221
+ '--ui-slider-end': endPct() + '%',
222
+ }));
223
+
224
+ return html`
225
+ <div class="root" style=${styleVars}>
226
+ ${() => (range()
227
+ ? html`<div class="dual">
228
+ <input part="input" type="range"
229
+ min=${min} max=${max} step=${step} .value=${valueStart}
230
+ ?disabled=${disabled}
231
+ aria-label=${() => (label() ? label() + ' start' : 'Start')}
232
+ @input=${onStartInput} @change=${onStartChange}
233
+ @pointerdown=${() => { active.set(true); activeThumb.set('start'); }}
234
+ @focus=${() => { active.set(true); activeThumb.set('start'); }}
235
+ @blur=${() => active.set(false)}>
236
+ <input part="input" type="range"
237
+ min=${min} max=${max} step=${step} .value=${valueEnd}
238
+ ?disabled=${disabled}
239
+ aria-label=${() => (label() ? label() + ' end' : 'End')}
240
+ @input=${onEndInput} @change=${onEndChange}
241
+ @pointerdown=${() => { active.set(true); activeThumb.set('end'); }}
242
+ @focus=${() => { active.set(true); activeThumb.set('end'); }}
243
+ @blur=${() => active.set(false)}>
244
+ </div>`
245
+ : html`<input part="input" type="range"
246
+ min=${min} max=${max} step=${step} .value=${value}
247
+ ?disabled=${disabled}
248
+ aria-label=${() => label() || 'Slider'}
249
+ @input=${onInput} @change=${onChange}
250
+ @pointerdown=${() => active.set(true)}
251
+ @focus=${() => active.set(true)}
252
+ @blur=${() => active.set(false)}>`)}
253
+ ${presence(() => showValue() && active(), () => html`
254
+ <output class="bubble" style=${() => ({ insetInlineStart: bubblePct() + '%' })}
255
+ aria-hidden="true">${bubbleText}</output>`, {
256
+ enter: fx.scaleIn,
257
+ exit: fx.scaleOut,
258
+ enterDuration: 'short4',
259
+ exitDuration: 'short2',
260
+ })}
261
+ </div>`;
262
+ },
263
+ });
264
+
265
+ export const tag = 'ui-slider';
266
+ export const themeVars = t;