@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,95 @@
|
|
|
1
|
+
// <ui-badge> — a small status badge anchored to the top-end corner of its
|
|
2
|
+
// slotted content (an icon button, a nav item, an avatar…).
|
|
3
|
+
//
|
|
4
|
+
// Hidden while `value` is 0 unless `dot`. Counts above `max` render '99+'
|
|
5
|
+
// style. Appearing/disappearing animates with a scale-in/out.
|
|
6
|
+
//
|
|
7
|
+
// @prop {number} value=0 — the count; 0 hides the badge (unless `dot`)
|
|
8
|
+
// @prop {number} max=99 — counts above this render as 'max+'
|
|
9
|
+
// @prop {boolean} dot=false — a plain 8px dot instead of a count
|
|
10
|
+
// @prop {boolean} show=true — master visibility switch
|
|
11
|
+
// @prop {string} label='' — accessible meaning of the badge ('3 unread');
|
|
12
|
+
// empty marks the badge decorative
|
|
13
|
+
// @slot (default) — the anchored content
|
|
14
|
+
// @part badge — the badge pill/dot
|
|
15
|
+
// @vars --ui-badge-bg, --ui-badge-fg, --ui-badge-size, --ui-badge-dot-size,
|
|
16
|
+
// --ui-badge-font
|
|
17
|
+
|
|
18
|
+
import { define, html, css, vars, computed } from '@alacris/core';
|
|
19
|
+
import { sys } from '../tokens/sys.js';
|
|
20
|
+
import { base } from './base.js';
|
|
21
|
+
import { presence } from '../motion/presence.js';
|
|
22
|
+
import { fx } from '../motion/animate.js';
|
|
23
|
+
|
|
24
|
+
const t = vars('ui-badge', {
|
|
25
|
+
bg: sys.color.error,
|
|
26
|
+
fg: sys.color.onError,
|
|
27
|
+
size: '16px',
|
|
28
|
+
dotSize: '8px',
|
|
29
|
+
font: sys.type.labelSm,
|
|
30
|
+
tracking: sys.tracking.labelSm,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const styles = css`
|
|
34
|
+
:host {
|
|
35
|
+
position: relative;
|
|
36
|
+
display: inline-flex;
|
|
37
|
+
vertical-align: middle;
|
|
38
|
+
}
|
|
39
|
+
.badge {
|
|
40
|
+
position: absolute;
|
|
41
|
+
inset-block-start: 0;
|
|
42
|
+
inset-inline-end: 0;
|
|
43
|
+
transform: translate(50%, -50%);
|
|
44
|
+
z-index: 1;
|
|
45
|
+
display: inline-flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
justify-content: center;
|
|
48
|
+
min-block-size: ${t.size};
|
|
49
|
+
min-inline-size: ${t.size};
|
|
50
|
+
padding-inline: ${sys.space(1)};
|
|
51
|
+
border-radius: ${sys.radius.full};
|
|
52
|
+
background: ${t.bg};
|
|
53
|
+
color: ${t.fg};
|
|
54
|
+
font: ${t.font};
|
|
55
|
+
letter-spacing: ${t.tracking};
|
|
56
|
+
line-height: 1;
|
|
57
|
+
pointer-events: none;
|
|
58
|
+
white-space: nowrap;
|
|
59
|
+
}
|
|
60
|
+
.badge.dot {
|
|
61
|
+
min-block-size: ${t.dotSize};
|
|
62
|
+
min-inline-size: ${t.dotSize};
|
|
63
|
+
block-size: ${t.dotSize};
|
|
64
|
+
inline-size: ${t.dotSize};
|
|
65
|
+
padding: 0;
|
|
66
|
+
}
|
|
67
|
+
`;
|
|
68
|
+
|
|
69
|
+
define('ui-badge', {
|
|
70
|
+
props: { value: 0, max: 99, dot: false, show: true, label: '' },
|
|
71
|
+
styles: [base, styles],
|
|
72
|
+
setup({ value, max, dot, show, label }) {
|
|
73
|
+
const visible = computed(() => show() && (dot() || value() > 0));
|
|
74
|
+
const text = computed(() =>
|
|
75
|
+
dot() ? '' : value() > max() ? `${max()}+` : String(value()));
|
|
76
|
+
|
|
77
|
+
const view = () => html`
|
|
78
|
+
<span part="badge"
|
|
79
|
+
class=${() => `badge${dot() ? ' dot' : ''}`}
|
|
80
|
+
aria-label=${() => label() || null}
|
|
81
|
+
aria-hidden=${() => (label() ? null : 'true')}>${text}</span>`;
|
|
82
|
+
|
|
83
|
+
return html`
|
|
84
|
+
<slot></slot>
|
|
85
|
+
${presence(visible, view, {
|
|
86
|
+
enter: fx.scaleIn,
|
|
87
|
+
exit: fx.scaleOut,
|
|
88
|
+
enterDuration: 'short4',
|
|
89
|
+
exitDuration: 'short2',
|
|
90
|
+
})}`;
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
export const tag = 'ui-badge';
|
|
95
|
+
export const themeVars = t;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// <ui-bottom-app-bar> — the Material bottom app bar.
|
|
2
|
+
//
|
|
3
|
+
// <ui-bottom-app-bar>
|
|
4
|
+
// <ui-icon-button slot="navigation" icon="menu" label="Menu"></ui-icon-button>
|
|
5
|
+
// <ui-icon-button slot="actions" icon="search" label="Search"></ui-icon-button>
|
|
6
|
+
// <ui-fab slot="fab" icon="add"></ui-fab>
|
|
7
|
+
// </ui-bottom-app-bar>
|
|
8
|
+
//
|
|
9
|
+
// A surface for a leading navigation icon, trailing actions, and an optional
|
|
10
|
+
// FAB. The host flows with the page — pin it with CSS when it should hug the
|
|
11
|
+
// viewport bottom. Distinct from <ui-bottom-nav>, which is destination tabs.
|
|
12
|
+
//
|
|
13
|
+
// @prop {string} label='Bottom app bar' — accessible name of the toolbar
|
|
14
|
+
// @prop {string} fabAlign='end' — end | center | start — where the FAB sits
|
|
15
|
+
// @slot navigation — leading icon button
|
|
16
|
+
// @slot actions — trailing icon buttons
|
|
17
|
+
// @slot fab — optional <ui-fab>
|
|
18
|
+
// @part bar, navigation, actions, fab
|
|
19
|
+
// @vars see `t` below (`themeVars.names`)
|
|
20
|
+
|
|
21
|
+
import { define, html, css, vars, computed } from '@alacris/core';
|
|
22
|
+
import { sys } from '../tokens/sys.js';
|
|
23
|
+
import { base } from './base.js';
|
|
24
|
+
|
|
25
|
+
const t = vars('ui-bottom-app-bar', {
|
|
26
|
+
height: '80px',
|
|
27
|
+
bg: sys.color.surfaceContainer,
|
|
28
|
+
fg: sys.color.onSurface,
|
|
29
|
+
shadow: sys.elevation[2],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const styles = css`
|
|
33
|
+
:host { display: block; }
|
|
34
|
+
.bar {
|
|
35
|
+
position: relative;
|
|
36
|
+
display: flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
block-size: ${t.height};
|
|
39
|
+
padding-inline: ${sys.space(1)} ${sys.space(4)};
|
|
40
|
+
background: ${t.bg};
|
|
41
|
+
color: ${t.fg};
|
|
42
|
+
box-shadow: ${t.shadow};
|
|
43
|
+
}
|
|
44
|
+
.nav, .actions, .fab {
|
|
45
|
+
display: inline-flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
gap: ${sys.space(1)};
|
|
48
|
+
}
|
|
49
|
+
.nav { order: 0; }
|
|
50
|
+
.actions { order: 1; margin-inline-start: auto; }
|
|
51
|
+
.fab { order: 2; margin-inline-start: ${sys.space(2)}; }
|
|
52
|
+
.start .fab { order: 1; }
|
|
53
|
+
.start .actions { order: 2; }
|
|
54
|
+
.center .fab {
|
|
55
|
+
order: 0;
|
|
56
|
+
position: absolute;
|
|
57
|
+
inset-inline-start: 50%;
|
|
58
|
+
translate: -50% 0;
|
|
59
|
+
margin: 0;
|
|
60
|
+
}
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
define('ui-bottom-app-bar', {
|
|
64
|
+
props: { label: 'Bottom app bar', fabAlign: 'end' },
|
|
65
|
+
styles: [base, styles],
|
|
66
|
+
setup({ label, fabAlign }) {
|
|
67
|
+
const cls = computed(() => `bar ${fabAlign() === 'center' ? 'center' : fabAlign() === 'start' ? 'start' : 'end'}`);
|
|
68
|
+
return html`
|
|
69
|
+
<div class=${cls} part="bar" role="toolbar" aria-label=${() => label() || 'Bottom app bar'}>
|
|
70
|
+
<div class="nav" part="navigation"><slot name="navigation"></slot></div>
|
|
71
|
+
<div class="fab" part="fab"><slot name="fab"></slot></div>
|
|
72
|
+
<div class="actions" part="actions"><slot name="actions"></slot></div>
|
|
73
|
+
</div>`;
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export const tag = 'ui-bottom-app-bar';
|
|
78
|
+
export const themeVars = t;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// <ui-bottom-nav> — the Material navigation bar.
|
|
2
|
+
//
|
|
3
|
+
// <ui-bottom-nav value=${route} @change=${(e) => route(e.detail.value)}>
|
|
4
|
+
// <ui-nav-item value="home" icon="home" label="Home"></ui-nav-item>
|
|
5
|
+
// <ui-nav-item value="search" icon="search" label="Search"></ui-nav-item>
|
|
6
|
+
// </ui-bottom-nav>
|
|
7
|
+
//
|
|
8
|
+
// @prop {string} value='' — the selected item's `value`
|
|
9
|
+
// @prop {string} label='' — accessible name of the <nav>
|
|
10
|
+
// @event change — a destination was chosen; detail: { value }
|
|
11
|
+
// @slot (default) — <ui-nav-item> children
|
|
12
|
+
// @part bar — the <nav> container
|
|
13
|
+
// @vars see `t` below (`themeVars.names`)
|
|
14
|
+
//
|
|
15
|
+
// The host is `display: block` and flows with the page — apps that want the
|
|
16
|
+
// bar pinned to the viewport position it themselves (position: fixed; bottom:
|
|
17
|
+
// 0; left: 0; right: 0). Selection reflects down: every `value` write (or
|
|
18
|
+
// slotchange) sets `selected` on the children. Keyboard: one Tab stop, arrow
|
|
19
|
+
// keys rove between items (rovingTabindex, horizontal).
|
|
20
|
+
|
|
21
|
+
import { define, html, css, vars, effect, onCleanup } from '@alacris/core';
|
|
22
|
+
import { sys } from '../tokens/sys.js';
|
|
23
|
+
import { base } from './base.js';
|
|
24
|
+
import { rovingTabindex } from '../util/keys.js';
|
|
25
|
+
import './ui-nav-item.js';
|
|
26
|
+
|
|
27
|
+
const t = vars('ui-bottom-nav', {
|
|
28
|
+
height: '80px',
|
|
29
|
+
bg: sys.color.surfaceContainer,
|
|
30
|
+
shadow: 'none',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const styles = css`
|
|
34
|
+
:host { display: block; }
|
|
35
|
+
.bar {
|
|
36
|
+
display: flex;
|
|
37
|
+
align-items: stretch;
|
|
38
|
+
justify-content: space-around;
|
|
39
|
+
block-size: ${t.height};
|
|
40
|
+
background: ${t.bg};
|
|
41
|
+
box-shadow: ${t.shadow};
|
|
42
|
+
}
|
|
43
|
+
::slotted(ui-nav-item) { flex: 1; max-inline-size: 168px; }
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
define('ui-bottom-nav', {
|
|
47
|
+
props: { value: '', label: '' },
|
|
48
|
+
styles: [base, styles],
|
|
49
|
+
setup({ value, label }, host) {
|
|
50
|
+
const roving = rovingTabindex(host, {
|
|
51
|
+
selector: 'ui-nav-item',
|
|
52
|
+
orientation: 'horizontal',
|
|
53
|
+
skip: (el) => el.disabled,
|
|
54
|
+
});
|
|
55
|
+
onCleanup(() => roving.destroy());
|
|
56
|
+
|
|
57
|
+
const sync = () => {
|
|
58
|
+
for (const item of host.querySelectorAll('ui-nav-item')) {
|
|
59
|
+
item.selected = item.value === value();
|
|
60
|
+
}
|
|
61
|
+
roving.refresh();
|
|
62
|
+
};
|
|
63
|
+
effect(sync); // re-runs on every `value` write; slotchange calls it too
|
|
64
|
+
|
|
65
|
+
const onSelect = (e) => {
|
|
66
|
+
const v = e.detail?.value;
|
|
67
|
+
if (v === undefined || v === value()) return;
|
|
68
|
+
value.set(v);
|
|
69
|
+
host.emit('change', { value: v });
|
|
70
|
+
};
|
|
71
|
+
// Items are light-DOM children; their bubbling select events reach the host.
|
|
72
|
+
host.addEventListener('ui-nav-select', onSelect);
|
|
73
|
+
onCleanup(() => host.removeEventListener('ui-nav-select', onSelect));
|
|
74
|
+
|
|
75
|
+
return html`
|
|
76
|
+
<nav class="bar" part="bar" aria-label=${() => label() || null}>
|
|
77
|
+
<slot @slotchange=${sync}></slot>
|
|
78
|
+
</nav>`;
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export const tag = 'ui-bottom-nav';
|
|
83
|
+
export const themeVars = t;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// <ui-breadcrumbs> — a navigation trail of slotted links.
|
|
2
|
+
//
|
|
3
|
+
// <ui-breadcrumbs separator-icon="chevron-right">
|
|
4
|
+
// <a href="/">Home</a>
|
|
5
|
+
// <a href="/library">Library</a>
|
|
6
|
+
// <span aria-current="page">Data</span>
|
|
7
|
+
// </ui-breadcrumbs>
|
|
8
|
+
//
|
|
9
|
+
// @prop {string} separator='/' — separator text between items
|
|
10
|
+
// @prop {string} separatorIcon='' — registry icon name; overrides `separator`
|
|
11
|
+
// @prop {string} label='Breadcrumb' — accessible name of the <nav>
|
|
12
|
+
// @slot (default) — the trail items. Children MUST be elements (<a>, <span>,
|
|
13
|
+
// <ui-button variant="text">, …), never bare text nodes —
|
|
14
|
+
// separators are drawn with ::slotted(*)::before, which only
|
|
15
|
+
// exists on element children. Mark the current page with
|
|
16
|
+
// aria-current="page".
|
|
17
|
+
// @part nav, list
|
|
18
|
+
// @vars see `t` below (`themeVars.names`)
|
|
19
|
+
//
|
|
20
|
+
// Separators are rendered as generated content on every item but the first:
|
|
21
|
+
// text mode sets the `content` from a private custom property; icon mode masks
|
|
22
|
+
// a currentColor box with the icon's path, so it themes like any glyph.
|
|
23
|
+
|
|
24
|
+
import { define, html, css, vars, effect } from '@alacris/core';
|
|
25
|
+
import { sys } from '../tokens/sys.js';
|
|
26
|
+
import { base } from './base.js';
|
|
27
|
+
import { iconPath, iconsVersion } from '../util/icons.js';
|
|
28
|
+
|
|
29
|
+
const t = vars('ui-breadcrumbs', {
|
|
30
|
+
fg: sys.color.onSurfaceVariant,
|
|
31
|
+
currentFg: sys.color.onSurface,
|
|
32
|
+
separatorFg: sys.color.onSurfaceVariant,
|
|
33
|
+
gap: sys.space(2),
|
|
34
|
+
font: sys.type.bodyMd,
|
|
35
|
+
tracking: sys.tracking.bodyMd,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const styles = css`
|
|
39
|
+
:host { display: block; }
|
|
40
|
+
nav {
|
|
41
|
+
font: ${t.font};
|
|
42
|
+
letter-spacing: ${t.tracking};
|
|
43
|
+
color: ${t.fg};
|
|
44
|
+
}
|
|
45
|
+
.list {
|
|
46
|
+
display: flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
flex-wrap: wrap;
|
|
49
|
+
gap: ${t.gap};
|
|
50
|
+
}
|
|
51
|
+
::slotted(*) { display: inline-flex; align-items: center; color: inherit; }
|
|
52
|
+
::slotted(a) { color: inherit; text-decoration: none; }
|
|
53
|
+
::slotted(a:hover) { color: ${t.currentFg}; text-decoration: underline; }
|
|
54
|
+
::slotted([aria-current="page"]) { color: ${t.currentFg}; }
|
|
55
|
+
::slotted(*:not(:first-child))::before {
|
|
56
|
+
content: var(--_ui-breadcrumbs-sep, "/");
|
|
57
|
+
display: inline-block;
|
|
58
|
+
flex: none;
|
|
59
|
+
margin-inline-end: ${t.gap};
|
|
60
|
+
color: ${t.separatorFg};
|
|
61
|
+
inline-size: var(--_ui-breadcrumbs-sep-size, auto);
|
|
62
|
+
block-size: var(--_ui-breadcrumbs-sep-size, auto);
|
|
63
|
+
background: var(--_ui-breadcrumbs-sep-bg, none);
|
|
64
|
+
-webkit-mask: var(--_ui-breadcrumbs-sep-mask, none) center / contain no-repeat;
|
|
65
|
+
mask: var(--_ui-breadcrumbs-sep-mask, none) center / contain no-repeat;
|
|
66
|
+
}
|
|
67
|
+
`;
|
|
68
|
+
|
|
69
|
+
define('ui-breadcrumbs', {
|
|
70
|
+
props: { separator: '/', separatorIcon: '', label: 'Breadcrumb' },
|
|
71
|
+
styles: [base, styles],
|
|
72
|
+
setup({ separator, separatorIcon, label }, host) {
|
|
73
|
+
effect(() => {
|
|
74
|
+
iconsVersion(); // pick up icons registered after first render
|
|
75
|
+
const d = separatorIcon() && iconPath(separatorIcon());
|
|
76
|
+
const s = host.style;
|
|
77
|
+
if (d) {
|
|
78
|
+
const svgMarkup =
|
|
79
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="${d}"/></svg>`;
|
|
80
|
+
s.setProperty('--_ui-breadcrumbs-sep', '""');
|
|
81
|
+
s.setProperty('--_ui-breadcrumbs-sep-mask', `url("data:image/svg+xml,${encodeURIComponent(svgMarkup)}")`);
|
|
82
|
+
s.setProperty('--_ui-breadcrumbs-sep-bg', 'currentColor');
|
|
83
|
+
s.setProperty('--_ui-breadcrumbs-sep-size', '1em');
|
|
84
|
+
} else {
|
|
85
|
+
s.setProperty('--_ui-breadcrumbs-sep', JSON.stringify(separator()));
|
|
86
|
+
s.removeProperty('--_ui-breadcrumbs-sep-mask');
|
|
87
|
+
s.removeProperty('--_ui-breadcrumbs-sep-bg');
|
|
88
|
+
s.removeProperty('--_ui-breadcrumbs-sep-size');
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return html`
|
|
93
|
+
<nav part="nav" aria-label=${label}>
|
|
94
|
+
<div class="list" part="list"><slot></slot></div>
|
|
95
|
+
</nav>`;
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
export const tag = 'ui-breadcrumbs';
|
|
100
|
+
export const themeVars = t;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// <ui-button-group> — joins slotted <ui-button>s into one segmented bar.
|
|
2
|
+
//
|
|
3
|
+
// Slotted buttons lose their own rounding (their `--ui-button-radius` is
|
|
4
|
+
// zeroed); the group container carries the full radius and clips, and a 1px
|
|
5
|
+
// gap lets the container's divider color show between segments — the Material
|
|
6
|
+
// connected button group.
|
|
7
|
+
//
|
|
8
|
+
// @prop {string} label='' — accessible name for the group
|
|
9
|
+
// @slot (default) — the <ui-button> children
|
|
10
|
+
// @part group — the clipping container
|
|
11
|
+
// @vars see `t` below (`themeVars.names`)
|
|
12
|
+
|
|
13
|
+
import { define, html, css, vars } from '@alacris/core';
|
|
14
|
+
import { sys } from '../tokens/sys.js';
|
|
15
|
+
import { base } from './base.js';
|
|
16
|
+
import './ui-button.js';
|
|
17
|
+
|
|
18
|
+
const t = vars('ui-button-group', {
|
|
19
|
+
radius: sys.radius.full,
|
|
20
|
+
divider: sys.color.outlineVariant,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const styles = css`
|
|
24
|
+
:host { display: inline-flex; vertical-align: middle; }
|
|
25
|
+
.group {
|
|
26
|
+
display: inline-flex;
|
|
27
|
+
align-items: stretch;
|
|
28
|
+
gap: 1px;
|
|
29
|
+
background: ${t.divider};
|
|
30
|
+
border-radius: ${t.radius};
|
|
31
|
+
overflow: hidden;
|
|
32
|
+
}
|
|
33
|
+
::slotted(ui-button) { --ui-button-radius: 0; }
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
define('ui-button-group', {
|
|
37
|
+
props: { label: '' },
|
|
38
|
+
styles: [base, styles],
|
|
39
|
+
setup({ label }) {
|
|
40
|
+
return html`
|
|
41
|
+
<div class="group" part="group" role="group"
|
|
42
|
+
aria-label=${() => label() || null}>
|
|
43
|
+
<slot></slot>
|
|
44
|
+
</div>`;
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export const tag = 'ui-button-group';
|
|
49
|
+
export const themeVars = t;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// <ui-button> — the Material common button, all five variants.
|
|
2
|
+
//
|
|
3
|
+
// @prop {string} variant='filled' — filled | tonal | outlined | text | elevated
|
|
4
|
+
// @prop {boolean} disabled=false
|
|
5
|
+
// @prop {string} type='button' — button | submit (submit reaches the enclosing form)
|
|
6
|
+
// @prop {string} href='' — renders a link styled as a button
|
|
7
|
+
// @prop {string} target='' — link target when href is set
|
|
8
|
+
// @event (native click bubbles; no custom event)
|
|
9
|
+
// @slot (default) — label
|
|
10
|
+
// @slot icon — leading icon
|
|
11
|
+
// @slot trailing — trailing icon
|
|
12
|
+
// @part control — the <button>/<a>
|
|
13
|
+
// @vars see `t` below (`themeVars.names`)
|
|
14
|
+
|
|
15
|
+
import { define, html, css, vars, computed, signal } from '@alacris/core';
|
|
16
|
+
import { sys } from '../tokens/sys.js';
|
|
17
|
+
import { base, focusRingOn, stateLayerOn } from './base.js';
|
|
18
|
+
import { ripple } from '../motion/ripple.js';
|
|
19
|
+
|
|
20
|
+
const t = vars('ui-button', {
|
|
21
|
+
height: '40px',
|
|
22
|
+
padInline: sys.space(6),
|
|
23
|
+
gap: sys.space(2),
|
|
24
|
+
radius: sys.radius.full,
|
|
25
|
+
font: sys.type.labelLg,
|
|
26
|
+
tracking: sys.tracking.labelLg,
|
|
27
|
+
filledBg: sys.color.primary,
|
|
28
|
+
filledFg: sys.color.onPrimary,
|
|
29
|
+
tonalBg: sys.color.secondaryContainer,
|
|
30
|
+
tonalFg: sys.color.onSecondaryContainer,
|
|
31
|
+
outlinedFg: sys.color.primary,
|
|
32
|
+
outlineColor: sys.color.outline,
|
|
33
|
+
textFg: sys.color.primary,
|
|
34
|
+
elevatedBg: sys.color.surfaceContainerLow,
|
|
35
|
+
elevatedFg: sys.color.primary,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const styles = css`
|
|
39
|
+
:host { display: inline-flex; vertical-align: middle; }
|
|
40
|
+
.control {
|
|
41
|
+
position: relative;
|
|
42
|
+
isolation: isolate;
|
|
43
|
+
display: inline-flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
justify-content: center;
|
|
46
|
+
gap: ${t.gap};
|
|
47
|
+
block-size: calc(${t.height} + var(--ui-density, 0) * 4px);
|
|
48
|
+
padding-inline: ${t.padInline};
|
|
49
|
+
border: none;
|
|
50
|
+
border-radius: ${t.radius};
|
|
51
|
+
font: ${t.font};
|
|
52
|
+
letter-spacing: ${t.tracking};
|
|
53
|
+
background: transparent;
|
|
54
|
+
color: inherit;
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
user-select: none;
|
|
57
|
+
text-decoration: none;
|
|
58
|
+
white-space: nowrap;
|
|
59
|
+
transition: box-shadow ${sys.duration.short4} ${sys.easing.standard};
|
|
60
|
+
--ui-icon-size: 1.125rem;
|
|
61
|
+
}
|
|
62
|
+
${focusRingOn('.control')}
|
|
63
|
+
.layer {
|
|
64
|
+
position: absolute; inset: 0; z-index: -1;
|
|
65
|
+
border-radius: inherit;
|
|
66
|
+
background: currentColor;
|
|
67
|
+
opacity: 0;
|
|
68
|
+
transition: opacity ${sys.duration.short2} ${sys.easing.standard};
|
|
69
|
+
}
|
|
70
|
+
${stateLayerOn('.control')}
|
|
71
|
+
|
|
72
|
+
.filled { background: ${t.filledBg}; color: ${t.filledFg}; }
|
|
73
|
+
.filled:hover { box-shadow: ${sys.elevation[1]}; }
|
|
74
|
+
.filled:active { box-shadow: none; }
|
|
75
|
+
.tonal { background: ${t.tonalBg}; color: ${t.tonalFg}; }
|
|
76
|
+
.outlined {
|
|
77
|
+
color: ${t.outlinedFg};
|
|
78
|
+
border: 1px solid ${t.outlineColor};
|
|
79
|
+
}
|
|
80
|
+
.text { color: ${t.textFg}; padding-inline: ${sys.space(3)}; }
|
|
81
|
+
.elevated { background: ${t.elevatedBg}; color: ${t.elevatedFg}; box-shadow: ${sys.elevation[1]}; }
|
|
82
|
+
.elevated:hover { box-shadow: ${sys.elevation[2]}; }
|
|
83
|
+
.elevated:active { box-shadow: ${sys.elevation[1]}; }
|
|
84
|
+
|
|
85
|
+
/* Leading icon: 16dp start padding. Trailing icon: 16dp end padding. */
|
|
86
|
+
.with-icon:not(.text) { padding-inline-start: ${sys.space(4)}; }
|
|
87
|
+
.with-trailing:not(.text) { padding-inline-end: ${sys.space(4)}; }
|
|
88
|
+
.text.with-icon { padding-inline-end: ${sys.space(4)}; }
|
|
89
|
+
.text.with-trailing { padding-inline-start: ${sys.space(4)}; }
|
|
90
|
+
|
|
91
|
+
.control:disabled, .control[aria-disabled="true"] {
|
|
92
|
+
cursor: default;
|
|
93
|
+
pointer-events: none;
|
|
94
|
+
box-shadow: none;
|
|
95
|
+
color: color-mix(in srgb, ${sys.color.onSurface} calc(${sys.state.disabledContent} * 100%), transparent);
|
|
96
|
+
}
|
|
97
|
+
.filled:disabled, .tonal:disabled, .elevated:disabled,
|
|
98
|
+
.filled[aria-disabled="true"], .tonal[aria-disabled="true"], .elevated[aria-disabled="true"] {
|
|
99
|
+
background: color-mix(in srgb, ${sys.color.onSurface} calc(${sys.state.disabledContainer} * 100%), transparent);
|
|
100
|
+
}
|
|
101
|
+
.outlined:disabled, .outlined[aria-disabled="true"] {
|
|
102
|
+
border-color: color-mix(in srgb, ${sys.color.onSurface} calc(${sys.state.disabledContainer} * 100%), transparent);
|
|
103
|
+
}
|
|
104
|
+
`;
|
|
105
|
+
|
|
106
|
+
define('ui-button', {
|
|
107
|
+
props: { variant: 'filled', disabled: false, type: 'button', href: '', target: '' },
|
|
108
|
+
styles: [base, styles],
|
|
109
|
+
setup({ variant, disabled, type, href, target }, host) {
|
|
110
|
+
const hasIcon = signal(false);
|
|
111
|
+
const hasTrailing = signal(false);
|
|
112
|
+
const onSlot = (sig) => (el) => {
|
|
113
|
+
const sync = () => sig.set(el.assignedElements().length > 0);
|
|
114
|
+
el.addEventListener('slotchange', sync);
|
|
115
|
+
sync();
|
|
116
|
+
};
|
|
117
|
+
const cls = computed(() =>
|
|
118
|
+
`control ${variant()}${hasIcon() ? ' with-icon' : ''}${hasTrailing() ? ' with-trailing' : ''}`);
|
|
119
|
+
|
|
120
|
+
const onClick = (e) => {
|
|
121
|
+
if (disabled()) { e.preventDefault(); e.stopPropagation(); return; }
|
|
122
|
+
// A shadow-DOM button cannot submit the host's form; bridge it.
|
|
123
|
+
if (!href() && type() === 'submit') host.closest('form')?.requestSubmit();
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const inner = html`
|
|
127
|
+
<span class="layer" aria-hidden="true"></span>
|
|
128
|
+
<slot name="icon" ref=${onSlot(hasIcon)}></slot>
|
|
129
|
+
<slot></slot>
|
|
130
|
+
<slot name="trailing" ref=${onSlot(hasTrailing)}></slot>`;
|
|
131
|
+
|
|
132
|
+
return html`${() =>
|
|
133
|
+
href()
|
|
134
|
+
? html`<a part="control" class=${cls} href=${href} target=${() => target() || null}
|
|
135
|
+
aria-disabled=${() => (disabled() ? 'true' : null)} tabindex=${() => (disabled() ? '-1' : null)}
|
|
136
|
+
@click=${onClick} ref=${(el) => ripple(el, { disabled })}>${inner}</a>`
|
|
137
|
+
: html`<button part="control" class=${cls} type="button" ?disabled=${disabled}
|
|
138
|
+
@click=${onClick} ref=${(el) => ripple(el, { disabled })}>${inner}</button>`}`;
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
export const tag = 'ui-button';
|
|
143
|
+
export const themeVars = t;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// <ui-card> — a Material card surface.
|
|
2
|
+
//
|
|
3
|
+
// @prop {string} variant='elevated' — elevated | filled | outlined
|
|
4
|
+
// @prop {boolean} interactive=false — hover elevation + ripple + button semantics
|
|
5
|
+
// @event action — interactive card activated (click/Enter/Space)
|
|
6
|
+
// @slot (default) — card body (compose freely; padding is yours via parts/vars)
|
|
7
|
+
// @slot media — full-bleed media at the top
|
|
8
|
+
// @part container — the card surface
|
|
9
|
+
// @part body — padded wrapper around the default slot
|
|
10
|
+
// @vars see `t` below
|
|
11
|
+
|
|
12
|
+
import { define, html, css, vars, computed } from '@alacris/core';
|
|
13
|
+
import { sys } from '../tokens/sys.js';
|
|
14
|
+
import { base, focusRingOn } from './base.js';
|
|
15
|
+
import { ripple } from '../motion/ripple.js';
|
|
16
|
+
|
|
17
|
+
const t = vars('ui-card', {
|
|
18
|
+
radius: sys.radius.md,
|
|
19
|
+
bg: sys.color.surfaceContainerLow,
|
|
20
|
+
fg: sys.color.onSurface,
|
|
21
|
+
filledBg: sys.color.surfaceContainerHighest,
|
|
22
|
+
outlineColor: sys.color.outlineVariant,
|
|
23
|
+
padding: sys.space(4),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const styles = css`
|
|
27
|
+
:host { display: block; }
|
|
28
|
+
.container {
|
|
29
|
+
position: relative;
|
|
30
|
+
isolation: isolate;
|
|
31
|
+
display: flex;
|
|
32
|
+
flex-direction: column;
|
|
33
|
+
border-radius: ${t.radius};
|
|
34
|
+
color: ${t.fg};
|
|
35
|
+
overflow: hidden;
|
|
36
|
+
transition: box-shadow ${sys.duration.short4} ${sys.easing.standard};
|
|
37
|
+
}
|
|
38
|
+
.elevated { background: ${t.bg}; box-shadow: ${sys.elevation[1]}; }
|
|
39
|
+
.filled { background: ${t.filledBg}; }
|
|
40
|
+
.outlined { background: ${sys.color.surface}; border: 1px solid ${t.outlineColor}; }
|
|
41
|
+
.body { padding: ${t.padding}; }
|
|
42
|
+
.interactive { cursor: pointer; }
|
|
43
|
+
${focusRingOn('.interactive')}
|
|
44
|
+
.interactive .layer {
|
|
45
|
+
position: absolute; inset: 0; z-index: 1;
|
|
46
|
+
background: currentColor; opacity: 0; pointer-events: none;
|
|
47
|
+
transition: opacity ${sys.duration.short2} ${sys.easing.standard};
|
|
48
|
+
}
|
|
49
|
+
.interactive:hover .layer { opacity: ${sys.state.hover}; }
|
|
50
|
+
.interactive:focus-visible .layer { opacity: ${sys.state.focus}; }
|
|
51
|
+
.interactive:active .layer { opacity: ${sys.state.pressed}; }
|
|
52
|
+
.interactive.elevated:hover { box-shadow: ${sys.elevation[2]}; }
|
|
53
|
+
.interactive.elevated:active { box-shadow: ${sys.elevation[1]}; }
|
|
54
|
+
.interactive.outlined:hover, .interactive.filled:hover { box-shadow: ${sys.elevation[1]}; }
|
|
55
|
+
.interactive.outlined:active, .interactive.filled:active { box-shadow: none; }
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
define('ui-card', {
|
|
59
|
+
props: { variant: 'elevated', interactive: false },
|
|
60
|
+
styles: [base, styles],
|
|
61
|
+
setup({ variant, interactive }, host) {
|
|
62
|
+
const cls = computed(() => `container ${variant()}${interactive() ? ' interactive' : ''}`);
|
|
63
|
+
|
|
64
|
+
const activate = () => interactive() && host.emit('action');
|
|
65
|
+
const onKeydown = (e) => {
|
|
66
|
+
if (interactive() && (e.key === 'Enter' || e.key === ' ')) {
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
activate();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return html`
|
|
73
|
+
<div part="container" class=${cls}
|
|
74
|
+
role=${() => (interactive() ? 'button' : null)}
|
|
75
|
+
tabindex=${() => (interactive() ? '0' : null)}
|
|
76
|
+
@click=${activate} @keydown=${onKeydown}
|
|
77
|
+
ref=${(el) => ripple(el, { disabled: () => !interactive() })}>
|
|
78
|
+
<span class="layer" aria-hidden="true"></span>
|
|
79
|
+
<slot name="media"></slot>
|
|
80
|
+
<div class="body" part="body"><slot></slot></div>
|
|
81
|
+
</div>`;
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export const tag = 'ui-card';
|
|
86
|
+
export const themeVars = t;
|