@raylang/elements 0.6.0 → 0.8.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/base.css +1 -1
- package/check/check.d.ts +2 -0
- package/check/check.js +18 -4
- package/check/cli.js +10 -6
- package/components/accordion/accordion.js +8 -5
- package/components/alert/alert.js +3 -2
- package/components/attachment/attachment.js +5 -6
- package/components/avatar/avatar.js +1 -1
- package/components/blockquote/blockquote.js +2 -2
- package/components/bottom-nav/bottom-nav.js +5 -3
- package/components/breadcrumbs/breadcrumbs.js +1 -1
- package/components/carousel/carousel.js +26 -14
- package/components/checkbox/checkbox.js +6 -3
- package/components/checkbox-group/checkbox-group.js +1 -1
- package/components/code/code.js +9 -7
- package/components/color-picker/color-picker.js +6 -6
- package/components/combobox/combobox.js +5 -4
- package/components/command-palette/command-palette.js +2 -2
- package/components/date-picker/date-picker.js +9 -7
- package/components/dialog/dialog.js +2 -2
- package/components/drawer/drawer.js +28 -10
- package/components/file-upload/file-upload.js +1 -1
- package/components/icon/icon.js +9 -1
- package/components/indicator/indicator.js +2 -1
- package/components/menu/menu.js +1 -1
- package/components/nav-link/nav-link.js +6 -3
- package/components/notification/notification.js +16 -13
- 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/progress/progress.js +2 -2
- package/components/radio/radio.js +4 -2
- package/components/scroll-area/scroll-area.js +14 -11
- package/components/search/search.js +5 -5
- package/components/segmented-control/segmented-control.js +5 -3
- package/components/select/select.js +5 -5
- package/components/separator/separator.js +8 -3
- package/components/sidebar/sidebar.js +14 -6
- package/components/slider/slider.js +5 -2
- package/components/split/split.js +12 -6
- package/components/stat/stat.js +1 -1
- package/components/step-dots/step-dots.js +11 -2
- package/components/stepper/stepper.js +5 -5
- package/components/switch/switch.js +3 -3
- package/components/table/table.js +6 -6
- package/components/tabs/tabs.js +11 -8
- package/components/theme-toggle/theme-toggle.js +2 -2
- package/components/timeline/timeline.js +13 -8
- package/components/toggle-group/toggle-group.js +6 -4
- package/core/core.js +10 -4
- package/core/direction.js +19 -0
- package/core/icons.js +107 -8
- package/core/position.js +11 -3
- package/core/styles.js +1 -1
- package/core.js +1 -0
- package/custom-elements.json +151 -79
- package/llms-full.txt +75 -31
- package/llms.txt +32 -3
- package/package.json +1 -1
- package/ray-elements.d.ts +56 -39
- package/vscode.html-custom-data.json +1102 -39
package/base.css
CHANGED
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
|
}
|
|
@@ -17,7 +17,7 @@ export class RayAccordion extends RayElement {
|
|
|
17
17
|
value: { type: String },
|
|
18
18
|
multiple: { type: Boolean, reflect: true },
|
|
19
19
|
variant: { type: String, reflect: true, default: "default" },
|
|
20
|
-
chevronPosition: { type: String, reflect: true, default: "
|
|
20
|
+
chevronPosition: { type: String, reflect: true, default: "end" },
|
|
21
21
|
};
|
|
22
22
|
static styles = [accordionStyles];
|
|
23
23
|
static template = `<div class="items" part="items"><slot></slot></div>`;
|
|
@@ -52,7 +52,7 @@ export class RayAccordion extends RayElement {
|
|
|
52
52
|
items.forEach((item, i) => {
|
|
53
53
|
item._setOpen(open.includes(item.value));
|
|
54
54
|
item.dataset.variant = this.variant || "default";
|
|
55
|
-
item.dataset.chevron = this.chevronPosition || "
|
|
55
|
+
item.dataset.chevron = this.chevronPosition || "end";
|
|
56
56
|
item.toggleAttribute("data-first", i === 0);
|
|
57
57
|
item.toggleAttribute("data-last", i === items.length - 1);
|
|
58
58
|
});
|
|
@@ -104,7 +104,7 @@ const itemStyles = css`
|
|
|
104
104
|
font-size: var(--ray-font-size-md);
|
|
105
105
|
font-weight: var(--ray-font-weight-medium);
|
|
106
106
|
color: var(--ray-foreground);
|
|
107
|
-
text-align:
|
|
107
|
+
text-align: start;
|
|
108
108
|
background: transparent;
|
|
109
109
|
border: 0;
|
|
110
110
|
border-radius: inherit;
|
|
@@ -116,7 +116,10 @@ const itemStyles = css`
|
|
|
116
116
|
.label { display: grid; flex: 1; gap: 0.125rem; min-width: 0; }
|
|
117
117
|
.description { font-size: var(--ray-font-size-sm); font-weight: var(--ray-font-weight-normal, 400); color: var(--ray-muted-foreground); }
|
|
118
118
|
.description:empty { display: none; }
|
|
119
|
-
|
|
119
|
+
/* The chevron before the label: start, or left/right as sides of the screen. */
|
|
120
|
+
:host([data-chevron="start"]) .chevron,
|
|
121
|
+
:host([data-chevron="left"]) .chevron:dir(ltr),
|
|
122
|
+
:host([data-chevron="right"]) .chevron:dir(rtl) { order: -1; }
|
|
120
123
|
.chevron { display: inline-flex; color: var(--ray-muted-foreground); transition: transform var(--ray-duration-base) var(--ray-easing-standard); }
|
|
121
124
|
.chevron svg { width: 1rem; height: 1rem; }
|
|
122
125
|
:host([data-open]) .chevron { transform: rotate(180deg); }
|
|
@@ -149,7 +152,7 @@ export class RayAccordionItem extends RayElement {
|
|
|
149
152
|
<button class="control focus-ring" id="control" part="control" type="button" aria-expanded="false" aria-controls="panel">
|
|
150
153
|
<span class="section" part="left-section"><slot name="left"></slot></span>
|
|
151
154
|
<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="
|
|
155
|
+
<span class="chevron" part="chevron" aria-hidden="true" data-ray-icon="ui.expand"></span>
|
|
153
156
|
</button>
|
|
154
157
|
</h3>
|
|
155
158
|
<div class="panel" id="panel" part="panel" role="region" aria-labelledby="control" inert>
|
|
@@ -34,7 +34,8 @@ const styles = css`
|
|
|
34
34
|
justify-content: center;
|
|
35
35
|
width: 1.5rem;
|
|
36
36
|
height: 1.5rem;
|
|
37
|
-
margin: -0.125rem
|
|
37
|
+
margin: -0.125rem 0 0;
|
|
38
|
+
margin-inline-end: -0.375rem;
|
|
38
39
|
padding: 0;
|
|
39
40
|
color: inherit;
|
|
40
41
|
background: transparent;
|
|
@@ -66,7 +67,7 @@ export class RayAlert extends RayElement {
|
|
|
66
67
|
<p class="title" part="title"></p>
|
|
67
68
|
<div class="text" part="text"><slot></slot></div>
|
|
68
69
|
</div>
|
|
69
|
-
<button class="close focus-ring" part="close" type="button" hidden><span data-ray-icon="close" aria-hidden="true"></span></button>
|
|
70
|
+
<button class="close focus-ring" part="close" type="button" hidden><span data-ray-icon="ui.close" aria-hidden="true"></span></button>
|
|
70
71
|
</div>`;
|
|
71
72
|
|
|
72
73
|
constructor() {
|
|
@@ -25,7 +25,7 @@ const styles = css`
|
|
|
25
25
|
width: 16rem;
|
|
26
26
|
max-width: 100%;
|
|
27
27
|
padding: var(--ray-space-2);
|
|
28
|
-
padding-
|
|
28
|
+
padding-inline-end: var(--ray-space-3);
|
|
29
29
|
color: var(--ray-foreground);
|
|
30
30
|
background: var(--ray-card);
|
|
31
31
|
border: 1px solid var(--ray-border);
|
|
@@ -53,7 +53,7 @@ const styles = css`
|
|
|
53
53
|
.badge {
|
|
54
54
|
position: absolute;
|
|
55
55
|
bottom: 3px;
|
|
56
|
-
left: 50%;
|
|
56
|
+
left: 50%; /* physical */
|
|
57
57
|
transform: translateX(-50%);
|
|
58
58
|
padding: 0 0.25rem;
|
|
59
59
|
font-size: 0.5625rem;
|
|
@@ -82,8 +82,7 @@ const styles = css`
|
|
|
82
82
|
:host([status="error"]) .meta { color: var(--ray-destructive); }
|
|
83
83
|
.bar {
|
|
84
84
|
position: absolute;
|
|
85
|
-
|
|
86
|
-
right: 0;
|
|
85
|
+
inset-inline: 0;
|
|
87
86
|
bottom: 0;
|
|
88
87
|
height: 2px;
|
|
89
88
|
background: var(--ray-muted);
|
|
@@ -131,7 +130,7 @@ const styles = css`
|
|
|
131
130
|
:host([variant="card"]) .remove {
|
|
132
131
|
position: absolute;
|
|
133
132
|
top: calc(var(--ray-space-2) + 4px);
|
|
134
|
-
|
|
133
|
+
inset-inline-end: calc(var(--ray-space-2) + 4px);
|
|
135
134
|
background: var(--ray-card);
|
|
136
135
|
box-shadow: var(--ray-shadow-xs);
|
|
137
136
|
opacity: 1;
|
|
@@ -191,7 +190,7 @@ export class RayAttachment extends RayElement {
|
|
|
191
190
|
<div class="item" part="item" role="group">
|
|
192
191
|
<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
192
|
<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="
|
|
193
|
+
<button class="remove focus-ring" part="remove" type="button" hidden><span data-ray-icon="ui.remove" aria-hidden="true"></span></button>
|
|
195
194
|
<span class="bar" part="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" hidden><span></span></span>
|
|
196
195
|
</div>`;
|
|
197
196
|
|
|
@@ -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) {
|
|
@@ -22,7 +22,7 @@ const styles = css`
|
|
|
22
22
|
:host([hide-edge]) { --_edge: 0px; }
|
|
23
23
|
figure { position: relative; margin: 0; }
|
|
24
24
|
/* Room for the mark, so it does not cover what is above or beside. */
|
|
25
|
-
:host([data-icon]) figure { padding: calc(var(--_icon) / 2)
|
|
25
|
+
:host([data-icon]) figure { padding-block-start: calc(var(--_icon) / 2); padding-inline-start: calc(var(--_icon) / 2); }
|
|
26
26
|
blockquote {
|
|
27
27
|
margin: 0;
|
|
28
28
|
padding: var(--_p) calc(var(--_p) * 1.25);
|
|
@@ -43,7 +43,7 @@ const styles = css`
|
|
|
43
43
|
.icon {
|
|
44
44
|
position: absolute;
|
|
45
45
|
top: 0;
|
|
46
|
-
|
|
46
|
+
inset-inline-start: 0;
|
|
47
47
|
display: grid;
|
|
48
48
|
place-items: center;
|
|
49
49
|
width: var(--_icon);
|
|
@@ -40,7 +40,7 @@ const navStyles = css`
|
|
|
40
40
|
grid-auto-columns: minmax(0, 1fr);
|
|
41
41
|
grid-auto-flow: column;
|
|
42
42
|
align-items: stretch;
|
|
43
|
-
padding: var(--ray-space-1) max(var(--ray-space-1), env(safe-area-inset-right, 0px)) max(var(--ray-space-1), env(safe-area-inset-bottom, 0px)) max(var(--ray-space-1), env(safe-area-inset-left, 0px));
|
|
43
|
+
padding: var(--ray-space-1) max(var(--ray-space-1), env(safe-area-inset-right, 0px)) max(var(--ray-space-1), env(safe-area-inset-bottom, 0px)) max(var(--ray-space-1), env(safe-area-inset-left, 0px)); /* physical */
|
|
44
44
|
background: var(--ray-bottom-nav, var(--ray-background));
|
|
45
45
|
border-top: 1px solid var(--ray-border);
|
|
46
46
|
}
|
|
@@ -109,7 +109,7 @@ const itemStyles = css`
|
|
|
109
109
|
.badge {
|
|
110
110
|
position: absolute;
|
|
111
111
|
top: 0;
|
|
112
|
-
|
|
112
|
+
inset-inline-start: calc(50% + 0.375rem);
|
|
113
113
|
min-width: 1rem;
|
|
114
114
|
height: 1rem;
|
|
115
115
|
padding: 0 0.25rem;
|
|
@@ -125,7 +125,7 @@ const itemStyles = css`
|
|
|
125
125
|
box-sizing: content-box;
|
|
126
126
|
}
|
|
127
127
|
.badge:empty:not([data-dot]) { display: none; }
|
|
128
|
-
.badge[data-dot] { min-width: 0; width: 0.5rem; height: 0.5rem; padding: 0; top: 0.25rem;
|
|
128
|
+
.badge[data-dot] { min-width: 0; width: 0.5rem; height: 0.5rem; padding: 0; top: 0.25rem; inset-inline-start: calc(50% + 0.5rem); }
|
|
129
129
|
.label {
|
|
130
130
|
max-width: 100%;
|
|
131
131
|
overflow: hidden;
|
|
@@ -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();
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { RayElement, css, define } from "../../core/core.js";
|
|
11
11
|
import { motionAllowed } from "../../core/motion.js";
|
|
12
|
+
import { isRtl, inlineKey } from "../../core/direction.js";
|
|
12
13
|
|
|
13
14
|
const styles = css`
|
|
14
15
|
:host {
|
|
@@ -74,9 +75,11 @@ const styles = css`
|
|
|
74
75
|
.control:hover:not(:disabled) { background: var(--ray-accent); }
|
|
75
76
|
.control:disabled { opacity: 0; pointer-events: none; }
|
|
76
77
|
.control svg { width: 1rem; height: 1rem; }
|
|
77
|
-
.prev {
|
|
78
|
-
.next {
|
|
79
|
-
:host([orientation="vertical"]) .control { top: auto; left: 50%; transform: translateX(-50%) rotate(90deg); }
|
|
78
|
+
.prev { inset-inline-start: var(--ray-space-2); }
|
|
79
|
+
.next { inset-inline-end: var(--ray-space-2); }
|
|
80
|
+
:host([orientation="vertical"]) .control { top: auto; left: 50%; transform: translateX(-50%) rotate(90deg); } /* physical */
|
|
81
|
+
/* Up and down do not depend on the writing direction: the turned arrows are not mirrored. */
|
|
82
|
+
:host([orientation="vertical"]) .control [data-ray-icon] > svg { transform: none; }
|
|
80
83
|
:host([orientation="vertical"]) .prev { top: var(--ray-space-2); }
|
|
81
84
|
:host([orientation="vertical"]) .next { bottom: var(--ray-space-2); }
|
|
82
85
|
:host([hide-controls]) .control { display: none; }
|
|
@@ -128,7 +131,7 @@ const styles = css`
|
|
|
128
131
|
.play svg { width: 0.875rem; height: 0.875rem; }
|
|
129
132
|
`;
|
|
130
133
|
|
|
131
|
-
const PAUSE_ICON = `<span data-ray-icon="pause" aria-hidden="true"></span>`;
|
|
134
|
+
const PAUSE_ICON = `<span data-ray-icon="ui.pause" aria-hidden="true"></span>`;
|
|
132
135
|
|
|
133
136
|
const reduced = typeof matchMedia === "function" ? matchMedia("(prefers-reduced-motion: reduce)") : null;
|
|
134
137
|
|
|
@@ -150,8 +153,8 @@ export class RayCarousel extends RayElement {
|
|
|
150
153
|
<div class="root" part="root" role="region" aria-roledescription="carousel">
|
|
151
154
|
<div class="stage">
|
|
152
155
|
<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="
|
|
156
|
+
<button class="control prev" part="previous" type="button" aria-label="Previous slide"><span data-ray-icon="ui.previous" aria-hidden="true"></span></button>
|
|
157
|
+
<button class="control next" part="next" type="button" aria-label="Next slide"><span data-ray-icon="ui.next" aria-hidden="true"></span></button>
|
|
155
158
|
</div>
|
|
156
159
|
<div class="footer" part="footer">
|
|
157
160
|
<button class="play" part="play" type="button" hidden></button>
|
|
@@ -272,6 +275,14 @@ export class RayCarousel extends RayElement {
|
|
|
272
275
|
return this.orientation === "vertical";
|
|
273
276
|
}
|
|
274
277
|
|
|
278
|
+
// The scroll offset from the start. Right to left, scrollLeft runs from 0
|
|
279
|
+
// (the start, on the right) down to negative values.
|
|
280
|
+
_scroll() {
|
|
281
|
+
const vp = this.viewport;
|
|
282
|
+
if (this._vertical()) return vp.scrollTop;
|
|
283
|
+
return isRtl(this) ? -vp.scrollLeft : vp.scrollLeft;
|
|
284
|
+
}
|
|
285
|
+
|
|
275
286
|
_current() {
|
|
276
287
|
return Math.min(Math.max(0, this.index ?? 0), this.count - 1);
|
|
277
288
|
}
|
|
@@ -294,11 +305,12 @@ export class RayCarousel extends RayElement {
|
|
|
294
305
|
const vr = vp.getBoundingClientRect();
|
|
295
306
|
const size = v ? vp.clientHeight : vp.clientWidth;
|
|
296
307
|
const max = Math.max(0, v ? vp.scrollHeight - vp.clientHeight : vp.scrollWidth - vp.clientWidth);
|
|
297
|
-
const scroll =
|
|
308
|
+
const scroll = this._scroll();
|
|
309
|
+
const rtl = !v && isRtl(this);
|
|
298
310
|
const snaps = [];
|
|
299
311
|
for (const s of slides) {
|
|
300
312
|
const r = s.getBoundingClientRect();
|
|
301
|
-
const start = (v ? r.top - vr.top : r.left - vr.left) + scroll;
|
|
313
|
+
const start = (v ? r.top - vr.top : rtl ? vr.right - r.right : r.left - vr.left) + scroll;
|
|
302
314
|
const length = v ? r.height : r.width;
|
|
303
315
|
const pos = this.align === "center" ? start - (size - length) / 2 : start;
|
|
304
316
|
const clamped = Math.round(Math.min(max, Math.max(0, pos)));
|
|
@@ -333,8 +345,7 @@ export class RayCarousel extends RayElement {
|
|
|
333
345
|
|
|
334
346
|
// The position closest to the current scroll offset.
|
|
335
347
|
_track() {
|
|
336
|
-
const
|
|
337
|
-
const scroll = this._vertical() ? vp.scrollTop : vp.scrollLeft;
|
|
348
|
+
const scroll = this._scroll();
|
|
338
349
|
let best = 0;
|
|
339
350
|
for (let i = 1; i < this.#snaps.length; i++) {
|
|
340
351
|
if (Math.abs(this.#snaps[i] - scroll) < Math.abs(this.#snaps[best] - scroll)) best = i;
|
|
@@ -358,7 +369,7 @@ export class RayCarousel extends RayElement {
|
|
|
358
369
|
const smooth = user && !instant && !reduced?.matches && motionAllowed(this);
|
|
359
370
|
const opts = { behavior: smooth ? "smooth" : "instant" };
|
|
360
371
|
if (this._vertical()) this.viewport.scrollTo({ top: pos, ...opts });
|
|
361
|
-
else this.viewport.scrollTo({ left: pos, ...opts });
|
|
372
|
+
else this.viewport.scrollTo({ left: isRtl(this) ? -pos : pos, ...opts });
|
|
362
373
|
this._setIndex(i);
|
|
363
374
|
this._paint();
|
|
364
375
|
this._schedule();
|
|
@@ -403,7 +414,7 @@ export class RayCarousel extends RayElement {
|
|
|
403
414
|
const paused = this.#userPaused;
|
|
404
415
|
if (auto && play.dataset.state !== (paused ? "paused" : "playing")) {
|
|
405
416
|
play.dataset.state = paused ? "paused" : "playing";
|
|
406
|
-
play.innerHTML = paused ? `<span data-ray-icon="play" aria-hidden="true"></span>` : PAUSE_ICON;
|
|
417
|
+
play.innerHTML = paused ? `<span data-ray-icon="ui.play" aria-hidden="true"></span>` : PAUSE_ICON;
|
|
407
418
|
this.paintIcons(play);
|
|
408
419
|
play.setAttribute("aria-label", paused ? "Start autoplay" : "Pause autoplay");
|
|
409
420
|
}
|
|
@@ -416,10 +427,11 @@ export class RayCarousel extends RayElement {
|
|
|
416
427
|
const v = this._vertical();
|
|
417
428
|
const back = v ? "ArrowUp" : "ArrowLeft";
|
|
418
429
|
const fwd = v ? "ArrowDown" : "ArrowRight";
|
|
419
|
-
|
|
430
|
+
const key = v ? e.key : inlineKey(e.key, this);
|
|
431
|
+
if (key === fwd) {
|
|
420
432
|
e.preventDefault();
|
|
421
433
|
this.next();
|
|
422
|
-
} else if (
|
|
434
|
+
} else if (key === back) {
|
|
423
435
|
e.preventDefault();
|
|
424
436
|
this.prev();
|
|
425
437
|
} else if (e.key === "Home") {
|
|
@@ -19,7 +19,10 @@ export const checkStyles = css`
|
|
|
19
19
|
align-items: flex-start;
|
|
20
20
|
gap: calc(var(--_s) * 0.6);
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
/* The label before the box: start, or left/right as sides of the screen. */
|
|
23
|
+
:host([label-position="start"]) .root,
|
|
24
|
+
:host([label-position="left"]) .root:dir(ltr),
|
|
25
|
+
:host([label-position="right"]) .root:dir(rtl) { flex-direction: row-reverse; justify-content: flex-end; }
|
|
23
26
|
.inner {
|
|
24
27
|
position: relative;
|
|
25
28
|
flex: none;
|
|
@@ -100,10 +103,10 @@ export class RayCheckbox extends FieldControl {
|
|
|
100
103
|
value: { type: String, reflect: true, default: "on" },
|
|
101
104
|
color: { type: String, reflect: true },
|
|
102
105
|
variant: { type: String, reflect: true, default: "filled" },
|
|
103
|
-
labelPosition: { type: String, reflect: true, default: "
|
|
106
|
+
labelPosition: { type: String, reflect: true, default: "end" },
|
|
104
107
|
};
|
|
105
108
|
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="
|
|
109
|
+
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
110
|
|
|
108
111
|
wire() {
|
|
109
112
|
// The `checked` attribute is the default state, like on <input>.
|
|
@@ -15,7 +15,7 @@ const styles = css`
|
|
|
15
15
|
.items { display: flex; flex-direction: column; gap: var(--ray-space-3); margin-top: var(--ray-space-2); }
|
|
16
16
|
:host([orientation="horizontal"]) .items { flex-direction: row; flex-wrap: wrap; gap: var(--ray-space-3) var(--ray-space-5); }
|
|
17
17
|
.label { margin-bottom: 0; }
|
|
18
|
-
.count { margin-
|
|
18
|
+
.count { margin-inline-start: var(--ray-space-1); font-weight: 400; color: var(--ray-muted-foreground); font-variant-numeric: tabular-nums; }
|
|
19
19
|
`;
|
|
20
20
|
|
|
21
21
|
/** "a, b" (an attribute) or an array, as an array of values. */
|
package/components/code/code.js
CHANGED
|
@@ -293,7 +293,8 @@ const styles = css`
|
|
|
293
293
|
align-items: center;
|
|
294
294
|
gap: var(--ray-space-2);
|
|
295
295
|
min-height: 2.5rem;
|
|
296
|
-
padding: 0
|
|
296
|
+
padding-block: 0;
|
|
297
|
+
padding-inline: var(--ray-space-4) var(--ray-space-2);
|
|
297
298
|
font-size: var(--ray-font-size-xs);
|
|
298
299
|
color: var(--ray-muted-foreground);
|
|
299
300
|
border-bottom: 1px solid var(--ray-border);
|
|
@@ -325,12 +326,13 @@ const styles = css`
|
|
|
325
326
|
.copy-label { display: none; }
|
|
326
327
|
.copy[data-copied] .copy-label { display: inline; }
|
|
327
328
|
/* Without a header, the button floats in the top-end corner. */
|
|
328
|
-
.copy.floating { position: absolute; z-index: 1; top: var(--ray-space-2);
|
|
329
|
+
.copy.floating { position: absolute; z-index: 1; top: var(--ray-space-2); inset-inline-end: var(--ray-space-2); background: var(--ray-code-background); opacity: 0; transition: opacity var(--ray-duration-fast); }
|
|
329
330
|
.frame:hover .copy.floating, .copy.floating:focus-visible, .copy.floating[data-copied] { opacity: 1; }
|
|
330
331
|
@media (hover: none) { .copy.floating { opacity: 1; } }
|
|
331
332
|
.copy[hidden] { display: none; }
|
|
332
333
|
|
|
333
|
-
|
|
334
|
+
/* Code reads left to right, in a right-to-left page too. */
|
|
335
|
+
.scroller { direction: ltr; overflow: auto; max-height: var(--_max, none); outline: none; }
|
|
334
336
|
.scroller:focus-visible { box-shadow: inset 0 0 0 2px var(--ray-ring); }
|
|
335
337
|
:host([expanded]) .scroller { max-height: none; }
|
|
336
338
|
pre { margin: 0; padding: var(--ray-space-3) 0; font-family: var(--ray-font-mono); font-size: var(--_fs); line-height: var(--_lh); tab-size: 2; }
|
|
@@ -339,19 +341,19 @@ const styles = css`
|
|
|
339
341
|
.line { display: block; padding: 0 var(--ray-space-4); white-space: pre; border-inline-start: 2px solid transparent; }
|
|
340
342
|
:host([wrap]) .line { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
341
343
|
.line:empty::after { content: " "; }
|
|
342
|
-
:host([line-numbers]) .line { counter-increment: line; padding-
|
|
344
|
+
:host([line-numbers]) .line { counter-increment: line; padding-inline-start: 0; }
|
|
343
345
|
:host([line-numbers]) .line::before {
|
|
344
346
|
content: counter(line);
|
|
345
347
|
display: inline-block;
|
|
346
348
|
width: calc(var(--_digits, 2) * 1ch + var(--ray-space-6));
|
|
347
349
|
padding-inline-end: var(--ray-space-4);
|
|
348
350
|
box-sizing: border-box;
|
|
349
|
-
text-align:
|
|
351
|
+
text-align: end;
|
|
350
352
|
color: var(--ray-code-comment);
|
|
351
353
|
opacity: 0.7;
|
|
352
354
|
user-select: none;
|
|
353
355
|
}
|
|
354
|
-
:host([line-numbers][wrap]) .line { padding-
|
|
356
|
+
:host([line-numbers][wrap]) .line { padding-inline-start: calc(var(--_digits, 2) * 1ch + var(--ray-space-6)); text-indent: calc((var(--_digits, 2) * 1ch + var(--ray-space-6)) * -1); }
|
|
355
357
|
.line[data-highlight] { background: color-mix(in srgb, var(--ray-primary) 12%, transparent); border-inline-start-color: var(--ray-primary); }
|
|
356
358
|
.line[data-diff="add"] { color: var(--ray-code-added); background: color-mix(in srgb, var(--ray-code-added) 12%, transparent); }
|
|
357
359
|
.line[data-diff="remove"] { color: var(--ray-code-removed); background: color-mix(in srgb, var(--ray-code-removed) 12%, transparent); }
|
|
@@ -427,7 +429,7 @@ export class RayCode extends RayElement {
|
|
|
427
429
|
b.type = "button";
|
|
428
430
|
b.className = "copy focus-ring";
|
|
429
431
|
b.setAttribute("part", "copy");
|
|
430
|
-
b.innerHTML = `<span class="idle" data-ray-icon="copy" aria-hidden="true"></span><span class="done" data-ray-icon="
|
|
432
|
+
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
433
|
b.addEventListener("click", () => this.copy());
|
|
432
434
|
this.$(".more button").addEventListener("click", () => { this.expanded = !this.expanded; });
|
|
433
435
|
this.$("slot").addEventListener("slotchange", () => this.requestUpdate());
|
|
@@ -172,7 +172,7 @@ const pickerStyles = css`
|
|
|
172
172
|
height: calc(var(--_w) * 0.62);
|
|
173
173
|
background:
|
|
174
174
|
linear-gradient(to top, #000, transparent),
|
|
175
|
-
linear-gradient(to right, #fff, hsl(var(--_hue) 100% 50%));
|
|
175
|
+
linear-gradient(to right, #fff, hsl(var(--_hue) 100% 50%)); /* physical */
|
|
176
176
|
border-radius: var(--ray-radius-sm);
|
|
177
177
|
cursor: crosshair;
|
|
178
178
|
touch-action: none;
|
|
@@ -180,7 +180,7 @@ const pickerStyles = css`
|
|
|
180
180
|
:host([full-width]) .area { height: auto; aspect-ratio: 5 / 3; }
|
|
181
181
|
.area-thumb {
|
|
182
182
|
position: absolute;
|
|
183
|
-
left: var(--_s);
|
|
183
|
+
left: var(--_s); /* physical */
|
|
184
184
|
top: calc(100% - var(--_v));
|
|
185
185
|
width: var(--_thumb);
|
|
186
186
|
height: var(--_thumb);
|
|
@@ -209,13 +209,13 @@ const pickerStyles = css`
|
|
|
209
209
|
inset: 0 calc(var(--_thumb) / -2);
|
|
210
210
|
border-radius: inherit;
|
|
211
211
|
}
|
|
212
|
-
.hue::before { background: linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00); }
|
|
213
|
-
.alpha::before { background: linear-gradient(to right, transparent, var(--_solid)), ${CHECKER}; }
|
|
212
|
+
.hue::before { background: linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00); } /* physical */
|
|
213
|
+
.alpha::before { background: linear-gradient(to right, transparent, var(--_solid)), ${CHECKER}; } /* physical */
|
|
214
214
|
:host(:not([data-alpha])) .alpha { display: none; }
|
|
215
215
|
.slider-thumb {
|
|
216
216
|
position: absolute;
|
|
217
217
|
top: 50%;
|
|
218
|
-
left: var(--_pos);
|
|
218
|
+
left: var(--_pos); /* physical */
|
|
219
219
|
width: var(--_thumb);
|
|
220
220
|
height: var(--_thumb);
|
|
221
221
|
box-sizing: border-box;
|
|
@@ -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
|
|
|
@@ -30,7 +30,8 @@ const styles = css`
|
|
|
30
30
|
max-width: 100%;
|
|
31
31
|
height: calc(var(--_h) - 0.75rem);
|
|
32
32
|
min-height: 1.375rem;
|
|
33
|
-
padding: 0
|
|
33
|
+
padding-block: 0;
|
|
34
|
+
padding-inline: 0.5rem 0.125rem;
|
|
34
35
|
font-size: calc(var(--_fs) - 0.0625rem);
|
|
35
36
|
color: var(--ray-foreground);
|
|
36
37
|
background: var(--ray-muted);
|
|
@@ -120,7 +121,7 @@ export class RayCombobox extends FieldControl {
|
|
|
120
121
|
static styles = [sizeStyles, radiusStyles, fieldStyles, boxStyles, styles];
|
|
121
122
|
static template = fieldTemplate(
|
|
122
123
|
`<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="
|
|
124
|
+
`<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
125
|
{ sections: false },
|
|
125
126
|
) + `<div class="listbox" id="listbox" part="dropdown" role="listbox" popover="manual" tabindex="-1"></div><div class="options"><slot></slot></div>`;
|
|
126
127
|
|
|
@@ -299,7 +300,7 @@ export class RayCombobox extends FieldControl {
|
|
|
299
300
|
b.tabIndex = -1;
|
|
300
301
|
b.dataset.value = v;
|
|
301
302
|
b.setAttribute("aria-label", `Remove ${this._labelOf(v)}`);
|
|
302
|
-
b.innerHTML = `<span data-ray-icon="
|
|
303
|
+
b.innerHTML = `<span data-ray-icon="ui.remove"></span>`;
|
|
303
304
|
chip.append(t, b);
|
|
304
305
|
box.append(chip);
|
|
305
306
|
}
|
|
@@ -364,7 +365,7 @@ export class RayCombobox extends FieldControl {
|
|
|
364
365
|
}
|
|
365
366
|
const check = document.createElement("span");
|
|
366
367
|
check.className = "mark";
|
|
367
|
-
check.setAttribute("data-ray-icon", "
|
|
368
|
+
check.setAttribute("data-ray-icon", "ui.checked");
|
|
368
369
|
el.append(text, check);
|
|
369
370
|
(groupEl ?? lb).append(el);
|
|
370
371
|
this.#items.push({ el, value: o.value, disabled, create: false });
|
|
@@ -110,7 +110,7 @@ const styles = css`
|
|
|
110
110
|
.hint b {
|
|
111
111
|
display: inline-block;
|
|
112
112
|
min-width: 1.25rem;
|
|
113
|
-
margin-
|
|
113
|
+
margin-inline-end: var(--ray-space-1);
|
|
114
114
|
padding: 0 0.3rem;
|
|
115
115
|
font-family: var(--ray-font-mono);
|
|
116
116
|
font-size: 0.6875rem;
|
|
@@ -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
|