@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.
- 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 +316 -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,119 @@
|
|
|
1
|
+
// <ui-app-bar> — the Material top app bar.
|
|
2
|
+
//
|
|
3
|
+
// <ui-app-bar variant="small" scroll-elevate>
|
|
4
|
+
// <ui-icon-button slot="navigation" icon="menu" label="Menu"></ui-icon-button>
|
|
5
|
+
// Page title
|
|
6
|
+
// <ui-icon-button slot="actions" icon="search" label="Search"></ui-icon-button>
|
|
7
|
+
// </ui-app-bar>
|
|
8
|
+
//
|
|
9
|
+
// The host is position:sticky at the top. `elevated` forces the on-scroll
|
|
10
|
+
// container tint + shadow; `scrollElevate` listens to window scroll and
|
|
11
|
+
// applies it automatically once the page is scrolled (listener removed when
|
|
12
|
+
// the prop turns off or the element leaves the document).
|
|
13
|
+
//
|
|
14
|
+
// @prop {string} variant='small' — small | center | medium | large
|
|
15
|
+
// @prop {boolean} elevated=false — force the scrolled container/elevation
|
|
16
|
+
// @prop {boolean} scrollElevate=false — auto-elevate once window is scrolled
|
|
17
|
+
// @slot navigation — leading icon button
|
|
18
|
+
// @slot (default) — title text
|
|
19
|
+
// @slot actions — trailing icon buttons
|
|
20
|
+
// @part bar — the header surface
|
|
21
|
+
// @vars see `t` below (`themeVars.names`)
|
|
22
|
+
|
|
23
|
+
import { define, html, css, vars, signal, effect, computed } from '@alacris/core';
|
|
24
|
+
import { sys } from '../tokens/sys.js';
|
|
25
|
+
import { base } from './base.js';
|
|
26
|
+
|
|
27
|
+
const t = vars('ui-app-bar', {
|
|
28
|
+
bg: sys.color.surface,
|
|
29
|
+
scrolledBg: sys.color.surfaceContainer,
|
|
30
|
+
fg: sys.color.onSurface,
|
|
31
|
+
heightSmall: '64px',
|
|
32
|
+
heightMedium: '112px',
|
|
33
|
+
heightLarge: '152px',
|
|
34
|
+
font: sys.type.titleLg,
|
|
35
|
+
tracking: sys.tracking.titleLg,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const styles = css`
|
|
39
|
+
:host {
|
|
40
|
+
display: block;
|
|
41
|
+
position: sticky;
|
|
42
|
+
inset-block-start: 0;
|
|
43
|
+
z-index: ${sys.z.appBar};
|
|
44
|
+
}
|
|
45
|
+
.bar {
|
|
46
|
+
display: grid;
|
|
47
|
+
grid-template-columns: auto 1fr auto;
|
|
48
|
+
align-items: center;
|
|
49
|
+
column-gap: ${sys.space(1)};
|
|
50
|
+
padding-inline: ${sys.space(1)};
|
|
51
|
+
block-size: ${t.heightSmall};
|
|
52
|
+
background: ${t.bg};
|
|
53
|
+
color: ${t.fg};
|
|
54
|
+
box-shadow: none;
|
|
55
|
+
transition: background-color ${sys.duration.short4} ${sys.easing.standard},
|
|
56
|
+
box-shadow ${sys.duration.short4} ${sys.easing.standard};
|
|
57
|
+
}
|
|
58
|
+
.raised { background: ${t.scrolledBg}; box-shadow: ${sys.elevation[2]}; }
|
|
59
|
+
.title {
|
|
60
|
+
font: ${t.font};
|
|
61
|
+
letter-spacing: ${t.tracking};
|
|
62
|
+
padding-inline: ${sys.space(1)};
|
|
63
|
+
white-space: nowrap;
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
text-overflow: ellipsis;
|
|
66
|
+
}
|
|
67
|
+
.center .title { text-align: center; }
|
|
68
|
+
.nav, .actions { display: inline-flex; align-items: center; gap: ${sys.space(1)}; }
|
|
69
|
+
.medium, .large {
|
|
70
|
+
grid-template-rows: ${t.heightSmall} 1fr;
|
|
71
|
+
grid-template-areas: 'nav . actions' 'title title title';
|
|
72
|
+
align-items: start;
|
|
73
|
+
}
|
|
74
|
+
.medium { block-size: ${t.heightMedium}; }
|
|
75
|
+
.large { block-size: ${t.heightLarge}; }
|
|
76
|
+
.medium .nav, .large .nav { grid-area: nav; align-self: center; }
|
|
77
|
+
.medium .actions, .large .actions { grid-area: actions; align-self: center; }
|
|
78
|
+
.medium .title, .large .title {
|
|
79
|
+
grid-area: title;
|
|
80
|
+
align-self: end;
|
|
81
|
+
padding-block-end: ${sys.space(4)};
|
|
82
|
+
padding-inline: ${sys.space(3)};
|
|
83
|
+
}
|
|
84
|
+
.medium .title { font: ${sys.type.headlineSm}; letter-spacing: ${sys.tracking.headlineSm}; }
|
|
85
|
+
.large .title { font: ${sys.type.headlineMd}; letter-spacing: ${sys.tracking.headlineMd}; }
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
define('ui-app-bar', {
|
|
89
|
+
props: { variant: 'small', elevated: false, scrollElevate: false },
|
|
90
|
+
styles: [base, styles],
|
|
91
|
+
setup({ variant, elevated, scrollElevate }, host) {
|
|
92
|
+
const scrolled = signal(false);
|
|
93
|
+
|
|
94
|
+
effect(() => {
|
|
95
|
+
if (!scrollElevate()) {
|
|
96
|
+
scrolled.set(false);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const onScroll = () => scrolled.set((window.scrollY || 0) > 0);
|
|
100
|
+
onScroll();
|
|
101
|
+
window.addEventListener('scroll', onScroll, { passive: true });
|
|
102
|
+
return () => window.removeEventListener('scroll', onScroll);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const cls = computed(
|
|
106
|
+
() => `bar ${variant()}${elevated() || scrolled() ? ' raised' : ''}`
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
return html`
|
|
110
|
+
<header class=${cls} part="bar">
|
|
111
|
+
<div class="nav"><slot name="navigation"></slot></div>
|
|
112
|
+
<div class="title"><slot></slot></div>
|
|
113
|
+
<div class="actions"><slot name="actions"></slot></div>
|
|
114
|
+
</header>`;
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
export const tag = 'ui-app-bar';
|
|
119
|
+
export const themeVars = t;
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
// <ui-autocomplete> — a combobox with a filtering text input.
|
|
2
|
+
//
|
|
3
|
+
// <ui-autocomplete label="Fruit" options=${['Apple', 'Banana']}
|
|
4
|
+
// @change=${(e) => pick(e.detail.value)}></ui-autocomplete>
|
|
5
|
+
//
|
|
6
|
+
// Typing filters the options case-insensitively and opens a listbox panel
|
|
7
|
+
// while there are matches. ArrowUp/Down move the active option, Enter commits
|
|
8
|
+
// the active option (or, with `freeSolo`, the raw text), Escape closes, blur
|
|
9
|
+
// commits an exact label match — or the raw text when `freeSolo` — and
|
|
10
|
+
// otherwise reverts to the last committed value.
|
|
11
|
+
//
|
|
12
|
+
// @prop {string} label=''
|
|
13
|
+
// @prop {string} value='' — the committed value
|
|
14
|
+
// @prop {Array} options=[] — strings or { value, label } objects
|
|
15
|
+
// (JSON attribute or property)
|
|
16
|
+
// @prop {string} variant='filled' — filled | outlined
|
|
17
|
+
// @prop {boolean} disabled=false
|
|
18
|
+
// @prop {boolean} required=false
|
|
19
|
+
// @prop {string} name='' — form participation
|
|
20
|
+
// @prop {string} placeholder=''
|
|
21
|
+
// @prop {boolean} freeSolo=false — allow values not present in options
|
|
22
|
+
// @event input — every keystroke; detail: { value } (the raw text)
|
|
23
|
+
// @event change — committed; detail: { value }
|
|
24
|
+
// @part field, input, label, panel
|
|
25
|
+
// @vars see `t` below (`themeVars.names`)
|
|
26
|
+
|
|
27
|
+
import { define, html, css, vars, computed, signal, effect, onCleanup, each } from '@alacris/core';
|
|
28
|
+
import { sys } from '../tokens/sys.js';
|
|
29
|
+
import { base } from './base.js';
|
|
30
|
+
import { formBind } from '../util/form.js';
|
|
31
|
+
import { presence } from '../motion/presence.js';
|
|
32
|
+
import { fx } from '../motion/animate.js';
|
|
33
|
+
import { autoUpdate } from '../util/position.js';
|
|
34
|
+
import './ui-icon.js';
|
|
35
|
+
|
|
36
|
+
const t = vars('ui-autocomplete', {
|
|
37
|
+
bg: sys.color.surfaceContainerHighest,
|
|
38
|
+
fg: sys.color.onSurface,
|
|
39
|
+
labelFg: sys.color.onSurfaceVariant,
|
|
40
|
+
accent: sys.color.primary,
|
|
41
|
+
outlineColor: sys.color.outline,
|
|
42
|
+
radius: sys.radius.xs,
|
|
43
|
+
font: sys.type.bodyLg,
|
|
44
|
+
height: '56px',
|
|
45
|
+
panelBg: sys.color.surfaceContainer,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const styles = css`
|
|
49
|
+
:host { display: block; inline-size: 240px; }
|
|
50
|
+
.root { display: block; position: relative; }
|
|
51
|
+
.field {
|
|
52
|
+
position: relative;
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
gap: ${sys.space(2)};
|
|
56
|
+
min-block-size: calc(${t.height} + var(--ui-density, 0) * 4px);
|
|
57
|
+
padding-inline: ${sys.space(4)};
|
|
58
|
+
border-radius: ${t.radius};
|
|
59
|
+
font: ${t.font};
|
|
60
|
+
letter-spacing: ${sys.tracking.bodyLg};
|
|
61
|
+
color: ${t.fg};
|
|
62
|
+
cursor: text;
|
|
63
|
+
--ui-icon-size: 1.5rem;
|
|
64
|
+
}
|
|
65
|
+
.filled .field {
|
|
66
|
+
background: ${t.bg};
|
|
67
|
+
border-start-start-radius: ${t.radius};
|
|
68
|
+
border-start-end-radius: ${t.radius};
|
|
69
|
+
border-end-start-radius: 0;
|
|
70
|
+
border-end-end-radius: 0;
|
|
71
|
+
}
|
|
72
|
+
.filled .field::after {
|
|
73
|
+
content: '';
|
|
74
|
+
position: absolute;
|
|
75
|
+
inset-inline: 0;
|
|
76
|
+
inset-block-end: 0;
|
|
77
|
+
block-size: 1px;
|
|
78
|
+
background: ${sys.color.onSurfaceVariant};
|
|
79
|
+
transition: block-size ${sys.duration.short2} ${sys.easing.standard},
|
|
80
|
+
background-color ${sys.duration.short2} ${sys.easing.standard};
|
|
81
|
+
}
|
|
82
|
+
.filled.focused .field::after { block-size: 2px; background: ${t.accent}; }
|
|
83
|
+
.filled .field::before {
|
|
84
|
+
content: '';
|
|
85
|
+
position: absolute; inset: 0; pointer-events: none;
|
|
86
|
+
background: ${sys.color.onSurface};
|
|
87
|
+
opacity: 0;
|
|
88
|
+
border-radius: inherit;
|
|
89
|
+
transition: opacity ${sys.duration.short2} ${sys.easing.standard};
|
|
90
|
+
}
|
|
91
|
+
.filled:hover:not(.focused):not(.disabled) .field::before {
|
|
92
|
+
opacity: ${sys.state.hover};
|
|
93
|
+
}
|
|
94
|
+
.filled:hover:not(.focused):not(.disabled) .field::after {
|
|
95
|
+
background: ${sys.color.onSurface};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
fieldset {
|
|
99
|
+
position: absolute;
|
|
100
|
+
inset: -6px 0 0;
|
|
101
|
+
margin: 0;
|
|
102
|
+
padding: 0 calc(${sys.space(3)} - 2px);
|
|
103
|
+
min-inline-size: 0;
|
|
104
|
+
box-sizing: border-box;
|
|
105
|
+
appearance: none;
|
|
106
|
+
border: 1px solid ${t.outlineColor};
|
|
107
|
+
border-radius: ${t.radius};
|
|
108
|
+
pointer-events: none;
|
|
109
|
+
transition: border-color ${sys.duration.short2} ${sys.easing.standard},
|
|
110
|
+
border-width ${sys.duration.short2} ${sys.easing.standard};
|
|
111
|
+
}
|
|
112
|
+
legend {
|
|
113
|
+
float: unset;
|
|
114
|
+
display: block;
|
|
115
|
+
width: max-content;
|
|
116
|
+
padding: 0;
|
|
117
|
+
margin: 0;
|
|
118
|
+
margin-inline-start: 0;
|
|
119
|
+
white-space: nowrap;
|
|
120
|
+
overflow: hidden;
|
|
121
|
+
font: ${t.font};
|
|
122
|
+
font-size: 0.75em;
|
|
123
|
+
letter-spacing: calc(${sys.tracking.bodyLg} * 0.75);
|
|
124
|
+
visibility: hidden;
|
|
125
|
+
max-inline-size: 0.01px;
|
|
126
|
+
height: 12px;
|
|
127
|
+
line-height: 12px;
|
|
128
|
+
transition: max-inline-size ${sys.duration.short2} ${sys.easing.standard};
|
|
129
|
+
}
|
|
130
|
+
legend span {
|
|
131
|
+
padding-inline: ${sys.space(1)} calc(${sys.space(1)} - 0.5px);
|
|
132
|
+
display: inline-block;
|
|
133
|
+
opacity: 0;
|
|
134
|
+
visibility: visible;
|
|
135
|
+
}
|
|
136
|
+
.outlined.floating legend {
|
|
137
|
+
max-inline-size: 100%;
|
|
138
|
+
transition: max-inline-size ${sys.duration.short3} ${sys.easing.standard};
|
|
139
|
+
}
|
|
140
|
+
.outlined.focused fieldset { border-width: 2px; border-color: ${t.accent}; }
|
|
141
|
+
.root:hover:not(.focused) fieldset { border-color: ${sys.color.onSurface}; }
|
|
142
|
+
|
|
143
|
+
.label {
|
|
144
|
+
position: absolute;
|
|
145
|
+
inset-inline-start: ${sys.space(4)};
|
|
146
|
+
inset-block-start: 50%;
|
|
147
|
+
translate: 0 -50%;
|
|
148
|
+
color: ${t.labelFg};
|
|
149
|
+
pointer-events: none;
|
|
150
|
+
transform-origin: 0 50%;
|
|
151
|
+
transition: translate ${sys.duration.short3} ${sys.easing.standard},
|
|
152
|
+
scale ${sys.duration.short3} ${sys.easing.standard},
|
|
153
|
+
color ${sys.duration.short2} ${sys.easing.standard};
|
|
154
|
+
}
|
|
155
|
+
.filled.floating .label { translate: 0 calc(-50% - 16px); scale: 0.75; }
|
|
156
|
+
.outlined.floating .label {
|
|
157
|
+
translate: 0 calc(-50% - (${t.height} + var(--ui-density, 0) * 4px) / 2);
|
|
158
|
+
scale: 0.75;
|
|
159
|
+
}
|
|
160
|
+
.focused .label { color: ${t.accent}; }
|
|
161
|
+
|
|
162
|
+
input {
|
|
163
|
+
flex: 1;
|
|
164
|
+
min-inline-size: 0;
|
|
165
|
+
margin: 0;
|
|
166
|
+
border: none;
|
|
167
|
+
outline: none;
|
|
168
|
+
appearance: none;
|
|
169
|
+
background: transparent;
|
|
170
|
+
font: inherit;
|
|
171
|
+
letter-spacing: inherit;
|
|
172
|
+
color: inherit;
|
|
173
|
+
padding: 0;
|
|
174
|
+
}
|
|
175
|
+
.filled.has-label input { padding-block-start: 18px; }
|
|
176
|
+
input::placeholder { color: ${t.labelFg}; opacity: 0; transition: opacity ${sys.duration.short2} linear; }
|
|
177
|
+
.floating input::placeholder { opacity: 1; }
|
|
178
|
+
|
|
179
|
+
.arrow {
|
|
180
|
+
color: ${t.labelFg};
|
|
181
|
+
transition: rotate ${sys.duration.short3} ${sys.easing.standard};
|
|
182
|
+
}
|
|
183
|
+
.expanded .arrow { rotate: 180deg; }
|
|
184
|
+
|
|
185
|
+
.panel {
|
|
186
|
+
position: fixed;
|
|
187
|
+
z-index: ${sys.z.modal};
|
|
188
|
+
min-inline-size: 112px;
|
|
189
|
+
max-block-size: 40vh;
|
|
190
|
+
overflow: auto;
|
|
191
|
+
padding-block: ${sys.space(2)};
|
|
192
|
+
background: ${t.panelBg};
|
|
193
|
+
border-radius: ${sys.radius.xs};
|
|
194
|
+
box-shadow: ${sys.elevation[2]};
|
|
195
|
+
transform-origin: top center;
|
|
196
|
+
}
|
|
197
|
+
.opt {
|
|
198
|
+
position: relative;
|
|
199
|
+
isolation: isolate;
|
|
200
|
+
display: flex;
|
|
201
|
+
align-items: center;
|
|
202
|
+
min-block-size: calc(48px + var(--ui-density, 0) * 4px);
|
|
203
|
+
padding-inline: ${sys.space(4)};
|
|
204
|
+
font: ${t.font};
|
|
205
|
+
letter-spacing: ${sys.tracking.bodyLg};
|
|
206
|
+
color: ${t.fg};
|
|
207
|
+
cursor: pointer;
|
|
208
|
+
user-select: none;
|
|
209
|
+
}
|
|
210
|
+
.opt .layer {
|
|
211
|
+
position: absolute; inset: 0; z-index: -1;
|
|
212
|
+
background: currentColor; opacity: 0;
|
|
213
|
+
transition: opacity ${sys.duration.short2} ${sys.easing.standard};
|
|
214
|
+
}
|
|
215
|
+
.opt:hover .layer { opacity: ${sys.state.hover}; }
|
|
216
|
+
.opt.active .layer { opacity: ${sys.state.focus}; }
|
|
217
|
+
.opt:active .layer { opacity: ${sys.state.pressed}; }
|
|
218
|
+
.opt[aria-selected="true"] { background: ${sys.color.secondaryContainer}; color: ${sys.color.onSecondaryContainer}; }
|
|
219
|
+
|
|
220
|
+
.disabled { opacity: ${sys.state.disabledContent}; pointer-events: none; }
|
|
221
|
+
`;
|
|
222
|
+
|
|
223
|
+
define('ui-autocomplete', {
|
|
224
|
+
formAssociated: true,
|
|
225
|
+
props: {
|
|
226
|
+
label: '', value: '', options: [], variant: 'filled', disabled: false,
|
|
227
|
+
required: false, name: '', placeholder: '', freeSolo: false,
|
|
228
|
+
},
|
|
229
|
+
styles: [base, styles],
|
|
230
|
+
setup(p, host) {
|
|
231
|
+
const { label, value, options, variant, disabled, required, name, placeholder, freeSolo } = p;
|
|
232
|
+
formBind(host, { name, value, disabled });
|
|
233
|
+
|
|
234
|
+
const query = signal('');
|
|
235
|
+
const focused = signal(false);
|
|
236
|
+
const open = signal(false);
|
|
237
|
+
const activeIndex = signal(-1);
|
|
238
|
+
let fieldEl = null;
|
|
239
|
+
let stopAuto = null;
|
|
240
|
+
|
|
241
|
+
const opts = computed(() => (options() || []).map((o) =>
|
|
242
|
+
typeof o === 'string'
|
|
243
|
+
? { value: o, label: o }
|
|
244
|
+
: { value: String(o.value), label: o.label != null ? String(o.label) : String(o.value) }));
|
|
245
|
+
|
|
246
|
+
const labelFor = (v) => opts().find((o) => o.value === v)?.label ?? v;
|
|
247
|
+
|
|
248
|
+
// The committed value drives the visible text (also picks up label text
|
|
249
|
+
// arriving late through `options`).
|
|
250
|
+
effect(() => query.set(labelFor(value())));
|
|
251
|
+
|
|
252
|
+
const filtered = computed(() => {
|
|
253
|
+
const q = query().trim().toLowerCase();
|
|
254
|
+
return q ? opts().filter((o) => o.label.toLowerCase().includes(q)) : opts();
|
|
255
|
+
});
|
|
256
|
+
// New matches invalidate the keyboard-active option.
|
|
257
|
+
effect(() => { filtered(); activeIndex.set(-1); });
|
|
258
|
+
|
|
259
|
+
const activeOpt = computed(() => filtered()[activeIndex()] ?? null);
|
|
260
|
+
const showPanel = computed(() => open() && !disabled() && filtered().length > 0);
|
|
261
|
+
|
|
262
|
+
const floating = computed(() => focused() || query() !== '' || placeholder() !== '');
|
|
263
|
+
const cls = computed(() =>
|
|
264
|
+
['root', variant(), floating() && 'floating', focused() && 'focused',
|
|
265
|
+
showPanel() && 'expanded', disabled() && 'disabled', label() && 'has-label']
|
|
266
|
+
.filter(Boolean).join(' '));
|
|
267
|
+
|
|
268
|
+
const commit = (v) => {
|
|
269
|
+
const text = labelFor(v);
|
|
270
|
+
if (v !== value()) {
|
|
271
|
+
value.set(v);
|
|
272
|
+
host.emit('change', { value: v });
|
|
273
|
+
}
|
|
274
|
+
query.set(text);
|
|
275
|
+
open.set(false);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const onInput = (e) => {
|
|
279
|
+
e.stopPropagation();
|
|
280
|
+
query.set(e.target.value);
|
|
281
|
+
open.set(true);
|
|
282
|
+
host.emit('input', { value: e.target.value });
|
|
283
|
+
};
|
|
284
|
+
const move = (delta) => {
|
|
285
|
+
const list = filtered();
|
|
286
|
+
if (!list.length) return;
|
|
287
|
+
const i = activeIndex();
|
|
288
|
+
activeIndex.set(i < 0 ? (delta > 0 ? 0 : list.length - 1) : (i + delta + list.length) % list.length);
|
|
289
|
+
};
|
|
290
|
+
const onKeydown = (e) => {
|
|
291
|
+
switch (e.key) {
|
|
292
|
+
case 'ArrowDown': e.preventDefault(); if (!open()) open.set(true); move(1); break;
|
|
293
|
+
case 'ArrowUp': e.preventDefault(); if (!open()) open.set(true); move(-1); break;
|
|
294
|
+
case 'Enter':
|
|
295
|
+
if (showPanel() && activeOpt()) { e.preventDefault(); commit(activeOpt().value); }
|
|
296
|
+
else if (freeSolo()) commit(query().trim());
|
|
297
|
+
break;
|
|
298
|
+
case 'Escape': open.set(false); break;
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
const onFocus = () => { focused.set(true); open.set(true); };
|
|
302
|
+
const onBlur = () => {
|
|
303
|
+
focused.set(false);
|
|
304
|
+
open.set(false);
|
|
305
|
+
const raw = query().trim();
|
|
306
|
+
if (freeSolo()) { commit(raw); return; }
|
|
307
|
+
const match = opts().find((o) => o.label.toLowerCase() === raw.toLowerCase());
|
|
308
|
+
if (match) commit(match.value);
|
|
309
|
+
else query.set(labelFor(value())); // revert to the committed value
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
effect(() => {
|
|
313
|
+
if (!showPanel() && stopAuto) { stopAuto(); stopAuto = null; }
|
|
314
|
+
});
|
|
315
|
+
onCleanup(() => stopAuto?.());
|
|
316
|
+
|
|
317
|
+
const panelRef = (el) => {
|
|
318
|
+
stopAuto?.();
|
|
319
|
+
stopAuto = autoUpdate(el, fieldEl, { placement: 'bottom-start', matchWidth: true, offset: 4 });
|
|
320
|
+
};
|
|
321
|
+
const panelView = () => html`
|
|
322
|
+
<div class="panel" part="panel" role="listbox" id="panel"
|
|
323
|
+
aria-label=${() => label() || null} ref=${panelRef}>
|
|
324
|
+
${each(
|
|
325
|
+
() => filtered(),
|
|
326
|
+
(o) => html`
|
|
327
|
+
<div role="option"
|
|
328
|
+
class=${() => ({ opt: true, active: activeOpt()?.value === o().value })}
|
|
329
|
+
id=${() => (activeOpt()?.value === o().value ? 'ui-ac-active' : null)}
|
|
330
|
+
aria-selected=${() => String(value() === o().value)}
|
|
331
|
+
@pointerdown=${(e) => e.preventDefault()}
|
|
332
|
+
@click=${() => commit(o().value)}>
|
|
333
|
+
<span class="layer" aria-hidden="true"></span>
|
|
334
|
+
${() => o().label}
|
|
335
|
+
</div>`,
|
|
336
|
+
(o) => o.value,
|
|
337
|
+
)}
|
|
338
|
+
</div>`;
|
|
339
|
+
|
|
340
|
+
return html`
|
|
341
|
+
<div class=${cls}>
|
|
342
|
+
<label class="field" part="field" ref=${(el) => (fieldEl = el)}>
|
|
343
|
+
${() => (variant() === 'outlined'
|
|
344
|
+
? html`<fieldset aria-hidden="true"><legend><span>${label}${() => (required() ? ' *' : '')}</span></legend></fieldset>`
|
|
345
|
+
: null)}
|
|
346
|
+
${() => (label() ? html`<span class="label" part="label">${label}${() => (required() ? ' *' : '')}</span>` : null)}
|
|
347
|
+
<input part="input" role="combobox" aria-autocomplete="list"
|
|
348
|
+
aria-haspopup="listbox" aria-controls="panel"
|
|
349
|
+
aria-expanded=${() => String(showPanel())}
|
|
350
|
+
aria-activedescendant=${() => (showPanel() && activeOpt() ? 'ui-ac-active' : null)}
|
|
351
|
+
.value=${query}
|
|
352
|
+
placeholder=${() => placeholder() || null}
|
|
353
|
+
?disabled=${disabled} ?required=${required}
|
|
354
|
+
@input=${onInput} @change=${(e) => e.stopPropagation()} @keydown=${onKeydown}
|
|
355
|
+
@focus=${onFocus} @blur=${onBlur}>
|
|
356
|
+
<ui-icon class="arrow" name="arrow-drop-down"></ui-icon>
|
|
357
|
+
</label>
|
|
358
|
+
${presence(showPanel, panelView, {
|
|
359
|
+
enter: fx.scaleIn,
|
|
360
|
+
exit: fx.scaleOut,
|
|
361
|
+
enterDuration: 'short4',
|
|
362
|
+
exitDuration: 'short2',
|
|
363
|
+
enterEasing: 'emphasizedDecelerate',
|
|
364
|
+
exitEasing: 'emphasizedAccelerate',
|
|
365
|
+
})}
|
|
366
|
+
</div>`;
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
export const tag = 'ui-autocomplete';
|
|
371
|
+
export const themeVars = t;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// <ui-avatar> — a circular avatar: image, initials, or icon.
|
|
2
|
+
//
|
|
3
|
+
// Fallback chain: `src` image → initials from `name` (first + last word) →
|
|
4
|
+
// `icon` → slotted content. A broken image (error event) falls back to the
|
|
5
|
+
// initials automatically. Initials scale with the avatar (~40% of its size).
|
|
6
|
+
//
|
|
7
|
+
// @prop {string} src='' — image URL (object-fit: cover)
|
|
8
|
+
// @prop {string} name='' — person's name; drives initials and the aria-label fallback
|
|
9
|
+
// @prop {string} icon='' — registry icon shown when there is no image and no name
|
|
10
|
+
// @prop {string} size='' — CSS length overriding the 40px default (sets --ui-avatar-size)
|
|
11
|
+
// @prop {string} label='' — accessible name; falls back to `name`, else decorative
|
|
12
|
+
// @slot (default) — custom content when src/name/icon are all empty
|
|
13
|
+
// @vars --ui-avatar-size, --ui-avatar-bg, --ui-avatar-fg, --ui-avatar-radius
|
|
14
|
+
|
|
15
|
+
import { define, html, css, vars, computed, effect, signal } 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-avatar', {
|
|
21
|
+
size: '40px',
|
|
22
|
+
bg: sys.color.primaryContainer,
|
|
23
|
+
fg: sys.color.onPrimaryContainer,
|
|
24
|
+
radius: sys.radius.full,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const styles = css`
|
|
28
|
+
:host {
|
|
29
|
+
display: inline-flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
justify-content: center;
|
|
32
|
+
flex: none;
|
|
33
|
+
inline-size: ${t.size};
|
|
34
|
+
block-size: ${t.size};
|
|
35
|
+
border-radius: ${t.radius};
|
|
36
|
+
background: ${t.bg};
|
|
37
|
+
color: ${t.fg};
|
|
38
|
+
overflow: hidden;
|
|
39
|
+
vertical-align: middle;
|
|
40
|
+
user-select: none;
|
|
41
|
+
/* em-based scaling: initials are 1em of this. */
|
|
42
|
+
font-size: calc(${t.size} * 0.4);
|
|
43
|
+
--ui-icon-size: calc(${t.size} * 0.6);
|
|
44
|
+
}
|
|
45
|
+
img {
|
|
46
|
+
inline-size: 100%;
|
|
47
|
+
block-size: 100%;
|
|
48
|
+
object-fit: cover;
|
|
49
|
+
display: block;
|
|
50
|
+
}
|
|
51
|
+
.initials {
|
|
52
|
+
font: ${sys.type.titleMd};
|
|
53
|
+
font-size: 1em;
|
|
54
|
+
letter-spacing: normal;
|
|
55
|
+
line-height: 1;
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
define('ui-avatar', {
|
|
60
|
+
props: { src: '', name: '', icon: '', size: '', label: '' },
|
|
61
|
+
styles: [base, styles],
|
|
62
|
+
setup({ src, name, icon, size, label }, host) {
|
|
63
|
+
const broken = signal(false);
|
|
64
|
+
effect(() => { src(); broken.set(false); }); // new source, new chance
|
|
65
|
+
|
|
66
|
+
const initials = computed(() => {
|
|
67
|
+
const words = name().trim().split(/\s+/).filter(Boolean);
|
|
68
|
+
if (!words.length) return '';
|
|
69
|
+
const first = words[0][0];
|
|
70
|
+
const last = words.length > 1 ? words[words.length - 1][0] : '';
|
|
71
|
+
return (first + last).toUpperCase();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
effect(() => {
|
|
75
|
+
const l = label() || name();
|
|
76
|
+
if (l) {
|
|
77
|
+
host.setAttribute('role', 'img');
|
|
78
|
+
host.setAttribute('aria-label', l);
|
|
79
|
+
host.removeAttribute('aria-hidden');
|
|
80
|
+
} else {
|
|
81
|
+
host.removeAttribute('role');
|
|
82
|
+
host.removeAttribute('aria-label');
|
|
83
|
+
host.setAttribute('aria-hidden', 'true');
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
effect(() => {
|
|
88
|
+
if (size()) host.style.setProperty('--ui-avatar-size', size());
|
|
89
|
+
else host.style.removeProperty('--ui-avatar-size');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return html`${() => {
|
|
93
|
+
if (src() && !broken()) {
|
|
94
|
+
return html`<img src=${src} alt="" @error=${() => broken.set(true)}>`;
|
|
95
|
+
}
|
|
96
|
+
if (initials()) return html`<span class="initials" aria-hidden="true">${initials}</span>`;
|
|
97
|
+
if (icon()) return html`<ui-icon name=${icon}></ui-icon>`;
|
|
98
|
+
return html`<slot></slot>`;
|
|
99
|
+
}}`;
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
export const tag = 'ui-avatar';
|
|
104
|
+
export const themeVars = t;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// <ui-backdrop> — a full-screen scrim behind custom overlays.
|
|
2
|
+
//
|
|
3
|
+
// <ui-backdrop open=${open} @close=${() => open(false)}></ui-backdrop>
|
|
4
|
+
//
|
|
5
|
+
// Fades in/out, locks page scroll while open, and requests closing by
|
|
6
|
+
// emitting `close` when the scrim is clicked — the PARENT owns the state and
|
|
7
|
+
// flips the signal.
|
|
8
|
+
//
|
|
9
|
+
// @prop {boolean} open=false
|
|
10
|
+
// @prop {boolean} invisible=false — transparent scrim (still catches clicks)
|
|
11
|
+
// @event close — scrim clicked; detail: { reason: 'scrim' }
|
|
12
|
+
// @part scrim
|
|
13
|
+
// @vars see `t` below (`themeVars.names`)
|
|
14
|
+
|
|
15
|
+
import { define, html, css, vars, effect, onCleanup } from '@alacris/core';
|
|
16
|
+
import { sys } from '../tokens/sys.js';
|
|
17
|
+
import { base } from './base.js';
|
|
18
|
+
import { presence } from '../motion/presence.js';
|
|
19
|
+
import { fx } from '../motion/animate.js';
|
|
20
|
+
import { scrollLock } from '../util/focus.js';
|
|
21
|
+
|
|
22
|
+
const t = vars('ui-backdrop', {
|
|
23
|
+
bg: `color-mix(in srgb, ${sys.color.scrim} 32%, transparent)`,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const styles = css`
|
|
27
|
+
:host { display: contents; }
|
|
28
|
+
.scrim {
|
|
29
|
+
position: fixed;
|
|
30
|
+
inset: 0;
|
|
31
|
+
z-index: ${sys.z.modal};
|
|
32
|
+
background: ${t.bg};
|
|
33
|
+
}
|
|
34
|
+
.invisible { background: transparent; }
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
define('ui-backdrop', {
|
|
38
|
+
props: { open: false, invisible: false },
|
|
39
|
+
styles: [base, styles],
|
|
40
|
+
setup({ open, invisible }, host) {
|
|
41
|
+
let unlock = null;
|
|
42
|
+
effect(() => {
|
|
43
|
+
if (open()) {
|
|
44
|
+
if (!unlock) unlock = scrollLock();
|
|
45
|
+
} else {
|
|
46
|
+
unlock?.();
|
|
47
|
+
unlock = null;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
onCleanup(() => unlock?.());
|
|
51
|
+
|
|
52
|
+
const view = () => html`
|
|
53
|
+
<div part="scrim" aria-hidden="true"
|
|
54
|
+
class=${() => `scrim${invisible() ? ' invisible' : ''}`}
|
|
55
|
+
@click=${() => host.emit('close', { reason: 'scrim' })}></div>`;
|
|
56
|
+
|
|
57
|
+
return html`${presence(open, view, {
|
|
58
|
+
enter: fx.fadeIn,
|
|
59
|
+
exit: fx.fadeOut,
|
|
60
|
+
exitDuration: 'short3',
|
|
61
|
+
})}`;
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export const tag = 'ui-backdrop';
|
|
66
|
+
export const themeVars = t;
|