@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,86 @@
|
|
|
1
|
+
// <ui-stepper> — a horizontal stepper of <ui-step> children.
|
|
2
|
+
//
|
|
3
|
+
// <ui-stepper active=${active}>
|
|
4
|
+
// <ui-step label="Cart"></ui-step>
|
|
5
|
+
// <ui-step label="Shipping" optional-text="Optional"></ui-step>
|
|
6
|
+
// <ui-step label="Payment"></ui-step>
|
|
7
|
+
// </ui-stepper>
|
|
8
|
+
//
|
|
9
|
+
// @prop {number} active=0 — index of the active step; steps before it become
|
|
10
|
+
// `completed`, steps after it `upcoming`
|
|
11
|
+
// @slot (default) — <ui-step> children
|
|
12
|
+
// @part row — the flex row wrapping the slot
|
|
13
|
+
// @vars see `t` below (`themeVars.names`)
|
|
14
|
+
//
|
|
15
|
+
// Presentation only — it emits nothing; wiring next/back navigation to
|
|
16
|
+
// `active` is application logic. On slotchange (and every `active` write) it
|
|
17
|
+
// assigns each child's `index`, `state`, a `data-state` attribute (used by the
|
|
18
|
+
// connector CSS), `role="listitem"`, and `aria-current="step"` on the active
|
|
19
|
+
// step. Connector lines between steps turn primary once the segment before a
|
|
20
|
+
// step is completed.
|
|
21
|
+
|
|
22
|
+
import { define, html, css, vars, effect } from '@alacris/core';
|
|
23
|
+
import { sys } from '../tokens/sys.js';
|
|
24
|
+
import { base } from './base.js';
|
|
25
|
+
import './ui-step.js';
|
|
26
|
+
|
|
27
|
+
const t = vars('ui-stepper', {
|
|
28
|
+
connector: sys.color.outlineVariant,
|
|
29
|
+
connectorActive: sys.color.primary,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const styles = css`
|
|
33
|
+
:host { display: block; }
|
|
34
|
+
.row {
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: flex-start;
|
|
37
|
+
}
|
|
38
|
+
::slotted(ui-step) {
|
|
39
|
+
flex: 1;
|
|
40
|
+
min-inline-size: 0;
|
|
41
|
+
position: relative;
|
|
42
|
+
}
|
|
43
|
+
::slotted(ui-step:not(:first-child))::before {
|
|
44
|
+
content: "";
|
|
45
|
+
position: absolute;
|
|
46
|
+
inset-block-start: 12px;
|
|
47
|
+
inset-inline-start: calc(-50% + 20px);
|
|
48
|
+
inset-inline-end: calc(50% + 20px);
|
|
49
|
+
block-size: 1px;
|
|
50
|
+
background: ${t.connector};
|
|
51
|
+
transition: background-color ${sys.duration.short4} ${sys.easing.standard};
|
|
52
|
+
}
|
|
53
|
+
::slotted(ui-step[data-state="completed"]:not(:first-child))::before,
|
|
54
|
+
::slotted(ui-step[data-state="active"]:not(:first-child))::before {
|
|
55
|
+
background: ${t.connectorActive};
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
define('ui-stepper', {
|
|
60
|
+
props: { active: 0 },
|
|
61
|
+
styles: [base, styles],
|
|
62
|
+
setup({ active }, host) {
|
|
63
|
+
const sync = () => {
|
|
64
|
+
const a = active();
|
|
65
|
+
const steps = host.querySelectorAll('ui-step');
|
|
66
|
+
let i = 0;
|
|
67
|
+
for (const el of steps) {
|
|
68
|
+
const s = i < a ? 'completed' : i === a ? 'active' : 'upcoming';
|
|
69
|
+
el.index = i;
|
|
70
|
+
el.state = s;
|
|
71
|
+
el.setAttribute('data-state', s);
|
|
72
|
+
el.setAttribute('role', 'listitem');
|
|
73
|
+
if (s === 'active') el.setAttribute('aria-current', 'step');
|
|
74
|
+
else el.removeAttribute('aria-current');
|
|
75
|
+
i++;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
effect(sync); // re-runs on every `active` write
|
|
80
|
+
|
|
81
|
+
return html`<div class="row" part="row" role="list"><slot @slotchange=${sync}></slot></div>`;
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export const tag = 'ui-stepper';
|
|
86
|
+
export const themeVars = t;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// <ui-surface> — a themable surface (the Paper equivalent).
|
|
2
|
+
//
|
|
3
|
+
// <ui-surface elevation="2" radius="lg" bg="surface-container">…</ui-surface>
|
|
4
|
+
//
|
|
5
|
+
// @prop {number} elevation=0 — shadow level 0..5 (--ui-elevation-N)
|
|
6
|
+
// @prop {string} radius='md' — none | xs | sm | md | lg | xl | full
|
|
7
|
+
// @prop {string} bg='surface' — any color role name, kebab or camel
|
|
8
|
+
// ('primary-container', 'surfaceContainerHigh');
|
|
9
|
+
// text color pairs automatically with the
|
|
10
|
+
// role's on- counterpart when one exists,
|
|
11
|
+
// falling back to on-surface
|
|
12
|
+
// @prop {boolean} outlined=false — 1px outline-variant border
|
|
13
|
+
// @slot (default)
|
|
14
|
+
// @vars (none — every value resolves through the system tokens)
|
|
15
|
+
|
|
16
|
+
import { define, html, css, effect } from '@alacris/core';
|
|
17
|
+
import { sys } from '../tokens/sys.js';
|
|
18
|
+
import { base } from './base.js';
|
|
19
|
+
|
|
20
|
+
const camel = (s) => s.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
21
|
+
|
|
22
|
+
define('ui-surface', {
|
|
23
|
+
props: { elevation: 0, radius: 'md', bg: 'surface', outlined: false },
|
|
24
|
+
styles: [base, css`
|
|
25
|
+
:host {
|
|
26
|
+
display: block;
|
|
27
|
+
transition: box-shadow ${sys.duration.short4} ${sys.easing.standard};
|
|
28
|
+
}
|
|
29
|
+
`],
|
|
30
|
+
setup({ elevation, radius, bg, outlined }, host) {
|
|
31
|
+
effect(() => {
|
|
32
|
+
const role = camel(bg());
|
|
33
|
+
host.style.background = sys.color[role] || sys.color.surface;
|
|
34
|
+
const on = sys.color['on' + role[0].toUpperCase() + role.slice(1)];
|
|
35
|
+
host.style.color = on || sys.color.onSurface;
|
|
36
|
+
});
|
|
37
|
+
effect(() => {
|
|
38
|
+
const lvl = Math.max(0, Math.min(5, Math.round(Number(elevation()) || 0)));
|
|
39
|
+
host.style.boxShadow = sys.elevation[lvl];
|
|
40
|
+
});
|
|
41
|
+
effect(() => {
|
|
42
|
+
host.style.borderRadius = sys.radius[radius()] || sys.radius.md;
|
|
43
|
+
});
|
|
44
|
+
effect(() => {
|
|
45
|
+
host.style.border = outlined() ? `1px solid ${sys.color.outlineVariant}` : '';
|
|
46
|
+
});
|
|
47
|
+
return html`<slot></slot>`;
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export const tag = 'ui-surface';
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// <ui-switch> — the Material switch.
|
|
2
|
+
//
|
|
3
|
+
// @prop {boolean} checked=false
|
|
4
|
+
// @prop {boolean} disabled=false
|
|
5
|
+
// @prop {string} label='' — visible label; also the accessible name.
|
|
6
|
+
// Empty leaves the control unnamed (authoring error).
|
|
7
|
+
// @prop {string} name='' — form field name (submits 'on'/value while checked)
|
|
8
|
+
// @prop {string} value='on'
|
|
9
|
+
// @prop {boolean} icons=false — show check/close glyphs in the handle
|
|
10
|
+
// @event change — detail: { checked }
|
|
11
|
+
// @part control — the switch track (role="switch")
|
|
12
|
+
// @part handle
|
|
13
|
+
// @vars see `t` below
|
|
14
|
+
|
|
15
|
+
import { define, html, css, vars } from '@alacris/core';
|
|
16
|
+
import { sys } from '../tokens/sys.js';
|
|
17
|
+
import { base, focusRingOn } from './base.js';
|
|
18
|
+
import { formBind } from '../util/form.js';
|
|
19
|
+
import './ui-icon.js';
|
|
20
|
+
|
|
21
|
+
const t = vars('ui-switch', {
|
|
22
|
+
trackWidth: '52px',
|
|
23
|
+
trackHeight: '32px',
|
|
24
|
+
trackBg: sys.color.surfaceContainerHighest,
|
|
25
|
+
trackBgChecked: sys.color.primary,
|
|
26
|
+
trackOutline: sys.color.outline,
|
|
27
|
+
handleBg: sys.color.outline,
|
|
28
|
+
handleBgChecked: sys.color.onPrimary,
|
|
29
|
+
iconFg: sys.color.surfaceContainerHighest,
|
|
30
|
+
iconFgChecked: sys.color.onPrimaryContainer,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const styles = css`
|
|
34
|
+
:host { display: inline-flex; }
|
|
35
|
+
label {
|
|
36
|
+
display: inline-flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
gap: ${sys.space(3)};
|
|
39
|
+
cursor: pointer;
|
|
40
|
+
font: ${sys.type.bodyMd};
|
|
41
|
+
letter-spacing: ${sys.tracking.bodyMd};
|
|
42
|
+
color: ${sys.color.onSurface};
|
|
43
|
+
user-select: none;
|
|
44
|
+
}
|
|
45
|
+
.control {
|
|
46
|
+
position: relative;
|
|
47
|
+
flex: none;
|
|
48
|
+
inline-size: ${t.trackWidth};
|
|
49
|
+
block-size: ${t.trackHeight};
|
|
50
|
+
border: 2px solid ${t.trackOutline};
|
|
51
|
+
border-radius: ${sys.radius.full};
|
|
52
|
+
background: ${t.trackBg};
|
|
53
|
+
cursor: pointer;
|
|
54
|
+
padding: 0;
|
|
55
|
+
overflow: visible;
|
|
56
|
+
transition: background-color ${sys.duration.short4} ${sys.easing.standard},
|
|
57
|
+
border-color ${sys.duration.short4} ${sys.easing.standard};
|
|
58
|
+
}
|
|
59
|
+
${focusRingOn('.control')}
|
|
60
|
+
.control[aria-checked="true"] {
|
|
61
|
+
background: ${t.trackBgChecked};
|
|
62
|
+
border-color: ${t.trackBgChecked};
|
|
63
|
+
}
|
|
64
|
+
.handle {
|
|
65
|
+
position: absolute;
|
|
66
|
+
inset-block-start: 50%;
|
|
67
|
+
inset-inline-start: 6px;
|
|
68
|
+
translate: 0 -50%;
|
|
69
|
+
inline-size: 16px;
|
|
70
|
+
block-size: 16px;
|
|
71
|
+
border-radius: ${sys.radius.full};
|
|
72
|
+
background: ${t.handleBg};
|
|
73
|
+
display: grid;
|
|
74
|
+
place-items: center;
|
|
75
|
+
color: ${t.iconFg};
|
|
76
|
+
--ui-icon-size: 12px;
|
|
77
|
+
transition: translate ${sys.duration.medium2} ${sys.easing.emphasizedOvershoot},
|
|
78
|
+
inline-size ${sys.duration.medium1} ${sys.easing.standard},
|
|
79
|
+
block-size ${sys.duration.medium1} ${sys.easing.standard},
|
|
80
|
+
background-color ${sys.duration.short2} ${sys.easing.standard};
|
|
81
|
+
}
|
|
82
|
+
.handle::after {
|
|
83
|
+
content: '';
|
|
84
|
+
position: absolute;
|
|
85
|
+
inset: 50%;
|
|
86
|
+
width: 40px;
|
|
87
|
+
height: 40px;
|
|
88
|
+
margin: -20px;
|
|
89
|
+
border-radius: 50%;
|
|
90
|
+
background: currentColor;
|
|
91
|
+
opacity: 0;
|
|
92
|
+
pointer-events: none;
|
|
93
|
+
z-index: -1;
|
|
94
|
+
transition: opacity ${sys.duration.short2} ${sys.easing.standard};
|
|
95
|
+
}
|
|
96
|
+
.control:hover .handle::after { opacity: ${sys.state.hover}; }
|
|
97
|
+
.control:focus-visible .handle::after { opacity: ${sys.state.focus}; }
|
|
98
|
+
.control:active .handle::after { opacity: ${sys.state.pressed}; }
|
|
99
|
+
.control:hover:not([aria-checked="true"]) .handle { background: ${sys.color.onSurface}; }
|
|
100
|
+
.control[aria-checked="true"]:hover .handle { background: ${sys.color.primaryContainer}; }
|
|
101
|
+
.control[aria-checked="true"] .handle {
|
|
102
|
+
translate: 16px -50%;
|
|
103
|
+
inline-size: 24px;
|
|
104
|
+
block-size: 24px;
|
|
105
|
+
background: ${t.handleBgChecked};
|
|
106
|
+
color: ${t.iconFgChecked};
|
|
107
|
+
--ui-icon-size: 16px;
|
|
108
|
+
}
|
|
109
|
+
.control:active:not([aria-checked="true"]) .handle {
|
|
110
|
+
inline-size: 28px;
|
|
111
|
+
block-size: 28px;
|
|
112
|
+
translate: -2px -50%;
|
|
113
|
+
}
|
|
114
|
+
.control:active[aria-checked="true"] .handle {
|
|
115
|
+
inline-size: 28px;
|
|
116
|
+
block-size: 28px;
|
|
117
|
+
translate: 14px -50%;
|
|
118
|
+
}
|
|
119
|
+
.control:disabled { cursor: default; pointer-events: none; opacity: ${sys.state.disabledContent}; }
|
|
120
|
+
:host([disabled]) label { cursor: default; pointer-events: none; }
|
|
121
|
+
`;
|
|
122
|
+
|
|
123
|
+
define('ui-switch', {
|
|
124
|
+
formAssociated: true,
|
|
125
|
+
props: { checked: false, disabled: false, label: '', name: '', value: 'on', icons: false },
|
|
126
|
+
styles: [base, styles],
|
|
127
|
+
setup({ checked, disabled, label, name, value, icons }, host) {
|
|
128
|
+
formBind(host, { name, value, checked, disabled });
|
|
129
|
+
|
|
130
|
+
const toggle = () => {
|
|
131
|
+
if (disabled()) return;
|
|
132
|
+
checked.set(!checked());
|
|
133
|
+
host.emit('change', { checked: checked() });
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
return html`
|
|
137
|
+
<label>
|
|
138
|
+
<button part="control" class=${() => `control${icons() ? ' with-icons' : ''}`} type="button" role="switch"
|
|
139
|
+
aria-checked=${() => String(checked())}
|
|
140
|
+
aria-labelledby=${() => (label() ? 'label' : null)}
|
|
141
|
+
?disabled=${disabled}
|
|
142
|
+
@click=${toggle}>
|
|
143
|
+
<span part="handle" class="handle" aria-hidden="true">
|
|
144
|
+
${() => (icons() ? html`<ui-icon name=${() => (checked() ? 'check' : 'close')}></ui-icon>` : null)}
|
|
145
|
+
</span>
|
|
146
|
+
</button>
|
|
147
|
+
${() => (label() ? html`<span id="label">${label}</span>` : null)}
|
|
148
|
+
</label>`;
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
export const tag = 'ui-switch';
|
|
153
|
+
export const themeVars = t;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// <ui-tab-panel> — the content pane paired with a <ui-tab>.
|
|
2
|
+
//
|
|
3
|
+
// Slot it into <ui-tabs slot="panels">. The parent sets `active` when its
|
|
4
|
+
// `value` matches; do not set `active` yourself. The panel is hidden while
|
|
5
|
+
// inactive and fades in when it becomes active.
|
|
6
|
+
//
|
|
7
|
+
// @prop {string} value='' — REQUIRED; matched against the tabs' value
|
|
8
|
+
// @prop {boolean} active=false — managed by the parent <ui-tabs>
|
|
9
|
+
// @slot (default) — panel content
|
|
10
|
+
// @vars see `t` below (`themeVars.names`)
|
|
11
|
+
|
|
12
|
+
import { define, html, css, vars, effect } from '@alacris/core';
|
|
13
|
+
import { sys } from '../tokens/sys.js';
|
|
14
|
+
import { base } from './base.js';
|
|
15
|
+
import { animate, fx } from '../motion/animate.js';
|
|
16
|
+
|
|
17
|
+
const t = vars('ui-tab-panel', {
|
|
18
|
+
fg: sys.color.onSurface,
|
|
19
|
+
font: sys.type.bodyMd,
|
|
20
|
+
padBlock: sys.space(4),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const styles = css`
|
|
24
|
+
:host { display: block; }
|
|
25
|
+
.body {
|
|
26
|
+
color: ${t.fg};
|
|
27
|
+
font: ${t.font};
|
|
28
|
+
padding-block: ${t.padBlock};
|
|
29
|
+
}
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
define('ui-tab-panel', {
|
|
33
|
+
props: { value: '', active: false },
|
|
34
|
+
styles: [base, styles],
|
|
35
|
+
setup({ value, active }, host) {
|
|
36
|
+
host.setAttribute('role', 'tabpanel');
|
|
37
|
+
host.tabIndex = 0;
|
|
38
|
+
effect(() => {
|
|
39
|
+
if (active()) host.removeAttribute('hidden');
|
|
40
|
+
else host.setAttribute('hidden', '');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return html`${() =>
|
|
44
|
+
active()
|
|
45
|
+
? html`<div class="body"
|
|
46
|
+
ref=${(el) => animate(el, fx.fadeIn, { duration: 'short4', easing: 'standard' })}>
|
|
47
|
+
<slot></slot>
|
|
48
|
+
</div>`
|
|
49
|
+
: null}`;
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const tag = 'ui-tab-panel';
|
|
54
|
+
export const themeVars = t;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// <ui-tab> — one tab inside <ui-tabs>.
|
|
2
|
+
//
|
|
3
|
+
// The host element carries the `tab` semantics (role, aria-selected,
|
|
4
|
+
// focusability) so <ui-tabs>' roving tabindex can manage it directly in the
|
|
5
|
+
// light DOM. `selected` is written by the parent <ui-tabs>; do not set it
|
|
6
|
+
// yourself — set the tabs' `value` instead.
|
|
7
|
+
//
|
|
8
|
+
// @prop {string} value='' — REQUIRED; matched against the tabs' value
|
|
9
|
+
// @prop {string} icon='' — leading registry icon
|
|
10
|
+
// @prop {boolean} disabled=false
|
|
11
|
+
// @prop {boolean} selected=false — managed by the parent <ui-tabs>
|
|
12
|
+
// @event ui-tab-select — internal; detail: { value } (consumed by <ui-tabs>)
|
|
13
|
+
// @slot (default) — label
|
|
14
|
+
// @part control — the styled tab surface
|
|
15
|
+
// @vars see `t` below (`themeVars.names`)
|
|
16
|
+
|
|
17
|
+
import { define, html, css, vars, effect } from '@alacris/core';
|
|
18
|
+
import { sys } from '../tokens/sys.js';
|
|
19
|
+
import { base, focusRingOn } from './base.js';
|
|
20
|
+
import { ripple } from '../motion/ripple.js';
|
|
21
|
+
import './ui-icon.js';
|
|
22
|
+
|
|
23
|
+
const t = vars('ui-tab', {
|
|
24
|
+
height: '48px',
|
|
25
|
+
padInline: sys.space(4),
|
|
26
|
+
gap: sys.space(2),
|
|
27
|
+
font: sys.type.titleSm,
|
|
28
|
+
tracking: sys.tracking.titleSm,
|
|
29
|
+
fg: sys.color.onSurfaceVariant,
|
|
30
|
+
selectedFg: sys.color.primary,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const styles = css`
|
|
34
|
+
:host {
|
|
35
|
+
display: inline-flex;
|
|
36
|
+
outline: none;
|
|
37
|
+
}
|
|
38
|
+
:host([aria-disabled="true"]) { pointer-events: none; }
|
|
39
|
+
${focusRingOn(':host')}
|
|
40
|
+
:host(:focus-visible) { outline-offset: -2px; }
|
|
41
|
+
.control {
|
|
42
|
+
position: relative;
|
|
43
|
+
isolation: isolate;
|
|
44
|
+
display: inline-flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
justify-content: center;
|
|
47
|
+
gap: ${t.gap};
|
|
48
|
+
inline-size: 100%;
|
|
49
|
+
block-size: calc(${t.height} + var(--ui-density, 0) * 4px);
|
|
50
|
+
padding-inline: ${t.padInline};
|
|
51
|
+
font: ${t.font};
|
|
52
|
+
letter-spacing: ${t.tracking};
|
|
53
|
+
color: ${t.fg};
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
user-select: none;
|
|
56
|
+
white-space: nowrap;
|
|
57
|
+
transition: color ${sys.duration.short4} ${sys.easing.standard};
|
|
58
|
+
--ui-icon-size: 1.5rem;
|
|
59
|
+
}
|
|
60
|
+
.inner {
|
|
61
|
+
display: inline-flex;
|
|
62
|
+
align-items: center;
|
|
63
|
+
gap: inherit;
|
|
64
|
+
}
|
|
65
|
+
.selected { color: ${t.selectedFg}; }
|
|
66
|
+
.layer {
|
|
67
|
+
position: absolute; inset: 0; z-index: -1;
|
|
68
|
+
background: currentColor; opacity: 0;
|
|
69
|
+
transition: opacity ${sys.duration.short2} ${sys.easing.standard};
|
|
70
|
+
}
|
|
71
|
+
.control:hover .layer { opacity: ${sys.state.hover}; }
|
|
72
|
+
:host(:focus-visible) .layer { opacity: ${sys.state.focus}; }
|
|
73
|
+
.control:active .layer { opacity: ${sys.state.pressed}; }
|
|
74
|
+
:host([aria-disabled="true"]) .control {
|
|
75
|
+
cursor: default;
|
|
76
|
+
color: color-mix(in srgb, ${sys.color.onSurface} calc(${sys.state.disabledContent} * 100%), transparent);
|
|
77
|
+
}
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
define('ui-tab', {
|
|
81
|
+
props: { value: '', icon: '', disabled: false, selected: false },
|
|
82
|
+
styles: [base, styles],
|
|
83
|
+
setup({ value, icon, disabled, selected }, host) {
|
|
84
|
+
host.setAttribute('role', 'tab');
|
|
85
|
+
effect(() => host.setAttribute('aria-selected', selected() ? 'true' : 'false'));
|
|
86
|
+
effect(() => {
|
|
87
|
+
if (disabled()) host.setAttribute('aria-disabled', 'true');
|
|
88
|
+
else host.removeAttribute('aria-disabled');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const select = () => {
|
|
92
|
+
if (disabled()) return;
|
|
93
|
+
host.emit('ui-tab-select', { value: value() });
|
|
94
|
+
};
|
|
95
|
+
host.addEventListener('click', select);
|
|
96
|
+
host.addEventListener('keydown', (e) => {
|
|
97
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
select();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return html`
|
|
104
|
+
<div part="control" class=${() => `control${selected() ? ' selected' : ''}`}
|
|
105
|
+
ref=${(el) => ripple(el, { disabled })}>
|
|
106
|
+
<span class="layer" aria-hidden="true"></span>
|
|
107
|
+
<span class="inner">
|
|
108
|
+
${() => (icon() ? html`<ui-icon name=${icon}></ui-icon>` : null)}
|
|
109
|
+
<slot></slot>
|
|
110
|
+
</span>
|
|
111
|
+
</div>`;
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
export const tag = 'ui-tab';
|
|
116
|
+
export const themeVars = t;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// <ui-table-footer> — pagination and row-count for <ui-table>.
|
|
2
|
+
// Composes <ui-pagination> for the page window and a rows-per-page <select>.
|
|
3
|
+
//
|
|
4
|
+
// <ui-table-footer page=${page} page-size="10" row-count=${n}
|
|
5
|
+
// @page=${(e) => page(e.detail.page)}></ui-table-footer>
|
|
6
|
+
//
|
|
7
|
+
// @prop {number} page=1
|
|
8
|
+
// @prop {number} pageSize=0 — 0 hides the pager and shows a total only
|
|
9
|
+
// @prop {number} rowCount=0
|
|
10
|
+
// @prop {Array} pageSizeOptions=[] — default 5, 10, 25
|
|
11
|
+
// @prop {boolean} disabled=false
|
|
12
|
+
// @prop {string} label='Table pagination'
|
|
13
|
+
// @event page — page or page-size changed; detail: { page, pageSize }
|
|
14
|
+
// @part footer, range
|
|
15
|
+
// @vars see `t` below (`themeVars.names`)
|
|
16
|
+
|
|
17
|
+
import { define, html, css, vars, computed } from '@alacris/core';
|
|
18
|
+
import { sys } from '../tokens/sys.js';
|
|
19
|
+
import { base, focusRingOn } from './base.js';
|
|
20
|
+
import './ui-pagination.js';
|
|
21
|
+
import './ui-icon.js';
|
|
22
|
+
|
|
23
|
+
const t = vars('ui-table-footer', {
|
|
24
|
+
height: '52px',
|
|
25
|
+
fg: sys.color.onSurfaceVariant,
|
|
26
|
+
borderColor: sys.color.outlineVariant,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const styles = css`
|
|
30
|
+
:host { display: block; }
|
|
31
|
+
.footer {
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
justify-content: flex-end;
|
|
35
|
+
flex-wrap: wrap;
|
|
36
|
+
gap: ${sys.space(4)};
|
|
37
|
+
min-block-size: ${t.height};
|
|
38
|
+
padding-inline: ${sys.space(4)};
|
|
39
|
+
border-block-start: 1px solid ${t.borderColor};
|
|
40
|
+
color: ${t.fg};
|
|
41
|
+
font: ${sys.type.bodySm};
|
|
42
|
+
letter-spacing: ${sys.tracking.bodySm};
|
|
43
|
+
}
|
|
44
|
+
.page-size {
|
|
45
|
+
display: inline-flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
gap: ${sys.space(2)};
|
|
48
|
+
}
|
|
49
|
+
.select-wrap {
|
|
50
|
+
position: relative;
|
|
51
|
+
display: inline-flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
--ui-icon-size: 1.25rem;
|
|
54
|
+
}
|
|
55
|
+
.page-size select {
|
|
56
|
+
appearance: none;
|
|
57
|
+
margin: 0;
|
|
58
|
+
padding: ${sys.space(1)} ${sys.space(6)} ${sys.space(1)} ${sys.space(2)};
|
|
59
|
+
border: none;
|
|
60
|
+
border-radius: ${sys.radius.xs};
|
|
61
|
+
background: ${sys.color.surfaceContainerHighest};
|
|
62
|
+
color: ${sys.color.onSurface};
|
|
63
|
+
font: ${sys.type.bodyMd};
|
|
64
|
+
letter-spacing: ${sys.tracking.bodyMd};
|
|
65
|
+
cursor: pointer;
|
|
66
|
+
}
|
|
67
|
+
.select-wrap ui-icon {
|
|
68
|
+
position: absolute;
|
|
69
|
+
inset-inline-end: ${sys.space(1)};
|
|
70
|
+
pointer-events: none;
|
|
71
|
+
color: ${sys.color.onSurfaceVariant};
|
|
72
|
+
}
|
|
73
|
+
${focusRingOn('.page-size select')}
|
|
74
|
+
.range { font: ${sys.type.bodySm}; letter-spacing: ${sys.tracking.bodySm}; }
|
|
75
|
+
ui-pagination { flex: none; }
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
define('ui-table-footer', {
|
|
79
|
+
props: {
|
|
80
|
+
page: 1,
|
|
81
|
+
pageSize: 0,
|
|
82
|
+
rowCount: 0,
|
|
83
|
+
pageSizeOptions: [],
|
|
84
|
+
disabled: false,
|
|
85
|
+
label: 'Table pagination',
|
|
86
|
+
},
|
|
87
|
+
styles: [base, styles],
|
|
88
|
+
setup({ page, pageSize, rowCount, pageSizeOptions, disabled, label }, host) {
|
|
89
|
+
const size = computed(() => Math.max(0, Math.floor(pageSize()) || 0));
|
|
90
|
+
const total = computed(() => Math.max(0, Math.floor(rowCount()) || 0));
|
|
91
|
+
const pages = computed(() => {
|
|
92
|
+
const s = size();
|
|
93
|
+
if (!s) return 1;
|
|
94
|
+
return Math.max(1, Math.ceil(total() / s));
|
|
95
|
+
});
|
|
96
|
+
const current = computed(() =>
|
|
97
|
+
Math.min(Math.max(1, Math.floor(page()) || 1), pages()));
|
|
98
|
+
const choices = computed(() => {
|
|
99
|
+
const o = pageSizeOptions();
|
|
100
|
+
return o && o.length ? o : [5, 10, 25];
|
|
101
|
+
});
|
|
102
|
+
const rangeText = computed(() => {
|
|
103
|
+
const n = total();
|
|
104
|
+
const s = size();
|
|
105
|
+
if (!s) return `Total rows: ${n}`;
|
|
106
|
+
if (!n) return '0 of 0';
|
|
107
|
+
const start = (current() - 1) * s + 1;
|
|
108
|
+
const end = Math.min(current() * s, n);
|
|
109
|
+
return `${start}–${end} of ${n}`;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const emitPage = (nextPage, nextSize = size()) => {
|
|
113
|
+
host.emit('page', { page: nextPage, pageSize: nextSize });
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return html`
|
|
117
|
+
<div class="footer" part="footer">
|
|
118
|
+
${() => (size() ? html`
|
|
119
|
+
<label class="page-size">
|
|
120
|
+
Rows per page
|
|
121
|
+
<span class="select-wrap">
|
|
122
|
+
<select aria-label="Rows per page" ?disabled=${disabled}
|
|
123
|
+
.value=${() => String(size())}
|
|
124
|
+
@change=${(e) => emitPage(1, +e.target.value)}>
|
|
125
|
+
${() => choices().map((n) => html`<option value=${n}>${n}</option>`)}
|
|
126
|
+
</select>
|
|
127
|
+
<ui-icon name="arrow-drop-down"></ui-icon>
|
|
128
|
+
</span>
|
|
129
|
+
</label>` : null)}
|
|
130
|
+
<span class="range" part="range">${rangeText}</span>
|
|
131
|
+
${() => (size() ? html`
|
|
132
|
+
<ui-pagination page=${current} count=${pages} disabled=${disabled}
|
|
133
|
+
label=${label}
|
|
134
|
+
@change=${(e) => { e.stopPropagation(); emitPage(e.detail.page); }}>
|
|
135
|
+
</ui-pagination>` : null)}
|
|
136
|
+
</div>`;
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
export const tag = 'ui-table-footer';
|
|
141
|
+
export const themeVars = t;
|