@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alacris contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1 +1,148 @@
1
- Placeholder. Real releases come from https://github.com/bmartel/alacris
1
+ # Alacris UI
2
+
3
+ A complete, themeable design system built with **[Alacris](https://github.com/bmartel/alacris)** and nothing else. No other runtime dependencies, no build step, no CSS framework — 68 components, a three-tier token system, a theme engine that re-skins the whole page with one stylesheet write, and a documented motion layer.
4
+
5
+ **Material Design is the default. Your design system is the point.** Every visual decision flows through tokens you can override at three levels — from "change the brand color" (one line) to "this is no longer Material at all".
6
+
7
+ The tags are real custom elements, so they work in plain HTML and inside React, Vue, Svelte, Angular, Rails, Django, and [Alacris-Go](https://github.com/bmartel/alacris-go) — anywhere that renders a tag.
8
+
9
+ **Docs:** [Getting started](https://bmartel.github.io/alacris/ui/getting-started/)
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install @alacris/ui
15
+ ```
16
+
17
+ That pulls in `@alacris/core` as well. Do not also load a second copy of Alacris from a CDN on the same page — two copies means two reactive graphs, and updates stop crossing the boundary.
18
+
19
+ ```js
20
+ import { applyTheme } from '@alacris/ui';
21
+
22
+ applyTheme({ seed: '#0b57d0' });
23
+ ```
24
+
25
+ ```html
26
+ <ui-button>Hello</ui-button>
27
+ <ui-text-field label="Email" clearable></ui-text-field>
28
+ <ui-switch label="Dark mode"></ui-switch>
29
+ ```
30
+
31
+ Importing `@alacris/ui` registers every component. For a smaller page, import only what you use — each component module is self-contained:
32
+
33
+ ```js
34
+ import { applyTheme } from '@alacris/ui/theme';
35
+ import '@alacris/ui/components/ui-button.js';
36
+ import '@alacris/ui/components/ui-text-field.js';
37
+
38
+ applyTheme({ seed: '#0b57d0' });
39
+ ```
40
+
41
+ ### Without a bundler
42
+
43
+ The published package is plain ESM. Point an import map at a pinned CDN build of Alacris (the only bare specifier the UI source uses) and at this package:
44
+
45
+ ```html
46
+ <script type="importmap">
47
+ {
48
+ "imports": {
49
+ "@alacris/core": "https://cdn.jsdelivr.net/npm/@alacris/core@0.11/dist/alacris.js",
50
+ "@alacris/ui": "https://cdn.jsdelivr.net/npm/@alacris/ui/src/index.js",
51
+ "@alacris/ui/theme": "https://cdn.jsdelivr.net/npm/@alacris/ui/src/theme/index.js",
52
+ "@alacris/ui/components/": "https://cdn.jsdelivr.net/npm/@alacris/ui/src/components/"
53
+ }
54
+ }
55
+ </script>
56
+ <script type="module">
57
+ import { applyTheme } from '@alacris/ui';
58
+ applyTheme({ seed: '#0b57d0' });
59
+ </script>
60
+
61
+ <ui-button>Hello</ui-button>
62
+ ```
63
+
64
+ Pin both packages in production.
65
+
66
+ ## Theming
67
+
68
+ ```js
69
+ import { createTheme, applyTheme, setScheme, toggleScheme } from '@alacris/ui/theme';
70
+
71
+ applyTheme({ seed: '#6750a4' });
72
+ applyTheme({ seed: '#b3261e', density: -1 });
73
+ setScheme('dark');
74
+ toggleScheme();
75
+
76
+ applyTheme(createTheme({
77
+ colors: { primary: '#0b57d0', tertiary: '#00695c' },
78
+ typography: 'google-sans',
79
+ shape: { radius: 0.5 },
80
+ overrides: { light: { 'color-surface': '#faf7f2' } },
81
+ }));
82
+ ```
83
+
84
+ `applyTheme` writes **one** document-level stylesheet of `--ui-*` custom properties and replaces it in place on the next call. Scheme switching after that is `data-ui-scheme` on `<html>` (`light` | `dark` | `auto`).
85
+
86
+ Three places to intervene, from broadest to narrowest:
87
+
88
+ 1. **Re-theme the system** — `applyTheme({ seed, shape, … })`. Every component follows. Dark mode is generated automatically.
89
+ 2. **Re-skin one component type** — `ui-button { --ui-button-radius: 4px; }`.
90
+ 3. **Reach inside one instance** — `ui-dialog::part(surface) { backdrop-filter: blur(8px) }`.
91
+
92
+ Full guide: [docs/theming.md](docs/theming.md).
93
+
94
+ ## Motion
95
+
96
+ ```js
97
+ import { animate, fx, presence, withFlip } from '@alacris/ui/motion';
98
+
99
+ animate(el, fx.slideInUp, { duration: 'medium2', easing: 'emphasizedDecelerate' });
100
+
101
+ html`${presence(open, () => html`<div class="sheet">…</div>`, {
102
+ enter: fx.slideInUp, exit: fx.slideOutDown,
103
+ })}`;
104
+ ```
105
+
106
+ Durations and easings are tokens, so the theme's `motion.scale` governs CSS transitions and JS animation alike, and `prefers-reduced-motion` is honoured automatically. Full guide: [docs/motion.md](docs/motion.md).
107
+
108
+ ## Forms
109
+
110
+ Named controls (`ui-text-field`, `ui-switch`, `ui-checkbox`, `ui-select`, …) are form-associated custom elements. Give a component a `name` and it submits, resets, and follows `<fieldset disabled>` like a native field.
111
+
112
+ ## Entry points
113
+
114
+ | Import | What it is |
115
+ | --- | --- |
116
+ | `@alacris/ui` | registers every component; re-exports theme, motion, tokens, utilities |
117
+ | `@alacris/ui/theme` | `createTheme`, `applyTheme`, scheme switching |
118
+ | `@alacris/ui/motion` | `animate`, `fx`, `presence`, `withFlip`, `ripple` |
119
+ | `@alacris/ui/tokens` | `sys` and the token engines |
120
+ | `@alacris/ui/components/ui-button.js` | one element, registered as a side effect |
121
+
122
+ Component modules have side effects (they call `define()`). Theme, motion and token modules do not.
123
+
124
+ ## In this repository
125
+
126
+ This package lives in `starter/` of the [Alacris repo](https://github.com/bmartel/alacris) so the library and the design system can be developed together. They **publish separately**: `@alacris/ui` has its own version, changelog, and npm release. A commit that only changes this folder does not cut an Alacris library release.
127
+
128
+ ```
129
+ starter/
130
+ src/ the published modules
131
+ types/ hand-written .d.ts
132
+ demo/ kitchen-sink app (not published)
133
+ docs/ theming, motion, component catalog
134
+ test/ node smoke tests
135
+ CONVENTIONS.md rules every component follows
136
+ ```
137
+
138
+ From a clone of the repo:
139
+
140
+ ```bash
141
+ npm install
142
+ npm run demo # http://localhost:5173/starter/
143
+ cd starter && npm test
144
+ ```
145
+
146
+ ## License
147
+
148
+ MIT
package/package.json CHANGED
@@ -1,7 +1,79 @@
1
1
  {
2
2
  "name": "@alacris/ui",
3
- "version": "0.0.0",
4
- "description": "Placeholder so trusted publishing can be configured. Do not use.",
3
+ "version": "0.1.1",
4
+ "description": "Themeable design system for Alacris Material defaults, sixty-eight custom elements, ESM-only, no build step.",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./types/index.d.ts",
9
+ "default": "./src/index.js"
10
+ },
11
+ "./theme": {
12
+ "types": "./types/index.d.ts",
13
+ "default": "./src/theme/index.js"
14
+ },
15
+ "./motion": {
16
+ "types": "./types/index.d.ts",
17
+ "default": "./src/motion/index.js"
18
+ },
19
+ "./tokens": {
20
+ "types": "./types/index.d.ts",
21
+ "default": "./src/tokens/index.js"
22
+ },
23
+ "./components/*": "./src/components/*",
24
+ "./src/*": "./src/*",
25
+ "./package.json": "./package.json"
26
+ },
27
+ "module": "./src/index.js",
28
+ "types": "./types/index.d.ts",
29
+ "unpkg": "./src/index.js",
30
+ "jsdelivr": "./src/index.js",
31
+ "sideEffects": [
32
+ "./src/index.js",
33
+ "./src/components/**/*.js"
34
+ ],
35
+ "files": [
36
+ "src",
37
+ "types",
38
+ "README.md",
39
+ "LICENSE"
40
+ ],
41
+ "scripts": {
42
+ "start": "node ../scripts/serve.js 5174",
43
+ "pretest": "node -e \"const fs=require('fs');fs.mkdirSync('node_modules/@alacris',{recursive:true});try{fs.symlinkSync('../../..','node_modules/@alacris/core')}catch{}\"",
44
+ "test": "node --import ./test/setup.js --test test/*.test.js",
45
+ "catalog": "node scripts/catalog.mjs"
46
+ },
47
+ "dependencies": {
48
+ "@alacris/core": "^0.11.0"
49
+ },
50
+ "peerDependencies": {
51
+ "@alacris/core": "^0.11.0"
52
+ },
53
+ "keywords": [
54
+ "alacris",
55
+ "web-components",
56
+ "custom-elements",
57
+ "design-system",
58
+ "material",
59
+ "theme",
60
+ "esm"
61
+ ],
5
62
  "license": "MIT",
6
- "publishConfig": { "access": "public" }
63
+ "author": "Brandon Martel <brandonmartel@gmail.com>",
64
+ "repository": {
65
+ "type": "git",
66
+ "url": "git+https://github.com/bmartel/alacris.git",
67
+ "directory": "starter"
68
+ },
69
+ "homepage": "https://bmartel.github.io/alacris/ui/getting-started/",
70
+ "bugs": {
71
+ "url": "https://github.com/bmartel/alacris/issues"
72
+ },
73
+ "publishConfig": {
74
+ "access": "public"
75
+ },
76
+ "engines": {
77
+ "node": ">=18"
78
+ }
7
79
  }
@@ -0,0 +1,46 @@
1
+ // Shared base for every component's `styles` list. `css` interns by text, so
2
+ // this parses exactly once for the whole page no matter how many components
3
+ // include it.
4
+
5
+ import { css } from '@alacris/core';
6
+ import { sys } from '../tokens/sys.js';
7
+
8
+ export const base = css`
9
+ *, *::before, *::after { box-sizing: border-box; }
10
+ :host {
11
+ font-family: ${sys.font.plain};
12
+ font-optical-sizing: auto;
13
+ font-synthesis: none;
14
+ -webkit-tap-highlight-color: transparent;
15
+ }
16
+ :host([hidden]) { display: none; }
17
+ @media (prefers-reduced-motion: reduce) {
18
+ *, *::before, *::after {
19
+ transition-duration: 0.01ms;
20
+ animation-duration: 0.01ms;
21
+ animation-iteration-count: 1;
22
+ }
23
+ }
24
+ `;
25
+
26
+ /**
27
+ * The focus ring rule for a selector, e.g. ${focusRingOn('.control')}.
28
+ * Interpolate into a component's css template.
29
+ */
30
+ export const focusRingOn = (selector) => `
31
+ ${selector}:focus-visible {
32
+ outline: var(--ui-focus-ring);
33
+ outline-offset: var(--ui-focus-ring-offset);
34
+ }
35
+ `;
36
+
37
+ /**
38
+ * Hover / focus-visible / pressed opacities for a `.layer` descendant.
39
+ * `host` is the interactive element (`.control`, `.header`, …). Pass
40
+ * `focus` when focus lives on a different node (the host custom element).
41
+ */
42
+ export const stateLayerOn = (host, { focus = host } = {}) => `
43
+ ${host}:hover .layer { opacity: var(--ui-state-hover); }
44
+ ${focus}:focus-visible .layer { opacity: var(--ui-state-focus); }
45
+ ${host}:active .layer { opacity: var(--ui-state-pressed); }
46
+ `;
@@ -0,0 +1,157 @@
1
+ // <ui-accordion-item> — one expandable panel inside <ui-accordion>.
2
+ //
3
+ // @prop {string} value='' — REQUIRED identity of the panel (reported in events)
4
+ // @prop {boolean} expanded=false
5
+ // @prop {boolean} disabled=false
6
+ // @prop {string} headline='' — header text
7
+ // @event ui-accordion-toggle — header activated; detail: { value, expanded }
8
+ // @slot (default) — panel content
9
+ // @part header — the header <button>
10
+ // @part content — the collapsible region
11
+ // @part body — padded wrapper around the default slot
12
+ // @vars see `t` below (`themeVars.names`)
13
+ //
14
+ // Expand/collapse animates block-size from the measured content height; in
15
+ // environments without layout (zero heights) the animation is skipped and the
16
+ // state applies instantly. The content region gets `hidden` only AFTER the
17
+ // collapse animation completes.
18
+
19
+ import { define, html, css, vars, effect } from '@alacris/core';
20
+ import { sys } from '../tokens/sys.js';
21
+ import { base, focusRingOn } from './base.js';
22
+ import { ripple } from '../motion/ripple.js';
23
+ import { animate } from '../motion/animate.js';
24
+ import './ui-icon.js';
25
+
26
+ const t = vars('ui-accordion-item', {
27
+ headerMinHeight: '48px',
28
+ padInline: sys.space(4),
29
+ headerFg: sys.color.onSurface,
30
+ iconFg: sys.color.onSurfaceVariant,
31
+ contentFg: sys.color.onSurfaceVariant,
32
+ font: sys.type.titleMd,
33
+ tracking: sys.tracking.titleMd,
34
+ contentFont: sys.type.bodyMd,
35
+ contentTracking: sys.tracking.bodyMd,
36
+ });
37
+
38
+ const styles = css`
39
+ :host { display: block; }
40
+ .heading { margin: 0; font: inherit; }
41
+ .header {
42
+ position: relative;
43
+ isolation: isolate;
44
+ display: flex;
45
+ align-items: center;
46
+ justify-content: space-between;
47
+ gap: ${sys.space(3)};
48
+ inline-size: 100%;
49
+ min-block-size: calc(${t.headerMinHeight} + var(--ui-density, 0) * 4px);
50
+ padding-inline: ${t.padInline};
51
+ padding-block: ${sys.space(2)};
52
+ border: none;
53
+ background: transparent;
54
+ color: ${t.headerFg};
55
+ font: ${t.font};
56
+ letter-spacing: ${t.tracking};
57
+ text-align: start;
58
+ cursor: pointer;
59
+ user-select: none;
60
+ }
61
+ ${focusRingOn('.header')}
62
+ .layer {
63
+ position: absolute; inset: 0; z-index: -1;
64
+ background: currentColor; opacity: 0;
65
+ transition: opacity ${sys.duration.short2} ${sys.easing.standard};
66
+ }
67
+ .header:hover .layer { opacity: ${sys.state.hover}; }
68
+ .header:focus-visible .layer { opacity: ${sys.state.focus}; }
69
+ .header:active .layer { opacity: ${sys.state.pressed}; }
70
+ .header: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
+ .chevron {
76
+ color: ${t.iconFg};
77
+ transition: rotate ${sys.duration.short4} ${sys.easing.standard};
78
+ }
79
+ .header:disabled .chevron { color: inherit; }
80
+ .chevron.open { rotate: 180deg; }
81
+ .content { overflow: hidden; }
82
+ .content[hidden] { display: none; }
83
+ .body {
84
+ padding-inline: ${t.padInline};
85
+ padding-block-end: ${sys.space(4)};
86
+ font: ${t.contentFont};
87
+ letter-spacing: ${t.contentTracking};
88
+ color: ${t.contentFg};
89
+ }
90
+ `;
91
+
92
+ define('ui-accordion-item', {
93
+ props: { value: '', expanded: false, disabled: false, headline: '' },
94
+ styles: [base, styles],
95
+ setup({ value, expanded, disabled, headline }, host) {
96
+ let contentEl = null;
97
+ let anim = null;
98
+
99
+ // Animate block-size between 0 and the measured content height. Zero
100
+ // heights (no layout, e.g. simulated DOM) skip the animation entirely.
101
+ const apply = (exp) => {
102
+ const el = contentEl;
103
+ anim?.cancel();
104
+ anim = null;
105
+ if (exp) {
106
+ el.hidden = false;
107
+ const h = el.scrollHeight;
108
+ if (!h) return;
109
+ const a = (anim = animate(el, [{ blockSize: '0px' }, { blockSize: `${h}px` }], {
110
+ duration: 'medium2', easing: 'emphasizedDecelerate',
111
+ }));
112
+ // Cancelling after it finishes drops the fill, so block-size clears
113
+ // back to auto (the end keyframe equals the natural height).
114
+ a.finished.then(() => { if (anim === a) { anim = null; a.cancel(); } }, () => {});
115
+ } else {
116
+ const h = el.scrollHeight;
117
+ if (!h) { el.hidden = true; return; }
118
+ const a = (anim = animate(el, [{ blockSize: `${h}px` }, { blockSize: '0px' }], {
119
+ duration: 'short4', easing: 'emphasizedAccelerate',
120
+ }));
121
+ a.finished.then(() => { if (anim === a) { anim = null; a.cancel(); el.hidden = true; } }, () => {});
122
+ }
123
+ };
124
+
125
+ effect(() => {
126
+ const exp = expanded();
127
+ if (contentEl) apply(exp); // first run precedes the ref; it syncs below
128
+ });
129
+
130
+ const toggle = () => {
131
+ if (disabled()) return;
132
+ expanded.set(!expanded());
133
+ host.emit('ui-accordion-toggle', { value: value(), expanded: expanded() });
134
+ };
135
+
136
+ return html`
137
+ <h3 class="heading">
138
+ <button part="header" class="header" type="button" id="header"
139
+ ?disabled=${disabled}
140
+ aria-expanded=${() => String(expanded())}
141
+ aria-controls="content"
142
+ @click=${toggle}
143
+ ref=${(el) => ripple(el, { disabled })}>
144
+ <span class="layer" aria-hidden="true"></span>
145
+ <span class="headline">${headline}</span>
146
+ <ui-icon class=${() => `chevron${expanded() ? ' open' : ''}`} name="expand-more"></ui-icon>
147
+ </button>
148
+ </h3>
149
+ <div part="content" class="content" id="content" role="region" aria-labelledby="header"
150
+ ref=${(el) => { contentEl = el; el.hidden = !expanded.peek(); }}>
151
+ <div class="body" part="body"><slot></slot></div>
152
+ </div>`;
153
+ },
154
+ });
155
+
156
+ export const tag = 'ui-accordion-item';
157
+ export const themeVars = t;
@@ -0,0 +1,58 @@
1
+ // <ui-accordion> — groups <ui-accordion-item> panels.
2
+ //
3
+ // <ui-accordion>
4
+ // <ui-accordion-item value="a" headline="First">…</ui-accordion-item>
5
+ // <ui-accordion-item value="b" headline="Second">…</ui-accordion-item>
6
+ // </ui-accordion>
7
+ //
8
+ // @prop {boolean} multi=false — allow several panels open at once; when
9
+ // false, expanding one collapses the others
10
+ // @event change — a panel was toggled by the user; detail: { value }
11
+ // @slot (default) — <ui-accordion-item> children
12
+ // @vars see `t` below (`themeVars.names`)
13
+ //
14
+ // Coordination rides the bubbling `ui-accordion-toggle` event from the items,
15
+ // so only user interaction triggers single-open collapse; programmatic
16
+ // `expanded` writes on an item are left alone.
17
+
18
+ import { define, html, css, vars, onCleanup } from '@alacris/core';
19
+ import { sys } from '../tokens/sys.js';
20
+ import { base } from './base.js';
21
+ import './ui-accordion-item.js';
22
+
23
+ const t = vars('ui-accordion', {
24
+ dividerColor: sys.color.outlineVariant,
25
+ });
26
+
27
+ const styles = css`
28
+ :host { display: block; }
29
+ ::slotted(ui-accordion-item:not(:first-child)) {
30
+ border-block-start: 1px solid ${t.dividerColor};
31
+ }
32
+ `;
33
+
34
+ define('ui-accordion', {
35
+ props: { multi: false },
36
+ styles: [base, styles],
37
+ setup({ multi }, host) {
38
+ const onToggle = (e) => {
39
+ const item = e.target;
40
+ if (!multi() && e.detail?.expanded) {
41
+ for (const other of host.querySelectorAll('ui-accordion-item')) {
42
+ if (other !== item && other.expanded) other.expanded = false;
43
+ }
44
+ }
45
+ host.emit('change', { value: e.detail?.value });
46
+ };
47
+
48
+ // Listen on the host: items are light-DOM children, so their bubbling
49
+ // toggle events reach it without crossing this shadow root.
50
+ host.addEventListener('ui-accordion-toggle', onToggle);
51
+ onCleanup(() => host.removeEventListener('ui-accordion-toggle', onToggle));
52
+
53
+ return html`<slot></slot>`;
54
+ },
55
+ });
56
+
57
+ export const tag = 'ui-accordion';
58
+ export const themeVars = t;
@@ -0,0 +1,168 @@
1
+ // <ui-alert> — a severity-colored callout for statuses and messages.
2
+ //
3
+ // <ui-alert severity="success" title="Saved" dismissible @dismiss=${remove}>
4
+ // Your changes are safe.
5
+ // <ui-button slot="action" variant="text">Undo</ui-button>
6
+ // </ui-alert>
7
+ //
8
+ // Live-region semantics: the host defaults to role="status" (announced
9
+ // politely). If the alert appears dynamically in response to an event and must
10
+ // interrupt, set role="alert" on the element yourself — an author-set role is
11
+ // never overwritten.
12
+ //
13
+ // Dismissing: the close button collapses the alert (height + opacity), then
14
+ // emits `dismiss`. The PARENT owns the DOM and removes the element (or flips
15
+ // the condition rendering it).
16
+ //
17
+ // @prop {string} severity='info' — info | success | warning | error
18
+ // @prop {string} variant='tonal' — tonal | filled | outlined
19
+ // @prop {boolean} dismissible=false — shows a trailing close button
20
+ // @prop {string} icon='' — leading icon override; defaults per severity
21
+ // (info → 'info', success → 'check-circle',
22
+ // warning → 'warning', error → 'error')
23
+ // @prop {string} title='' — optional bold first line
24
+ // @event dismiss — close button pressed, after the collapse animation
25
+ // @slot (default) — message body
26
+ // @slot action — trailing action, e.g. a text <ui-button>
27
+ // @part container, icon, title, message, action, close
28
+ // @vars see `t` below (`themeVars.names`)
29
+
30
+ import { define, html, css, vars, computed } from '@alacris/core';
31
+ import { sys } from '../tokens/sys.js';
32
+ import { base, focusRingOn } from './base.js';
33
+ import { animate, settled } from '../motion/animate.js';
34
+ import { ripple } from '../motion/ripple.js';
35
+ import './ui-icon.js';
36
+
37
+ const SEVERITY_ICONS = {
38
+ info: 'info',
39
+ success: 'check-circle',
40
+ warning: 'warning',
41
+ error: 'error',
42
+ };
43
+
44
+ const t = vars('ui-alert', {
45
+ radius: sys.radius.sm,
46
+ padding: sys.space(4),
47
+ gap: sys.space(3),
48
+ font: sys.type.bodyMd,
49
+ titleFont: sys.type.titleSm,
50
+ });
51
+
52
+ const styles = css`
53
+ :host { display: block; }
54
+ .container {
55
+ display: flex;
56
+ align-items: flex-start;
57
+ gap: ${t.gap};
58
+ padding: ${t.padding};
59
+ border: 1px solid transparent; /* structural: keeps outlined the same size */
60
+ border-radius: ${t.radius};
61
+ font: ${t.font};
62
+ letter-spacing: ${sys.tracking.bodyMd};
63
+ }
64
+ .icon { flex: none; --ui-icon-size: 1.375rem; }
65
+ .content { flex: 1; min-inline-size: 0; }
66
+ .title {
67
+ font: ${t.titleFont};
68
+ letter-spacing: ${sys.tracking.titleSm};
69
+ margin-block-end: ${sys.space(1)};
70
+ }
71
+ .action { flex: none; align-self: center; display: flex; align-items: center; }
72
+
73
+ /* tonal — severity container bg, on-container fg */
74
+ .tonal.info { background: ${sys.color.infoContainer}; color: ${sys.color.onInfoContainer}; }
75
+ .tonal.success { background: ${sys.color.successContainer}; color: ${sys.color.onSuccessContainer}; }
76
+ .tonal.warning { background: ${sys.color.warningContainer}; color: ${sys.color.onWarningContainer}; }
77
+ .tonal.error { background: ${sys.color.errorContainer}; color: ${sys.color.onErrorContainer}; }
78
+
79
+ /* filled — severity bg, on-severity fg */
80
+ .filled.info { background: ${sys.color.info}; color: ${sys.color.onInfo}; }
81
+ .filled.success { background: ${sys.color.success}; color: ${sys.color.onSuccess}; }
82
+ .filled.warning { background: ${sys.color.warning}; color: ${sys.color.onWarning}; }
83
+ .filled.error { background: ${sys.color.error}; color: ${sys.color.onError}; }
84
+
85
+ /* outlined — transparent bg, severity fg + border */
86
+ .outlined { background: transparent; border-color: currentColor; }
87
+ .outlined.info { color: ${sys.color.info}; }
88
+ .outlined.success { color: ${sys.color.success}; }
89
+ .outlined.warning { color: ${sys.color.warning}; }
90
+ .outlined.error { color: ${sys.color.error}; }
91
+
92
+ .close {
93
+ position: relative;
94
+ isolation: isolate;
95
+ flex: none;
96
+ align-self: center;
97
+ display: inline-flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ inline-size: 28px;
101
+ block-size: 28px;
102
+ padding: 0;
103
+ border: none;
104
+ border-radius: ${sys.radius.full};
105
+ background: transparent;
106
+ color: inherit;
107
+ cursor: pointer;
108
+ --ui-icon-size: 18px;
109
+ }
110
+ ${focusRingOn('.close')}
111
+ .layer {
112
+ position: absolute; inset: 0; z-index: -1;
113
+ border-radius: inherit; background: currentColor; opacity: 0;
114
+ transition: opacity ${sys.duration.short2} ${sys.easing.standard};
115
+ }
116
+ .close:hover .layer { opacity: ${sys.state.hover}; }
117
+ .close:focus-visible .layer { opacity: ${sys.state.focus}; }
118
+ .close:active .layer { opacity: ${sys.state.pressed}; }
119
+ `;
120
+
121
+ define('ui-alert', {
122
+ props: { severity: 'info', variant: 'tonal', dismissible: false, icon: '', title: '' },
123
+ styles: [base, styles],
124
+ setup({ severity, variant, dismissible, icon, title }, host) {
125
+ // Polite live region by default; authors set role="alert" for interrupts.
126
+ if (!host.hasAttribute('role')) host.setAttribute('role', 'status');
127
+
128
+ const cls = computed(() => `container ${variant()} ${severity()}`);
129
+ const shownIcon = computed(() => icon() || SEVERITY_ICONS[severity()] || SEVERITY_ICONS.info);
130
+
131
+ let dismissing = false;
132
+ const onDismiss = () => {
133
+ if (dismissing) return;
134
+ dismissing = true;
135
+ const height = host.scrollHeight || 0;
136
+ // Simulated DOMs (and display:none hosts) measure 0 — skip straight out.
137
+ if (!height) { host.emit('dismiss'); return; }
138
+ host.style.overflow = 'hidden';
139
+ const anim = animate(
140
+ host,
141
+ [{ blockSize: `${height}px`, opacity: 1 }, { blockSize: '0px', opacity: 0 }],
142
+ { duration: 'medium1', easing: 'emphasizedAccelerate' },
143
+ );
144
+ settled(anim).then(() => host.emit('dismiss'));
145
+ };
146
+
147
+ return html`
148
+ <div class=${cls} part="container">
149
+ <ui-icon class="icon" part="icon" name=${shownIcon}></ui-icon>
150
+ <div class="content">
151
+ ${() => (title() ? html`<div class="title" part="title">${title}</div>` : null)}
152
+ <div class="message" part="message"><slot></slot></div>
153
+ </div>
154
+ <div class="action" part="action"><slot name="action"></slot></div>
155
+ ${() =>
156
+ dismissible()
157
+ ? html`<button class="close" part="close" type="button" aria-label="Dismiss"
158
+ @click=${onDismiss} ref=${(el) => ripple(el, { centered: true })}>
159
+ <span class="layer" aria-hidden="true"></span>
160
+ <ui-icon name="close"></ui-icon>
161
+ </button>`
162
+ : null}
163
+ </div>`;
164
+ },
165
+ });
166
+
167
+ export const tag = 'ui-alert';
168
+ export const themeVars = t;