@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,170 @@
|
|
|
1
|
+
// <ui-toggle-button> — one Material segmented button, used inside
|
|
2
|
+
// <ui-toggle-group> (the group draws the outlined container and dividers;
|
|
3
|
+
// standalone the segment is a flat pill).
|
|
4
|
+
//
|
|
5
|
+
// Selecting animates a leading check icon in smoothly via width and scale transitions,
|
|
6
|
+
// adhering to Material Design 3. When an icon is already present, selecting smoothly
|
|
7
|
+
// crossfades and morphs between the custom icon and the checkmark in both directions.
|
|
8
|
+
// The button does not own its selection: it emits `ui-toggle` and the group
|
|
9
|
+
// (or any parent) sets `selected` back down.
|
|
10
|
+
//
|
|
11
|
+
// @prop {string} value='' — REQUIRED identity within the group
|
|
12
|
+
// @prop {boolean} selected=false — set by the owning group
|
|
13
|
+
// @prop {boolean} disabled=false
|
|
14
|
+
// @prop {string} icon='' — optional leading icon
|
|
15
|
+
// @event ui-toggle — pressed; detail: { value } (consumed by ui-toggle-group)
|
|
16
|
+
// @slot (default) — label
|
|
17
|
+
// @part control — the <button>
|
|
18
|
+
// @vars see `t` below (`themeVars.names`)
|
|
19
|
+
|
|
20
|
+
import { define, html, css, vars, computed } from '@alacris/core';
|
|
21
|
+
import { sys } from '../tokens/sys.js';
|
|
22
|
+
import { base, focusRingOn } from './base.js';
|
|
23
|
+
import { ripple } from '../motion/ripple.js';
|
|
24
|
+
import './ui-icon.js';
|
|
25
|
+
|
|
26
|
+
const t = vars('ui-toggle-button', {
|
|
27
|
+
height: '40px',
|
|
28
|
+
radius: sys.radius.full,
|
|
29
|
+
bg: sys.color.surface,
|
|
30
|
+
fg: sys.color.onSurface,
|
|
31
|
+
selectedBg: sys.color.secondaryContainer,
|
|
32
|
+
selectedFg: sys.color.onSecondaryContainer,
|
|
33
|
+
font: sys.type.labelLg,
|
|
34
|
+
tracking: sys.tracking.labelLg,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const styles = css`
|
|
38
|
+
:host { display: inline-flex; vertical-align: middle; flex: 1; }
|
|
39
|
+
.control {
|
|
40
|
+
position: relative;
|
|
41
|
+
isolation: isolate;
|
|
42
|
+
display: inline-flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
justify-content: center;
|
|
45
|
+
inline-size: 100%;
|
|
46
|
+
block-size: calc(${t.height} + var(--ui-density, 0) * 4px);
|
|
47
|
+
padding-inline: ${sys.space(3)};
|
|
48
|
+
border: none;
|
|
49
|
+
border-radius: ${t.radius};
|
|
50
|
+
background: ${t.bg};
|
|
51
|
+
color: ${t.fg};
|
|
52
|
+
font: ${t.font};
|
|
53
|
+
letter-spacing: ${t.tracking};
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
user-select: none;
|
|
56
|
+
white-space: nowrap;
|
|
57
|
+
transition: background-color ${sys.duration.short4} ${sys.easing.standard},
|
|
58
|
+
color ${sys.duration.short4} ${sys.easing.standard};
|
|
59
|
+
--ui-icon-size: 1.125rem;
|
|
60
|
+
}
|
|
61
|
+
.lead {
|
|
62
|
+
position: relative;
|
|
63
|
+
display: inline-grid;
|
|
64
|
+
place-items: center;
|
|
65
|
+
inline-size: 0;
|
|
66
|
+
min-inline-size: 0;
|
|
67
|
+
block-size: 1.125rem;
|
|
68
|
+
overflow: hidden;
|
|
69
|
+
opacity: 0;
|
|
70
|
+
pointer-events: none;
|
|
71
|
+
transition: inline-size ${sys.duration.short4} ${sys.easing.emphasized},
|
|
72
|
+
margin-inline-end ${sys.duration.short4} ${sys.easing.emphasized},
|
|
73
|
+
opacity ${sys.duration.short2} ${sys.easing.standard};
|
|
74
|
+
}
|
|
75
|
+
.lead.on {
|
|
76
|
+
inline-size: 1.125rem;
|
|
77
|
+
margin-inline-end: ${sys.space(2)};
|
|
78
|
+
opacity: 1;
|
|
79
|
+
pointer-events: auto;
|
|
80
|
+
}
|
|
81
|
+
.lead ui-icon {
|
|
82
|
+
grid-area: 1 / 1;
|
|
83
|
+
display: inline-flex;
|
|
84
|
+
align-items: center;
|
|
85
|
+
justify-content: center;
|
|
86
|
+
transition: opacity ${sys.duration.short3} ${sys.easing.standard},
|
|
87
|
+
scale ${sys.duration.short3} ${sys.easing.emphasized},
|
|
88
|
+
transform ${sys.duration.short3} ${sys.easing.emphasized};
|
|
89
|
+
}
|
|
90
|
+
.lead .check-glyph {
|
|
91
|
+
opacity: 0;
|
|
92
|
+
scale: 0.5;
|
|
93
|
+
pointer-events: none;
|
|
94
|
+
}
|
|
95
|
+
.lead .check-glyph.on {
|
|
96
|
+
opacity: 1;
|
|
97
|
+
scale: 1;
|
|
98
|
+
pointer-events: auto;
|
|
99
|
+
}
|
|
100
|
+
.lead .custom-glyph {
|
|
101
|
+
opacity: 1;
|
|
102
|
+
scale: 1;
|
|
103
|
+
pointer-events: auto;
|
|
104
|
+
}
|
|
105
|
+
.lead .custom-glyph.off {
|
|
106
|
+
opacity: 0;
|
|
107
|
+
scale: 0.5;
|
|
108
|
+
pointer-events: none;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
${focusRingOn('.control')}
|
|
112
|
+
.layer {
|
|
113
|
+
position: absolute; inset: 0; z-index: -1;
|
|
114
|
+
border-radius: inherit;
|
|
115
|
+
background: currentColor;
|
|
116
|
+
opacity: 0;
|
|
117
|
+
transition: opacity ${sys.duration.short2} ${sys.easing.standard};
|
|
118
|
+
}
|
|
119
|
+
.control:hover .layer { opacity: ${sys.state.hover}; }
|
|
120
|
+
.control:focus-visible .layer { opacity: ${sys.state.focus}; }
|
|
121
|
+
.control:active .layer { opacity: ${sys.state.pressed}; }
|
|
122
|
+
|
|
123
|
+
.control[aria-pressed="true"] {
|
|
124
|
+
background: ${t.selectedBg};
|
|
125
|
+
color: ${t.selectedFg};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.control:disabled {
|
|
129
|
+
cursor: default;
|
|
130
|
+
pointer-events: none;
|
|
131
|
+
color: color-mix(in srgb, ${sys.color.onSurface} calc(${sys.state.disabledContent} * 100%), transparent);
|
|
132
|
+
}
|
|
133
|
+
.control:disabled[aria-pressed="true"] {
|
|
134
|
+
background: color-mix(in srgb, ${sys.color.onSurface} calc(${sys.state.disabledContainer} * 100%), transparent);
|
|
135
|
+
}
|
|
136
|
+
`;
|
|
137
|
+
|
|
138
|
+
define('ui-toggle-button', {
|
|
139
|
+
props: { value: '', selected: false, disabled: false, icon: '' },
|
|
140
|
+
styles: [base, styles],
|
|
141
|
+
setup({ value, selected, disabled, icon }, host) {
|
|
142
|
+
const onClick = () => {
|
|
143
|
+
if (disabled()) return;
|
|
144
|
+
host.emit('ui-toggle', { value: value() });
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const isSelected = () => selected();
|
|
148
|
+
const hasIcon = () => !!icon();
|
|
149
|
+
const showLead = () => isSelected() || hasIcon();
|
|
150
|
+
|
|
151
|
+
return html`
|
|
152
|
+
<button part="control" class="control" type="button" ?disabled=${disabled}
|
|
153
|
+
aria-pressed=${() => String(selected())}
|
|
154
|
+
@click=${onClick} ref=${(el) => ripple(el, { disabled })}>
|
|
155
|
+
<span class="layer" aria-hidden="true"></span>
|
|
156
|
+
<span class=${() => `lead${showLead() ? ' on' : ''}`}>
|
|
157
|
+
<ui-icon class=${() => `check-glyph${isSelected() ? ' on' : ''}`}
|
|
158
|
+
name="check"></ui-icon>
|
|
159
|
+
${() => (hasIcon()
|
|
160
|
+
? html`<ui-icon class=${() => `custom-glyph${isSelected() ? ' off' : ''}`}
|
|
161
|
+
name=${icon}></ui-icon>`
|
|
162
|
+
: null)}
|
|
163
|
+
</span>
|
|
164
|
+
<slot></slot>
|
|
165
|
+
</button>`;
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
export const tag = 'ui-toggle-button';
|
|
170
|
+
export const themeVars = t;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// <ui-toggle-group> — Material segmented buttons: an outlined container that
|
|
2
|
+
// owns the selection of its slotted <ui-toggle-button>s.
|
|
3
|
+
//
|
|
4
|
+
// <ui-toggle-group value=${align} @change=${(e) => align.set(e.detail.value)}>
|
|
5
|
+
// <ui-toggle-button value="left">Left</ui-toggle-button>
|
|
6
|
+
// <ui-toggle-button value="right">Right</ui-toggle-button>
|
|
7
|
+
// </ui-toggle-group>
|
|
8
|
+
//
|
|
9
|
+
// Single-select by default (`value` is the selected string, '' for none;
|
|
10
|
+
// pressing the selected segment deselects it). With `multi`, `value` is an
|
|
11
|
+
// array (a JSON array string works as an attribute). Buttons are natural tab
|
|
12
|
+
// stops — no roving tabindex, per the toolbar-of-toggle-buttons pattern.
|
|
13
|
+
//
|
|
14
|
+
// @prop {string|array} value='' — selected value, or array when `multi`
|
|
15
|
+
// @prop {boolean} multi=false — multiple segments may be selected
|
|
16
|
+
// @prop {boolean} disabled=false — disables every segment
|
|
17
|
+
// @prop {string} label='' — accessible name for the group
|
|
18
|
+
// @event change — selection changed; detail: { value } (string, or array when multi)
|
|
19
|
+
// @slot (default) — the <ui-toggle-button> children
|
|
20
|
+
// @part group — the outlined clipping container
|
|
21
|
+
// @vars see `t` below (`themeVars.names`)
|
|
22
|
+
|
|
23
|
+
import { define, html, css, vars, effect, untrack } from '@alacris/core';
|
|
24
|
+
import { sys } from '../tokens/sys.js';
|
|
25
|
+
import { base } from './base.js';
|
|
26
|
+
import './ui-toggle-button.js';
|
|
27
|
+
|
|
28
|
+
const t = vars('ui-toggle-group', {
|
|
29
|
+
radius: sys.radius.full,
|
|
30
|
+
outlineColor: sys.color.outline,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const styles = css`
|
|
34
|
+
:host { display: inline-flex; vertical-align: middle; }
|
|
35
|
+
.group {
|
|
36
|
+
display: inline-flex;
|
|
37
|
+
align-items: stretch;
|
|
38
|
+
gap: 1px;
|
|
39
|
+
border: 1px solid ${t.outlineColor};
|
|
40
|
+
background: ${t.outlineColor};
|
|
41
|
+
border-radius: ${t.radius};
|
|
42
|
+
overflow: hidden;
|
|
43
|
+
}
|
|
44
|
+
::slotted(ui-toggle-button) { --ui-toggle-button-radius: 0; flex: 1; }
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
define('ui-toggle-group', {
|
|
48
|
+
props: { value: '', multi: false, disabled: false, label: '' },
|
|
49
|
+
styles: [base, styles],
|
|
50
|
+
setup({ value, multi, disabled, label }, host) {
|
|
51
|
+
const buttons = () => [...host.querySelectorAll('ui-toggle-button')];
|
|
52
|
+
|
|
53
|
+
/** Current selection, normalized to an array of values. */
|
|
54
|
+
const selection = () => {
|
|
55
|
+
const v = value();
|
|
56
|
+
if (Array.isArray(v)) return v;
|
|
57
|
+
if (typeof v === 'string' && v.trim().startsWith('[')) {
|
|
58
|
+
try { return JSON.parse(v); } catch { return []; }
|
|
59
|
+
}
|
|
60
|
+
return v == null || v === '' ? [] : [v];
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Reflect the selection down whenever value changes …
|
|
64
|
+
const sync = () => {
|
|
65
|
+
const sel = selection();
|
|
66
|
+
for (const b of buttons()) b.selected = sel.includes(b.value);
|
|
67
|
+
};
|
|
68
|
+
effect(sync);
|
|
69
|
+
// … and once after children have upgraded/distributed.
|
|
70
|
+
queueMicrotask(() => untrack(sync));
|
|
71
|
+
|
|
72
|
+
// Group-level disabling: remember which children we disabled so their own
|
|
73
|
+
// `disabled` survives the round trip.
|
|
74
|
+
let claimed = null;
|
|
75
|
+
effect(() => {
|
|
76
|
+
if (disabled()) {
|
|
77
|
+
if (claimed) return;
|
|
78
|
+
claimed = new Set();
|
|
79
|
+
for (const b of buttons()) if (!b.disabled) { b.disabled = true; claimed.add(b); }
|
|
80
|
+
} else if (claimed) {
|
|
81
|
+
for (const b of claimed) b.disabled = false;
|
|
82
|
+
claimed = null;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
host.addEventListener('ui-toggle', (e) => {
|
|
87
|
+
e.stopPropagation();
|
|
88
|
+
if (disabled()) return;
|
|
89
|
+
const v = e.detail.value;
|
|
90
|
+
let next;
|
|
91
|
+
if (multi()) {
|
|
92
|
+
const sel = selection();
|
|
93
|
+
next = sel.includes(v) ? sel.filter((x) => x !== v) : [...sel, v];
|
|
94
|
+
} else {
|
|
95
|
+
next = selection().includes(v) ? '' : v;
|
|
96
|
+
}
|
|
97
|
+
value.set(next);
|
|
98
|
+
host.emit('change', { value: next });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return html`
|
|
102
|
+
<div class="group" part="group" role="group"
|
|
103
|
+
aria-label=${() => label() || null}
|
|
104
|
+
aria-disabled=${() => (disabled() ? 'true' : null)}>
|
|
105
|
+
<slot @slotchange=${() => untrack(sync)}></slot>
|
|
106
|
+
</div>`;
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
export const tag = 'ui-toggle-group';
|
|
111
|
+
export const themeVars = t;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// <ui-toolbar> — a Material contextual toolbar: a floating strip of icon
|
|
2
|
+
// actions, optionally with an attached FAB.
|
|
3
|
+
//
|
|
4
|
+
// <ui-toolbar label="Selection">
|
|
5
|
+
// <ui-icon-button icon="edit" label="Edit"></ui-icon-button>
|
|
6
|
+
// <ui-icon-button icon="delete" label="Delete"></ui-icon-button>
|
|
7
|
+
// <ui-fab slot="fab" icon="add" size="sm"></ui-fab>
|
|
8
|
+
// </ui-toolbar>
|
|
9
|
+
//
|
|
10
|
+
// @prop {string} label='' — accessible name for the toolbar
|
|
11
|
+
// @slot (default) — icon buttons and other actions
|
|
12
|
+
// @slot fab — optional <ui-fab> attached to the end
|
|
13
|
+
// @part bar, actions, fab
|
|
14
|
+
// @vars see `t` below (`themeVars.names`)
|
|
15
|
+
|
|
16
|
+
import { define, html, css, vars } from '@alacris/core';
|
|
17
|
+
import { sys } from '../tokens/sys.js';
|
|
18
|
+
import { base } from './base.js';
|
|
19
|
+
|
|
20
|
+
const t = vars('ui-toolbar', {
|
|
21
|
+
height: '64px',
|
|
22
|
+
bg: sys.color.surfaceContainer,
|
|
23
|
+
fg: sys.color.onSurface,
|
|
24
|
+
radius: sys.radius.full,
|
|
25
|
+
pad: sys.space(2),
|
|
26
|
+
elevation: sys.elevation[2],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const styles = css`
|
|
30
|
+
:host { display: inline-flex; vertical-align: middle; }
|
|
31
|
+
.bar {
|
|
32
|
+
display: inline-flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
gap: ${sys.space(1)};
|
|
35
|
+
min-block-size: ${t.height};
|
|
36
|
+
padding: ${t.pad};
|
|
37
|
+
background: ${t.bg};
|
|
38
|
+
color: ${t.fg};
|
|
39
|
+
border-radius: ${t.radius};
|
|
40
|
+
box-shadow: ${t.elevation};
|
|
41
|
+
}
|
|
42
|
+
.actions {
|
|
43
|
+
display: inline-flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
gap: ${sys.space(1)};
|
|
46
|
+
padding-inline: ${sys.space(1)};
|
|
47
|
+
}
|
|
48
|
+
.fab { display: inline-flex; align-items: center; }
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
define('ui-toolbar', {
|
|
52
|
+
props: { label: '' },
|
|
53
|
+
styles: [base, styles],
|
|
54
|
+
setup({ label }) {
|
|
55
|
+
return html`
|
|
56
|
+
<div class="bar" part="bar" role="toolbar" aria-label=${() => label() || null}>
|
|
57
|
+
<div class="actions" part="actions"><slot></slot></div>
|
|
58
|
+
<div class="fab" part="fab"><slot name="fab"></slot></div>
|
|
59
|
+
</div>`;
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export const tag = 'ui-toolbar';
|
|
64
|
+
export const themeVars = t;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// <ui-tooltip> — a plain (or rich) tooltip on hover/focus around its target.
|
|
2
|
+
//
|
|
3
|
+
// <ui-tooltip text="Save changes"><ui-button>Save</ui-button></ui-tooltip>
|
|
4
|
+
//
|
|
5
|
+
// Shows after `delay` ms on pointerenter (timer cancelled on leave) and
|
|
6
|
+
// immediately on focusin; hides on pointerleave, focusout, and Escape. The
|
|
7
|
+
// panel is position: fixed, anchored to the host with position()/autoUpdate,
|
|
8
|
+
// flipping to the opposite side when it would overflow the viewport.
|
|
9
|
+
//
|
|
10
|
+
// a11y: `aria-describedby` cannot point across shadow boundaries, so the
|
|
11
|
+
// association is not programmatic — the panel carries role="tooltip" and the
|
|
12
|
+
// target keeps its own accessible name. Give the target a matching
|
|
13
|
+
// label/aria-label when the tooltip is the only description.
|
|
14
|
+
//
|
|
15
|
+
// @prop {string} text='' — plain tooltip content
|
|
16
|
+
// @prop {string} position='top' — top | bottom | left | right
|
|
17
|
+
// @prop {number} delay=500 — hover show delay (ms); focus shows instantly
|
|
18
|
+
// @prop {boolean} rich=false — renders the `content` slot in a surface
|
|
19
|
+
// panel instead of the text pill
|
|
20
|
+
// @slot (default) — the target
|
|
21
|
+
// @slot content — rich tooltip content (title/body/actions) when `rich`
|
|
22
|
+
// @part panel — the tooltip surface
|
|
23
|
+
// @vars see `t` below
|
|
24
|
+
|
|
25
|
+
import { define, html, css, vars, effect, onCleanup, signal } from '@alacris/core';
|
|
26
|
+
import { sys } from '../tokens/sys.js';
|
|
27
|
+
import { base } from './base.js';
|
|
28
|
+
import { presence } from '../motion/presence.js';
|
|
29
|
+
import { fx } from '../motion/animate.js';
|
|
30
|
+
import { autoUpdate } from '../util/position.js';
|
|
31
|
+
|
|
32
|
+
const t = vars('ui-tooltip', {
|
|
33
|
+
bg: sys.color.inverseSurface,
|
|
34
|
+
fg: sys.color.inverseOnSurface,
|
|
35
|
+
font: sys.type.labelSm,
|
|
36
|
+
radius: sys.radius.xs,
|
|
37
|
+
maxWidth: '200px',
|
|
38
|
+
richBg: sys.color.surfaceContainer,
|
|
39
|
+
richFg: sys.color.onSurfaceVariant,
|
|
40
|
+
richRadius: sys.radius.md,
|
|
41
|
+
richMaxWidth: '320px',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const styles = css`
|
|
45
|
+
:host { display: inline-block; }
|
|
46
|
+
.panel {
|
|
47
|
+
position: fixed;
|
|
48
|
+
inset-inline-start: 0;
|
|
49
|
+
inset-block-start: 0;
|
|
50
|
+
z-index: ${sys.z.tooltip};
|
|
51
|
+
inline-size: max-content;
|
|
52
|
+
}
|
|
53
|
+
.plain {
|
|
54
|
+
background: ${t.bg};
|
|
55
|
+
color: ${t.fg};
|
|
56
|
+
font: ${t.font};
|
|
57
|
+
letter-spacing: ${sys.tracking.labelSm};
|
|
58
|
+
border-radius: ${t.radius};
|
|
59
|
+
padding: ${sys.space(1)} ${sys.space(2)};
|
|
60
|
+
max-inline-size: ${t.maxWidth};
|
|
61
|
+
pointer-events: none;
|
|
62
|
+
}
|
|
63
|
+
.rich {
|
|
64
|
+
background: ${t.richBg};
|
|
65
|
+
color: ${t.richFg};
|
|
66
|
+
font: ${sys.type.bodyMd};
|
|
67
|
+
letter-spacing: ${sys.tracking.bodyMd};
|
|
68
|
+
border-radius: ${t.richRadius};
|
|
69
|
+
box-shadow: ${sys.elevation[2]};
|
|
70
|
+
padding: ${sys.space(3)};
|
|
71
|
+
max-inline-size: ${t.richMaxWidth};
|
|
72
|
+
}
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
define('ui-tooltip', {
|
|
76
|
+
props: { text: '', position: 'top', delay: 500, rich: false },
|
|
77
|
+
styles: [base, styles],
|
|
78
|
+
setup({ text, position: pos, delay, rich }, host) {
|
|
79
|
+
const open = signal(false);
|
|
80
|
+
let timer = 0;
|
|
81
|
+
let stopAuto = null;
|
|
82
|
+
|
|
83
|
+
const show = () => open.set(true);
|
|
84
|
+
const hide = () => {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
timer = 0;
|
|
87
|
+
open.set(false);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
host.addEventListener('pointerenter', () => {
|
|
91
|
+
clearTimeout(timer);
|
|
92
|
+
const d = Math.max(0, delay());
|
|
93
|
+
if (d) timer = setTimeout(show, d);
|
|
94
|
+
else show();
|
|
95
|
+
});
|
|
96
|
+
host.addEventListener('pointerleave', hide);
|
|
97
|
+
host.addEventListener('focusin', show); // no delay on keyboard focus
|
|
98
|
+
host.addEventListener('focusout', hide);
|
|
99
|
+
|
|
100
|
+
// Escape hides even while focus is elsewhere on the page.
|
|
101
|
+
effect(() => {
|
|
102
|
+
if (!open()) return;
|
|
103
|
+
const onKey = (e) => { if (e.key === 'Escape') hide(); };
|
|
104
|
+
document.addEventListener('keydown', onKey, true);
|
|
105
|
+
return () => document.removeEventListener('keydown', onKey, true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Anchor tracking lives exactly as long as the panel does.
|
|
109
|
+
effect(() => {
|
|
110
|
+
if (!open()) {
|
|
111
|
+
stopAuto?.();
|
|
112
|
+
stopAuto = null;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
onCleanup(() => {
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
stopAuto?.();
|
|
118
|
+
stopAuto = null;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const anchor = (el) => {
|
|
122
|
+
stopAuto?.();
|
|
123
|
+
stopAuto = autoUpdate(el, host, {
|
|
124
|
+
placement: `${pos()}-center`,
|
|
125
|
+
offset: 8,
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const view = () => html`
|
|
130
|
+
<div part="panel" role="tooltip"
|
|
131
|
+
class=${() => `panel ${rich() ? 'rich' : 'plain'}`}
|
|
132
|
+
ref=${anchor}>
|
|
133
|
+
${() => (rich() ? html`<slot name="content"></slot>` : html`${text}`)}
|
|
134
|
+
</div>`;
|
|
135
|
+
|
|
136
|
+
return html`
|
|
137
|
+
<slot></slot>
|
|
138
|
+
${presence(open, view, {
|
|
139
|
+
enter: fx.fadeIn,
|
|
140
|
+
exit: fx.fadeOut,
|
|
141
|
+
enterDuration: 'short2',
|
|
142
|
+
exitDuration: 'short2',
|
|
143
|
+
})}`;
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
export const tag = 'ui-tooltip';
|
|
148
|
+
export const themeVars = t;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Alacris UI — public surface (`@alacris/ui` on npm).
|
|
2
|
+
//
|
|
3
|
+
// Importing this module registers every component and re-exports the theme,
|
|
4
|
+
// motion, token, and utility APIs. Tree-shaking-friendly alternative: import
|
|
5
|
+
// only the component files and helpers you use — each component module is
|
|
6
|
+
// self-contained. Do not bundle a second copy of `@alacris/core`; these modules
|
|
7
|
+
// import it by specifier so the app and the design system share one graph.
|
|
8
|
+
|
|
9
|
+
// Theme
|
|
10
|
+
export {
|
|
11
|
+
createTheme, applyTheme, themeCss, activeTheme, loadThemeFonts,
|
|
12
|
+
scheme, schemePreference, setScheme, toggleScheme,
|
|
13
|
+
} from './theme/index.js';
|
|
14
|
+
|
|
15
|
+
// Tokens
|
|
16
|
+
export { sys, typeRule } from './tokens/sys.js';
|
|
17
|
+
export {
|
|
18
|
+
FONT_PRESETS, FONT_STACKS, DEFAULT_FONT_PRESET, TYPE_ROLES,
|
|
19
|
+
resolveTypography, googleFontsHref,
|
|
20
|
+
} from './tokens/typography.js';
|
|
21
|
+
|
|
22
|
+
// Motion
|
|
23
|
+
export { animate, settled, duration, easing, fx, prefersReducedMotion } from './motion/animate.js';
|
|
24
|
+
export { presence } from './motion/presence.js';
|
|
25
|
+
export { withFlip } from './motion/flip.js';
|
|
26
|
+
export { ripple } from './motion/ripple.js';
|
|
27
|
+
|
|
28
|
+
// Utilities
|
|
29
|
+
export { position, autoUpdate } from './util/position.js';
|
|
30
|
+
export { focusTrap, focusables, scrollLock } from './util/focus.js';
|
|
31
|
+
export { rovingTabindex } from './util/keys.js';
|
|
32
|
+
export { formBind } from './util/form.js';
|
|
33
|
+
export { registerIcons, iconPath, iconNames } from './util/icons.js';
|
|
34
|
+
export {
|
|
35
|
+
processTable, filterRows, sortRows, groupRows, paginate,
|
|
36
|
+
aggregateRow, toCsv, downloadText, inferColumns, visibleColumns,
|
|
37
|
+
} from './util/table.js';
|
|
38
|
+
|
|
39
|
+
// Components — importing registers the custom element.
|
|
40
|
+
import './components/ui-icon.js';
|
|
41
|
+
import './components/ui-text.js';
|
|
42
|
+
|
|
43
|
+
// Actions
|
|
44
|
+
import './components/ui-button.js';
|
|
45
|
+
import './components/ui-icon-button.js';
|
|
46
|
+
import './components/ui-fab.js';
|
|
47
|
+
import './components/ui-fab-menu.js';
|
|
48
|
+
import './components/ui-button-group.js';
|
|
49
|
+
import './components/ui-split-button.js';
|
|
50
|
+
import './components/ui-toggle-button.js';
|
|
51
|
+
import './components/ui-toggle-group.js';
|
|
52
|
+
|
|
53
|
+
// Inputs
|
|
54
|
+
import './components/ui-checkbox.js';
|
|
55
|
+
import './components/ui-radio.js';
|
|
56
|
+
import './components/ui-radio-group.js';
|
|
57
|
+
import './components/ui-switch.js';
|
|
58
|
+
import './components/ui-slider.js';
|
|
59
|
+
import './components/ui-rating.js';
|
|
60
|
+
import './components/ui-text-field.js';
|
|
61
|
+
import './components/ui-select.js';
|
|
62
|
+
import './components/ui-option.js';
|
|
63
|
+
import './components/ui-autocomplete.js';
|
|
64
|
+
import './components/ui-chip.js';
|
|
65
|
+
import './components/ui-chip-set.js';
|
|
66
|
+
import './components/ui-date-picker.js';
|
|
67
|
+
import './components/ui-time-picker.js';
|
|
68
|
+
import './components/ui-search.js';
|
|
69
|
+
|
|
70
|
+
// Data display
|
|
71
|
+
import './components/ui-avatar.js';
|
|
72
|
+
import './components/ui-badge.js';
|
|
73
|
+
import './components/ui-divider.js';
|
|
74
|
+
import './components/ui-list.js';
|
|
75
|
+
import './components/ui-list-item.js';
|
|
76
|
+
import './components/ui-table.js';
|
|
77
|
+
import './components/ui-table-toolbar.js';
|
|
78
|
+
import './components/ui-table-footer.js';
|
|
79
|
+
import './components/ui-tooltip.js';
|
|
80
|
+
import './components/ui-carousel.js';
|
|
81
|
+
import './components/ui-carousel-item.js';
|
|
82
|
+
|
|
83
|
+
// Feedback
|
|
84
|
+
import './components/ui-alert.js';
|
|
85
|
+
import './components/ui-progress.js';
|
|
86
|
+
import './components/ui-spinner.js';
|
|
87
|
+
import './components/ui-loading-indicator.js';
|
|
88
|
+
import './components/ui-skeleton.js';
|
|
89
|
+
import './components/ui-snackbar.js';
|
|
90
|
+
import './components/ui-backdrop.js';
|
|
91
|
+
import './components/ui-sheet.js';
|
|
92
|
+
import './components/ui-side-sheet.js';
|
|
93
|
+
|
|
94
|
+
export { showSnackbar } from './components/ui-snackbar.js';
|
|
95
|
+
|
|
96
|
+
// Surfaces & navigation
|
|
97
|
+
import './components/ui-card.js';
|
|
98
|
+
import './components/ui-dialog.js';
|
|
99
|
+
import './components/ui-tabs.js';
|
|
100
|
+
import './components/ui-tab.js';
|
|
101
|
+
import './components/ui-tab-panel.js';
|
|
102
|
+
import './components/ui-menu.js';
|
|
103
|
+
import './components/ui-menu-item.js';
|
|
104
|
+
import './components/ui-drawer.js';
|
|
105
|
+
import './components/ui-app-bar.js';
|
|
106
|
+
import './components/ui-bottom-app-bar.js';
|
|
107
|
+
import './components/ui-toolbar.js';
|
|
108
|
+
import './components/ui-accordion.js';
|
|
109
|
+
import './components/ui-accordion-item.js';
|
|
110
|
+
import './components/ui-breadcrumbs.js';
|
|
111
|
+
import './components/ui-pagination.js';
|
|
112
|
+
import './components/ui-stepper.js';
|
|
113
|
+
import './components/ui-step.js';
|
|
114
|
+
import './components/ui-bottom-nav.js';
|
|
115
|
+
import './components/ui-nav-item.js';
|
|
116
|
+
import './components/ui-nav-rail.js';
|
|
117
|
+
|
|
118
|
+
// Layout
|
|
119
|
+
import './components/ui-stack.js';
|
|
120
|
+
import './components/ui-container.js';
|
|
121
|
+
import './components/ui-surface.js';
|