@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.
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 +316 -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,178 @@
1
+ // <ui-tabs> — Material tabs: a tab bar with an animated active indicator.
2
+ //
3
+ // <ui-tabs value="one" label="Demo tabs" @change=${(e) => ...}>
4
+ // <ui-tab value="one" icon="home">One</ui-tab>
5
+ // <ui-tab value="two">Two</ui-tab>
6
+ // <ui-tab-panel slot="panels" value="one">…</ui-tab-panel>
7
+ // <ui-tab-panel slot="panels" value="two">…</ui-tab-panel>
8
+ // </ui-tabs>
9
+ //
10
+ // Tabs go in the default slot, panels in the "panels" slot. The parent
11
+ // reflects `selected` onto each <ui-tab> and `active` onto each
12
+ // <ui-tab-panel>, and wires aria-controls/aria-labelledby ids between them.
13
+ // Activation is AUTOMATIC per the ARIA APG: arrow keys move focus AND select
14
+ // (one Tab stop for the whole bar via roving tabindex).
15
+ //
16
+ // @prop {string} value='' — the selected tab's value
17
+ // @prop {string} variant='primary' — primary | secondary
18
+ // @prop {string} label='' — accessible name for the tablist
19
+ // @event change — user selected a tab; detail: { value }
20
+ // @slot (default) — <ui-tab> children
21
+ // @slot panels — <ui-tab-panel> children
22
+ // @part tablist, indicator
23
+ // @vars see `t` below (`themeVars.names`)
24
+
25
+ import { define, html, css, vars, signal, effect, onCleanup } from '@alacris/core';
26
+ import { sys } from '../tokens/sys.js';
27
+ import { base } from './base.js';
28
+ import { rovingTabindex } from '../util/keys.js';
29
+ import './ui-tab.js';
30
+ import './ui-tab-panel.js';
31
+
32
+ const t = vars('ui-tabs', {
33
+ indicator: sys.color.primary,
34
+ indicatorHeight: '3px',
35
+ divider: sys.color.outlineVariant,
36
+ });
37
+
38
+ const styles = css`
39
+ :host { display: block; position: relative; }
40
+ .tablist {
41
+ position: relative;
42
+ display: flex;
43
+ align-items: stretch;
44
+ border-block-end: 1px solid ${t.divider};
45
+ }
46
+ .indicator {
47
+ position: absolute;
48
+ inset-block-end: 0;
49
+ inset-inline-start: 0;
50
+ block-size: ${t.indicatorHeight};
51
+ background: ${t.indicator};
52
+ border-start-start-radius: ${t.indicatorHeight};
53
+ border-start-end-radius: ${t.indicatorHeight};
54
+ pointer-events: none;
55
+ transition: transform ${sys.duration.medium2} ${sys.easing.emphasized},
56
+ width ${sys.duration.medium2} ${sys.easing.emphasized},
57
+ opacity ${sys.duration.short2} ${sys.easing.standard};
58
+ }
59
+ .secondary .indicator {
60
+ block-size: 2px;
61
+ border-radius: 0;
62
+ background: ${sys.color.onSurface};
63
+ }
64
+ `;
65
+
66
+ let uid = 0;
67
+
68
+ define('ui-tabs', {
69
+ props: { value: '', variant: 'primary', label: '' },
70
+ styles: [base, styles],
71
+ setup({ value, variant, label }, host) {
72
+ const id = ++uid;
73
+ const rev = signal(0); // bumped on slotchange so syncs re-run
74
+ const bump = () => rev.update((n) => n + 1);
75
+
76
+ const tabsOf = () => [...host.querySelectorAll('ui-tab')];
77
+ const panelsOf = () => [...host.querySelectorAll('ui-tab-panel')];
78
+
79
+ const select = (v) => {
80
+ if (v === undefined || v === value.peek()) return;
81
+ value.set(v);
82
+ host.emit('change', { value: v });
83
+ };
84
+
85
+ // Selection made by a tab (click / Enter / Space).
86
+ host.addEventListener('ui-tab-select', (e) => {
87
+ e.stopPropagation();
88
+ select(e.detail.value);
89
+ });
90
+
91
+ // Automatic activation: arrows move focus and select.
92
+ const roving = rovingTabindex(host, {
93
+ selector: 'ui-tab',
94
+ orientation: 'horizontal',
95
+ skip: (el) => el.disabled,
96
+ onMove: (el) => select(el.value),
97
+ });
98
+ onCleanup(() => roving.destroy());
99
+
100
+ // Reflect selection down and keep the ARIA wiring current.
101
+ effect(() => {
102
+ const v = value();
103
+ rev();
104
+ const tabs = tabsOf();
105
+ const panels = panelsOf();
106
+ let hasStop = false;
107
+ for (const tab of tabs) {
108
+ const on = tab.value === v;
109
+ tab.selected = on;
110
+ tab.tabIndex = on && !tab.disabled ? 0 : -1;
111
+ if (on && !tab.disabled) hasStop = true;
112
+ if (!tab.id) tab.id = `ui-tab-${id}-${tab.value}`;
113
+ }
114
+ if (!hasStop) {
115
+ const first = tabs.find((el) => !el.disabled);
116
+ if (first) first.tabIndex = 0;
117
+ }
118
+ for (const panel of panels) {
119
+ panel.active = panel.value === v;
120
+ if (!panel.id) panel.id = `ui-tab-panel-${id}-${panel.value}`;
121
+ const tab = tabs.find((el) => el.value === panel.value);
122
+ if (tab) {
123
+ tab.setAttribute('aria-controls', panel.id);
124
+ panel.setAttribute('aria-labelledby', tab.id);
125
+ }
126
+ }
127
+ });
128
+
129
+ // Animated indicator: measure the selected tab in the host's coordinate
130
+ // space. Zero sizes (simulated DOM, display:none) hide the bar instead of
131
+ // animating to garbage.
132
+ const ix = signal(0);
133
+ const iw = signal(0);
134
+ let tablistEl = null;
135
+ const measure = () => {
136
+ const tab = tabsOf().find((el) => el.selected);
137
+ if (!tab || !tablistEl) { iw.set(0); return; }
138
+ if (variant.peek() === 'primary') {
139
+ const inner = tab.shadowRoot?.querySelector('.inner');
140
+ const listRect = tablistEl.getBoundingClientRect();
141
+ const r = (inner || tab).getBoundingClientRect();
142
+ if (!r.width) { iw.set(0); return; }
143
+ ix.set(r.left - listRect.left);
144
+ iw.set(r.width);
145
+ } else {
146
+ const w = tab.offsetWidth || 0;
147
+ if (!w) { iw.set(0); return; }
148
+ ix.set(tab.offsetLeft || 0);
149
+ iw.set(w);
150
+ }
151
+ };
152
+ effect(() => {
153
+ value();
154
+ rev();
155
+ queueMicrotask(measure);
156
+ });
157
+ effect(() => {
158
+ window.addEventListener('resize', measure, { passive: true });
159
+ return () => window.removeEventListener('resize', measure);
160
+ });
161
+
162
+ return html`
163
+ <div class=${() => `tablist ${variant()}`} part="tablist" role="tablist" aria-label=${() => label() || null}
164
+ ref=${(el) => (tablistEl = el)}>
165
+ <slot @slotchange=${bump}></slot>
166
+ <span class="indicator" part="indicator" aria-hidden="true"
167
+ style=${() => ({
168
+ transform: `translateX(${ix()}px)`,
169
+ width: `${iw()}px`,
170
+ opacity: iw() ? '1' : '0',
171
+ })}></span>
172
+ </div>
173
+ <slot name="panels" @slotchange=${bump}></slot>`;
174
+ },
175
+ });
176
+
177
+ export const tag = 'ui-tabs';
178
+ export const themeVars = t;
@@ -0,0 +1,319 @@
1
+ // <ui-text-field> — Material text field, filled and outlined, floating label.
2
+ //
3
+ // @prop {string} variant='filled' — filled | outlined
4
+ // @prop {string} label=''
5
+ // @prop {string} value=''
6
+ // @prop {string} type='text' — any native input type; 'textarea' renders one
7
+ // @prop {string} placeholder=''
8
+ // @prop {string} helper='' — supporting text under the field
9
+ // @prop {string} error='' — error message; non-empty switches to error state
10
+ // @prop {boolean} disabled=false
11
+ // @prop {boolean} required=false
12
+ // @prop {boolean} clearable=false — trailing ✕ while there is content
13
+ // @prop {string} name='' — form participation
14
+ // @prop {number} maxlength=0 — >0 shows a character counter and enforces it
15
+ // @prop {number} rows=3 — textarea rows
16
+ // @event input — every keystroke; detail: { value }
17
+ // @event change — committed (blur/Enter); detail: { value }
18
+ // @event clear — the clear affordance was used
19
+ // @slot leading — icon before the input
20
+ // @slot trailing — icon after the input (replaced by ✕ while clearable+content)
21
+ // @part field, input, label, helper
22
+ // @vars see `t` below
23
+
24
+ import { define, html, css, vars, computed, signal } from '@alacris/core';
25
+ import { sys } from '../tokens/sys.js';
26
+ import { base } from './base.js';
27
+ import { formBind } from '../util/form.js';
28
+ import './ui-icon-button.js';
29
+
30
+ const t = vars('ui-text-field', {
31
+ bg: sys.color.surfaceContainerHighest,
32
+ fg: sys.color.onSurface,
33
+ labelFg: sys.color.onSurfaceVariant,
34
+ accent: sys.color.primary,
35
+ errorFg: sys.color.error,
36
+ outlineColor: sys.color.outline,
37
+ radius: sys.radius.xs,
38
+ font: sys.type.bodyLg,
39
+ height: '56px',
40
+ });
41
+
42
+ const styles = css`
43
+ :host { display: block; inline-size: 240px; }
44
+ .root { display: block; }
45
+ .field {
46
+ position: relative;
47
+ display: flex;
48
+ align-items: center;
49
+ gap: ${sys.space(2)};
50
+ min-block-size: calc(${t.height} + var(--ui-density, 0) * 4px);
51
+ padding-inline: ${sys.space(4)};
52
+ border-radius: ${t.radius};
53
+ font: ${t.font};
54
+ color: ${t.fg};
55
+ cursor: text;
56
+ --ui-icon-size: 1.5rem;
57
+ }
58
+ /* Resize lives on .grow, not the textarea, so padding on the text
59
+ does not inset the native grip from the field's corner. */
60
+ .multiline .field {
61
+ padding-inline: 0;
62
+ align-items: stretch;
63
+ }
64
+ .grow {
65
+ flex: 1;
66
+ min-inline-size: 0;
67
+ display: flex;
68
+ align-self: stretch;
69
+ }
70
+ .multiline .grow {
71
+ resize: vertical;
72
+ overflow: hidden;
73
+ min-block-size: calc(${t.height} + var(--ui-density, 0) * 4px);
74
+ }
75
+ .disabled .grow { resize: none; }
76
+ .filled .field {
77
+ background: ${t.bg};
78
+ border-start-start-radius: ${t.radius};
79
+ border-start-end-radius: ${t.radius};
80
+ border-end-start-radius: 0;
81
+ border-end-end-radius: 0;
82
+ }
83
+ .filled .field::after {
84
+ content: '';
85
+ position: absolute;
86
+ inset-inline: 0;
87
+ inset-block-end: 0;
88
+ block-size: 1px;
89
+ background: ${sys.color.onSurfaceVariant};
90
+ transition: block-size ${sys.duration.short2} ${sys.easing.standard},
91
+ background-color ${sys.duration.short2} ${sys.easing.standard};
92
+ }
93
+ .filled.focused .field::after { block-size: 2px; background: ${t.accent}; }
94
+ .filled.error .field::after { background: ${t.errorFg}; }
95
+ /* Opaque band so scrolled lines do not paint over the floating label. */
96
+ .filled.multiline.floating.has-label .field::before {
97
+ content: '';
98
+ position: absolute;
99
+ inset-inline: 0;
100
+ inset-block-start: 0;
101
+ block-size: 24px;
102
+ background: ${t.bg};
103
+ pointer-events: none;
104
+ z-index: 1;
105
+ border-start-start-radius: inherit;
106
+ border-start-end-radius: inherit;
107
+ }
108
+
109
+ /* Outlined: a fieldset draws the border; its legend opens the label notch. */
110
+ fieldset {
111
+ position: absolute;
112
+ inset: -6px 0 0;
113
+ margin: 0;
114
+ padding: 0 calc(${sys.space(3)} - 2px);
115
+ min-inline-size: 0;
116
+ box-sizing: border-box;
117
+ appearance: none;
118
+ border: 1px solid ${t.outlineColor};
119
+ border-radius: ${t.radius};
120
+ pointer-events: none;
121
+ transition: border-color ${sys.duration.short2} ${sys.easing.standard},
122
+ border-width ${sys.duration.short2} ${sys.easing.standard};
123
+ }
124
+ legend {
125
+ float: unset;
126
+ display: block;
127
+ width: max-content;
128
+ padding: 0;
129
+ margin: 0;
130
+ margin-inline-start: 0;
131
+ white-space: nowrap;
132
+ overflow: hidden;
133
+ font: ${t.font};
134
+ font-size: 0.75em;
135
+ letter-spacing: calc(${sys.tracking.bodyLg} * 0.75);
136
+ visibility: hidden;
137
+ max-inline-size: 0.01px;
138
+ height: 12px;
139
+ line-height: 12px;
140
+ transition: max-inline-size ${sys.duration.short2} ${sys.easing.standard};
141
+ }
142
+ legend span {
143
+ padding-inline: ${sys.space(1)} calc(${sys.space(1)} - 3px);
144
+ display: inline-block;
145
+ opacity: 0;
146
+ visibility: visible;
147
+ }
148
+ .outlined.floating legend {
149
+ max-inline-size: 100%;
150
+ transition: max-inline-size ${sys.duration.short3} ${sys.easing.standard};
151
+ }
152
+ .outlined.focused fieldset { border-width: 2px; border-color: ${t.accent}; }
153
+ .outlined.error fieldset { border-color: ${t.errorFg}; }
154
+ .root:hover:not(.focused):not(.error) fieldset { border-color: ${sys.color.onSurface}; }
155
+ .filled .filled-hover {
156
+ position: absolute; inset: 0; pointer-events: none;
157
+ background: ${sys.color.onSurface};
158
+ opacity: 0;
159
+ border-radius: inherit;
160
+ transition: opacity ${sys.duration.short2} ${sys.easing.standard};
161
+ }
162
+ .filled:hover:not(.focused):not(.error):not(.disabled) .filled-hover {
163
+ opacity: ${sys.state.hover};
164
+ }
165
+ .filled:hover:not(.focused):not(.error):not(.disabled) .field::after {
166
+ background: ${sys.color.onSurface};
167
+ }
168
+
169
+ .label {
170
+ position: absolute;
171
+ inset-inline-start: ${sys.space(4)};
172
+ inset-block-start: 50%;
173
+ translate: 0 -50%;
174
+ color: ${t.labelFg};
175
+ pointer-events: none;
176
+ transform-origin: 0 50%;
177
+ transition: translate ${sys.duration.short3} ${sys.easing.standard},
178
+ scale ${sys.duration.short3} ${sys.easing.standard},
179
+ color ${sys.duration.short2} ${sys.easing.standard};
180
+ }
181
+ .with-leading .label { inset-inline-start: calc(${sys.space(4)} + 1.5rem + ${sys.space(2)}); }
182
+ /* Multi-line fields anchor the label to the top, not the vertical center. */
183
+ .multiline .label { inset-block-start: 24px; z-index: 2; }
184
+ .filled.floating .label { translate: 0 calc(-50% - 16px); scale: 0.75; }
185
+ .filled.multiline.floating .label { translate: 0 -85%; scale: 0.75; }
186
+ .outlined.multiline.floating .label { inset-block-start: 8px; }
187
+ .outlined.floating .label {
188
+ inset-inline-start: ${sys.space(4)};
189
+ translate: 0 calc(-50% - (${t.height} + var(--ui-density, 0) * 4px) / 2);
190
+ scale: 0.75;
191
+ }
192
+ .focused .label { color: ${t.accent}; }
193
+ .error .label { color: ${t.errorFg}; }
194
+
195
+ input, textarea {
196
+ flex: 1;
197
+ min-inline-size: 0;
198
+ margin: 0;
199
+ border: none;
200
+ outline: none;
201
+ appearance: none;
202
+ background: transparent;
203
+ font: inherit;
204
+ letter-spacing: inherit;
205
+ color: inherit;
206
+ padding: 0;
207
+ }
208
+ .filled.has-label input { padding-block-start: 18px; }
209
+ textarea {
210
+ display: block;
211
+ flex: 1;
212
+ inline-size: 100%;
213
+ min-block-size: 0;
214
+ resize: none;
215
+ padding: ${sys.space(4)};
216
+ }
217
+ .filled.has-label textarea { padding-top: ${sys.space(7)}; }
218
+ .with-leading.multiline .field { padding-inline-start: ${sys.space(4)}; }
219
+ .with-leading.multiline textarea { padding-inline-start: 0; }
220
+ input::placeholder, textarea::placeholder { color: ${t.labelFg}; opacity: 0; transition: opacity ${sys.duration.short2} linear; }
221
+ .floating input::placeholder, .floating textarea::placeholder { opacity: 1; }
222
+
223
+ .below {
224
+ display: flex;
225
+ justify-content: space-between;
226
+ gap: ${sys.space(4)};
227
+ padding-inline: ${sys.space(4)};
228
+ padding-block-start: ${sys.space(1)};
229
+ font: ${sys.type.bodySm};
230
+ letter-spacing: ${sys.tracking.bodySm};
231
+ color: ${t.labelFg};
232
+ }
233
+ .error .below .msg { color: ${t.errorFg}; }
234
+ .disabled { opacity: ${sys.state.disabledContent}; pointer-events: none; }
235
+ ::slotted([slot]) { color: ${t.labelFg}; }
236
+ `;
237
+
238
+ define('ui-text-field', {
239
+ formAssociated: true,
240
+ props: {
241
+ variant: 'filled', label: '', value: '', type: 'text', placeholder: '',
242
+ helper: '', error: '', disabled: false, required: false, clearable: false,
243
+ name: '', maxlength: 0, rows: 3,
244
+ },
245
+ styles: [base, styles],
246
+ setup(p, host) {
247
+ const { variant, label, value, type, placeholder, helper, error, disabled, required, clearable, name, maxlength, rows } = p;
248
+ formBind(host, { name, value, disabled });
249
+
250
+ const focused = signal(false);
251
+ const hasLeading = signal(false);
252
+ let input;
253
+
254
+ const floating = computed(() => focused() || value() !== '' || placeholder() !== '');
255
+ const cls = computed(() =>
256
+ ['root', variant(), floating() && 'floating', focused() && 'focused',
257
+ error() && 'error', disabled() && 'disabled', label() && 'has-label',
258
+ hasLeading() && 'with-leading', type() === 'textarea' && 'multiline'].filter(Boolean).join(' '));
259
+
260
+ const onInput = (e) => {
261
+ value.set(e.target.value);
262
+ host.emit('input', { value: value() });
263
+ };
264
+ const commit = () => host.emit('change', { value: value() });
265
+ const clear = () => {
266
+ value.set('');
267
+ host.emit('clear');
268
+ host.emit('input', { value: '' });
269
+ input?.focus();
270
+ };
271
+
272
+ const describe = computed(() => error() || helper() || null);
273
+ const counter = computed(() => (maxlength() > 0 ? `${value().length} / ${maxlength()}` : null));
274
+
275
+ const fieldBody = html`
276
+ <slot name="leading" ref=${(el) => el.addEventListener('slotchange', () => hasLeading.set(el.assignedElements().length > 0))}></slot>
277
+ ${() => (type() === 'textarea'
278
+ ? html`<span class="grow"><textarea part="input" ref=${(el) => (input = el)}
279
+ .value=${value} rows=${rows}
280
+ placeholder=${() => placeholder() || null}
281
+ maxlength=${() => (maxlength() > 0 ? maxlength() : null)}
282
+ ?required=${required} ?disabled=${disabled}
283
+ aria-invalid=${() => (error() ? 'true' : null)}
284
+ @input=${onInput} @change=${commit}
285
+ @focus=${() => focused.set(true)} @blur=${() => focused.set(false)}></textarea></span>`
286
+ : html`<input part="input" ref=${(el) => (input = el)}
287
+ type=${type} .value=${value}
288
+ placeholder=${() => placeholder() || null}
289
+ maxlength=${() => (maxlength() > 0 ? maxlength() : null)}
290
+ ?required=${required} ?disabled=${disabled}
291
+ aria-invalid=${() => (error() ? 'true' : null)}
292
+ @input=${onInput} @change=${commit}
293
+ @focus=${() => focused.set(true)} @blur=${() => focused.set(false)}>`)}
294
+ ${() => (clearable() && value() !== '' && !disabled()
295
+ ? html`<ui-icon-button icon="close" label="Clear" @click=${clear}></ui-icon-button>`
296
+ : html`<slot name="trailing"></slot>`)}`;
297
+
298
+ return html`
299
+ <label class=${cls}>
300
+ <span class="field" part="field">
301
+ ${() => (variant() === 'filled' ? html`<span class="filled-hover" aria-hidden="true"></span>` : null)}
302
+ ${() => (variant() === 'outlined'
303
+ ? html`<fieldset aria-hidden="true"><legend><span>${label}${() => (required() ? ' *' : '')}</span></legend></fieldset>`
304
+ : null)}
305
+ ${() => (label() ? html`<span class="label" part="label">${label}${() => (required() ? ' *' : '')}</span>` : null)}
306
+ ${fieldBody}
307
+ </span>
308
+ ${() => (describe() || counter()
309
+ ? html`<span class="below" part="helper">
310
+ <span class="msg" role=${() => (error() ? 'alert' : null)}>${describe}</span>
311
+ ${() => counter() && html`<span class="count">${counter}</span>`}
312
+ </span>`
313
+ : null)}
314
+ </label>`;
315
+ },
316
+ });
317
+
318
+ export const tag = 'ui-text-field';
319
+ export const themeVars = t;
@@ -0,0 +1,36 @@
1
+ // <ui-text> — typography, the type scale as an element.
2
+ //
3
+ // <ui-text variant="headline-md" as="h2">Section</ui-text>
4
+ //
5
+ // @prop {string} variant='body-md' — any type role: display|headline|title|body|label × lg|md|sm
6
+ // @prop {string} color='' — a color role name ('primary', 'onSurfaceVariant', …)
7
+ // @slot (default)
8
+ // @vars (uses the --ui-type-* system tokens directly)
9
+ //
10
+ // Semantics: <ui-text> is styling only. Keep real headings in your markup
11
+ // (`<h2><ui-text variant="headline-md">…</ui-text></h2>`) or set an ARIA role
12
+ // on the host when the document outline needs one.
13
+
14
+ import { define, html, css, effect } from '@alacris/core';
15
+ import { base } from './base.js';
16
+
17
+ const kebab = (s) => s.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase());
18
+
19
+ define('ui-text', {
20
+ props: { variant: 'body-md', color: '' },
21
+ styles: [base, css`:host { display: block; margin: 0; }`],
22
+ setup({ variant, color }, host) {
23
+ effect(() => {
24
+ const v = kebab(variant());
25
+ host.style.font = `var(--ui-type-${v})`;
26
+ host.style.letterSpacing = `var(--ui-type-${v}-tracking)`;
27
+ });
28
+ effect(() => {
29
+ if (color()) host.style.color = `var(--ui-color-${kebab(color())})`;
30
+ else host.style.removeProperty('color');
31
+ });
32
+ return html`<slot></slot>`;
33
+ },
34
+ });
35
+
36
+ export const tag = 'ui-text';