@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.
- package/check/check.d.ts +2 -0
- package/check/check.js +18 -4
- package/check/cli.js +10 -6
- package/components/accordion/accordion.js +1 -1
- package/components/alert/alert.js +1 -1
- package/components/attachment/attachment.js +1 -1
- package/components/avatar/avatar.js +1 -1
- package/components/bottom-nav/bottom-nav.js +2 -0
- package/components/breadcrumbs/breadcrumbs.js +1 -1
- package/components/carousel/carousel.js +4 -4
- package/components/checkbox/checkbox.js +1 -1
- package/components/code/code.js +1 -1
- package/components/color-picker/color-picker.js +1 -1
- package/components/combobox/combobox.js +3 -3
- package/components/command-palette/command-palette.js +1 -1
- package/components/date-picker/date-picker.js +4 -4
- package/components/dialog/dialog.js +1 -1
- package/components/drawer/drawer.js +1 -1
- package/components/file-upload/file-upload.js +1 -1
- package/components/icon/icon.js +6 -1
- package/components/nav-link/nav-link.js +1 -1
- package/components/notification/notification.js +1 -1
- package/components/number-input/number-input.js +1 -1
- package/components/pagination/pagination.js +4 -4
- package/components/password-input/password-input.js +2 -2
- package/components/search/search.js +2 -2
- package/components/select/select.js +2 -2
- package/components/stat/stat.js +1 -1
- package/components/step-dots/step-dots.js +11 -2
- package/components/stepper/stepper.js +1 -1
- package/components/table/table.js +1 -1
- package/components/theme-toggle/theme-toggle.js +2 -2
- package/core/core.js +4 -3
- package/core/icons.js +95 -8
- package/custom-elements.json +71 -17
- package/llms-full.txt +36 -16
- package/llms.txt +14 -3
- package/package.json +1 -1
- package/ray-elements.d.ts +24 -14
- 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
|
-
|
|
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 (
|
|
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))
|
|
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-]*)\
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
154
|
-
<button class="control next" part="next" type="button" aria-label="Next slide"><span data-ray-icon="
|
|
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="
|
|
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>.
|
package/components/code/code.js
CHANGED
|
@@ -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="
|
|
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="
|
|
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="
|
|
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="
|
|
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", "
|
|
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="
|
|
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="
|
|
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="
|
|
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>
|
package/components/icon/icon.js
CHANGED
|
@@ -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
|
-
|
|
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="
|
|
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="
|
|
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, "
|
|
143
|
-
items.push(control("previous", 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, "
|
|
150
|
-
if (this.withEdges) items.push(control("last", 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="
|
|
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 ? "
|
|
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="
|
|
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="
|
|
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="
|
|
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;
|
package/components/stat/stat.js
CHANGED
|
@@ -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: "
|
|
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
|
-
|
|
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 :
|
|
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 || "
|
|
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="
|
|
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: "
|
|
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="
|
|
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
|
}
|