@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.
- package/LICENSE +21 -0
- package/README.md +148 -1
- package/package.json +75 -3
- package/src/components/base.js +46 -0
- package/src/components/ui-accordion-item.js +157 -0
- package/src/components/ui-accordion.js +58 -0
- package/src/components/ui-alert.js +168 -0
- package/src/components/ui-app-bar.js +119 -0
- package/src/components/ui-autocomplete.js +371 -0
- package/src/components/ui-avatar.js +104 -0
- package/src/components/ui-backdrop.js +66 -0
- package/src/components/ui-badge.js +95 -0
- package/src/components/ui-bottom-app-bar.js +78 -0
- package/src/components/ui-bottom-nav.js +83 -0
- package/src/components/ui-breadcrumbs.js +100 -0
- package/src/components/ui-button-group.js +49 -0
- package/src/components/ui-button.js +143 -0
- package/src/components/ui-card.js +86 -0
- package/src/components/ui-carousel-item.js +53 -0
- package/src/components/ui-carousel.js +301 -0
- package/src/components/ui-checkbox.js +165 -0
- package/src/components/ui-chip-set.js +90 -0
- package/src/components/ui-chip.js +226 -0
- package/src/components/ui-container.js +59 -0
- package/src/components/ui-date-picker.js +774 -0
- package/src/components/ui-dialog.js +179 -0
- package/src/components/ui-divider.js +58 -0
- package/src/components/ui-drawer.js +169 -0
- package/src/components/ui-fab-menu.js +122 -0
- package/src/components/ui-fab.js +117 -0
- package/src/components/ui-icon-button.js +112 -0
- package/src/components/ui-icon.js +64 -0
- package/src/components/ui-list-item.js +166 -0
- package/src/components/ui-list.js +38 -0
- package/src/components/ui-loading-indicator.js +74 -0
- package/src/components/ui-menu-item.js +117 -0
- package/src/components/ui-menu.js +192 -0
- package/src/components/ui-nav-item.js +138 -0
- package/src/components/ui-nav-rail.js +111 -0
- package/src/components/ui-option.js +85 -0
- package/src/components/ui-pagination.js +173 -0
- package/src/components/ui-progress.js +100 -0
- package/src/components/ui-radio-group.js +104 -0
- package/src/components/ui-radio.js +143 -0
- package/src/components/ui-rating.js +126 -0
- package/src/components/ui-search.js +318 -0
- package/src/components/ui-select.js +418 -0
- package/src/components/ui-sheet.js +205 -0
- package/src/components/ui-side-sheet.js +221 -0
- package/src/components/ui-skeleton.js +89 -0
- package/src/components/ui-slider.js +266 -0
- package/src/components/ui-snackbar.js +233 -0
- package/src/components/ui-spinner.js +101 -0
- package/src/components/ui-split-button.js +158 -0
- package/src/components/ui-stack.js +37 -0
- package/src/components/ui-step.js +91 -0
- package/src/components/ui-stepper.js +86 -0
- package/src/components/ui-surface.js +51 -0
- package/src/components/ui-switch.js +153 -0
- package/src/components/ui-tab-panel.js +54 -0
- package/src/components/ui-tab.js +116 -0
- package/src/components/ui-table-footer.js +141 -0
- package/src/components/ui-table-toolbar.js +219 -0
- package/src/components/ui-table.js +735 -0
- package/src/components/ui-tabs.js +178 -0
- package/src/components/ui-text-field.js +319 -0
- package/src/components/ui-text.js +36 -0
- package/src/components/ui-time-picker.js +753 -0
- package/src/components/ui-toggle-button.js +170 -0
- package/src/components/ui-toggle-group.js +111 -0
- package/src/components/ui-toolbar.js +64 -0
- package/src/components/ui-tooltip.js +148 -0
- package/src/index.js +121 -0
- package/src/motion/animate.js +145 -0
- package/src/motion/flip.js +46 -0
- package/src/motion/index.js +4 -0
- package/src/motion/presence.js +104 -0
- package/src/motion/ripple.js +108 -0
- package/src/theme/apply-theme.js +142 -0
- package/src/theme/create-theme.js +70 -0
- package/src/theme/index.js +5 -0
- package/src/tokens/color.js +237 -0
- package/src/tokens/index.js +14 -0
- package/src/tokens/sys.js +76 -0
- package/src/tokens/system.js +114 -0
- package/src/tokens/typography.js +189 -0
- package/src/util/focus.js +89 -0
- package/src/util/form.js +69 -0
- package/src/util/icons.js +161 -0
- package/src/util/keys.js +110 -0
- package/src/util/position.js +102 -0
- package/src/util/table.js +230 -0
- package/types/elements.d.ts +78 -0
- 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;
|