@alacris/ui 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/package.json +3 -3
- package/src/components/ui-icon.js +15 -6
- package/src/components/ui-nav-item.js +1 -0
- package/src/components/ui-slider.js +32 -3
- package/src/util/icons.js +25 -8
- package/types/index.d.ts +3 -0
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ A complete, themeable design system built with **[Alacris](https://github.com/bm
|
|
|
6
6
|
|
|
7
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
8
|
|
|
9
|
-
**Docs:** [Getting started](https://bmartel.github.io/alacris/ui/getting-started/)
|
|
9
|
+
**Docs:** [Live catalog](https://bmartel.github.io/alacris/ui/) · [Getting started](https://bmartel.github.io/alacris/ui/getting-started/)
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
@@ -123,10 +123,10 @@ Component modules have side effects (they call `define()`). Theme, motion and to
|
|
|
123
123
|
|
|
124
124
|
## In this repository
|
|
125
125
|
|
|
126
|
-
This package lives in `
|
|
126
|
+
This package lives in `ui/` 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. `starter/` in the same repo is a usage example and install guide, not this package.
|
|
127
127
|
|
|
128
128
|
```
|
|
129
|
-
|
|
129
|
+
ui/
|
|
130
130
|
src/ the published modules
|
|
131
131
|
types/ hand-written .d.ts
|
|
132
132
|
demo/ kitchen-sink app (not published)
|
|
@@ -135,12 +135,15 @@ starter/
|
|
|
135
135
|
CONVENTIONS.md rules every component follows
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
+
The live catalog is [bmartel.github.io/alacris/ui](https://bmartel.github.io/alacris/ui/) — every component plus a theme playground, no clone required.
|
|
139
|
+
|
|
138
140
|
From a clone of the repo:
|
|
139
141
|
|
|
140
142
|
```bash
|
|
141
143
|
npm install
|
|
142
|
-
npm run demo # http://localhost:5173/
|
|
143
|
-
|
|
144
|
+
npm run demo # kitchen sink: http://localhost:5173/ui/
|
|
145
|
+
# starter app: http://localhost:5173/starter/
|
|
146
|
+
cd ui && npm test
|
|
144
147
|
```
|
|
145
148
|
|
|
146
149
|
## License
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alacris/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Themeable design system for Alacris — Material defaults, sixty-eight custom elements, ESM-only, no build step.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -64,9 +64,9 @@
|
|
|
64
64
|
"repository": {
|
|
65
65
|
"type": "git",
|
|
66
66
|
"url": "git+https://github.com/bmartel/alacris.git",
|
|
67
|
-
"directory": "
|
|
67
|
+
"directory": "ui"
|
|
68
68
|
},
|
|
69
|
-
"homepage": "https://bmartel.github.io/alacris/ui/
|
|
69
|
+
"homepage": "https://bmartel.github.io/alacris/ui/",
|
|
70
70
|
"bugs": {
|
|
71
71
|
"url": "https://github.com/bmartel/alacris/issues"
|
|
72
72
|
},
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
// <ui-icon> — an icon from the registry, or any slotted SVG.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// Names are kebab-case (`arrow-forward`). Underscores are accepted
|
|
4
|
+
// (`arrow_forward`). `iconNames()` lists the built-in set; apps add more
|
|
5
|
+
// with `registerIcons({ name: 'M…' })`. An unknown name logs a warning
|
|
6
|
+
// once and renders a placeholder instead of an empty hole.
|
|
7
|
+
//
|
|
8
|
+
// @prop {string} name='' — registry name; empty renders the slot
|
|
4
9
|
// @prop {string} label='' — accessible name; empty marks the icon decorative
|
|
5
10
|
// @prop {string} size='' — CSS length; overrides --ui-icon-size for this element
|
|
6
11
|
// @slot (default) — a custom <svg> when no name is given
|
|
@@ -8,7 +13,10 @@
|
|
|
8
13
|
|
|
9
14
|
import { define, html, svg, css, vars, effect } from '@alacris/core';
|
|
10
15
|
import { base } from './base.js';
|
|
11
|
-
import { iconPath, iconsVersion } from '../util/icons.js';
|
|
16
|
+
import { iconPath, iconsVersion, warnUnknownIcon } from '../util/icons.js';
|
|
17
|
+
|
|
18
|
+
// Hollow square — obviously not a real glyph, so a typo is visible.
|
|
19
|
+
const MISSING = 'M3 3v18h18V3H3zm2 2h14v14H5V5z';
|
|
12
20
|
|
|
13
21
|
const t = vars('ui-icon', {
|
|
14
22
|
size: '1.5rem',
|
|
@@ -52,10 +60,11 @@ define('ui-icon', {
|
|
|
52
60
|
});
|
|
53
61
|
return html`${() => {
|
|
54
62
|
iconsVersion(); // re-render if icons register late
|
|
55
|
-
const
|
|
56
|
-
return
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
const n = name();
|
|
64
|
+
if (!n) return html`<slot></slot>`;
|
|
65
|
+
const d = iconPath(n);
|
|
66
|
+
if (!d) warnUnknownIcon(n);
|
|
67
|
+
return svg`<svg viewBox="0 0 24 24" aria-hidden="true" data-icon=${d ? n : 'unknown'}><path d=${d || MISSING}></path></svg>`;
|
|
59
68
|
}}`;
|
|
60
69
|
},
|
|
61
70
|
});
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
// <ui-slider> — a Material slider on native <input type="range">s for
|
|
2
2
|
// keyboard and screen-reader behavior.
|
|
3
3
|
//
|
|
4
|
+
// <ui-slider label="Volume" value=${volume}
|
|
5
|
+
// @input=${(e) => volume.set(e.detail.value)}></ui-slider>
|
|
6
|
+
//
|
|
7
|
+
// Bind `value` (or `.value`) to a signal like any other control. `input` /
|
|
8
|
+
// `change` report a number in `detail.value` (or `detail.start` / `detail.end`
|
|
9
|
+
// when `range`). The host `.value` is a string, matching a native range
|
|
10
|
+
// input, so composed-path helpers that look for `typeof node.value ===
|
|
11
|
+
// 'string'` work the same as they do for text fields.
|
|
12
|
+
//
|
|
4
13
|
// The active track portion is painted with `--ui-slider-fill` (or start/end
|
|
5
14
|
// when `range`) bound from the template into a gradient; the thumb's
|
|
6
15
|
// hover/focus halo is a box-shadow state layer.
|
|
@@ -157,6 +166,23 @@ define('ui-slider', {
|
|
|
157
166
|
});
|
|
158
167
|
formBind(host, { name, value: submitted, disabled });
|
|
159
168
|
|
|
169
|
+
// Native <input type="range">.value is a string. Present the same on the
|
|
170
|
+
// host so composed-path helpers and attribute-based bindings see it, while
|
|
171
|
+
// the prop signal stays numeric for math and `detail.value`.
|
|
172
|
+
const toNum = (v) => {
|
|
173
|
+
const n = typeof v === 'number' ? v : parseFloat(v);
|
|
174
|
+
return Number.isFinite(n) ? n : min();
|
|
175
|
+
};
|
|
176
|
+
Object.defineProperty(host, 'value', {
|
|
177
|
+
configurable: true,
|
|
178
|
+
enumerable: true,
|
|
179
|
+
get() { return String(value()); },
|
|
180
|
+
set(v) { value.set(toNum(v)); },
|
|
181
|
+
});
|
|
182
|
+
effect(() => {
|
|
183
|
+
host.setAttribute('value', String(value()));
|
|
184
|
+
});
|
|
185
|
+
|
|
160
186
|
const active = signal(false);
|
|
161
187
|
const activeThumb = signal('end');
|
|
162
188
|
|
|
@@ -226,7 +252,8 @@ define('ui-slider', {
|
|
|
226
252
|
${() => (range()
|
|
227
253
|
? html`<div class="dual">
|
|
228
254
|
<input part="input" type="range"
|
|
229
|
-
min=${min} max=${max} step=${step}
|
|
255
|
+
min=${min} max=${max} step=${step}
|
|
256
|
+
value=${valueStart} .value=${valueStart}
|
|
230
257
|
?disabled=${disabled}
|
|
231
258
|
aria-label=${() => (label() ? label() + ' start' : 'Start')}
|
|
232
259
|
@input=${onStartInput} @change=${onStartChange}
|
|
@@ -234,7 +261,8 @@ define('ui-slider', {
|
|
|
234
261
|
@focus=${() => { active.set(true); activeThumb.set('start'); }}
|
|
235
262
|
@blur=${() => active.set(false)}>
|
|
236
263
|
<input part="input" type="range"
|
|
237
|
-
min=${min} max=${max} step=${step}
|
|
264
|
+
min=${min} max=${max} step=${step}
|
|
265
|
+
value=${valueEnd} .value=${valueEnd}
|
|
238
266
|
?disabled=${disabled}
|
|
239
267
|
aria-label=${() => (label() ? label() + ' end' : 'End')}
|
|
240
268
|
@input=${onEndInput} @change=${onEndChange}
|
|
@@ -243,7 +271,8 @@ define('ui-slider', {
|
|
|
243
271
|
@blur=${() => active.set(false)}>
|
|
244
272
|
</div>`
|
|
245
273
|
: html`<input part="input" type="range"
|
|
246
|
-
min=${min} max=${max} step=${step}
|
|
274
|
+
min=${min} max=${max} step=${step}
|
|
275
|
+
value=${value} .value=${value}
|
|
247
276
|
?disabled=${disabled}
|
|
248
277
|
aria-label=${() => label() || 'Slider'}
|
|
249
278
|
@input=${onInput} @change=${onChange}
|
package/src/util/icons.js
CHANGED
|
@@ -1,29 +1,45 @@
|
|
|
1
1
|
// Icon registry.
|
|
2
2
|
//
|
|
3
3
|
// A built-in set of Material Design icon paths (24×24 viewBox, from Google's
|
|
4
|
-
// material-design-icons, Apache-2.0).
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// material-design-icons, Apache-2.0). Names are kebab-case; underscores are
|
|
5
|
+
// treated as hyphens so Material Symbols names (`arrow_forward`) resolve.
|
|
6
|
+
// Components take what they need; the rest is the everyday MD3 filled set so
|
|
7
|
+
// an app can start without a custom registry. Apps add their own with
|
|
8
|
+
// `registerIcons` — any 24×24 path data works — or slot a whole `<svg>` into
|
|
9
|
+
// `<ui-icon>` for one-offs.
|
|
7
10
|
|
|
8
11
|
import { signal } from '@alacris/core';
|
|
9
12
|
|
|
10
13
|
const registry = new Map();
|
|
14
|
+
const warned = new Set();
|
|
11
15
|
|
|
12
16
|
// Bumped on registration so already-rendered <ui-icon>s pick up late icons.
|
|
13
17
|
export const iconsVersion = signal(0);
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
// Material Symbols use underscores; the registry is kebab-case. Treat them
|
|
20
|
+
// as the same name so `arrow_forward` resolves to `arrow-forward`.
|
|
21
|
+
const canon = (name) => String(name).replace(/_/g, '-');
|
|
22
|
+
|
|
23
|
+
/** registerIcons({ name: 'M…' }) — later registrations win. Names are stored kebab-case. */
|
|
16
24
|
export function registerIcons(icons) {
|
|
17
|
-
for (const name in icons) registry.set(name, icons[name]);
|
|
25
|
+
for (const name in icons) registry.set(canon(name), icons[name]);
|
|
18
26
|
iconsVersion.update((n) => n + 1);
|
|
19
27
|
}
|
|
20
28
|
|
|
21
|
-
/** Path data for a name, or undefined. */
|
|
22
|
-
export const iconPath = (name) => registry.get(name);
|
|
29
|
+
/** Path data for a name, or undefined. Underscores and hyphens are equivalent. */
|
|
30
|
+
export const iconPath = (name) => (name ? registry.get(canon(name)) : undefined);
|
|
23
31
|
|
|
24
|
-
/** Registered names (built-ins included) — the demo lists these. */
|
|
32
|
+
/** Registered names (built-ins included, kebab-case) — the demo lists these. */
|
|
25
33
|
export const iconNames = () => [...registry.keys()];
|
|
26
34
|
|
|
35
|
+
/** Warn once per unknown name so a blank icon is grep-able, not a silent hole. */
|
|
36
|
+
export function warnUnknownIcon(name) {
|
|
37
|
+
const n = canon(name);
|
|
38
|
+
if (!n || registry.has(n) || warned.has(n)) return;
|
|
39
|
+
warned.add(n);
|
|
40
|
+
console.warn(`ui-icon: "${name}" is not registered. Names are kebab-case (arrow-forward); underscores are accepted. iconNames() lists the set; registerIcons() adds more.`);
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
registerIcons({
|
|
28
44
|
'add': 'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z',
|
|
29
45
|
'arrow-back': 'M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z',
|
|
@@ -31,6 +47,7 @@ registerIcons({
|
|
|
31
47
|
'arrow-drop-down': 'M7 10l5 5 5-5z',
|
|
32
48
|
'arrow-drop-up': 'M7 14l5-5 5 5z',
|
|
33
49
|
'arrow-upward': 'M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z',
|
|
50
|
+
'arrow-downward': 'M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z',
|
|
34
51
|
'calendar': 'M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7z',
|
|
35
52
|
'cancel': 'M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z',
|
|
36
53
|
'check': 'M8.95 19.1 3.65 13.8 6.45 11 8.95 13.5 17.55 4.9 20.35 7.7z',
|
package/types/index.d.ts
CHANGED
|
@@ -147,8 +147,11 @@ export function formBind(
|
|
|
147
147
|
}
|
|
148
148
|
): void;
|
|
149
149
|
|
|
150
|
+
/** Register SVG path data. Names are stored kebab-case; underscores are equivalent. */
|
|
150
151
|
export function registerIcons(icons: Record<string, string>): void;
|
|
152
|
+
/** Path data for a name, or undefined. Underscores and hyphens are equivalent. */
|
|
151
153
|
export function iconPath(name: string): string | undefined;
|
|
154
|
+
/** Registered names (built-ins included, kebab-case). */
|
|
152
155
|
export function iconNames(): string[];
|
|
153
156
|
|
|
154
157
|
export function processTable(opts: Record<string, unknown>): Record<string, unknown>;
|