@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,318 @@
|
|
|
1
|
+
// <ui-search> — Material search bar.
|
|
2
|
+
//
|
|
3
|
+
// <ui-search label="Search mail" value=${q}
|
|
4
|
+
// @input=${(e) => q(e.detail.value)}
|
|
5
|
+
// @submit=${(e) => run(e.detail.value)}></ui-search>
|
|
6
|
+
//
|
|
7
|
+
// A pill-shaped field with a leading search icon, a trailing clear control
|
|
8
|
+
// while there is text, and an optional trailing slot (avatar, voice, …).
|
|
9
|
+
// Enter emits `submit`. The field chrome is the focus indicator — the inner
|
|
10
|
+
// input has no extra outline.
|
|
11
|
+
//
|
|
12
|
+
// `presentation="view"` opens a search view: a back control and a suggestions
|
|
13
|
+
// list (the default slot) while open. The list overlays the page — it does
|
|
14
|
+
// not grow the layout. Typing a query opens the view; clearing the field
|
|
15
|
+
// (keyboard or the clear button) closes it back to the pill bar. Focus on an
|
|
16
|
+
// empty view still shows recents, but a clear while focused stays on the bar.
|
|
17
|
+
// The open surface is one extra-large rounded container (bar + overlay list)
|
|
18
|
+
// with a divider between the field and the list.
|
|
19
|
+
//
|
|
20
|
+
// @prop {string} label='Search' — accessible name (and the floating placeholder)
|
|
21
|
+
// @prop {string} value=''
|
|
22
|
+
// @prop {string} placeholder='' — shown in the field; falls back to `label`
|
|
23
|
+
// @prop {string} presentation='bar' — bar | view
|
|
24
|
+
// @prop {boolean} open=false — view / suggestions visibility
|
|
25
|
+
// @prop {boolean} disabled=false
|
|
26
|
+
// @prop {string} name='' — form participation
|
|
27
|
+
// @event input — every keystroke; detail: { value }
|
|
28
|
+
// @event change — committed (blur/Enter); detail: { value }
|
|
29
|
+
// @event submit — Enter pressed; detail: { value }
|
|
30
|
+
// @event clear — the clear affordance was used
|
|
31
|
+
// @event open — suggestions visible (after the enter animation)
|
|
32
|
+
// @event close — suggestions removed (after the exit animation)
|
|
33
|
+
// @slot leading — replaces the search icon
|
|
34
|
+
// @slot trailing — after the clear button (avatar, extra actions)
|
|
35
|
+
// @slot (default) — suggestion rows (ui-list-item, …). The panel is a list
|
|
36
|
+
// so those rows keep role="listitem"; it is not a listbox.
|
|
37
|
+
// @part bar, input, panel
|
|
38
|
+
// @vars see `t` below (`themeVars.names`)
|
|
39
|
+
|
|
40
|
+
import { define, html, css, vars, computed, signal } from '@alacris/core';
|
|
41
|
+
import { sys } from '../tokens/sys.js';
|
|
42
|
+
import { base } from './base.js';
|
|
43
|
+
import { formBind } from '../util/form.js';
|
|
44
|
+
import { presence } from '../motion/presence.js';
|
|
45
|
+
import { fx } from '../motion/animate.js';
|
|
46
|
+
import './ui-icon.js';
|
|
47
|
+
import './ui-icon-button.js';
|
|
48
|
+
|
|
49
|
+
const t = vars('ui-search', {
|
|
50
|
+
bg: sys.color.surfaceContainerHigh,
|
|
51
|
+
bgActive: sys.color.surfaceContainerHigh,
|
|
52
|
+
fg: sys.color.onSurface,
|
|
53
|
+
placeholderFg: sys.color.onSurfaceVariant,
|
|
54
|
+
radius: sys.radius.xl,
|
|
55
|
+
font: sys.type.bodyLg,
|
|
56
|
+
height: '56px',
|
|
57
|
+
panelBg: sys.color.surfaceContainerHigh,
|
|
58
|
+
panelRadius: sys.radius.xl,
|
|
59
|
+
divider: sys.color.outlineVariant,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const styles = css`
|
|
63
|
+
:host { display: block; position: relative; inline-size: min(100%, 720px); }
|
|
64
|
+
.shell {
|
|
65
|
+
position: relative;
|
|
66
|
+
overflow: hidden;
|
|
67
|
+
border-radius: ${t.radius};
|
|
68
|
+
background: ${t.bg};
|
|
69
|
+
box-shadow: none;
|
|
70
|
+
transition: box-shadow ${sys.duration.short4} ${sys.easing.standard},
|
|
71
|
+
background-color ${sys.duration.short2} ${sys.easing.standard};
|
|
72
|
+
}
|
|
73
|
+
.shell:focus-within:not(.open) {
|
|
74
|
+
background: ${t.bgActive};
|
|
75
|
+
}
|
|
76
|
+
.shell.open {
|
|
77
|
+
overflow: visible;
|
|
78
|
+
z-index: ${sys.z.modal};
|
|
79
|
+
background: ${t.panelBg};
|
|
80
|
+
border-start-start-radius: ${t.panelRadius};
|
|
81
|
+
border-start-end-radius: ${t.panelRadius};
|
|
82
|
+
border-end-start-radius: 0;
|
|
83
|
+
border-end-end-radius: 0;
|
|
84
|
+
box-shadow: ${sys.elevation[2]};
|
|
85
|
+
}
|
|
86
|
+
.bar {
|
|
87
|
+
position: relative;
|
|
88
|
+
isolation: isolate;
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
gap: ${sys.space(2)};
|
|
92
|
+
min-block-size: calc(${t.height} + var(--ui-density, 0) * 4px);
|
|
93
|
+
padding-inline: ${sys.space(2)};
|
|
94
|
+
border-radius: inherit;
|
|
95
|
+
background: transparent;
|
|
96
|
+
color: ${t.fg};
|
|
97
|
+
font: ${t.font};
|
|
98
|
+
letter-spacing: ${sys.tracking.bodyLg};
|
|
99
|
+
cursor: text;
|
|
100
|
+
--ui-icon-size: 1.5rem;
|
|
101
|
+
}
|
|
102
|
+
.bar::before {
|
|
103
|
+
content: '';
|
|
104
|
+
position: absolute; inset: 0;
|
|
105
|
+
border-radius: inherit;
|
|
106
|
+
background: ${sys.color.onSurface};
|
|
107
|
+
opacity: 0;
|
|
108
|
+
pointer-events: none;
|
|
109
|
+
transition: opacity ${sys.duration.short2} ${sys.easing.standard};
|
|
110
|
+
}
|
|
111
|
+
.bar:hover:not(:focus-within)::before { opacity: ${sys.state.hover}; }
|
|
112
|
+
.shell.open .bar::before { opacity: 0; }
|
|
113
|
+
.lead { color: ${t.placeholderFg}; display: grid; place-items: center; }
|
|
114
|
+
.leads {
|
|
115
|
+
position: relative;
|
|
116
|
+
display: grid;
|
|
117
|
+
place-items: center;
|
|
118
|
+
/* Cap at 48px for the 56px bar; shrink with --ui-search-height so a
|
|
119
|
+
compact field in an app bar can match the 40px icon buttons. */
|
|
120
|
+
inline-size: min(48px, ${t.height});
|
|
121
|
+
block-size: min(48px, ${t.height});
|
|
122
|
+
}
|
|
123
|
+
.leads > * { grid-area: 1 / 1; }
|
|
124
|
+
.lead-slot {
|
|
125
|
+
display: grid;
|
|
126
|
+
place-items: center;
|
|
127
|
+
transform-origin: center center;
|
|
128
|
+
transition: opacity ${sys.duration.short3} ${sys.easing.standard},
|
|
129
|
+
transform ${sys.duration.short4} ${sys.easing.emphasized};
|
|
130
|
+
}
|
|
131
|
+
.lead-search {
|
|
132
|
+
opacity: 1;
|
|
133
|
+
transform: rotate(0deg) scale(1);
|
|
134
|
+
}
|
|
135
|
+
.lead-search.off {
|
|
136
|
+
opacity: 0;
|
|
137
|
+
transform: rotate(-90deg) scale(0.6);
|
|
138
|
+
pointer-events: none;
|
|
139
|
+
}
|
|
140
|
+
.lead-back {
|
|
141
|
+
opacity: 1;
|
|
142
|
+
transform: rotate(0deg) scale(1);
|
|
143
|
+
}
|
|
144
|
+
.lead-back.off {
|
|
145
|
+
opacity: 0;
|
|
146
|
+
transform: rotate(90deg) scale(0.6);
|
|
147
|
+
pointer-events: none;
|
|
148
|
+
}
|
|
149
|
+
input {
|
|
150
|
+
flex: 1;
|
|
151
|
+
min-inline-size: 0;
|
|
152
|
+
margin: 0;
|
|
153
|
+
border: none;
|
|
154
|
+
outline: none;
|
|
155
|
+
appearance: none;
|
|
156
|
+
background: transparent;
|
|
157
|
+
font: inherit;
|
|
158
|
+
letter-spacing: inherit;
|
|
159
|
+
color: inherit;
|
|
160
|
+
padding: 0;
|
|
161
|
+
}
|
|
162
|
+
input::placeholder { color: ${t.placeholderFg}; }
|
|
163
|
+
.panel {
|
|
164
|
+
position: absolute;
|
|
165
|
+
inset-inline: 0;
|
|
166
|
+
inset-block-start: 100%;
|
|
167
|
+
z-index: 1;
|
|
168
|
+
padding-block: ${sys.space(2)};
|
|
169
|
+
background: ${t.panelBg};
|
|
170
|
+
overflow-y: auto;
|
|
171
|
+
overflow-x: hidden;
|
|
172
|
+
box-sizing: border-box;
|
|
173
|
+
max-block-size: min(70vh, 360px);
|
|
174
|
+
border-block-start: 1px solid ${t.divider};
|
|
175
|
+
border-end-start-radius: ${t.panelRadius};
|
|
176
|
+
border-end-end-radius: ${t.panelRadius};
|
|
177
|
+
box-shadow: ${sys.elevation[2]};
|
|
178
|
+
}
|
|
179
|
+
.disabled { opacity: ${sys.state.disabledContent}; pointer-events: none; }
|
|
180
|
+
`;
|
|
181
|
+
|
|
182
|
+
define('ui-search', {
|
|
183
|
+
formAssociated: true,
|
|
184
|
+
props: {
|
|
185
|
+
label: 'Search', value: '', placeholder: '', presentation: 'bar',
|
|
186
|
+
open: false, disabled: false, name: '',
|
|
187
|
+
},
|
|
188
|
+
styles: [base, styles],
|
|
189
|
+
setup({ label, value, placeholder, presentation, open, disabled, name }, host) {
|
|
190
|
+
formBind(host, { name, value, disabled });
|
|
191
|
+
const hasLeading = signal(false);
|
|
192
|
+
let input;
|
|
193
|
+
let skipFocusOpen = false;
|
|
194
|
+
|
|
195
|
+
const ph = computed(() => placeholder() || label() || 'Search');
|
|
196
|
+
const isView = computed(() => presentation() === 'view');
|
|
197
|
+
const slotted = () => [...host.children].some((c) => !c.slot);
|
|
198
|
+
const showPanel = computed(() => open() && (isView() || slotted()));
|
|
199
|
+
const hasClear = computed(() => (value() ?? '') !== '' && !disabled());
|
|
200
|
+
const rootCls = computed(() =>
|
|
201
|
+
['shell', isView() ? 'view' : 'bar-mode', open() && 'open', disabled() && 'disabled']
|
|
202
|
+
.filter(Boolean).join(' '));
|
|
203
|
+
const hasQuery = () => (value() ?? '') !== '';
|
|
204
|
+
|
|
205
|
+
const openView = () => {
|
|
206
|
+
if (disabled() || open()) return;
|
|
207
|
+
if (isView() || slotted()) open.set(true);
|
|
208
|
+
};
|
|
209
|
+
const closeView = () => { if (open()) open.set(false); };
|
|
210
|
+
|
|
211
|
+
const onInput = (e) => {
|
|
212
|
+
// Native `input` is composed. Stop it so a host listener is not handed
|
|
213
|
+
// UIEvent.detail (0). The CustomEvent we emit carries `{ value }`.
|
|
214
|
+
// Do not bind the host with @input.capture — that runs before this stop
|
|
215
|
+
// and will see the native event.
|
|
216
|
+
e.stopPropagation();
|
|
217
|
+
value.set(e.target.value);
|
|
218
|
+
host.emit('input', { value: value() });
|
|
219
|
+
if (hasQuery()) openView();
|
|
220
|
+
else closeView();
|
|
221
|
+
};
|
|
222
|
+
const commit = (e) => {
|
|
223
|
+
e?.stopPropagation();
|
|
224
|
+
host.emit('change', { value: value() });
|
|
225
|
+
};
|
|
226
|
+
const submit = (e) => {
|
|
227
|
+
if (e.key !== 'Enter') return;
|
|
228
|
+
host.emit('submit', { value: value() });
|
|
229
|
+
};
|
|
230
|
+
const clear = (e) => {
|
|
231
|
+
e?.stopPropagation();
|
|
232
|
+
value.set('');
|
|
233
|
+
host.emit('clear');
|
|
234
|
+
host.emit('input', { value: '' });
|
|
235
|
+
closeView();
|
|
236
|
+
skipFocusOpen = true;
|
|
237
|
+
input?.focus();
|
|
238
|
+
queueMicrotask(() => { skipFocusOpen = false; });
|
|
239
|
+
};
|
|
240
|
+
const onFocus = () => {
|
|
241
|
+
if (skipFocusOpen) return;
|
|
242
|
+
openView();
|
|
243
|
+
};
|
|
244
|
+
const onFocusOut = (e) => {
|
|
245
|
+
const next = e.relatedTarget;
|
|
246
|
+
if (next && (host.contains(next) || host.shadowRoot?.contains(next))) return;
|
|
247
|
+
if (!hasQuery()) closeView();
|
|
248
|
+
};
|
|
249
|
+
const onKeydown = (e) => {
|
|
250
|
+
submit(e);
|
|
251
|
+
if (e.key === 'Escape' && open()) {
|
|
252
|
+
e.preventDefault();
|
|
253
|
+
closeView();
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const panelView = () => html`
|
|
258
|
+
<div class="panel" part="panel" role="list" id="suggestions"
|
|
259
|
+
aria-label=${() => label() || 'Suggestions'}>
|
|
260
|
+
<slot></slot>
|
|
261
|
+
</div>`;
|
|
262
|
+
|
|
263
|
+
return html`
|
|
264
|
+
<div class=${rootCls} @focusout=${onFocusOut}>
|
|
265
|
+
<div class=${() => `bar${disabled() ? ' disabled' : ''}`} part="bar"
|
|
266
|
+
@click=${() => input?.focus()}>
|
|
267
|
+
${() => (isView()
|
|
268
|
+
? html`<span class="leads">
|
|
269
|
+
<span class=${() => `lead-slot lead-search${open() ? ' off' : ''}`}>
|
|
270
|
+
<slot name="leading" ref=${(el) => el.addEventListener('slotchange', () => hasLeading.set(el.assignedElements().length > 0))}></slot>
|
|
271
|
+
${() => (hasLeading() ? null : html`<ui-icon name="search"></ui-icon>`)}
|
|
272
|
+
</span>
|
|
273
|
+
<span class=${() => `lead-slot lead-back${open() ? '' : ' off'}`}>
|
|
274
|
+
<ui-icon-button icon="arrow-back" label="Back"
|
|
275
|
+
@click=${(e) => { e.stopPropagation(); closeView(); }}></ui-icon-button>
|
|
276
|
+
</span>
|
|
277
|
+
</span>`
|
|
278
|
+
: html`<span class="lead">
|
|
279
|
+
<slot name="leading" ref=${(el) => el.addEventListener('slotchange', () => hasLeading.set(el.assignedElements().length > 0))}></slot>
|
|
280
|
+
${() => (hasLeading() ? null : html`<ui-icon name="search"></ui-icon>`)}
|
|
281
|
+
</span>`)}
|
|
282
|
+
<input part="input" ref=${(el) => (input = el)}
|
|
283
|
+
.value=${() => value() ?? ''}
|
|
284
|
+
placeholder=${ph}
|
|
285
|
+
aria-label=${() => label() || 'Search'}
|
|
286
|
+
aria-expanded=${() => String(open())}
|
|
287
|
+
aria-controls=${() => (showPanel() ? 'suggestions' : null)}
|
|
288
|
+
aria-autocomplete="list"
|
|
289
|
+
aria-haspopup=${() => (isView() || slotted() ? 'true' : null)}
|
|
290
|
+
?disabled=${disabled}
|
|
291
|
+
@input=${onInput} @change=${commit} @keydown=${onKeydown}
|
|
292
|
+
@focus=${onFocus}>
|
|
293
|
+
${presence(hasClear, () => html`<ui-icon-button icon="close" label="Clear" @click=${clear}></ui-icon-button>`, {
|
|
294
|
+
enter: fx.scaleIn,
|
|
295
|
+
exit: fx.scaleOut,
|
|
296
|
+
enterDuration: 'short2',
|
|
297
|
+
exitDuration: 'short2',
|
|
298
|
+
enterEasing: 'emphasizedDecelerate',
|
|
299
|
+
exitEasing: 'emphasizedAccelerate',
|
|
300
|
+
})}
|
|
301
|
+
<slot name="trailing"></slot>
|
|
302
|
+
</div>
|
|
303
|
+
${presence(showPanel, panelView, {
|
|
304
|
+
enter: fx.expandDown,
|
|
305
|
+
exit: fx.collapseUp,
|
|
306
|
+
enterDuration: 'medium2',
|
|
307
|
+
enterEasing: 'emphasizedDecelerate',
|
|
308
|
+
exitDuration: 'short4',
|
|
309
|
+
exitEasing: 'emphasizedAccelerate',
|
|
310
|
+
onEntered: () => host.emit('open'),
|
|
311
|
+
onExited: () => host.emit('close'),
|
|
312
|
+
})}
|
|
313
|
+
</div>`;
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
export const tag = 'ui-search';
|
|
318
|
+
export const themeVars = t;
|