@alacris/ui 0.0.0 → 0.1.0

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 +316 -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,112 @@
1
+ // <ui-icon-button> — a 40px icon button, optionally a toggle.
2
+ //
3
+ // @prop {string} icon='' — registry icon name (or slot an <ui-icon>)
4
+ // @prop {string} selectedIcon='' — icon while selected (toggle mode; defaults to `icon`)
5
+ // @prop {string} label='' — REQUIRED accessible name
6
+ // @prop {string} variant='standard' — standard | filled | tonal | outlined
7
+ // @prop {boolean} toggle=false — makes it a pressed-state toggle
8
+ // @prop {boolean} selected=false — toggle state
9
+ // @prop {boolean} disabled=false
10
+ // @event change — toggle flipped; detail: { selected }
11
+ // @slot (default) — custom icon content when `icon` is empty
12
+ // @part control — the <button>
13
+ // @vars see `t` below
14
+
15
+ import { define, html, css, vars, computed } from '@alacris/core';
16
+ import { sys } from '../tokens/sys.js';
17
+ import { base, focusRingOn } from './base.js';
18
+ import { ripple } from '../motion/ripple.js';
19
+ import './ui-icon.js';
20
+
21
+ const t = vars('ui-icon-button', {
22
+ size: '40px',
23
+ radius: sys.radius.full,
24
+ fg: sys.color.onSurfaceVariant,
25
+ selectedFg: sys.color.primary,
26
+ filledBg: sys.color.primary,
27
+ filledFg: sys.color.onPrimary,
28
+ tonalBg: sys.color.secondaryContainer,
29
+ tonalFg: sys.color.onSecondaryContainer,
30
+ outlineColor: sys.color.outline,
31
+ });
32
+
33
+ const styles = css`
34
+ :host { display: inline-flex; vertical-align: middle; }
35
+ .control {
36
+ position: relative;
37
+ isolation: isolate;
38
+ display: inline-flex;
39
+ align-items: center;
40
+ justify-content: center;
41
+ inline-size: calc(${t.size} + var(--ui-density, 0) * 4px);
42
+ block-size: calc(${t.size} + var(--ui-density, 0) * 4px);
43
+ border: none;
44
+ border-radius: ${t.radius};
45
+ background: transparent;
46
+ color: ${t.fg};
47
+ cursor: pointer;
48
+ user-select: none;
49
+ transition: background-color ${sys.duration.short4} ${sys.easing.standard},
50
+ color ${sys.duration.short4} ${sys.easing.standard};
51
+ }
52
+ ${focusRingOn('.control')}
53
+ .layer {
54
+ position: absolute; inset: 0; z-index: -1;
55
+ border-radius: inherit; background: currentColor; opacity: 0;
56
+ transition: opacity ${sys.duration.short2} ${sys.easing.standard};
57
+ }
58
+ .control:hover .layer { opacity: ${sys.state.hover}; }
59
+ .control:focus-visible .layer { opacity: ${sys.state.focus}; }
60
+ .control:active .layer { opacity: ${sys.state.pressed}; }
61
+
62
+ .standard[aria-pressed="true"] { color: ${t.selectedFg}; }
63
+ .filled { background: ${t.filledBg}; color: ${t.filledFg}; }
64
+ .filled:not([aria-pressed="true"]).toggle { background: ${sys.color.surfaceContainerHighest}; color: ${t.selectedFg}; }
65
+ .tonal { background: ${t.tonalBg}; color: ${t.tonalFg}; }
66
+ .tonal:not([aria-pressed="true"]).toggle { background: ${sys.color.surfaceContainerHighest}; color: ${sys.color.onSurfaceVariant}; }
67
+ .outlined { border: 1px solid ${t.outlineColor}; }
68
+ .outlined[aria-pressed="true"] { background: ${sys.color.inverseSurface}; color: ${sys.color.inverseOnSurface}; border-color: transparent; }
69
+
70
+ .control:disabled {
71
+ cursor: default;
72
+ pointer-events: none;
73
+ color: color-mix(in srgb, ${sys.color.onSurface} calc(${sys.state.disabledContent} * 100%), transparent);
74
+ }
75
+ .filled:disabled, .tonal:disabled {
76
+ background: color-mix(in srgb, ${sys.color.onSurface} calc(${sys.state.disabledContainer} * 100%), transparent);
77
+ }
78
+ .outlined:disabled {
79
+ border-color: color-mix(in srgb, ${sys.color.onSurface} calc(${sys.state.disabledContainer} * 100%), transparent);
80
+ }
81
+ `;
82
+
83
+ define('ui-icon-button', {
84
+ props: {
85
+ icon: '', selectedIcon: '', label: '',
86
+ variant: 'standard', toggle: false, selected: false, disabled: false,
87
+ },
88
+ styles: [base, styles],
89
+ setup({ icon, selectedIcon, label, variant, toggle, selected, disabled }, host) {
90
+ const cls = computed(() => `control ${variant()}${toggle() ? ' toggle' : ''}`);
91
+ const shownIcon = computed(() => (toggle() && selected() && selectedIcon()) || icon());
92
+
93
+ const onClick = () => {
94
+ if (!toggle()) return;
95
+ selected.set(!selected());
96
+ host.emit('change', { selected: selected() });
97
+ };
98
+
99
+ return html`
100
+ <button part="control" class=${cls} type="button" ?disabled=${disabled}
101
+ aria-label=${() => label() || icon() || null}
102
+ aria-pressed=${() => (toggle() ? String(selected()) : null)}
103
+ @click=${onClick}
104
+ ref=${(el) => ripple(el, { disabled, centered: true })}>
105
+ <span class="layer" aria-hidden="true"></span>
106
+ ${() => (shownIcon() ? html`<ui-icon name=${shownIcon}></ui-icon>` : html`<slot></slot>`)}
107
+ </button>`;
108
+ },
109
+ });
110
+
111
+ export const tag = 'ui-icon-button';
112
+ export const themeVars = t;
@@ -0,0 +1,64 @@
1
+ // <ui-icon> — an icon from the registry, or any slotted SVG.
2
+ //
3
+ // @prop {string} name='' — registry name (see util/icons.js); empty renders the slot
4
+ // @prop {string} label='' — accessible name; empty marks the icon decorative
5
+ // @prop {string} size='' — CSS length; overrides --ui-icon-size for this element
6
+ // @slot (default) — a custom <svg> when no name is given
7
+ // @vars see `t` below (`themeVars.names`)
8
+
9
+ import { define, html, svg, css, vars, effect } from '@alacris/core';
10
+ import { base } from './base.js';
11
+ import { iconPath, iconsVersion } from '../util/icons.js';
12
+
13
+ const t = vars('ui-icon', {
14
+ size: '1.5rem',
15
+ });
16
+
17
+ define('ui-icon', {
18
+ props: { name: '', label: '', size: '' },
19
+ styles: [base, css`
20
+ :host {
21
+ display: inline-flex;
22
+ align-items: center;
23
+ justify-content: center;
24
+ vertical-align: middle;
25
+ inline-size: ${t.size};
26
+ block-size: ${t.size};
27
+ flex: none;
28
+ color: inherit;
29
+ }
30
+ svg, ::slotted(svg) {
31
+ display: block;
32
+ inline-size: 100%;
33
+ block-size: 100%;
34
+ fill: currentColor;
35
+ }
36
+ `],
37
+ setup({ name, label, size }, host) {
38
+ effect(() => {
39
+ if (label()) {
40
+ host.setAttribute('role', 'img');
41
+ host.setAttribute('aria-label', label());
42
+ host.removeAttribute('aria-hidden');
43
+ } else {
44
+ host.removeAttribute('role');
45
+ host.removeAttribute('aria-label');
46
+ host.setAttribute('aria-hidden', 'true');
47
+ }
48
+ });
49
+ effect(() => {
50
+ if (size()) host.style.setProperty('--ui-icon-size', size());
51
+ else host.style.removeProperty('--ui-icon-size');
52
+ });
53
+ return html`${() => {
54
+ iconsVersion(); // re-render if icons register late
55
+ const d = name() && iconPath(name());
56
+ return d
57
+ ? svg`<svg viewBox="0 0 24 24" aria-hidden="true"><path d=${d}></path></svg>`
58
+ : html`<slot></slot>`;
59
+ }}`;
60
+ },
61
+ });
62
+
63
+ export const tag = 'ui-icon';
64
+ export const themeVars = t;
@@ -0,0 +1,166 @@
1
+ // <ui-list-item> — one row of a <ui-list>.
2
+ //
3
+ // One line (56px) by default; supplying supporting text (prop or slot) makes
4
+ // it two lines (72px). `interactive` adds button semantics, a state layer and
5
+ // a ripple; `href` renders the row as a link instead. Activation emits only
6
+ // the native (bubbling) click — no custom event.
7
+ //
8
+ // @prop {string} headline='' — primary text (or use the default slot)
9
+ // @prop {string} supporting='' — secondary text (or use the supporting slot)
10
+ // @prop {boolean} interactive=false — state layer + ripple + role="button"
11
+ // @prop {string} href='' — renders the row as an <a>
12
+ // @prop {boolean} selected=false — secondaryContainer background
13
+ // @prop {boolean} disabled=false
14
+ // @slot (default) — headline content when the prop is empty
15
+ // @slot leading — icon / avatar / checkbox
16
+ // @slot supporting — secondary line when the prop is empty
17
+ // @slot trailing — trailing meta text or icon
18
+ // @part control — the row element (<div> or <a>)
19
+ // @vars see `t` below
20
+
21
+ import { define, html, css, vars, computed, effect, signal } from '@alacris/core';
22
+ import { sys } from '../tokens/sys.js';
23
+ import { base, focusRingOn } from './base.js';
24
+ import { ripple } from '../motion/ripple.js';
25
+
26
+ const t = vars('ui-list-item', {
27
+ height: '56px',
28
+ twoLineHeight: '72px',
29
+ padInline: sys.space(4),
30
+ gap: sys.space(4),
31
+ headlineFg: sys.color.onSurface,
32
+ headlineFont: sys.type.bodyLg,
33
+ supportingFg: sys.color.onSurfaceVariant,
34
+ supportingFont: sys.type.bodyMd,
35
+ trailingFg: sys.color.onSurfaceVariant,
36
+ trailingFont: sys.type.labelSm,
37
+ selectedBg: sys.color.secondaryContainer,
38
+ selectedFg: sys.color.onSecondaryContainer,
39
+ });
40
+
41
+ const styles = css`
42
+ :host { display: block; }
43
+ .control {
44
+ position: relative;
45
+ isolation: isolate;
46
+ display: flex;
47
+ align-items: center;
48
+ gap: ${t.gap};
49
+ min-block-size: calc(${t.height} + var(--ui-density, 0) * 4px);
50
+ padding-inline: ${t.padInline};
51
+ padding-block: ${sys.space(2)};
52
+ color: ${t.headlineFg};
53
+ text-decoration: none;
54
+ background: transparent;
55
+ border: none;
56
+ inline-size: 100%;
57
+ text-align: start;
58
+ }
59
+ .two-line { min-block-size: calc(${t.twoLineHeight} + var(--ui-density, 0) * 4px); }
60
+ ${focusRingOn('.interactive')}
61
+ .interactive { cursor: pointer; user-select: none; }
62
+ .layer {
63
+ position: absolute; inset: 0; z-index: -1;
64
+ background: currentColor; opacity: 0; pointer-events: none;
65
+ transition: opacity ${sys.duration.short2} ${sys.easing.standard};
66
+ }
67
+ .interactive:hover .layer { opacity: ${sys.state.hover}; }
68
+ .interactive:focus-visible .layer { opacity: ${sys.state.focus}; }
69
+ .interactive:active .layer { opacity: ${sys.state.pressed}; }
70
+ .selected { background: ${t.selectedBg}; color: ${t.selectedFg}; }
71
+
72
+ .text {
73
+ display: flex;
74
+ flex-direction: column;
75
+ flex: 1;
76
+ min-inline-size: 0;
77
+ gap: calc(${sys.space(1)} / 2);
78
+ }
79
+ .headline {
80
+ font: ${t.headlineFont};
81
+ letter-spacing: ${sys.tracking.bodyLg};
82
+ }
83
+ .supporting {
84
+ font: ${t.supportingFont};
85
+ letter-spacing: ${sys.tracking.bodyMd};
86
+ color: ${t.supportingFg};
87
+ }
88
+ .control:not(.two-line) .supporting { display: none; }
89
+ .trailing {
90
+ display: inline-flex;
91
+ align-items: center;
92
+ flex: none;
93
+ font: ${t.trailingFont};
94
+ letter-spacing: ${sys.tracking.labelSm};
95
+ color: ${t.trailingFg};
96
+ }
97
+ .disabled {
98
+ pointer-events: none;
99
+ color: color-mix(in srgb, ${sys.color.onSurface} calc(${sys.state.disabledContent} * 100%), transparent);
100
+ }
101
+ .disabled .supporting, .disabled .trailing { color: inherit; }
102
+ `;
103
+
104
+ define('ui-list-item', {
105
+ props: {
106
+ headline: '', supporting: '',
107
+ interactive: false, href: '', selected: false, disabled: false,
108
+ },
109
+ styles: [base, styles],
110
+ setup({ headline, supporting, interactive, href, selected, disabled }, host) {
111
+ host.setAttribute('role', 'listitem');
112
+
113
+ const slotSupporting = signal(false);
114
+ const twoLine = computed(() => !!supporting() || slotSupporting());
115
+ const isInteractive = computed(() => !!href() || interactive());
116
+
117
+ const cls = computed(() =>
118
+ 'control' +
119
+ (isInteractive() ? ' interactive' : '') +
120
+ (selected() ? ' selected' : '') +
121
+ (twoLine() ? ' two-line' : '') +
122
+ (disabled() ? ' disabled' : ''));
123
+
124
+ const onSupportingSlot = (el) =>
125
+ el.addEventListener('slotchange', () => slotSupporting.set(el.assignedNodes().length > 0));
126
+
127
+ const onKeydown = (e) => {
128
+ if (!href() && interactive() && !disabled() && (e.key === 'Enter' || e.key === ' ')) {
129
+ e.preventDefault();
130
+ e.currentTarget.click();
131
+ }
132
+ };
133
+ const onClick = (e) => {
134
+ if (disabled()) { e.preventDefault(); e.stopPropagation(); }
135
+ };
136
+
137
+ const inner = html`
138
+ <span class="layer" aria-hidden="true"></span>
139
+ <slot name="leading"></slot>
140
+ <span class="text">
141
+ <span class="headline">${headline}<slot></slot></span>
142
+ <span class="supporting">${supporting}<slot name="supporting" ref=${onSupportingSlot}></slot></span>
143
+ </span>
144
+ <span class="trailing"><slot name="trailing"></slot></span>`;
145
+
146
+ const notInteractive = () => disabled() || !isInteractive();
147
+
148
+ return html`${() =>
149
+ href()
150
+ ? html`<a part="control" class=${cls} href=${href}
151
+ aria-disabled=${() => (disabled() ? 'true' : null)}
152
+ tabindex=${() => (disabled() ? '-1' : null)}
153
+ @click=${onClick}
154
+ ref=${(el) => ripple(el, { disabled: notInteractive })}>${inner}</a>`
155
+ : html`<div part="control" class=${cls}
156
+ role=${() => (interactive() ? 'button' : null)}
157
+ tabindex=${() => (interactive() && !disabled() ? '0' : null)}
158
+ aria-disabled=${() => (interactive() && disabled() ? 'true' : null)}
159
+ aria-current=${() => (selected() ? 'true' : null)}
160
+ @click=${onClick} @keydown=${onKeydown}
161
+ ref=${(el) => ripple(el, { disabled: notInteractive })}>${inner}</div>`}`;
162
+ },
163
+ });
164
+
165
+ export const tag = 'ui-list-item';
166
+ export const themeVars = t;
@@ -0,0 +1,38 @@
1
+ // <ui-list> — a Material list container for <ui-list-item> children.
2
+ //
3
+ // @prop {string} label='' — accessible name for the list
4
+ // @slot (default) — <ui-list-item> elements (and <ui-divider>s)
5
+ // @vars --ui-list-bg, --ui-list-pad-block
6
+ //
7
+ // role="list" on the host; items carry role="listitem".
8
+
9
+ import { define, html, css, vars, effect } from '@alacris/core';
10
+ import { sys } from '../tokens/sys.js';
11
+ import { base } from './base.js';
12
+
13
+ const t = vars('ui-list', {
14
+ bg: 'transparent',
15
+ padBlock: sys.space(2),
16
+ });
17
+
18
+ define('ui-list', {
19
+ props: { label: '' },
20
+ styles: [base, css`
21
+ :host {
22
+ display: block;
23
+ background: ${t.bg};
24
+ padding-block: ${t.padBlock};
25
+ }
26
+ `],
27
+ setup({ label }, host) {
28
+ host.setAttribute('role', 'list');
29
+ effect(() => {
30
+ if (label()) host.setAttribute('aria-label', label());
31
+ else host.removeAttribute('aria-label');
32
+ });
33
+ return html`<slot></slot>`;
34
+ },
35
+ });
36
+
37
+ export const tag = 'ui-list';
38
+ export const themeVars = t;
@@ -0,0 +1,74 @@
1
+ // <ui-loading-indicator> — the Material loading indicator: morphing dots,
2
+ // distinct from determinate <ui-progress> / <ui-spinner>.
3
+ //
4
+ // <ui-loading-indicator label="Loading"></ui-loading-indicator>
5
+ // <ui-loading-indicator variant="contained"></ui-loading-indicator>
6
+ //
7
+ // Always indeterminate. `contained` draws the dots on a tonal pill.
8
+ //
9
+ // @prop {string} variant='uncontained' — uncontained | contained
10
+ // @prop {string} label='Loading' — accessible name
11
+ // @part track, dot
12
+ // @vars see `t` below (`themeVars.names`)
13
+
14
+ import { define, html, css, vars, computed } from '@alacris/core';
15
+ import { sys } from '../tokens/sys.js';
16
+ import { base } from './base.js';
17
+
18
+ const t = vars('ui-loading-indicator', {
19
+ size: '10px',
20
+ gap: sys.space(2),
21
+ dot: sys.color.primary,
22
+ containedBg: sys.color.secondaryContainer,
23
+ containedPad: sys.space(3),
24
+ containedRadius: sys.radius.full,
25
+ });
26
+
27
+ const styles = css`
28
+ :host { display: inline-flex; vertical-align: middle; }
29
+ .track {
30
+ display: inline-flex;
31
+ align-items: center;
32
+ justify-content: center;
33
+ gap: ${t.gap};
34
+ }
35
+ .contained {
36
+ padding: ${t.containedPad} ${sys.space(4)};
37
+ background: ${t.containedBg};
38
+ border-radius: ${t.containedRadius};
39
+ }
40
+ .dot {
41
+ inline-size: ${t.size};
42
+ block-size: ${t.size};
43
+ border-radius: ${sys.radius.full};
44
+ background: ${t.dot};
45
+ animation: ui-loading-dot ${sys.duration.extraLong2} ${sys.easing.standard} infinite;
46
+ }
47
+ .dot:nth-child(2) { animation-delay: calc(${sys.duration.short4} * 0.5); }
48
+ .dot:nth-child(3) { animation-delay: ${sys.duration.short4}; }
49
+ .dot:nth-child(4) { animation-delay: calc(${sys.duration.short4} * 1.5); }
50
+ @keyframes ui-loading-dot {
51
+ 0%, 80%, 100% { transform: scale(0.55); opacity: 0.4; }
52
+ 40% { transform: scale(1); opacity: 1; }
53
+ }
54
+ `;
55
+
56
+ define('ui-loading-indicator', {
57
+ props: { variant: 'uncontained', label: 'Loading' },
58
+ styles: [base, styles],
59
+ setup({ variant, label }) {
60
+ const cls = computed(() =>
61
+ `track${variant() === 'contained' ? ' contained' : ''}`);
62
+ return html`
63
+ <div class=${cls} part="track" role="progressbar" aria-busy="true"
64
+ aria-label=${() => label() || 'Loading'}>
65
+ <span class="dot" part="dot" aria-hidden="true"></span>
66
+ <span class="dot" part="dot" aria-hidden="true"></span>
67
+ <span class="dot" part="dot" aria-hidden="true"></span>
68
+ <span class="dot" part="dot" aria-hidden="true"></span>
69
+ </div>`;
70
+ },
71
+ });
72
+
73
+ export const tag = 'ui-loading-indicator';
74
+ export const themeVars = t;
@@ -0,0 +1,117 @@
1
+ // <ui-menu-item> — one action inside <ui-menu>.
2
+ //
3
+ // The host carries the `menuitem` semantics so the menu's roving tabindex can
4
+ // manage it directly in the light DOM.
5
+ //
6
+ // @prop {string} value='' — reported in the menu's `select` detail
7
+ // @prop {string} icon='' — leading registry icon
8
+ // @prop {boolean} disabled=false
9
+ // @prop {boolean} danger=false — error color for destructive actions
10
+ // @event ui-menu-select — internal; detail: { value } (consumed by <ui-menu>)
11
+ // @slot (default) — label
12
+ // @slot icon — custom leading icon when `icon` is empty
13
+ // @slot trailing — trailing hint (keyboard shortcut, badge)
14
+ // @part control — the styled item 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-menu-item', {
24
+ height: '48px',
25
+ padInline: sys.space(3),
26
+ gap: sys.space(3),
27
+ font: sys.type.bodyLg,
28
+ tracking: sys.tracking.bodyLg,
29
+ fg: sys.color.onSurface,
30
+ iconFg: sys.color.onSurfaceVariant,
31
+ trailingFg: sys.color.onSurfaceVariant,
32
+ dangerFg: sys.color.error,
33
+ });
34
+
35
+ const styles = css`
36
+ :host { display: block; outline: none; }
37
+ :host([aria-disabled="true"]) { pointer-events: none; }
38
+ ${focusRingOn(':host')}
39
+ :host(:focus-visible) { outline-offset: -2px; }
40
+ .control {
41
+ position: relative;
42
+ isolation: isolate;
43
+ display: flex;
44
+ align-items: center;
45
+ gap: ${t.gap};
46
+ min-block-size: calc(${t.height} + var(--ui-density, 0) * 4px);
47
+ padding-inline: ${t.padInline};
48
+ font: ${t.font};
49
+ letter-spacing: ${t.tracking};
50
+ color: ${t.fg};
51
+ cursor: pointer;
52
+ user-select: none;
53
+ white-space: nowrap;
54
+ --ui-icon-size: 1.5rem;
55
+ }
56
+ .danger { color: ${t.dangerFg}; }
57
+ .layer {
58
+ position: absolute; inset: 0; z-index: -1;
59
+ background: currentColor; opacity: 0;
60
+ transition: opacity ${sys.duration.short2} ${sys.easing.standard};
61
+ }
62
+ .control:hover .layer { opacity: ${sys.state.hover}; }
63
+ :host(:focus-visible) .layer { opacity: ${sys.state.focus}; }
64
+ .control:active .layer { opacity: ${sys.state.pressed}; }
65
+ .leading { display: inline-flex; color: ${t.iconFg}; }
66
+ .danger .leading { color: inherit; }
67
+ .label { flex: 1; }
68
+ .trailing {
69
+ display: inline-flex;
70
+ color: ${t.trailingFg};
71
+ font: ${sys.type.labelMd};
72
+ letter-spacing: ${sys.tracking.labelMd};
73
+ }
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-menu-item', {
81
+ props: { value: '', icon: '', disabled: false, danger: false },
82
+ styles: [base, styles],
83
+ setup({ value, icon, disabled, danger }, host) {
84
+ host.setAttribute('role', 'menuitem');
85
+ if (!host.hasAttribute('tabindex')) host.tabIndex = -1;
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-menu-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${danger() ? ' danger' : ''}`}
105
+ ref=${(el) => ripple(el, { disabled })}>
106
+ <span class="layer" aria-hidden="true"></span>
107
+ <span class="leading" aria-hidden="true">
108
+ ${() => (icon() ? html`<ui-icon name=${icon}></ui-icon>` : html`<slot name="icon"></slot>`)}
109
+ </span>
110
+ <span class="label"><slot></slot></span>
111
+ <span class="trailing"><slot name="trailing"></slot></span>
112
+ </div>`;
113
+ },
114
+ });
115
+
116
+ export const tag = 'ui-menu-item';
117
+ export const themeVars = t;