@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.
- 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 +316 -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
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
|
-
|
|
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.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
-
"
|
|
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;
|