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