@alacris/ui 0.0.0 → 0.1.1

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 +318 -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,233 @@
1
+ // <ui-snackbar> — a transient bottom-center message, plus `showSnackbar()`,
2
+ // an imperative service that queues messages through one shared instance.
3
+ //
4
+ // Declarative (parent owns the state):
5
+ //
6
+ // <ui-snackbar open=${open} message="Draft saved" action="Undo"
7
+ // @action=${undo} @close=${() => open(false)}></ui-snackbar>
8
+ //
9
+ // Imperative (fire and forget; FIFO — one visible at a time):
10
+ //
11
+ // const { close, closed } = showSnackbar('Message archived', { action: 'Undo' });
12
+ //
13
+ // The component requests closing by emitting `close` with a reason — the
14
+ // PARENT flips `open`. `closed` fires after the exit animation finishes.
15
+ //
16
+ // @prop {boolean} open=false
17
+ // @prop {string} message=''
18
+ // @prop {string} action='' — label for a trailing text action button
19
+ // @prop {number} duration=4000 — auto-dismiss after ms; 0 = sticky
20
+ // @prop {boolean} closeButton=false — trailing close icon button
21
+ // @event action — action button pressed
22
+ // @event close — detail: { reason: 'timeout' | 'action' | 'close' | 'method' }
23
+ // @event opened — enter animation finished
24
+ // @event closed — exit animation finished, DOM removed
25
+ // @part surface, message, action, close
26
+ // @vars see `t` below (`themeVars.names`)
27
+
28
+ import { define, html, css, vars, effect } from '@alacris/core';
29
+ import { sys } from '../tokens/sys.js';
30
+ import { base, focusRingOn } from './base.js';
31
+ import { presence } from '../motion/presence.js';
32
+ import { fx } from '../motion/animate.js';
33
+ import { ripple } from '../motion/ripple.js';
34
+ import './ui-icon.js';
35
+
36
+ const t = vars('ui-snackbar', {
37
+ bg: sys.color.inverseSurface,
38
+ fg: sys.color.inverseOnSurface,
39
+ actionFg: sys.color.inversePrimary,
40
+ radius: sys.radius.xs,
41
+ minWidth: '288px',
42
+ maxWidth: '560px',
43
+ elevation: sys.elevation[3],
44
+ });
45
+
46
+ const styles = css`
47
+ :host { display: contents; }
48
+ .region {
49
+ position: fixed;
50
+ inset-inline: 0;
51
+ inset-block-end: ${sys.space(4)};
52
+ z-index: ${sys.z.snackbar};
53
+ display: flex;
54
+ justify-content: center;
55
+ padding-inline: ${sys.space(4)};
56
+ pointer-events: none;
57
+ }
58
+ .surface {
59
+ pointer-events: auto;
60
+ display: flex;
61
+ align-items: center;
62
+ gap: ${sys.space(1)};
63
+ min-inline-size: min(${t.minWidth}, 100%);
64
+ max-inline-size: ${t.maxWidth};
65
+ min-block-size: 48px;
66
+ padding-block: ${sys.space(1)};
67
+ padding-inline: ${sys.space(4)} ${sys.space(2)};
68
+ background: ${t.bg};
69
+ color: ${t.fg};
70
+ border-radius: ${t.radius};
71
+ box-shadow: ${t.elevation};
72
+ font: ${sys.type.bodyMd};
73
+ letter-spacing: ${sys.tracking.bodyMd};
74
+ }
75
+ .message { flex: 1; padding-block: ${sys.space(2)}; }
76
+ .surface:not(.trailing) { padding-inline-end: ${sys.space(4)}; }
77
+
78
+ .action, .close {
79
+ position: relative;
80
+ isolation: isolate;
81
+ flex: none;
82
+ display: inline-flex;
83
+ align-items: center;
84
+ justify-content: center;
85
+ border: none;
86
+ background: transparent;
87
+ cursor: pointer;
88
+ border-radius: ${sys.radius.full};
89
+ }
90
+ ${focusRingOn('.action')}
91
+ ${focusRingOn('.close')}
92
+ .layer {
93
+ position: absolute; inset: 0; z-index: -1;
94
+ border-radius: inherit; background: currentColor; opacity: 0;
95
+ transition: opacity ${sys.duration.short2} ${sys.easing.standard};
96
+ }
97
+ .action:hover .layer, .close:hover .layer { opacity: ${sys.state.hover}; }
98
+ .action:focus-visible .layer, .close:focus-visible .layer { opacity: ${sys.state.focus}; }
99
+ .action:active .layer, .close:active .layer { opacity: ${sys.state.pressed}; }
100
+
101
+ .action {
102
+ block-size: 36px;
103
+ padding-inline: ${sys.space(2)};
104
+ color: ${t.actionFg};
105
+ font: ${sys.type.labelLg};
106
+ letter-spacing: ${sys.tracking.labelLg};
107
+ white-space: nowrap;
108
+ }
109
+ .close {
110
+ inline-size: 32px;
111
+ block-size: 32px;
112
+ padding: 0;
113
+ color: inherit;
114
+ --ui-icon-size: 18px;
115
+ }
116
+ `;
117
+
118
+ define('ui-snackbar', {
119
+ props: { open: false, message: '', action: '', duration: 4000, closeButton: false },
120
+ styles: [base, styles],
121
+ setup({ open, message, action, duration, closeButton }, host) {
122
+ const requestClose = (reason) => host.emit('close', { reason });
123
+
124
+ // Auto-dismiss while open; the cleanup clears the pending timer whenever
125
+ // `open`/`duration` change or the element goes away.
126
+ effect(() => {
127
+ if (!open()) return;
128
+ const ms = duration();
129
+ if (!(ms > 0)) return;
130
+ const id = setTimeout(() => requestClose('timeout'), ms);
131
+ return () => clearTimeout(id);
132
+ });
133
+
134
+ const view = () => html`
135
+ <div class="region">
136
+ <div part="surface" role="status" aria-live="polite"
137
+ class=${() => `surface${action() || closeButton() ? ' trailing' : ''}`}>
138
+ <span class="message" part="message">${message}</span>
139
+ ${() =>
140
+ action()
141
+ ? html`<button class="action" part="action" type="button"
142
+ @click=${() => { host.emit('action'); requestClose('action'); }}
143
+ ref=${(el) => ripple(el)}>
144
+ <span class="layer" aria-hidden="true"></span>${action}
145
+ </button>`
146
+ : null}
147
+ ${() =>
148
+ closeButton()
149
+ ? html`<button class="close" part="close" type="button" aria-label="Dismiss"
150
+ @click=${() => requestClose('close')}
151
+ ref=${(el) => ripple(el, { centered: true })}>
152
+ <span class="layer" aria-hidden="true"></span>
153
+ <ui-icon name="close"></ui-icon>
154
+ </button>`
155
+ : null}
156
+ </div>
157
+ </div>`;
158
+
159
+ return html`${presence(open, view, {
160
+ enter: fx.slideInUp,
161
+ exit: fx.slideOutDown,
162
+ exitDuration: 'short4',
163
+ target: '.surface',
164
+ onEntered: () => host.emit('opened'),
165
+ onExited: () => host.emit('closed'),
166
+ })}`;
167
+ },
168
+ });
169
+
170
+ export const tag = 'ui-snackbar';
171
+ export const themeVars = t;
172
+
173
+ // --------------------------------------------------------------- service
174
+ //
175
+ // One shared light-DOM <ui-snackbar> appended to <body>; messages queue FIFO
176
+ // and the next one shows only after the previous fully exited.
177
+
178
+ let serviceEl = null; // singleton host
179
+ let current = null; // entry on screen (or animating out)
180
+ const queue = [];
181
+
182
+ function serviceHost() {
183
+ if (!serviceEl || !serviceEl.isConnected) {
184
+ serviceEl = document.createElement('ui-snackbar');
185
+ // The service is the "parent": it owns the open state.
186
+ serviceEl.addEventListener('close', () => { serviceEl.open = false; });
187
+ serviceEl.addEventListener('closed', () => {
188
+ const done = current;
189
+ current = null;
190
+ done?.resolve();
191
+ next();
192
+ });
193
+ document.body.append(serviceEl);
194
+ }
195
+ return serviceEl;
196
+ }
197
+
198
+ function next() {
199
+ if (current || !queue.length) return;
200
+ current = queue.shift();
201
+ const el = serviceHost();
202
+ el.message = current.message;
203
+ el.action = current.action;
204
+ el.duration = current.duration;
205
+ el.closeButton = current.closeButton;
206
+ el.open = true;
207
+ }
208
+
209
+ /**
210
+ * showSnackbar(message, { action, duration, closeButton })
211
+ *
212
+ * Returns { close, closed }: `close()` dismisses this snackbar (or removes it
213
+ * from the queue if it has not shown yet); `closed` resolves once it has fully
214
+ * left the screen.
215
+ */
216
+ export function showSnackbar(message, { action = '', duration = 4000, closeButton = false } = {}) {
217
+ let resolve;
218
+ const closed = new Promise((r) => { resolve = r; });
219
+ const entry = { message, action, duration, closeButton, resolve };
220
+ queue.push(entry);
221
+ next();
222
+ return {
223
+ closed,
224
+ close() {
225
+ if (current === entry) {
226
+ serviceEl.open = false; // exit animation → 'closed' → resolve + next
227
+ } else {
228
+ const i = queue.indexOf(entry);
229
+ if (i > -1) { queue.splice(i, 1); entry.resolve(); }
230
+ }
231
+ },
232
+ };
233
+ }
@@ -0,0 +1,101 @@
1
+ // <ui-spinner> — Material circular progress.
2
+ //
3
+ // <ui-spinner label="Loading"></ui-spinner> indeterminate
4
+ // <ui-spinner label="Upload" value=${pct}></ui-spinner> determinate
5
+ // <ui-spinner label="Loading" size="24px"></ui-spinner>
6
+ //
7
+ // @prop {number} value=-1 — current value; -1 (any negative) = indeterminate
8
+ // @prop {number} max=100 — value scale
9
+ // @prop {string} size='' — CSS length; overrides --ui-spinner-size (default 48px)
10
+ // @prop {string} label='' — accessible name (aria-label on the progressbar)
11
+ // @part progress — the progressbar wrapper
12
+ // @vars see `t` below (`themeVars.names`)
13
+ //
14
+ // One SVG circle in a 44-unit viewBox with a 4-unit stroke, so the stroke
15
+ // scales with `size`. Determinate progress binds stroke-dashoffset from the
16
+ // value; indeterminate is the classic rotate + dash-grow loop as CSS
17
+ // keyframes, its cycle derived from the motion tokens.
18
+
19
+ import { define, html, svg, css, vars, computed, effect } from '@alacris/core';
20
+ import { sys } from '../tokens/sys.js';
21
+ import { base } from './base.js';
22
+
23
+ const t = vars('ui-spinner', {
24
+ size: '48px',
25
+ color: sys.color.primary,
26
+ trackColor: 'transparent',
27
+ });
28
+
29
+ // r=20 in a 44-unit viewBox; the arc length everything is dashed against.
30
+ const R = 20;
31
+ const C = 2 * Math.PI * R; // ≈ 125.66
32
+
33
+ const styles = css`
34
+ :host {
35
+ display: inline-flex;
36
+ inline-size: ${t.size};
37
+ block-size: ${t.size};
38
+ flex: none;
39
+ color: ${t.color};
40
+ }
41
+ .progress, svg { inline-size: 100%; block-size: 100%; }
42
+ svg { transform: rotate(-90deg); } /* start determinate sweep at 12 o'clock */
43
+ circle { fill: none; stroke-width: 4; }
44
+ .track { stroke: ${t.trackColor}; }
45
+ .arc {
46
+ stroke: currentColor;
47
+ stroke-linecap: round;
48
+ transition: stroke-dashoffset ${sys.duration.short4} ${sys.easing.standard};
49
+ }
50
+
51
+ @keyframes ui-spinner-rotate {
52
+ 100% { transform: rotate(360deg); }
53
+ }
54
+ @keyframes ui-spinner-dash {
55
+ 0% { stroke-dasharray: 1, 200; stroke-dashoffset: 0; }
56
+ 50% { stroke-dasharray: 90, 200; stroke-dashoffset: -35; }
57
+ 100% { stroke-dasharray: 90, 200; stroke-dashoffset: -124; }
58
+ }
59
+ .indeterminate svg {
60
+ transform: none;
61
+ animation: ui-spinner-rotate calc(${sys.duration.extraLong4} * 1.4) ${sys.easing.linear} infinite;
62
+ }
63
+ .indeterminate .arc {
64
+ animation: ui-spinner-dash calc(${sys.duration.extraLong4} * 1.4) ${sys.easing.standard} infinite;
65
+ }
66
+ `;
67
+
68
+ define('ui-spinner', {
69
+ props: { value: -1, max: 100, size: '', label: '' },
70
+ styles: [base, styles],
71
+ setup({ value, max, size, label }, host) {
72
+ const indeterminate = computed(() => value() < 0);
73
+ const clamped = computed(() => Math.min(Math.max(value(), 0), max() || 100));
74
+ const offset = computed(() => C * (1 - clamped() / (max() || 100)));
75
+
76
+ effect(() => {
77
+ if (size()) host.style.setProperty('--ui-spinner-size', size());
78
+ else host.style.removeProperty('--ui-spinner-size');
79
+ });
80
+
81
+ return html`
82
+ <div part="progress"
83
+ class=${() => `progress${indeterminate() ? ' indeterminate' : ''}`}
84
+ role="progressbar"
85
+ aria-label=${() => label() || null}
86
+ aria-valuemin="0"
87
+ aria-valuemax=${max}
88
+ aria-valuenow=${() => (indeterminate() ? null : String(clamped()))}>
89
+ ${svg`
90
+ <svg viewBox="0 0 44 44" aria-hidden="true">
91
+ <circle class="track" cx="22" cy="22" r="20"></circle>
92
+ <circle class="arc" cx="22" cy="22" r="20"
93
+ stroke-dasharray=${() => (indeterminate() ? null : `${C}`)}
94
+ stroke-dashoffset=${() => (indeterminate() ? null : String(offset()))}></circle>
95
+ </svg>`}
96
+ </div>`;
97
+ },
98
+ });
99
+
100
+ export const tag = 'ui-spinner';
101
+ export const themeVars = t;
@@ -0,0 +1,158 @@
1
+ // <ui-split-button> — a Material split button: a primary action plus a
2
+ // connected chevron that opens related actions.
3
+ //
4
+ // <ui-split-button variant="filled" @click=${save}>
5
+ // Save
6
+ // <ui-menu-item slot="menu" value="draft">Save draft</ui-menu-item>
7
+ // <ui-menu-item slot="menu" value="copy">Save a copy</ui-menu-item>
8
+ // </ui-split-button>
9
+ //
10
+ // The leading segment is the primary action (native click bubbles). Choosing
11
+ // a menu item emits `select` with that item's value.
12
+ //
13
+ // @prop {string} variant='filled' — filled | tonal | outlined | elevated
14
+ // @prop {boolean} disabled=false
15
+ // @event (native click bubbles from the leading segment)
16
+ // @event select — a menu action was chosen; detail: { value }
17
+ // @slot (default) — leading-segment label
18
+ // @slot icon — leading icon on the primary action
19
+ // @slot menu — <ui-menu-item> children
20
+ // @part group, action, chevron
21
+ // @vars see `t` below (`themeVars.names`)
22
+
23
+ import { define, html, css, vars, computed, onCleanup } from '@alacris/core';
24
+ import { sys } from '../tokens/sys.js';
25
+ import { base, focusRingOn } from './base.js';
26
+ import { ripple } from '../motion/ripple.js';
27
+ import './ui-icon.js';
28
+ import './ui-menu.js';
29
+ import './ui-menu-item.js';
30
+
31
+ const t = vars('ui-split-button', {
32
+ height: '40px',
33
+ padInline: '20px',
34
+ radius: sys.radius.full,
35
+ font: sys.type.labelLg,
36
+ tracking: sys.tracking.labelLg,
37
+ filledBg: sys.color.primary,
38
+ filledFg: sys.color.onPrimary,
39
+ tonalBg: sys.color.secondaryContainer,
40
+ tonalFg: sys.color.onSecondaryContainer,
41
+ outlinedFg: sys.color.primary,
42
+ outlineColor: sys.color.outline,
43
+ elevatedBg: sys.color.surfaceContainerLow,
44
+ elevatedFg: sys.color.primary,
45
+ divider: sys.color.outlineVariant,
46
+ });
47
+
48
+ const styles = css`
49
+ :host { display: inline-flex; vertical-align: middle; }
50
+ .group {
51
+ display: inline-flex;
52
+ align-items: stretch;
53
+ block-size: calc(${t.height} + var(--ui-density, 0) * 4px);
54
+ border-radius: ${t.radius};
55
+ overflow: hidden;
56
+ font: ${t.font};
57
+ letter-spacing: ${t.tracking};
58
+ }
59
+ .action, .chevron {
60
+ position: relative;
61
+ isolation: isolate;
62
+ display: inline-flex;
63
+ align-items: center;
64
+ justify-content: center;
65
+ margin: 0;
66
+ border: none;
67
+ background: transparent;
68
+ color: inherit;
69
+ cursor: pointer;
70
+ user-select: none;
71
+ }
72
+ ${focusRingOn('.action')}
73
+ ${focusRingOn('.chevron')}
74
+ .layer {
75
+ position: absolute; inset: 0; z-index: -1;
76
+ background: currentColor; opacity: 0;
77
+ transition: opacity ${sys.duration.short2} ${sys.easing.standard};
78
+ }
79
+ .action:hover .layer, .chevron:hover .layer { opacity: ${sys.state.hover}; }
80
+ .action:focus-visible .layer, .chevron:focus-visible .layer { opacity: ${sys.state.focus}; }
81
+ .action:active .layer, .chevron:active .layer { opacity: ${sys.state.pressed}; }
82
+
83
+ .action {
84
+ gap: ${sys.space(2)};
85
+ padding-inline: ${t.padInline} ${sys.space(3)};
86
+ --ui-icon-size: 1.125rem;
87
+ }
88
+ .chevron {
89
+ padding-inline: ${sys.space(2)} ${sys.space(3)};
90
+ border-inline-start: 1px solid ${t.divider};
91
+ --ui-icon-size: 1.5rem;
92
+ }
93
+
94
+ .filled { background: ${t.filledBg}; color: ${t.filledFg}; }
95
+ .tonal { background: ${t.tonalBg}; color: ${t.tonalFg}; }
96
+ .outlined {
97
+ color: ${t.outlinedFg};
98
+ box-shadow: inset 0 0 0 1px ${t.outlineColor};
99
+ background: transparent;
100
+ }
101
+ .elevated {
102
+ background: ${t.elevatedBg};
103
+ color: ${t.elevatedFg};
104
+ box-shadow: ${sys.elevation[1]};
105
+ }
106
+
107
+ .disabled { pointer-events: none; }
108
+ .filled.disabled, .tonal.disabled, .elevated.disabled {
109
+ background: color-mix(in srgb, ${sys.color.onSurface} calc(${sys.state.disabledContainer} * 100%), transparent);
110
+ color: color-mix(in srgb, ${sys.color.onSurface} calc(${sys.state.disabledContent} * 100%), transparent);
111
+ box-shadow: none;
112
+ }
113
+ .outlined.disabled {
114
+ color: color-mix(in srgb, ${sys.color.onSurface} calc(${sys.state.disabledContent} * 100%), transparent);
115
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, ${sys.color.onSurface} calc(${sys.state.disabledContainer} * 100%), transparent);
116
+ }
117
+ .group ui-menu { display: contents; }
118
+ `;
119
+
120
+ define('ui-split-button', {
121
+ props: { variant: 'filled', disabled: false },
122
+ styles: [base, styles],
123
+ setup({ variant, disabled }, host) {
124
+ const cls = computed(() =>
125
+ `group ${variant()}${disabled() ? ' disabled' : ''}`);
126
+
127
+ const onSelect = (e) => {
128
+ e.stopPropagation();
129
+ host.emit('select', { value: e.detail.value });
130
+ const menu = host.shadowRoot?.querySelector('ui-menu');
131
+ if (menu) menu.open = false;
132
+ };
133
+ host.addEventListener('ui-menu-select', onSelect);
134
+ onCleanup(() => host.removeEventListener('ui-menu-select', onSelect));
135
+
136
+ return html`
137
+ <div class=${cls} part="group" role="group">
138
+ <button part="action" class="action" type="button" ?disabled=${disabled}
139
+ ref=${(el) => ripple(el, { disabled })}>
140
+ <span class="layer" aria-hidden="true"></span>
141
+ <slot name="icon"></slot>
142
+ <slot></slot>
143
+ </button>
144
+ <ui-menu>
145
+ <button slot="anchor" part="chevron" class="chevron" type="button"
146
+ ?disabled=${disabled} aria-label="More actions"
147
+ ref=${(el) => ripple(el, { disabled })}>
148
+ <span class="layer" aria-hidden="true"></span>
149
+ <ui-icon name="arrow-drop-down"></ui-icon>
150
+ </button>
151
+ <slot name="menu"></slot>
152
+ </ui-menu>
153
+ </div>`;
154
+ },
155
+ });
156
+
157
+ export const tag = 'ui-split-button';
158
+ export const themeVars = t;
@@ -0,0 +1,37 @@
1
+ // <ui-stack> — a flexbox layout primitive.
2
+ //
3
+ // <ui-stack direction="row" gap="4" align="center">…</ui-stack>
4
+ //
5
+ // @prop {string} direction='column' — row | column | row-reverse | column-reverse
6
+ // @prop {number|string} gap=2 — a spacing step (maps to the --ui-space-N
7
+ // token: 1,2,3,4,5,6,7,8,10,12,16,20,24) or
8
+ // any raw CSS length ('1.5rem', '12px')
9
+ // @prop {string} align='' — align-items value ('' leaves the default)
10
+ // @prop {string} justify='' — justify-content value
11
+ // @prop {boolean} wrap=false — flex-wrap: wrap
12
+ // @slot (default)
13
+ // @vars (none — spacing flows from the --ui-space-* system tokens)
14
+
15
+ import { define, html, css, effect } from '@alacris/core';
16
+ import { sys } from '../tokens/sys.js';
17
+ import { base } from './base.js';
18
+
19
+ define('ui-stack', {
20
+ // gap defaults to a string so raw CSS lengths survive attribute coercion;
21
+ // numeric property writes are normalized below.
22
+ props: { direction: 'column', gap: '2', align: '', justify: '', wrap: false },
23
+ styles: [base, css`:host { display: flex; }`],
24
+ setup({ direction, gap, align, justify, wrap }, host) {
25
+ effect(() => { host.style.flexDirection = direction(); });
26
+ effect(() => {
27
+ const g = String(gap() ?? '').trim();
28
+ host.style.gap = /^\d+$/.test(g) ? sys.space(g) : g;
29
+ });
30
+ effect(() => { host.style.alignItems = align() || ''; });
31
+ effect(() => { host.style.justifyContent = justify() || ''; });
32
+ effect(() => { host.style.flexWrap = wrap() ? 'wrap' : ''; });
33
+ return html`<slot></slot>`;
34
+ },
35
+ });
36
+
37
+ export const tag = 'ui-stack';
@@ -0,0 +1,91 @@
1
+ // <ui-step> — one step inside <ui-stepper>.
2
+ //
3
+ // @prop {string} label=''
4
+ // @prop {number} index=0 — 0-based position; assigned by <ui-stepper>
5
+ // @prop {string} state='upcoming' — upcoming | active | completed; assigned
6
+ // by <ui-stepper> from its `active` prop
7
+ // @prop {string} optionalText='' — small secondary line under the label
8
+ // @part indicator — the 24px circle
9
+ // @part label
10
+ // @vars see `t` below (`themeVars.names`)
11
+ //
12
+ // Presentation only: the circle shows the 1-based number (or a check when
13
+ // completed); <ui-stepper> owns index/state assignment and the connectors.
14
+
15
+ import { define, html, css, vars } from '@alacris/core';
16
+ import { sys } from '../tokens/sys.js';
17
+ import { base } from './base.js';
18
+ import './ui-icon.js';
19
+
20
+ const t = vars('ui-step', {
21
+ indicatorSize: '24px',
22
+ bg: sys.color.surfaceContainerHighest,
23
+ fg: sys.color.onSurfaceVariant,
24
+ activeBg: sys.color.primary,
25
+ activeFg: sys.color.onPrimary,
26
+ labelFg: sys.color.onSurfaceVariant,
27
+ labelFgActive: sys.color.onSurface,
28
+ font: sys.type.titleSm,
29
+ tracking: sys.tracking.titleSm,
30
+ optionalFont: sys.type.bodySm,
31
+ optionalTracking: sys.tracking.bodySm,
32
+ });
33
+
34
+ const styles = css`
35
+ :host {
36
+ display: flex;
37
+ flex-direction: column;
38
+ align-items: center;
39
+ gap: ${sys.space(2)};
40
+ padding-inline: ${sys.space(2)};
41
+ text-align: center;
42
+ }
43
+ .indicator {
44
+ display: grid;
45
+ place-items: center;
46
+ inline-size: ${t.indicatorSize};
47
+ block-size: ${t.indicatorSize};
48
+ border-radius: ${sys.radius.full};
49
+ background: ${t.bg};
50
+ color: ${t.fg};
51
+ font: ${sys.type.labelMd};
52
+ letter-spacing: ${sys.tracking.labelMd};
53
+ --ui-icon-size: 16px;
54
+ transition: background-color ${sys.duration.short4} ${sys.easing.standard},
55
+ color ${sys.duration.short4} ${sys.easing.standard};
56
+ }
57
+ .indicator.active, .indicator.completed {
58
+ background: ${t.activeBg};
59
+ color: ${t.activeFg};
60
+ }
61
+ .label {
62
+ font: ${t.font};
63
+ letter-spacing: ${t.tracking};
64
+ color: ${t.labelFg};
65
+ transition: color ${sys.duration.short4} ${sys.easing.standard};
66
+ }
67
+ .label.active { color: ${t.labelFgActive}; }
68
+ .optional {
69
+ font: ${t.optionalFont};
70
+ letter-spacing: ${t.optionalTracking};
71
+ color: ${t.labelFg};
72
+ }
73
+ `;
74
+
75
+ define('ui-step', {
76
+ props: { label: '', index: 0, state: 'upcoming', optionalText: '' },
77
+ styles: [base, styles],
78
+ setup({ label, index, state, optionalText }) {
79
+ return html`
80
+ <span part="indicator" class=${() => `indicator ${state()}`} aria-hidden="true">
81
+ ${() => (state() === 'completed'
82
+ ? html`<ui-icon name="check"></ui-icon>`
83
+ : html`${() => index() + 1}`)}
84
+ </span>
85
+ <span part="label" class=${() => `label ${state()}`}>${label}</span>
86
+ ${() => (optionalText() ? html`<span class="optional">${optionalText}</span>` : null)}`;
87
+ },
88
+ });
89
+
90
+ export const tag = 'ui-step';
91
+ export const themeVars = t;