@raylang/elements 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/check/check.d.ts +2 -0
  2. package/check/check.js +18 -4
  3. package/check/cli.js +10 -6
  4. package/components/accordion/accordion.js +1 -1
  5. package/components/alert/alert.js +1 -1
  6. package/components/attachment/attachment.js +1 -1
  7. package/components/avatar/avatar.js +1 -1
  8. package/components/bottom-nav/bottom-nav.js +2 -0
  9. package/components/breadcrumbs/breadcrumbs.js +1 -1
  10. package/components/carousel/carousel.js +4 -4
  11. package/components/checkbox/checkbox.js +1 -1
  12. package/components/code/code.js +1 -1
  13. package/components/color-picker/color-picker.js +1 -1
  14. package/components/combobox/combobox.js +3 -3
  15. package/components/command-palette/command-palette.js +1 -1
  16. package/components/date-picker/date-picker.js +4 -4
  17. package/components/dialog/dialog.js +1 -1
  18. package/components/drawer/drawer.js +1 -1
  19. package/components/file-upload/file-upload.js +1 -1
  20. package/components/icon/icon.js +6 -1
  21. package/components/nav-link/nav-link.js +1 -1
  22. package/components/notification/notification.js +1 -1
  23. package/components/number-input/number-input.js +1 -1
  24. package/components/pagination/pagination.js +4 -4
  25. package/components/password-input/password-input.js +2 -2
  26. package/components/search/search.js +2 -2
  27. package/components/select/select.js +2 -2
  28. package/components/stat/stat.js +1 -1
  29. package/components/step-dots/step-dots.js +11 -2
  30. package/components/stepper/stepper.js +1 -1
  31. package/components/table/table.js +1 -1
  32. package/components/theme-toggle/theme-toggle.js +2 -2
  33. package/core/core.js +4 -3
  34. package/core/icons.js +95 -8
  35. package/custom-elements.json +71 -17
  36. package/llms-full.txt +36 -16
  37. package/llms.txt +14 -3
  38. package/package.json +1 -1
  39. package/ray-elements.d.ts +24 -14
  40. package/vscode.html-custom-data.json +1093 -92
package/check/check.d.ts CHANGED
@@ -14,6 +14,8 @@ export interface CheckOptions {
14
14
  manifest: object | object[];
15
15
  /** The registered icon names; without them icons are not checked. */
16
16
  icons?: string[];
17
+ /** The icon names that have a filled version (iconNames({ filled: true })); without them filled is not checked. */
18
+ filledIcons?: string[];
17
19
  /** More values of an attribute, for every element: an app's own variant, { variant: ["brand"] }. */
18
20
  allow?: Record<string, string[]>;
19
21
  /** Reported in the diagnostics; its extension picks the language. */
package/check/check.js CHANGED
@@ -20,6 +20,7 @@ export const RULES = {
20
20
  "unknown-event": "a ray-* event the element does not fire",
21
21
  "unknown-icon": "an icon name that is not registered",
22
22
  "missing-label": "an icon-only button without its accessible name",
23
+ "no-filled-icon": "filled on an icon that has no filled version (the outline one is drawn)",
23
24
  "literal-color": "a literal color in CSS instead of a token",
24
25
  };
25
26
 
@@ -83,7 +84,9 @@ function attribute(a) {
83
84
  const literals = parts.filter((s) => /^"[^"]*"$/.test(s)).map((s) => s.slice(1, -1));
84
85
  const others = parts.filter((s) => !/^"[^"]*"$/.test(s) && s !== "undefined" && s !== "null");
85
86
  const length = others.length === 1 && others[0] === "(string & {})";
86
- return { name: a.name, boolean: text === "boolean", values: literals.length > 0 && (others.length === 0 || length) ? literals : null, length };
87
+ // IconName (ray-icon's name, the icon of an alert, a nav item…): checked against the icons.
88
+ const icon = others.includes("IconName");
89
+ return { name: a.name, boolean: text === "boolean", values: literals.length > 0 && (others.length === 0 || length) ? literals : null, length, icon };
87
90
  }
88
91
 
89
92
  const LENGTH = /^(?:-?(?:\d+\.?\d*|\.\d+)(?:px|r?em|ex|ch|lh|vw|vh|vmin|vmax|dvh|svh|lvh|cm|mm|in|pt|pc|q|%)|0|(?:calc|min|max|clamp|var)\(.*\))$/i;
@@ -99,14 +102,16 @@ export function languageOf(file) {
99
102
  /**
100
103
  * Checks one file's markup.
101
104
  * @param {string} source
102
- * @param {{ manifest: object | object[], icons?: string[], allow?: Record<string, string[]>, file?: string, lang?: "html" | "vue" | "jsx" }} options
105
+ * @param {{ manifest: object | object[], icons?: string[], filledIcons?: string[], allow?: Record<string, string[]>, file?: string, lang?: "html" | "vue" | "jsx" }} options
106
+ * filledIcons: the names with a filled version (iconNames({ filled: true }))
103
107
  * allow: more values of an attribute, for every element (an app's own variant: { variant: ["brand"] })
104
108
  * @returns {Diagnostic[]}
105
109
  */
106
- export function checkMarkup(source, { manifest, icons, allow = {}, file = "<input>", lang } = {}) {
110
+ export function checkMarkup(source, { manifest, icons, filledIcons, allow = {}, file = "<input>", lang } = {}) {
107
111
  const index = indexManifest(manifest);
108
112
  const language = lang ?? languageOf(file);
109
113
  const iconSet = icons ? new Set(icons) : null;
114
+ const filledSet = filledIcons ? new Set(filledIcons) : null;
110
115
  const lines = lineStarts(source);
111
116
  const out = [];
112
117
  const report = (pos, severity, rule, message) => {
@@ -138,6 +143,15 @@ export function checkMarkup(source, { manifest, icons, allow = {}, file = "<inpu
138
143
  const named = tok.attrs.some((a) => ["label", "aria-label", "aria-labelledby"].includes(normalize(a.name, language).name));
139
144
  if (!named) report(tok.pos, "error", "missing-label", `<${tok.name}> needs a label: it is the button's accessible name (and its tooltip)`);
140
145
  }
146
+ if (el?.tag === "ray-icon" && filledSet) {
147
+ const attr = (n) => tok.attrs.find((a) => normalize(a.name, language).name === n);
148
+ const filled = attr("filled");
149
+ const name = attr("name");
150
+ // Bound or not: an icon that is ever filled needs the filled version.
151
+ if (filled && name && name.value && !name.dynamic && !normalize(name.name, language).bound && name.value !== "none" && !filledSet.has(name.value)) {
152
+ report(filled.pos, "warning", "no-filled-icon", `"${name.value}" has no filled version: the outline icon is drawn (add one with setIcons({ "${name.value}:filled": … }))`);
153
+ }
154
+ }
141
155
  if (!tok.selfClosing && !(language !== "jsx" && VOID.has(tok.name.toLowerCase()))) {
142
156
  stack.push({ name: tok.name, el });
143
157
  }
@@ -231,7 +245,7 @@ function attributeChecks(a, tok, el, parent, language, icons, allow, report) {
231
245
  report(a.pos, "error", "invalid-value", `${name}="${a.value}" is not valid on <${tok.name}>: one of ${attr.values.map((v) => `"${v}"`).join(", ")}${or}`);
232
246
  }
233
247
  // "none" (or nothing) draws no icon: a blank that keeps the alignment.
234
- if (el.tag === "ray-icon" && name === "name" && icons && a.value !== "" && a.value !== "none" && !icons.has(a.value)) {
248
+ if (attr.icon && icons && a.value !== "" && a.value !== "none" && !icons.has(a.value)) {
235
249
  report(a.pos, "warning", "unknown-icon", `no icon named "${a.value}"${hint(a.value, [...icons], "icons")} (register it with setIcons)`);
236
250
  }
237
251
  }
package/check/cli.js CHANGED
@@ -8,7 +8,7 @@
8
8
  import { readFileSync, readdirSync, statSync } from "node:fs";
9
9
  import { join, relative } from "node:path";
10
10
  import { checkMarkup, customValues, RULES } from "./check.js";
11
- import { iconNames } from "../core/icons.js";
11
+ import { iconNames, iconAliases } from "../core/icons.js";
12
12
 
13
13
  const EXTENSIONS = new Set(["html", "htm", "vue", "svelte", "astro", "jsx", "tsx"]);
14
14
  // Read for what an app adds: setIcons({ … }) and its own variants in CSS.
@@ -88,10 +88,14 @@ function main(argv) {
88
88
  const files = [];
89
89
  const scripts = [];
90
90
  for (const p of paths.length > 0 ? paths : ["."]) collect(p, files, scripts, true);
91
- const icons = new Set([...iconNames(), ...extraIcons]);
91
+ const icons = new Set([...iconNames(), ...Object.keys(iconAliases()), ...extraIcons]);
92
+ const filledIcons = new Set(iconNames({ filled: true }));
92
93
  for (const f of scripts) {
93
94
  const src = readFileSync(f, "utf8");
94
- for (const name of registeredIcons(src)) icons.add(name);
95
+ for (const name of registeredIcons(src)) {
96
+ if (name.endsWith(":filled")) filledIcons.add(name.slice(0, -":filled".length));
97
+ else icons.add(name);
98
+ }
95
99
  for (const [attr, values] of Object.entries(customValues(src))) values.forEach((v) => allowValue(attr, v));
96
100
  }
97
101
 
@@ -99,7 +103,7 @@ function main(argv) {
99
103
  for (const f of files) {
100
104
  const source = readFileSync(f, "utf8");
101
105
  const file = relative(process.cwd(), f) || f;
102
- for (const d of checkMarkup(source, { manifest: manifests, icons: [...icons], allow, file })) {
106
+ for (const d of checkMarkup(source, { manifest: manifests, icons: [...icons], filledIcons: [...filledIcons], allow, file })) {
103
107
  if (!ignore.has(d.rule)) diagnostics.push(d);
104
108
  }
105
109
  }
@@ -128,11 +132,11 @@ function collect(path, files, scripts, explicit) {
128
132
  if (SCRIPTS.has(ext)) scripts.push(path);
129
133
  }
130
134
 
131
- // The keys of setIcons({ inbox: '<svg…>', "file-text": … }) calls.
135
+ // The keys of setIcons({ inbox: '<svg…>', "file-text": …, "star:filled": …, "ui.remove": … }) calls.
132
136
  function registeredIcons(src) {
133
137
  const out = [];
134
138
  for (const call of src.matchAll(/setIcons\(\s*\{([\s\S]*?)\}\s*\)/g)) {
135
- for (const key of call[1].matchAll(/(?:^|[,{\s])(["']?)([A-Za-z][\w-]*)\1\s*:/g)) out.push(key[2]);
139
+ for (const key of call[1].matchAll(/(?:^|[,{\s])(?:(["'])([A-Za-z][\w.-]*(?::filled)?)\1|([A-Za-z][\w-]*))\s*:/g)) out.push(key[2] ?? key[3]);
136
140
  }
137
141
  return out;
138
142
  }
@@ -149,7 +149,7 @@ export class RayAccordionItem extends RayElement {
149
149
  <button class="control focus-ring" id="control" part="control" type="button" aria-expanded="false" aria-controls="panel">
150
150
  <span class="section" part="left-section"><slot name="left"></slot></span>
151
151
  <span class="label" part="label"><span class="label-text"><slot name="label"></slot></span><span class="description" part="description"></span></span>
152
- <span class="chevron" part="chevron" aria-hidden="true" data-ray-icon="chevron-down"></span>
152
+ <span class="chevron" part="chevron" aria-hidden="true" data-ray-icon="ui.expand"></span>
153
153
  </button>
154
154
  </h3>
155
155
  <div class="panel" id="panel" part="panel" role="region" aria-labelledby="control" inert>
@@ -66,7 +66,7 @@ export class RayAlert extends RayElement {
66
66
  <p class="title" part="title"></p>
67
67
  <div class="text" part="text"><slot></slot></div>
68
68
  </div>
69
- <button class="close focus-ring" part="close" type="button" hidden><span data-ray-icon="close" aria-hidden="true"></span></button>
69
+ <button class="close focus-ring" part="close" type="button" hidden><span data-ray-icon="ui.close" aria-hidden="true"></span></button>
70
70
  </div>`;
71
71
 
72
72
  constructor() {
@@ -191,7 +191,7 @@ export class RayAttachment extends RayElement {
191
191
  <div class="item" part="item" role="group">
192
192
  <span class="thumb" part="thumbnail"><img alt="" hidden><span class="icon" aria-hidden="true"></span><span class="badge" part="badge" aria-hidden="true"></span></span>
193
193
  <span class="text"><span class="name-box"></span><span class="meta" part="meta"></span></span>
194
- <button class="remove focus-ring" part="remove" type="button" hidden><span data-ray-icon="close" aria-hidden="true"></span></button>
194
+ <button class="remove focus-ring" part="remove" type="button" hidden><span data-ray-icon="ui.remove" aria-hidden="true"></span></button>
195
195
  <span class="bar" part="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" hidden><span></span></span>
196
196
  </div>`;
197
197
 
@@ -32,7 +32,7 @@ const styles = css`
32
32
  [hidden] { display: none !important; }
33
33
  `;
34
34
 
35
- const PLACEHOLDER = `<span data-ray-icon="user" aria-hidden="true"></span>`;
35
+ const PLACEHOLDER = `<span data-ray-icon="ui.avatar" aria-hidden="true"></span>`;
36
36
 
37
37
  /** Up to two initials: "Ada Lovelace" -> "AL", "ray" -> "R". */
38
38
  export function initials(name) {
@@ -193,8 +193,10 @@ export class RayBottomNavItem extends RayElement {
193
193
  }
194
194
  if (this.active && !this.action) item.setAttribute("aria-current", "page");
195
195
  else item.removeAttribute("aria-current");
196
+ // The current destination draws its icon filled (when it has a filled version).
196
197
  const icon = this.$(".icon");
197
198
  icon.setAttribute("data-ray-icon", this.icon ?? "");
199
+ icon.toggleAttribute("data-ray-icon-filled", Boolean(this.active && !this.action));
198
200
  this.paintIcons();
199
201
  this.$(".label").textContent = this.label ?? "";
200
202
  const badge = this.$(".badge");
@@ -150,7 +150,7 @@ export class RayBreadcrumb extends RayElement {
150
150
  if (sep === this.#sep) return;
151
151
  this.#sep = sep;
152
152
  for (const el of this.shadowRoot.querySelectorAll(".sep")) {
153
- if (sep === "chevron") el.innerHTML = `<span data-ray-icon="chevron-right"></span>`;
153
+ if (sep === "chevron") el.innerHTML = `<span data-ray-icon="ui.separator"></span>`;
154
154
  else el.textContent = sep;
155
155
  }
156
156
  this.paintIcons();
@@ -128,7 +128,7 @@ const styles = css`
128
128
  .play svg { width: 0.875rem; height: 0.875rem; }
129
129
  `;
130
130
 
131
- const PAUSE_ICON = `<span data-ray-icon="pause" aria-hidden="true"></span>`;
131
+ const PAUSE_ICON = `<span data-ray-icon="ui.pause" aria-hidden="true"></span>`;
132
132
 
133
133
  const reduced = typeof matchMedia === "function" ? matchMedia("(prefers-reduced-motion: reduce)") : null;
134
134
 
@@ -150,8 +150,8 @@ export class RayCarousel extends RayElement {
150
150
  <div class="root" part="root" role="region" aria-roledescription="carousel">
151
151
  <div class="stage">
152
152
  <div class="viewport" part="viewport" tabindex="0" aria-live="polite"><slot></slot></div>
153
- <button class="control prev" part="previous" type="button" aria-label="Previous slide"><span data-ray-icon="chevron-left" aria-hidden="true"></span></button>
154
- <button class="control next" part="next" type="button" aria-label="Next slide"><span data-ray-icon="chevron-right" aria-hidden="true"></span></button>
153
+ <button class="control prev" part="previous" type="button" aria-label="Previous slide"><span data-ray-icon="ui.previous" aria-hidden="true"></span></button>
154
+ <button class="control next" part="next" type="button" aria-label="Next slide"><span data-ray-icon="ui.next" aria-hidden="true"></span></button>
155
155
  </div>
156
156
  <div class="footer" part="footer">
157
157
  <button class="play" part="play" type="button" hidden></button>
@@ -403,7 +403,7 @@ export class RayCarousel extends RayElement {
403
403
  const paused = this.#userPaused;
404
404
  if (auto && play.dataset.state !== (paused ? "paused" : "playing")) {
405
405
  play.dataset.state = paused ? "paused" : "playing";
406
- play.innerHTML = paused ? `<span data-ray-icon="play" aria-hidden="true"></span>` : PAUSE_ICON;
406
+ play.innerHTML = paused ? `<span data-ray-icon="ui.play" aria-hidden="true"></span>` : PAUSE_ICON;
407
407
  this.paintIcons(play);
408
408
  play.setAttribute("aria-label", paused ? "Start autoplay" : "Pause autoplay");
409
409
  }
@@ -103,7 +103,7 @@ export class RayCheckbox extends FieldControl {
103
103
  labelPosition: { type: String, reflect: true, default: "right" },
104
104
  };
105
105
  static styles = [sizeStyles, radiusStyles, fieldStyles, checkStyles, styles];
106
- static template = checkTemplate(`<span class="indicator" part="indicator" aria-hidden="true"><span class="check" data-ray-icon="check"></span><span class="dash" data-ray-icon="minus"></span></span>`);
106
+ static template = checkTemplate(`<span class="indicator" part="indicator" aria-hidden="true"><span class="check" data-ray-icon="ui.checked"></span><span class="dash" data-ray-icon="ui.indeterminate"></span></span>`);
107
107
 
108
108
  wire() {
109
109
  // The `checked` attribute is the default state, like on <input>.
@@ -427,7 +427,7 @@ export class RayCode extends RayElement {
427
427
  b.type = "button";
428
428
  b.className = "copy focus-ring";
429
429
  b.setAttribute("part", "copy");
430
- b.innerHTML = `<span class="idle" data-ray-icon="copy" aria-hidden="true"></span><span class="done" data-ray-icon="check" aria-hidden="true"></span><span class="copy-label"></span>`;
430
+ b.innerHTML = `<span class="idle" data-ray-icon="ui.copy" aria-hidden="true"></span><span class="done" data-ray-icon="ui.copied" aria-hidden="true"></span><span class="copy-label"></span>`;
431
431
  b.addEventListener("click", () => this.copy());
432
432
  this.$(".more button").addEventListener("click", () => { this.expanded = !this.expanded; });
433
433
  this.$("slot").addEventListener("slotchange", () => this.requestUpdate());
@@ -540,7 +540,7 @@ export class RayColorInput extends FieldControl {
540
540
  static template = fieldTemplate(
541
541
  `<button type="button" class="preview" part="preview" tabindex="-1" aria-hidden="true"></button>` +
542
542
  `<input id="control" class="control" part="input" autocomplete="off" spellcheck="false" role="combobox" aria-autocomplete="none" aria-haspopup="dialog" aria-expanded="false" aria-controls="panel">` +
543
- `<button type="button" class="dropper focus-ring" part="eyedropper" aria-label="Pick a color from the screen" hidden><span data-ray-icon="pipette" aria-hidden="true"></span></button>`,
543
+ `<button type="button" class="dropper focus-ring" part="eyedropper" aria-label="Pick a color from the screen" hidden><span data-ray-icon="ui.eyedropper" aria-hidden="true"></span></button>`,
544
544
  { sections: false },
545
545
  ) + `<div class="panel" id="panel" part="dropdown" popover="manual" role="dialog" aria-label="Choose a color"><ray-color-picker part="picker"></ray-color-picker></div>`;
546
546
 
@@ -120,7 +120,7 @@ export class RayCombobox extends FieldControl {
120
120
  static styles = [sizeStyles, radiusStyles, fieldStyles, boxStyles, styles];
121
121
  static template = fieldTemplate(
122
122
  `<span class="chips" part="chips"></span><input id="control" class="control" part="input" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="listbox" autocomplete="off" spellcheck="false">` +
123
- `<span class="tail"><button type="button" class="clear" part="clear" aria-label="Clear" hidden tabindex="-1"><span data-ray-icon="close"></span></button><span class="chevron" aria-hidden="true" data-ray-icon="chevron-down"></span></span>`,
123
+ `<span class="tail"><button type="button" class="clear" part="clear" aria-label="Clear" hidden tabindex="-1"><span data-ray-icon="ui.clear"></span></button><span class="chevron" aria-hidden="true" data-ray-icon="ui.expand"></span></span>`,
124
124
  { sections: false },
125
125
  ) + `<div class="listbox" id="listbox" part="dropdown" role="listbox" popover="manual" tabindex="-1"></div><div class="options"><slot></slot></div>`;
126
126
 
@@ -299,7 +299,7 @@ export class RayCombobox extends FieldControl {
299
299
  b.tabIndex = -1;
300
300
  b.dataset.value = v;
301
301
  b.setAttribute("aria-label", `Remove ${this._labelOf(v)}`);
302
- b.innerHTML = `<span data-ray-icon="close"></span>`;
302
+ b.innerHTML = `<span data-ray-icon="ui.remove"></span>`;
303
303
  chip.append(t, b);
304
304
  box.append(chip);
305
305
  }
@@ -364,7 +364,7 @@ export class RayCombobox extends FieldControl {
364
364
  }
365
365
  const check = document.createElement("span");
366
366
  check.className = "mark";
367
- check.setAttribute("data-ray-icon", "check");
367
+ check.setAttribute("data-ray-icon", "ui.checked");
368
368
  el.append(text, check);
369
369
  (groupEl ?? lb).append(el);
370
370
  this.#items.push({ el, value: o.value, disabled, create: false });
@@ -124,7 +124,7 @@ const styles = css`
124
124
  }
125
125
  `;
126
126
 
127
- const SEARCH_ICON = `<span data-ray-icon="search" aria-hidden="true"></span>`;
127
+ const SEARCH_ICON = `<span data-ray-icon="ui.search" aria-hidden="true"></span>`;
128
128
 
129
129
  /**
130
130
  * How well `action` matches `query` (0 = not at all). Every word of the query
@@ -144,9 +144,9 @@ export class RayCalendar extends RayElement {
144
144
  static styles = [sizeStyles, calendarStyles];
145
145
  static template = `
146
146
  <div class="header" part="header">
147
- <button class="nav prev focus-ring" part="nav" type="button" data-ray-icon="chevron-left"></button>
147
+ <button class="nav prev focus-ring" part="nav" type="button" data-ray-icon="ui.previous"></button>
148
148
  <div class="caption" part="caption" id="caption" aria-live="polite"></div>
149
- <button class="nav next focus-ring" part="nav" type="button" data-ray-icon="chevron-right"></button>
149
+ <button class="nav next focus-ring" part="nav" type="button" data-ray-icon="ui.next"></button>
150
150
  </div>
151
151
  <table role="grid" aria-labelledby="caption" part="grid"><thead><tr></tr></thead><tbody></tbody></table>`;
152
152
 
@@ -382,8 +382,8 @@ export class RayDatePicker extends FieldControl {
382
382
  static template = fieldTemplate(
383
383
  `<input id="control" class="native" part="input" type="date" tabindex="-1" aria-hidden="true">` +
384
384
  `<button type="button" id="trigger" class="control trigger" part="trigger" aria-haspopup="dialog" aria-expanded="false" aria-controls="panel">` +
385
- `<span class="icon" aria-hidden="true" data-ray-icon="calendar"></span><span class="value" id="value"></span></button>` +
386
- `<button type="button" class="clear focus-ring" part="clear" aria-label="Clear date" hidden><span data-ray-icon="close" aria-hidden="true"></span></button>`,
385
+ `<span class="icon" aria-hidden="true" data-ray-icon="ui.calendar"></span><span class="value" id="value"></span></button>` +
386
+ `<button type="button" class="clear focus-ring" part="clear" aria-label="Clear date" hidden><span data-ray-icon="ui.clear" aria-hidden="true"></span></button>`,
387
387
  { sections: false },
388
388
  ) + `<div class="panel" id="panel" part="dropdown" popover="manual" role="dialog" aria-label="Choose a date"><ray-calendar part="calendar" exportparts="day, caption, nav, grid"></ray-calendar></div>`;
389
389
 
@@ -106,7 +106,7 @@ export class RayDialog extends RayElement {
106
106
  <dialog part="dialog" aria-labelledby="title">
107
107
  <header class="header" part="header">
108
108
  <div class="heading"><h2 class="title" id="title" part="title"></h2><p class="description" id="description" part="description" hidden></p></div>
109
- <div class="actions"><slot name="header"></slot><button class="close focus-ring" part="close" type="button" aria-label="Close"><span data-ray-icon="close" aria-hidden="true"></span></button></div>
109
+ <div class="actions"><slot name="header"></slot><button class="close focus-ring" part="close" type="button" aria-label="Close"><span data-ray-icon="ui.close" aria-hidden="true"></span></button></div>
110
110
  </header>
111
111
  <div class="body" part="body"><slot></slot></div>
112
112
  <footer class="footer" part="footer"><slot name="footer"></slot></footer>
@@ -111,7 +111,7 @@ export class RayDrawer extends RayDialog {
111
111
  <dialog part="drawer" aria-labelledby="title">
112
112
  <header class="header" part="header">
113
113
  <div class="heading"><h2 class="title" id="title" part="title"></h2><p class="description" id="description" part="description" hidden></p></div>
114
- <div class="actions"><slot name="header"></slot><button class="close focus-ring" part="close" type="button" aria-label="Close"><span data-ray-icon="close" aria-hidden="true"></span></button></div>
114
+ <div class="actions"><slot name="header"></slot><button class="close focus-ring" part="close" type="button" aria-label="Close"><span data-ray-icon="ui.close" aria-hidden="true"></span></button></div>
115
115
  </header>
116
116
  <div class="body" part="body"><slot></slot></div>
117
117
  <footer class="footer" part="footer"><slot name="footer"></slot></footer>
@@ -233,7 +233,7 @@ export class RayFileUpload extends FormControl {
233
233
  <span class="label" part="label" id="label" hidden><span class="label-text"></span><span class="required" aria-hidden="true" hidden> *</span></span>
234
234
  <div class="description" part="description" id="description" hidden></div>
235
235
  <button type="button" class="zone" part="dropzone" aria-labelledby="label title hint pill" aria-describedby="description error">
236
- <span class="glyph" part="icon"><slot name="icon"><span data-ray-icon="upload"></span></slot></span>
236
+ <span class="glyph" part="icon"><slot name="icon"><span data-ray-icon="ui.upload"></span></slot></span>
237
237
  <span class="copy">
238
238
  <span class="title" id="title"></span>
239
239
  <span class="hint" id="hint" part="hint"></span>
@@ -1,5 +1,7 @@
1
1
  // <ray-icon name="…">: an icon of the registry (setIcons), so the app's own
2
2
  // markup uses the same set as the components. Decorative unless `label`.
3
+ // `filled` draws the icon's filled version (a state: active, selected,
4
+ // favorite), or the outline one when it has none.
3
5
 
4
6
  import { RayElement, css, define } from "../../core/core.js";
5
7
 
@@ -20,6 +22,7 @@ export class RayIcon extends RayElement {
20
22
  label: { type: String },
21
23
  color: { type: String, reflect: true },
22
24
  stroke: { type: Number },
25
+ filled: { type: Boolean, reflect: true },
23
26
  };
24
27
  static styles = [styles];
25
28
  static template = `<span class="icon" part="icon" data-ray-icon=""></span>`;
@@ -31,8 +34,10 @@ export class RayIcon extends RayElement {
31
34
 
32
35
  update() {
33
36
  const icon = this.$(".icon");
34
- if (icon.getAttribute("data-ray-icon") !== (this.name ?? "")) {
37
+ const filled = Boolean(this.filled);
38
+ if (icon.getAttribute("data-ray-icon") !== (this.name ?? "") || icon.hasAttribute("data-ray-icon-filled") !== filled) {
35
39
  icon.setAttribute("data-ray-icon", this.name ?? "");
40
+ icon.toggleAttribute("data-ray-icon-filled", filled);
36
41
  this.paintIcons();
37
42
  }
38
43
  // xs…xl from the scale, or any CSS length.
@@ -65,7 +65,7 @@ const INNER = `
65
65
  <span class="section" part="left-section"><slot name="left"></slot></span>
66
66
  <span class="body"><span class="label" part="label"></span><span class="description" part="description"></span></span>
67
67
  <span class="section right" part="right-section"><slot name="right"></slot></span>
68
- <span class="chevron" aria-hidden="true" data-ray-icon="chevron-right"></span>`;
68
+ <span class="chevron" aria-hidden="true" data-ray-icon="ui.disclosure"></span>`;
69
69
 
70
70
  export class RayNavLink extends RayElement {
71
71
  static delegatesFocus = true;
@@ -150,7 +150,7 @@ export class RayNotification extends RayElement {
150
150
  <div class="message" part="message"><slot></slot></div>
151
151
  </div>
152
152
  <button class="action focus-ring" part="action" type="button" hidden></button>
153
- <button class="close focus-ring" part="close" type="button" aria-label="Close notification"><span data-ray-icon="close" aria-hidden="true"></span></button>
153
+ <button class="close focus-ring" part="close" type="button" aria-label="Close notification"><span data-ray-icon="ui.close" aria-hidden="true"></span></button>
154
154
  </div>`;
155
155
 
156
156
  constructor() {
@@ -115,7 +115,7 @@ export class RayNumberInput extends FieldControl {
115
115
  static styles = [sizeStyles, radiusStyles, fieldStyles, boxStyles, styles];
116
116
  static template = fieldTemplate(
117
117
  `<input id="control" class="control" part="input" role="spinbutton" inputmode="decimal" autocomplete="off" spellcheck="false">` +
118
- `<span class="steppers" part="controls"><button type="button" class="step up" part="increment" tabindex="-1" aria-label="Increment"><span data-ray-icon="chevron-up"></span></button><button type="button" class="step down" part="decrement" tabindex="-1" aria-label="Decrement"><span data-ray-icon="chevron-down"></span></button></span>`,
118
+ `<span class="steppers" part="controls"><button type="button" class="step up" part="increment" tabindex="-1" aria-label="Increment"><span data-ray-icon="ui.increment"></span></button><button type="button" class="step down" part="decrement" tabindex="-1" aria-label="Decrement"><span data-ray-icon="ui.decrement"></span></button></span>`,
119
119
  );
120
120
 
121
121
  #typing = false;
@@ -139,15 +139,15 @@ export class RayPagination extends RayElement {
139
139
  const items = [];
140
140
  const control = (role, target, icon, label, off) => ({ kind: "control", role, target, icon, label, off });
141
141
  if (!this.hideControls) {
142
- if (this.withEdges) items.push(control("first", 1, "chevrons-left", "First page", page <= 1));
143
- items.push(control("previous", page - 1, "chevron-left", "Previous page", page <= 1));
142
+ if (this.withEdges) items.push(control("first", 1, "ui.first", "First page", page <= 1));
143
+ items.push(control("previous", page - 1, "ui.previous", "Previous page", page <= 1));
144
144
  }
145
145
  for (const it of paginationRange(total, page, this.siblings ?? 1, this.boundaries ?? 1)) {
146
146
  items.push(it === "dots" ? { kind: "dots" } : { kind: "page", target: it });
147
147
  }
148
148
  if (!this.hideControls) {
149
- items.push(control("next", page + 1, "chevron-right", "Next page", page >= total));
150
- if (this.withEdges) items.push(control("last", total, "chevrons-right", "Last page", page >= total));
149
+ items.push(control("next", page + 1, "ui.next", "Next page", page >= total));
150
+ if (this.withEdges) items.push(control("last", total, "ui.last", "Last page", page >= total));
151
151
  }
152
152
  list.replaceChildren(...items.map((it) => this._item(it, page)));
153
153
  this.paintIcons(list);
@@ -43,7 +43,7 @@ export class RayPasswordInput extends RayInput {
43
43
  static styles = [...RayInput.styles, styles];
44
44
  static template = fieldTemplate(
45
45
  `<input id="control" class="control" part="input" type="password" autocomplete="current-password" spellcheck="false" autocapitalize="off">` +
46
- `<button type="button" class="toggle focus-ring" part="toggle" aria-pressed="false"><span data-ray-icon="eye" aria-hidden="true"></span></button>`,
46
+ `<button type="button" class="toggle focus-ring" part="toggle" aria-pressed="false"><span data-ray-icon="ui.show-password" aria-hidden="true"></span></button>`,
47
47
  );
48
48
 
49
49
  wire() {
@@ -69,7 +69,7 @@ export class RayPasswordInput extends RayInput {
69
69
  toggle.title = toggle.getAttribute("aria-label");
70
70
  toggle.disabled = this.effectivelyDisabled;
71
71
  const icon = toggle.firstElementChild;
72
- const name = this.visible ? "eye-off" : "eye";
72
+ const name = this.visible ? "ui.hide-password" : "ui.show-password";
73
73
  if (icon.dataset.rayIcon !== name) {
74
74
  icon.dataset.rayIcon = name;
75
75
  this.paintIcons(toggle);
@@ -62,9 +62,9 @@ export class RaySearch extends RayInput {
62
62
  };
63
63
  static styles = [...RayInput.styles, styles];
64
64
  static template = fieldTemplate(
65
- `<span class="lead" part="icon" aria-hidden="true" data-ray-icon="search"></span>` +
65
+ `<span class="lead" part="icon" aria-hidden="true" data-ray-icon="ui.search"></span>` +
66
66
  `<input id="control" class="control" part="input" type="search" enterkeyhint="search" autocomplete="off" spellcheck="false">` +
67
- `<span class="tail"><button class="clear focus-ring" part="clear" type="button" aria-label="Clear search" tabindex="-1" hidden><span data-ray-icon="close" aria-hidden="true"></span></button>` +
67
+ `<span class="tail"><button class="clear focus-ring" part="clear" type="button" aria-label="Clear search" tabindex="-1" hidden><span data-ray-icon="ui.clear" aria-hidden="true"></span></button>` +
68
68
  `<kbd class="shortcut" part="shortcut" aria-hidden="true" hidden></kbd></span>`,
69
69
  );
70
70
 
@@ -101,7 +101,7 @@ const styles = css`
101
101
  .empty { padding: var(--ray-space-2); color: var(--ray-muted-foreground); }
102
102
  `;
103
103
 
104
- const CHEVRON = `<span class="chevron" aria-hidden="true" data-ray-icon="chevron-down"></span>`;
104
+ const CHEVRON = `<span class="chevron" aria-hidden="true" data-ray-icon="ui.expand"></span>`;
105
105
 
106
106
  export class RaySelect extends FieldControl {
107
107
  static properties = {
@@ -261,7 +261,7 @@ export class RaySelect extends FieldControl {
261
261
  const group = o.parentElement?.localName === "optgroup" ? o.parentElement : null;
262
262
  const disabled = o.disabled || Boolean(group?.disabled);
263
263
  el.setAttribute("aria-disabled", String(disabled));
264
- el.innerHTML = `<span class="text"></span><span class="mark" aria-hidden="true" data-ray-icon="check"></span>`;
264
+ el.innerHTML = `<span class="text"></span><span class="mark" aria-hidden="true" data-ray-icon="ui.checked"></span>`;
265
265
  el.firstChild.textContent = o.label || o.text;
266
266
  this.#items.push({ el, value: o.value, disabled, text: (o.label || o.text).trim().toLowerCase() });
267
267
  return el;
@@ -34,7 +34,7 @@ const styles = css`
34
34
  `;
35
35
 
36
36
  // Icon of each trend (from the icon registry).
37
- const ARROWS = { up: "arrow-up-right", down: "arrow-down-right", flat: "minus" };
37
+ const ARROWS = { up: "ui.trend-up", down: "ui.trend-down", flat: "ui.trend-flat" };
38
38
 
39
39
  export class RayStat extends RayElement {
40
40
  static properties = {
@@ -45,6 +45,8 @@ export class RayStepDots extends RayElement {
45
45
  size: { type: String, reflect: true, default: "sm" },
46
46
  align: { type: String, reflect: true, default: "center" },
47
47
  label: { type: String },
48
+ valueText: { type: String, attribute: "value-text" },
49
+ doneText: { type: String, attribute: "done-text" },
48
50
  };
49
51
  static styles = [styles];
50
52
  static template = `<div class="dots" part="dots" aria-hidden="true"></div>`;
@@ -72,14 +74,21 @@ export class RayStepDots extends RayElement {
72
74
  dot.dataset.state = i < value ? "done" : i === value ? "current" : "todo";
73
75
  });
74
76
 
75
- const noun = this.label || "Step";
77
+ // The announced text: value-text / done-text ({step}: the current one,
78
+ // 1-based; {count}), numbers in the element's language; English otherwise.
79
+ const lang = this.closest("[lang]")?.lang || undefined;
80
+ const n = (x) => new Intl.NumberFormat(lang).format(x);
81
+ const fill = (tpl) => tpl.replaceAll("{step}", n(Math.min(value + 1, count))).replaceAll("{count}", n(count));
76
82
  const done = count > 0 && value >= count;
83
+ const text = done
84
+ ? fill(this.doneText ?? this.valueText ?? "{count} of {count}, done")
85
+ : fill(this.valueText ?? `${this.label || "Step"} {step} of {count}`);
77
86
  const i = this.internals;
78
87
  i.ariaLabel = this.label || this.getAttribute("aria-label") || "Steps";
79
88
  i.ariaValueMin = "0";
80
89
  i.ariaValueMax = String(count);
81
90
  i.ariaValueNow = String(value);
82
- i.ariaValueText = count === 0 ? null : done ? `${count} of ${count}, done` : `${noun} ${value + 1} of ${count}`;
91
+ i.ariaValueText = count === 0 ? null : text;
83
92
  }
84
93
  }
85
94
 
@@ -172,7 +172,7 @@ export class RayStepper extends RayElement {
172
172
  indicator.setAttribute("aria-hidden", "true");
173
173
  if (step.loading) indicator.innerHTML = `<span class="spinner"></span>`;
174
174
  else if (step.error) indicator.append(glyph("close"));
175
- else if (state === "done") indicator.append(glyph(step.completedIcon || "check"));
175
+ else if (state === "done") indicator.append(glyph(step.completedIcon || "ui.checked"));
176
176
  else if (step.icon) indicator.append(glyph(step.icon));
177
177
  else indicator.textContent = String(i + 1);
178
178
  const text = document.createElement("span");
@@ -67,7 +67,7 @@ const styles = css`
67
67
  .empty:hover td { background: inherit; }
68
68
  `;
69
69
 
70
- const SORT_ICON = `<span data-ray-icon="chevron-up" aria-hidden="true"></span>`;
70
+ const SORT_ICON = `<span data-ray-icon="ui.sort" aria-hidden="true"></span>`;
71
71
 
72
72
  export class RayTable extends RayElement {
73
73
  static properties = {
@@ -6,7 +6,7 @@ const ORDER = ["system", "light", "dark"];
6
6
  const LABEL = { system: "System theme", light: "Light theme", dark: "Dark theme" };
7
7
 
8
8
  // Icon of each theme (from the icon registry).
9
- const ICONS = { system: "monitor", light: "sun", dark: "moon" };
9
+ const ICONS = { system: "ui.theme-system", light: "ui.theme-light", dark: "ui.theme-dark" };
10
10
 
11
11
  const styles = css`
12
12
  :host {
@@ -41,7 +41,7 @@ export class RayThemeToggle extends RayElement {
41
41
  };
42
42
  static styles = [styles];
43
43
  static delegatesFocus = true;
44
- static template = `<button part="button" class="focus-ring" type="button"><span class="icon" data-ray-icon="monitor" aria-hidden="true"></span></button>`;
44
+ static template = `<button part="button" class="focus-ring" type="button"><span class="icon" data-ray-icon="ui.theme-system" aria-hidden="true"></span></button>`;
45
45
 
46
46
  connectedCallback() {
47
47
  super.connectedCallback();
package/core/core.js CHANGED
@@ -5,7 +5,7 @@
5
5
  import { getIcon, onIconsChange } from "./icons.js";
6
6
  import { parseColor, shades, contrastRatio, toHex, toOklch } from "./color.js";
7
7
 
8
- export { setIcons, getIcon, iconNames } from "./icons.js";
8
+ export { setIcons, getIcon, iconNames, iconAliases } from "./icons.js";
9
9
 
10
10
  export const PREFIX = "ray";
11
11
 
@@ -313,12 +313,13 @@ export class RayElement extends HTMLElement {
313
313
  * Protected. Fills every [data-ray-icon="name"] under `root` (default: the
314
314
  * shadow root) with that icon of the registry (setIcons). Components call
315
315
  * it for markup they create after the template. An empty name or "none"
316
- * (the value components use to hide an icon) leaves the slot empty.
316
+ * (the value components use to hide an icon) leaves the slot empty;
317
+ * data-ray-icon-filled asks for the filled version (the outline without one).
317
318
  */
318
319
  paintIcons(root = this.shadowRoot) {
319
320
  for (const slot of root.querySelectorAll("[data-ray-icon]")) {
320
321
  const name = slot.getAttribute("data-ray-icon");
321
- slot.innerHTML = name && name !== "none" ? getIcon(name) : "";
322
+ slot.innerHTML = name && name !== "none" ? getIcon(name, { filled: slot.hasAttribute("data-ray-icon-filled") }) : "";
322
323
  }
323
324
  }
324
325
  }