@raylang/elements 0.5.2 → 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 +27 -6
- 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/button/button.js +17 -0
- 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 +55 -9
- package/components/pagination/pagination.js +4 -4
- package/components/password-input/password-input.js +2 -2
- package/components/radio/radio.js +68 -5
- package/components/ring-progress/ring-progress.js +127 -0
- 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 +95 -0
- 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 +700 -43
- package/index.js +3 -1
- package/llms-full.txt +117 -21
- package/llms.txt +18 -5
- package/package.json +9 -1
- package/ray-elements.d.ts +77 -19
- package/vscode.html-custom-data.json +1203 -73
|
@@ -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);
|
|
@@ -3,12 +3,17 @@
|
|
|
3
3
|
// Native radios cannot group across shadow roots, so the group owns the
|
|
4
4
|
// state: it is the form control (value submitted under `name`), keeps one
|
|
5
5
|
// radio checked, and moves focus and selection with the arrow keys (roving
|
|
6
|
-
// tabindex), like native radios.
|
|
6
|
+
// tabindex), like native radios. <ray-radio-card> is a radio drawn as a
|
|
7
|
+
// card (a title, a detail, an icon), for choices that need more than a line:
|
|
8
|
+
// plans, presets, shipping methods. Both can share a group.
|
|
7
9
|
|
|
8
10
|
import { RayElement, css, define } from "../../core/core.js";
|
|
9
11
|
import { applyColor, sizeStyles } from "../../core/styles.js";
|
|
10
12
|
import { FormControl, fieldStyles } from "../../core/form.js";
|
|
11
13
|
|
|
14
|
+
/** The elements a group owns: radios and radio cards. */
|
|
15
|
+
const RADIOS = "ray-radio, ray-radio-card";
|
|
16
|
+
|
|
12
17
|
const groupStyles = css`
|
|
13
18
|
:host { --_rs: 1.25rem; }
|
|
14
19
|
:host([size="xs"]) { --_rs: 1rem; }
|
|
@@ -53,7 +58,7 @@ export class RayRadioGroup extends FormControl {
|
|
|
53
58
|
this.internals.role = "radiogroup";
|
|
54
59
|
this.addEventListener("keydown", (e) => this._keydown(e));
|
|
55
60
|
this.addEventListener("click", (e) => {
|
|
56
|
-
const radio = e.target.closest?.(
|
|
61
|
+
const radio = e.target.closest?.(RADIOS);
|
|
57
62
|
if (radio && this.radios.includes(radio)) this._choose(radio, true);
|
|
58
63
|
});
|
|
59
64
|
}
|
|
@@ -67,9 +72,9 @@ export class RayRadioGroup extends FormControl {
|
|
|
67
72
|
}
|
|
68
73
|
}
|
|
69
74
|
|
|
70
|
-
/** The radios of this group (direct or nested, not in a nested group). */
|
|
75
|
+
/** The radios (and radio cards) of this group (direct or nested, not in a nested group). */
|
|
71
76
|
get radios() {
|
|
72
|
-
return [...this.querySelectorAll(
|
|
77
|
+
return [...this.querySelectorAll(RADIOS)].filter((r) => r.closest("ray-radio-group") === this);
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
update() {
|
|
@@ -129,7 +134,7 @@ export class RayRadioGroup extends FormControl {
|
|
|
129
134
|
|
|
130
135
|
_keydown(e) {
|
|
131
136
|
const radios = this.radios.filter((r) => !r.disabled);
|
|
132
|
-
const i = radios.indexOf(e.target.closest?.(
|
|
137
|
+
const i = radios.indexOf(e.target.closest?.(RADIOS));
|
|
133
138
|
if (i < 0) return;
|
|
134
139
|
let next = null;
|
|
135
140
|
if (e.key === "ArrowDown" || e.key === "ArrowRight") next = radios[(i + 1) % radios.length];
|
|
@@ -266,5 +271,63 @@ export class RayRadio extends RayElement {
|
|
|
266
271
|
}
|
|
267
272
|
}
|
|
268
273
|
|
|
274
|
+
const cardStyles = css`
|
|
275
|
+
:host { --_s: var(--_rs, 1.125rem); }
|
|
276
|
+
.root {
|
|
277
|
+
align-items: flex-start;
|
|
278
|
+
gap: var(--ray-space-3);
|
|
279
|
+
height: 100%;
|
|
280
|
+
box-sizing: border-box;
|
|
281
|
+
padding: var(--ray-space-3) var(--ray-space-4);
|
|
282
|
+
background: var(--ray-background);
|
|
283
|
+
border: 1px solid var(--ray-border);
|
|
284
|
+
border-radius: var(--ray-radius-default);
|
|
285
|
+
transition: background-color var(--ray-duration-fast) var(--ray-easing-standard), border-color var(--ray-duration-fast) var(--ray-easing-standard);
|
|
286
|
+
}
|
|
287
|
+
:host(:not([data-disabled])) .root:hover { background: var(--ray-default-hover); }
|
|
288
|
+
:host([data-checked]) .root, :host([data-checked]) .root:hover {
|
|
289
|
+
background: var(--_light, var(--ray-primary-light));
|
|
290
|
+
border-color: var(--_filled, var(--ray-primary-filled));
|
|
291
|
+
}
|
|
292
|
+
:host(:focus-visible) .root { outline: var(--ray-focus-ring-width) solid var(--ray-ring); outline-offset: var(--ray-focus-ring-offset); }
|
|
293
|
+
:host(:focus-visible) .indicator { outline: none; }
|
|
294
|
+
:host([data-disabled]) .root { background: var(--ray-muted); opacity: 0.6; }
|
|
295
|
+
.body { flex: 1; min-width: 0; min-height: 0; }
|
|
296
|
+
.label { font-weight: var(--ray-font-weight-semibold); }
|
|
297
|
+
.icon { display: inline-flex; flex: none; color: var(--_filled, var(--ray-primary-filled)); font-size: 1.25rem; line-height: 1; }
|
|
298
|
+
.extra { margin-top: var(--ray-space-2); font-size: var(--ray-font-size-sm); }
|
|
299
|
+
:host([hide-indicator]) .indicator { display: none; }
|
|
300
|
+
`;
|
|
301
|
+
|
|
302
|
+
export class RayRadioCard extends RayRadio {
|
|
303
|
+
static properties = {
|
|
304
|
+
...RayRadio.properties,
|
|
305
|
+
hideIndicator: { type: Boolean, reflect: true, attribute: "hide-indicator" },
|
|
306
|
+
};
|
|
307
|
+
static styles = [radioStyles, cardStyles];
|
|
308
|
+
static template = `
|
|
309
|
+
<div class="root" part="root">
|
|
310
|
+
<span class="icon" part="icon" hidden><slot name="icon"></slot></span>
|
|
311
|
+
<span class="body">
|
|
312
|
+
<span class="label" part="label"><span class="label-text"></span></span>
|
|
313
|
+
<span class="description" part="description" hidden></span>
|
|
314
|
+
<span class="extra" hidden><slot></slot></span>
|
|
315
|
+
</span>
|
|
316
|
+
<span class="indicator" part="indicator"><span class="dot"></span></span>
|
|
317
|
+
</div>`;
|
|
318
|
+
|
|
319
|
+
constructor() {
|
|
320
|
+
super();
|
|
321
|
+
// The icon and the extra content take room only when given.
|
|
322
|
+
for (const slot of this.shadowRoot.querySelectorAll("slot")) {
|
|
323
|
+
slot.addEventListener("slotchange", () => {
|
|
324
|
+
const given = slot.assignedNodes().some((n) => n.nodeType === Node.ELEMENT_NODE || n.textContent.trim());
|
|
325
|
+
slot.parentElement.hidden = !given;
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
269
331
|
define("ray-radio-group", RayRadioGroup);
|
|
270
332
|
define("ray-radio", RayRadio);
|
|
333
|
+
define("ray-radio-card", RayRadioCard);
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// <ray-ring-progress>: progress as a ring (Mantine's RingProgress), with
|
|
2
|
+
// room in the middle for content (a countdown's clock, a percentage).
|
|
3
|
+
//
|
|
4
|
+
// The arc is a stroke-dasharray on a circle with pathLength=100, drawn from
|
|
5
|
+
// 12 o'clock clockwise; r is a CSS length (half the box minus half the
|
|
6
|
+
// stroke), so any size and thickness need no viewBox arithmetic.
|
|
7
|
+
|
|
8
|
+
import { RayElement, css, define } from "../../core/core.js";
|
|
9
|
+
import { applyColor } from "../../core/styles.js";
|
|
10
|
+
|
|
11
|
+
const SIZES = { xs: "3rem", sm: "5rem", md: "7.5rem", lg: "10rem", xl: "12.5rem" };
|
|
12
|
+
|
|
13
|
+
const styles = css`
|
|
14
|
+
:host {
|
|
15
|
+
--_d: 7.5rem;
|
|
16
|
+
--_t: calc(var(--_d) / 10);
|
|
17
|
+
position: relative;
|
|
18
|
+
display: inline-grid;
|
|
19
|
+
/* One cell of exactly the diameter: the svg's intrinsic 300×150 must not size it. */
|
|
20
|
+
grid-template: minmax(0, 1fr) / minmax(0, 1fr);
|
|
21
|
+
vertical-align: middle;
|
|
22
|
+
width: var(--_d);
|
|
23
|
+
height: var(--_d);
|
|
24
|
+
flex: none;
|
|
25
|
+
}
|
|
26
|
+
svg {
|
|
27
|
+
grid-area: 1 / 1;
|
|
28
|
+
width: 100%;
|
|
29
|
+
height: 100%;
|
|
30
|
+
overflow: visible;
|
|
31
|
+
transform: rotate(-90deg);
|
|
32
|
+
}
|
|
33
|
+
circle {
|
|
34
|
+
cx: 50%;
|
|
35
|
+
cy: 50%;
|
|
36
|
+
r: calc(50% - var(--_t) / 2);
|
|
37
|
+
fill: none;
|
|
38
|
+
stroke-width: var(--_t);
|
|
39
|
+
}
|
|
40
|
+
.track { stroke: var(--ray-muted); }
|
|
41
|
+
.bar {
|
|
42
|
+
stroke: var(--_filled);
|
|
43
|
+
stroke-dasharray: var(--_pct, 0) 100;
|
|
44
|
+
transition: stroke-dasharray var(--ray-duration-slow) var(--ray-easing-standard);
|
|
45
|
+
}
|
|
46
|
+
:host([round-caps]) .bar { stroke-linecap: round; }
|
|
47
|
+
/* A round cap draws a dot at 0. */
|
|
48
|
+
.bar[data-empty] { visibility: hidden; }
|
|
49
|
+
svg[data-indeterminate] { animation: ray-ring-spin 1.2s linear infinite; }
|
|
50
|
+
svg[data-indeterminate] .bar { transition: none; }
|
|
51
|
+
@keyframes ray-ring-spin { from { transform: rotate(-90deg); } to { transform: rotate(270deg); } }
|
|
52
|
+
.center {
|
|
53
|
+
grid-area: 1 / 1;
|
|
54
|
+
display: grid;
|
|
55
|
+
place-items: center;
|
|
56
|
+
padding: calc(var(--_t) * 1.5);
|
|
57
|
+
min-width: 0;
|
|
58
|
+
text-align: center;
|
|
59
|
+
font-size: max(var(--ray-font-size-xs), calc(var(--_d) * 0.16));
|
|
60
|
+
font-variant-numeric: tabular-nums;
|
|
61
|
+
line-height: 1.1;
|
|
62
|
+
}
|
|
63
|
+
.value { font-weight: var(--ray-font-weight-semibold); }
|
|
64
|
+
.value:empty { display: none; }
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
export class RayRingProgress extends RayElement {
|
|
68
|
+
static properties = {
|
|
69
|
+
value: { type: Number },
|
|
70
|
+
max: { type: Number, default: 100 },
|
|
71
|
+
color: { type: String, reflect: true },
|
|
72
|
+
size: { type: String, reflect: true, default: "md" },
|
|
73
|
+
thickness: { type: String, reflect: true },
|
|
74
|
+
roundCaps: { type: Boolean, reflect: true, attribute: "round-caps" },
|
|
75
|
+
showValue: { type: Boolean, reflect: true, attribute: "show-value" },
|
|
76
|
+
label: { type: String },
|
|
77
|
+
};
|
|
78
|
+
static styles = [styles];
|
|
79
|
+
static template = `<svg aria-hidden="true"><circle class="track" part="track" pathLength="100"></circle><circle class="bar" part="bar" pathLength="100"></circle></svg><div class="center" part="center"><slot></slot><span class="value" aria-hidden="true"></span></div>`;
|
|
80
|
+
|
|
81
|
+
constructor() {
|
|
82
|
+
super();
|
|
83
|
+
this.internals = this.attachInternals();
|
|
84
|
+
this.internals.role = "progressbar";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
update() {
|
|
88
|
+
const svg = this.$("svg");
|
|
89
|
+
const bar = this.$(".bar");
|
|
90
|
+
applyColor(svg, this.color);
|
|
91
|
+
// size: xs…xl or a length; thickness: a length (a bare number is px).
|
|
92
|
+
const d = SIZES[this.size] ?? this.size ?? "";
|
|
93
|
+
if (d) this.style.setProperty("--_d", d);
|
|
94
|
+
else this.style.removeProperty("--_d");
|
|
95
|
+
const t = this.thickness ? (/^\d*\.?\d+$/.test(this.thickness) ? `${this.thickness}px` : this.thickness) : "";
|
|
96
|
+
if (t) this.style.setProperty("--_t", t);
|
|
97
|
+
else this.style.removeProperty("--_t");
|
|
98
|
+
|
|
99
|
+
const max = this.max > 0 ? this.max : 100;
|
|
100
|
+
const indeterminate = this.value === undefined || this.value === null || Number.isNaN(this.value);
|
|
101
|
+
svg.toggleAttribute("data-indeterminate", indeterminate);
|
|
102
|
+
const pct = indeterminate ? 0 : Math.min(100, Math.max(0, (this.value / max) * 100));
|
|
103
|
+
// Indeterminate: a quarter arc that spins.
|
|
104
|
+
bar.style.setProperty("--_pct", String(indeterminate ? 25 : pct));
|
|
105
|
+
bar.toggleAttribute("data-empty", !indeterminate && pct === 0);
|
|
106
|
+
// The percentage in the middle only when the app puts nothing there.
|
|
107
|
+
const empty = this.$("slot").assignedNodes().every((n) => n.nodeType === Node.TEXT_NODE && !n.textContent.trim());
|
|
108
|
+
this.$(".value").textContent = this.showValue && !indeterminate && empty ? `${Math.round(pct)}%` : "";
|
|
109
|
+
|
|
110
|
+
const i = this.internals;
|
|
111
|
+
i.ariaValueMin = "0";
|
|
112
|
+
i.ariaValueMax = String(max);
|
|
113
|
+
i.ariaValueNow = indeterminate ? null : String(this.value);
|
|
114
|
+
i.ariaValueText = indeterminate ? null : `${Math.round(pct)}%`;
|
|
115
|
+
i.ariaLabel = this.label || this.getAttribute("aria-label") || "Progress";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
connectedCallback() {
|
|
119
|
+
super.connectedCallback();
|
|
120
|
+
if (!this._wired) {
|
|
121
|
+
this._wired = true;
|
|
122
|
+
this.$("slot").addEventListener("slotchange", () => this.requestUpdate());
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
define("ray-ring-progress", RayRingProgress);
|
|
@@ -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 = {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// <ray-step-dots>: a row of dots, one per step: done (filled), the current
|
|
2
|
+
// one (a filled pill), the rest (empty). A compact progress for rounds,
|
|
3
|
+
// onboarding pages or a quiz. Not interactive: the carousel's dots are
|
|
4
|
+
// buttons, these are a progressbar.
|
|
5
|
+
|
|
6
|
+
import { RayElement, css, define } from "../../core/core.js";
|
|
7
|
+
import { applyColor } from "../../core/styles.js";
|
|
8
|
+
|
|
9
|
+
const styles = css`
|
|
10
|
+
:host {
|
|
11
|
+
--_dot: 0.5rem;
|
|
12
|
+
display: block;
|
|
13
|
+
}
|
|
14
|
+
:host([size="xs"]) { --_dot: 0.3125rem; }
|
|
15
|
+
:host([size="md"]) { --_dot: 0.625rem; }
|
|
16
|
+
:host([size="lg"]) { --_dot: 0.75rem; }
|
|
17
|
+
:host([size="xl"]) { --_dot: 1rem; }
|
|
18
|
+
.dots {
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-wrap: wrap;
|
|
21
|
+
align-items: center;
|
|
22
|
+
justify-content: var(--_justify, center);
|
|
23
|
+
gap: calc(var(--_dot) * 0.75);
|
|
24
|
+
}
|
|
25
|
+
:host([align="start"]) { --_justify: flex-start; }
|
|
26
|
+
:host([align="end"]) { --_justify: flex-end; }
|
|
27
|
+
.dot {
|
|
28
|
+
flex: none;
|
|
29
|
+
width: var(--_dot);
|
|
30
|
+
height: var(--_dot);
|
|
31
|
+
box-sizing: border-box;
|
|
32
|
+
border-radius: var(--ray-radius-full);
|
|
33
|
+
border: 1.5px solid color-mix(in srgb, var(--ray-foreground) 28%, transparent);
|
|
34
|
+
transition: width var(--ray-duration-base) var(--ray-easing-standard), background-color var(--ray-duration-base) var(--ray-easing-standard), border-color var(--ray-duration-base) var(--ray-easing-standard);
|
|
35
|
+
}
|
|
36
|
+
.dot[data-state="done"] { background: var(--_filled); border-color: var(--_filled); }
|
|
37
|
+
.dot[data-state="current"] { width: calc(var(--_dot) * 2.5); background: var(--_filled); border-color: var(--_filled); }
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
export class RayStepDots extends RayElement {
|
|
41
|
+
static properties = {
|
|
42
|
+
count: { type: Number, default: 0 },
|
|
43
|
+
value: { type: Number, default: 0 },
|
|
44
|
+
color: { type: String, reflect: true },
|
|
45
|
+
size: { type: String, reflect: true, default: "sm" },
|
|
46
|
+
align: { type: String, reflect: true, default: "center" },
|
|
47
|
+
label: { type: String },
|
|
48
|
+
valueText: { type: String, attribute: "value-text" },
|
|
49
|
+
doneText: { type: String, attribute: "done-text" },
|
|
50
|
+
};
|
|
51
|
+
static styles = [styles];
|
|
52
|
+
static template = `<div class="dots" part="dots" aria-hidden="true"></div>`;
|
|
53
|
+
|
|
54
|
+
constructor() {
|
|
55
|
+
super();
|
|
56
|
+
this.internals = this.attachInternals();
|
|
57
|
+
this.internals.role = "progressbar";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
update() {
|
|
61
|
+
applyColor(this, this.color);
|
|
62
|
+
const count = Math.max(0, Math.floor(this.count || 0));
|
|
63
|
+
// value: the current step, 0-based; count (or more) is all done.
|
|
64
|
+
const value = Math.min(count, Math.max(0, Math.floor(this.value || 0)));
|
|
65
|
+
const box = this.$(".dots");
|
|
66
|
+
while (box.children.length < count) {
|
|
67
|
+
const dot = document.createElement("span");
|
|
68
|
+
dot.className = "dot";
|
|
69
|
+
dot.setAttribute("part", "dot");
|
|
70
|
+
box.append(dot);
|
|
71
|
+
}
|
|
72
|
+
while (box.children.length > count) box.lastElementChild.remove();
|
|
73
|
+
[...box.children].forEach((dot, i) => {
|
|
74
|
+
dot.dataset.state = i < value ? "done" : i === value ? "current" : "todo";
|
|
75
|
+
});
|
|
76
|
+
|
|
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));
|
|
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}`);
|
|
86
|
+
const i = this.internals;
|
|
87
|
+
i.ariaLabel = this.label || this.getAttribute("aria-label") || "Steps";
|
|
88
|
+
i.ariaValueMin = "0";
|
|
89
|
+
i.ariaValueMax = String(count);
|
|
90
|
+
i.ariaValueNow = String(value);
|
|
91
|
+
i.ariaValueText = count === 0 ? null : text;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
define("ray-step-dots", RayStepDots);
|
|
@@ -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
|
}
|
package/core/icons.js
CHANGED
|
@@ -6,6 +6,16 @@
|
|
|
6
6
|
//
|
|
7
7
|
// setIcons() also adds new names, for <ray-icon name="…">. Icons are SVG
|
|
8
8
|
// markup (or an SVGElement) drawn with currentColor; components size them.
|
|
9
|
+
//
|
|
10
|
+
// The components ask for roles, not glyphs: "ui.close" (a dialog's close
|
|
11
|
+
// button), "ui.remove" (a chip's ×), "ui.clear" (a field's ×)… Each role
|
|
12
|
+
// draws its glyph (ALIASES) unless the app registers the role itself, so
|
|
13
|
+
// setIcons({ close: … }) still changes every × and setIcons({ "ui.remove": … })
|
|
14
|
+
// only the chips'.
|
|
15
|
+
//
|
|
16
|
+
// An icon may have a filled version, "<name>:filled", for a state: the active
|
|
17
|
+
// tab, a favorite, unread. <ray-icon name="star" filled> draws it, and the
|
|
18
|
+
// outline one when there is none. setIcons({ "star:filled": … }) adds one.
|
|
9
19
|
// The markup is inserted as HTML: register icons from your own code only,
|
|
10
20
|
// never from user input.
|
|
11
21
|
|
|
@@ -62,12 +72,72 @@ const DEFAULTS = {
|
|
|
62
72
|
"message-circle": svg(`<path d="M20 11.5a8 8 0 0 1-11.6 7.2L4 20l1.3-4.2A8 8 0 1 1 20 11.5z"/>`),
|
|
63
73
|
};
|
|
64
74
|
|
|
65
|
-
|
|
75
|
+
// The filled versions: the outline's shapes filled (same silhouette: the
|
|
76
|
+
// stroke stays), or one even-odd path where a mark is cut out of a disc.
|
|
77
|
+
const solid = (body) =>
|
|
78
|
+
`<svg viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`;
|
|
79
|
+
const cut = (d) => `<svg viewBox="0 0 24 24" fill="currentColor" fill-rule="evenodd" aria-hidden="true"><path d="${d}"/></svg>`;
|
|
80
|
+
|
|
81
|
+
/** The filled versions of the default set (registered as "<name>:filled"). */
|
|
82
|
+
const FILLED = {
|
|
83
|
+
"user": solid(`<circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0z"/>`),
|
|
84
|
+
"info": cut("M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm1 15h-2v-6h2zm0-8h-2V7h2z"),
|
|
85
|
+
"circle-check": cut("M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm-1.5 14.5-4-4 1.4-1.4 2.6 2.6 5.6-5.6 1.4 1.4z"),
|
|
86
|
+
"alert-triangle": cut("M10.3 3.2 1.6 18.1a2 2 0 0 0 1.7 3h17.4a2 2 0 0 0 1.7-3L13.7 3.2a2 2 0 0 0-3.4 0zM11 8.5h2v5.5h-2zM12 16a1.25 1.25 0 1 1 0 2.5 1.25 1.25 0 0 1 0-2.5z"),
|
|
87
|
+
"circle-x": cut("M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM14.12 8.46 15.54 9.88 13.41 12 15.54 14.12 14.12 15.54 12 13.41 9.88 15.54 8.46 14.12 10.59 12 8.46 9.88 9.88 8.46 12 10.59z"),
|
|
88
|
+
"folder": solid(`<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>`),
|
|
89
|
+
bell: solid(`<path d="M10.3 21a1.9 1.9 0 0 0 3.4 0z"/><path d="M3.3 15.3A1 1 0 0 0 4 17h16a1 1 0 0 0 .7-1.7C19.4 14 18 12.6 18 8A6 6 0 0 0 6 8c0 4.6-1.4 6-2.7 7.3"/>`),
|
|
90
|
+
"star": solid(`<path d="M12 3.5l2.6 5.3 5.9.9-4.3 4.1 1 5.8L12 16.9l-5.2 2.7 1-5.8-4.3-4.1 5.9-.9z"/>`),
|
|
91
|
+
"settings": solid(`<path d="M4 6h10M18 6h2M4 12h4M12 12h8M4 18h12"/><circle cx="16" cy="6" r="2"/><circle cx="10" cy="12" r="2"/><circle cx="18" cy="18" r="2"/>`),
|
|
92
|
+
"home": solid(`<path d="M4 10.5 12 4l8 6.5V19a1 1 0 0 1-1 1h-4.5v-5.5h-5V20H5a1 1 0 0 1-1-1z"/>`),
|
|
93
|
+
"message-circle": solid(`<path d="M20 11.5a8 8 0 0 1-11.6 7.2L4 20l1.3-4.2A8 8 0 1 1 20 11.5z"/>`),
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const FILLED_SUFFIX = ":filled";
|
|
97
|
+
|
|
98
|
+
/** The roles the components draw, and the glyph each one uses by default. */
|
|
99
|
+
const ALIASES = {
|
|
100
|
+
"ui.close": "close",
|
|
101
|
+
"ui.clear": "close",
|
|
102
|
+
"ui.remove": "close",
|
|
103
|
+
"ui.expand": "chevron-down",
|
|
104
|
+
"ui.disclosure": "chevron-right",
|
|
105
|
+
"ui.previous": "chevron-left",
|
|
106
|
+
"ui.next": "chevron-right",
|
|
107
|
+
"ui.first": "chevrons-left",
|
|
108
|
+
"ui.last": "chevrons-right",
|
|
109
|
+
"ui.separator": "chevron-right",
|
|
110
|
+
"ui.increment": "chevron-up",
|
|
111
|
+
"ui.decrement": "chevron-down",
|
|
112
|
+
"ui.sort": "chevron-up",
|
|
113
|
+
"ui.checked": "check",
|
|
114
|
+
"ui.indeterminate": "minus",
|
|
115
|
+
"ui.copy": "copy",
|
|
116
|
+
"ui.copied": "check",
|
|
117
|
+
"ui.show-password": "eye",
|
|
118
|
+
"ui.hide-password": "eye-off",
|
|
119
|
+
"ui.play": "play",
|
|
120
|
+
"ui.pause": "pause",
|
|
121
|
+
"ui.search": "search",
|
|
122
|
+
"ui.calendar": "calendar",
|
|
123
|
+
"ui.eyedropper": "pipette",
|
|
124
|
+
"ui.upload": "upload",
|
|
125
|
+
"ui.avatar": "user",
|
|
126
|
+
"ui.theme-light": "sun",
|
|
127
|
+
"ui.theme-dark": "moon",
|
|
128
|
+
"ui.theme-system": "monitor",
|
|
129
|
+
"ui.trend-up": "arrow-up-right",
|
|
130
|
+
"ui.trend-down": "arrow-down-right",
|
|
131
|
+
"ui.trend-flat": "minus",
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const registry = new Map([...Object.entries(DEFAULTS), ...Object.entries(FILLED).map(([name, markup]) => [name + FILLED_SUFFIX, markup])]);
|
|
66
135
|
const listeners = new Set();
|
|
67
136
|
const warned = new Set();
|
|
68
137
|
|
|
69
138
|
/**
|
|
70
|
-
* Replaces or adds icons. Values are SVG markup or an SVGElement
|
|
139
|
+
* Replaces or adds icons. Values are SVG markup or an SVGElement; a name
|
|
140
|
+
* ending in ":filled" is the filled version of an icon.
|
|
71
141
|
* @param {Record<string, string | SVGElement>} icons
|
|
72
142
|
*/
|
|
73
143
|
export function setIcons(icons) {
|
|
@@ -81,9 +151,19 @@ export function setIcons(icons) {
|
|
|
81
151
|
for (const cb of listeners) cb();
|
|
82
152
|
}
|
|
83
153
|
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
|
|
154
|
+
/**
|
|
155
|
+
* The SVG markup of an icon or a role ("" and one warning when it is not
|
|
156
|
+
* registered). With `filled`, its filled version, or the outline one when it
|
|
157
|
+
* has none.
|
|
158
|
+
*/
|
|
159
|
+
export function getIcon(name, { filled = false } = {}) {
|
|
160
|
+
// A role the app has not registered draws its glyph.
|
|
161
|
+
const glyph = ALIASES[name];
|
|
162
|
+
if (filled) {
|
|
163
|
+
const solidMarkup = registry.get(name + FILLED_SUFFIX) ?? (glyph && !registry.has(name) ? registry.get(glyph + FILLED_SUFFIX) : undefined);
|
|
164
|
+
if (solidMarkup !== undefined) return solidMarkup;
|
|
165
|
+
}
|
|
166
|
+
const markup = registry.get(name) ?? (glyph ? registry.get(glyph) : undefined);
|
|
87
167
|
if (markup !== undefined) return markup;
|
|
88
168
|
if (!warned.has(name)) {
|
|
89
169
|
warned.add(name);
|
|
@@ -92,9 +172,16 @@ export function getIcon(name) {
|
|
|
92
172
|
return "";
|
|
93
173
|
}
|
|
94
174
|
|
|
95
|
-
/** The
|
|
96
|
-
export function
|
|
97
|
-
return
|
|
175
|
+
/** The roles the components draw ("ui.close"…) and the glyph each one uses unless registered. */
|
|
176
|
+
export function iconAliases() {
|
|
177
|
+
return { ...ALIASES };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The registered icon names; with `filled`, those that have a filled version. */
|
|
181
|
+
export function iconNames({ filled = false } = {}) {
|
|
182
|
+
const names = [...registry.keys()];
|
|
183
|
+
if (filled) return names.filter((n) => n.endsWith(FILLED_SUFFIX)).map((n) => n.slice(0, -FILLED_SUFFIX.length));
|
|
184
|
+
return names.filter((n) => !n.endsWith(FILLED_SUFFIX));
|
|
98
185
|
}
|
|
99
186
|
|
|
100
187
|
/** @internal Called by the core runtime to repaint components after setIcons. */
|