@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,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;