@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.
Files changed (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -1
  3. package/package.json +75 -3
  4. package/src/components/base.js +46 -0
  5. package/src/components/ui-accordion-item.js +157 -0
  6. package/src/components/ui-accordion.js +58 -0
  7. package/src/components/ui-alert.js +168 -0
  8. package/src/components/ui-app-bar.js +119 -0
  9. package/src/components/ui-autocomplete.js +371 -0
  10. package/src/components/ui-avatar.js +104 -0
  11. package/src/components/ui-backdrop.js +66 -0
  12. package/src/components/ui-badge.js +95 -0
  13. package/src/components/ui-bottom-app-bar.js +78 -0
  14. package/src/components/ui-bottom-nav.js +83 -0
  15. package/src/components/ui-breadcrumbs.js +100 -0
  16. package/src/components/ui-button-group.js +49 -0
  17. package/src/components/ui-button.js +143 -0
  18. package/src/components/ui-card.js +86 -0
  19. package/src/components/ui-carousel-item.js +53 -0
  20. package/src/components/ui-carousel.js +301 -0
  21. package/src/components/ui-checkbox.js +165 -0
  22. package/src/components/ui-chip-set.js +90 -0
  23. package/src/components/ui-chip.js +226 -0
  24. package/src/components/ui-container.js +59 -0
  25. package/src/components/ui-date-picker.js +774 -0
  26. package/src/components/ui-dialog.js +179 -0
  27. package/src/components/ui-divider.js +58 -0
  28. package/src/components/ui-drawer.js +169 -0
  29. package/src/components/ui-fab-menu.js +122 -0
  30. package/src/components/ui-fab.js +117 -0
  31. package/src/components/ui-icon-button.js +112 -0
  32. package/src/components/ui-icon.js +64 -0
  33. package/src/components/ui-list-item.js +166 -0
  34. package/src/components/ui-list.js +38 -0
  35. package/src/components/ui-loading-indicator.js +74 -0
  36. package/src/components/ui-menu-item.js +117 -0
  37. package/src/components/ui-menu.js +192 -0
  38. package/src/components/ui-nav-item.js +138 -0
  39. package/src/components/ui-nav-rail.js +111 -0
  40. package/src/components/ui-option.js +85 -0
  41. package/src/components/ui-pagination.js +173 -0
  42. package/src/components/ui-progress.js +100 -0
  43. package/src/components/ui-radio-group.js +104 -0
  44. package/src/components/ui-radio.js +143 -0
  45. package/src/components/ui-rating.js +126 -0
  46. package/src/components/ui-search.js +318 -0
  47. package/src/components/ui-select.js +418 -0
  48. package/src/components/ui-sheet.js +205 -0
  49. package/src/components/ui-side-sheet.js +221 -0
  50. package/src/components/ui-skeleton.js +89 -0
  51. package/src/components/ui-slider.js +266 -0
  52. package/src/components/ui-snackbar.js +233 -0
  53. package/src/components/ui-spinner.js +101 -0
  54. package/src/components/ui-split-button.js +158 -0
  55. package/src/components/ui-stack.js +37 -0
  56. package/src/components/ui-step.js +91 -0
  57. package/src/components/ui-stepper.js +86 -0
  58. package/src/components/ui-surface.js +51 -0
  59. package/src/components/ui-switch.js +153 -0
  60. package/src/components/ui-tab-panel.js +54 -0
  61. package/src/components/ui-tab.js +116 -0
  62. package/src/components/ui-table-footer.js +141 -0
  63. package/src/components/ui-table-toolbar.js +219 -0
  64. package/src/components/ui-table.js +735 -0
  65. package/src/components/ui-tabs.js +178 -0
  66. package/src/components/ui-text-field.js +319 -0
  67. package/src/components/ui-text.js +36 -0
  68. package/src/components/ui-time-picker.js +753 -0
  69. package/src/components/ui-toggle-button.js +170 -0
  70. package/src/components/ui-toggle-group.js +111 -0
  71. package/src/components/ui-toolbar.js +64 -0
  72. package/src/components/ui-tooltip.js +148 -0
  73. package/src/index.js +121 -0
  74. package/src/motion/animate.js +145 -0
  75. package/src/motion/flip.js +46 -0
  76. package/src/motion/index.js +4 -0
  77. package/src/motion/presence.js +104 -0
  78. package/src/motion/ripple.js +108 -0
  79. package/src/theme/apply-theme.js +142 -0
  80. package/src/theme/create-theme.js +70 -0
  81. package/src/theme/index.js +5 -0
  82. package/src/tokens/color.js +237 -0
  83. package/src/tokens/index.js +14 -0
  84. package/src/tokens/sys.js +76 -0
  85. package/src/tokens/system.js +114 -0
  86. package/src/tokens/typography.js +189 -0
  87. package/src/util/focus.js +89 -0
  88. package/src/util/form.js +69 -0
  89. package/src/util/icons.js +161 -0
  90. package/src/util/keys.js +110 -0
  91. package/src/util/position.js +102 -0
  92. package/src/util/table.js +230 -0
  93. package/types/elements.d.ts +78 -0
  94. package/types/index.d.ts +168 -0
@@ -0,0 +1,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;