@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 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 `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.
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
- starter/
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/starter/
143
- cd starter && npm test
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.1.1",
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": "starter"
67
+ "directory": "ui"
68
68
  },
69
- "homepage": "https://bmartel.github.io/alacris/ui/getting-started/",
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
- // @prop {string} name='' — registry name (see util/icons.js); empty renders the slot
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 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>`;
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
  });
@@ -90,6 +90,7 @@ const styles = css`
90
90
  font: ${t.font};
91
91
  letter-spacing: ${t.tracking};
92
92
  color: ${t.labelFg};
93
+ white-space: nowrap;
93
94
  transition: color ${sys.duration.short4} ${sys.easing.standard};
94
95
  }
95
96
  .selected .label { color: ${t.labelFgActive}; }
@@ -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} .value=${valueStart}
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} .value=${valueEnd}
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} .value=${value}
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). Components take what they need; the rest
5
- // is the everyday MD3 filled set so an app can start without a custom registry. Apps add their own with `registerIcons` — any 24×24
6
- // path data works or slot a whole `<svg>` into `<ui-icon>` for one-offs.
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
- /** registerIcons({ name: 'M…' }) later registrations win. */
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>;