@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
|
@@ -18,7 +18,7 @@ const styles = css`
|
|
|
18
18
|
:host([variant="dotted"]) { --_style: dotted; }
|
|
19
19
|
:host([orientation="vertical"]) { display: inline-block; align-self: stretch; height: auto; min-height: 1em; vertical-align: middle; }
|
|
20
20
|
.line { border-top: var(--_w) var(--_style) var(--_color); }
|
|
21
|
-
:host([orientation="vertical"]) .line { height: 100%; border-top: 0; border-
|
|
21
|
+
:host([orientation="vertical"]) .line { height: 100%; border-top: 0; border-inline-start: var(--_w) var(--_style) var(--_color); }
|
|
22
22
|
.labeled {
|
|
23
23
|
display: flex;
|
|
24
24
|
align-items: center;
|
|
@@ -27,8 +27,13 @@ const styles = css`
|
|
|
27
27
|
color: var(--ray-muted-foreground);
|
|
28
28
|
}
|
|
29
29
|
.labeled::before, .labeled::after { content: ""; flex: 1; border-top: var(--_w) var(--_style) var(--_color); }
|
|
30
|
-
|
|
31
|
-
:host([label-position="
|
|
30
|
+
/* start/end follow the writing direction; left/right are sides of the screen. */
|
|
31
|
+
:host([label-position="start"]) .labeled::before,
|
|
32
|
+
:host([label-position="left"]) .labeled:dir(ltr)::before,
|
|
33
|
+
:host([label-position="right"]) .labeled:dir(rtl)::before,
|
|
34
|
+
:host([label-position="end"]) .labeled::after,
|
|
35
|
+
:host([label-position="right"]) .labeled:dir(ltr)::after,
|
|
36
|
+
:host([label-position="left"]) .labeled:dir(rtl)::after { flex: 0 0 var(--ray-space-3); }
|
|
32
37
|
[hidden] { display: none !important; }
|
|
33
38
|
`;
|
|
34
39
|
|
|
@@ -38,7 +38,11 @@ const styles = css`
|
|
|
38
38
|
background: var(--ray-sidebar, color-mix(in srgb, var(--ray-muted) 70%, var(--ray-background)));
|
|
39
39
|
border-inline-end: 1px solid var(--ray-border);
|
|
40
40
|
}
|
|
41
|
-
:
|
|
41
|
+
/* side: start/end follow the writing direction, left/right are sides of
|
|
42
|
+
the screen; the border faces the content. */
|
|
43
|
+
:host([side="end"]) .panel { border-inline-end: 0; border-inline-start: 1px solid var(--ray-border); }
|
|
44
|
+
:host([side="left"]) .panel { border: 0; border-right: 1px solid var(--ray-border); } /* physical */
|
|
45
|
+
:host([side="right"]) .panel { border: 0; border-left: 1px solid var(--ray-border); } /* physical */
|
|
42
46
|
:host([variant="floating"]:not([data-mobile])) { padding: var(--ray-space-2); box-sizing: border-box; }
|
|
43
47
|
:host([variant="floating"]) .panel { border: 1px solid var(--ray-border); border-radius: var(--ray-radius-lg); box-shadow: var(--ray-shadow-sm); }
|
|
44
48
|
.header, .footer { flex: none; padding: var(--ray-space-2); }
|
|
@@ -49,7 +53,8 @@ const styles = css`
|
|
|
49
53
|
dialog {
|
|
50
54
|
--_from: -100%;
|
|
51
55
|
position: fixed;
|
|
52
|
-
inset: 0
|
|
56
|
+
inset-block: 0;
|
|
57
|
+
inset-inline: 0 auto;
|
|
53
58
|
box-sizing: border-box;
|
|
54
59
|
width: min(var(--_w), calc(100vw - 3rem));
|
|
55
60
|
max-width: none;
|
|
@@ -61,11 +66,14 @@ const styles = css`
|
|
|
61
66
|
background: transparent;
|
|
62
67
|
overflow: visible;
|
|
63
68
|
}
|
|
64
|
-
:
|
|
69
|
+
dialog:dir(rtl) { --_from: 100%; }
|
|
70
|
+
:host([side="end"]) dialog { inset-inline: auto 0; --_from: 100%; }
|
|
71
|
+
:host([side="end"]) dialog:dir(rtl) { --_from: -100%; }
|
|
72
|
+
:host([side="left"]) dialog { inset: 0 auto 0 0; --_from: -100%; } /* physical */
|
|
73
|
+
:host([side="right"]) dialog { inset: 0 0 0 auto; --_from: 100%; } /* physical */
|
|
65
74
|
dialog[open] { display: block; animation: ray-sidebar-in var(--ray-duration-slow) var(--ray-easing-out); }
|
|
66
75
|
dialog .panel { box-shadow: var(--ray-shadow-xl); background: var(--ray-card); }
|
|
67
76
|
@keyframes ray-sidebar-in { from { transform: translateX(var(--_from)); } }
|
|
68
|
-
:host([side="right"]) dialog { --_from: 100%; }
|
|
69
77
|
dialog::backdrop { animation: ray-fade-in var(--ray-duration-base) var(--ray-easing-out); }
|
|
70
78
|
@keyframes ray-fade-in { from { opacity: 0; } }
|
|
71
79
|
`;
|
|
@@ -74,7 +82,7 @@ export class RaySidebar extends RayElement {
|
|
|
74
82
|
static properties = {
|
|
75
83
|
collapsed: { type: Boolean, reflect: true },
|
|
76
84
|
open: { type: Boolean, reflect: true },
|
|
77
|
-
side: { type: String, reflect: true, default: "
|
|
85
|
+
side: { type: String, reflect: true, default: "start" },
|
|
78
86
|
variant: { type: String, reflect: true, default: "sidebar" },
|
|
79
87
|
width: { type: String },
|
|
80
88
|
collapsedWidth: { type: String },
|
|
@@ -193,7 +201,7 @@ export class RaySidebar extends RayElement {
|
|
|
193
201
|
}
|
|
194
202
|
|
|
195
203
|
async _leave() {
|
|
196
|
-
const to = this.
|
|
204
|
+
const to = `translateX(${getComputedStyle(this.dialog).getPropertyValue("--_from").trim() || "-100%"})`;
|
|
197
205
|
const exit = playAll([
|
|
198
206
|
play(this.dialog, [{ transform: "none" }, { transform: to }]),
|
|
199
207
|
play(this.dialog, [{ opacity: 1 }, { opacity: 0 }], { pseudoElement: "::backdrop" }),
|
|
@@ -33,9 +33,11 @@ const styles = css`
|
|
|
33
33
|
input::-webkit-slider-runnable-track {
|
|
34
34
|
height: var(--_track);
|
|
35
35
|
border-radius: var(--ray-radius-full);
|
|
36
|
-
background: linear-gradient(to right, var(--_filled) var(--_pct), var(--ray-muted) var(--_pct));
|
|
36
|
+
background: linear-gradient(to right, var(--_filled) var(--_pct), var(--ray-muted) var(--_pct)); /* physical */
|
|
37
37
|
box-shadow: inset 0 0 0 1px var(--ray-border);
|
|
38
38
|
}
|
|
39
|
+
/* Right to left the native range runs from the right: so does its fill. */
|
|
40
|
+
input:dir(rtl)::-webkit-slider-runnable-track { background: linear-gradient(to left, var(--_filled) var(--_pct), var(--ray-muted) var(--_pct)); } /* physical */
|
|
39
41
|
input::-moz-range-track { height: var(--_track); border-radius: var(--ray-radius-full); background: var(--ray-muted); }
|
|
40
42
|
input::-moz-range-progress { height: var(--_track); border-radius: var(--ray-radius-full); background: var(--_filled); }
|
|
41
43
|
input::-webkit-slider-thumb {
|
|
@@ -62,6 +64,7 @@ const styles = css`
|
|
|
62
64
|
.marks { position: relative; height: 1.25rem; margin: var(--ray-space-1) calc(var(--_thumb) / 2) 0; font-size: var(--ray-font-size-xs); color: var(--ray-muted-foreground); }
|
|
63
65
|
.marks:empty { display: none; }
|
|
64
66
|
.mark { position: absolute; transform: translateX(-50%); white-space: nowrap; }
|
|
67
|
+
.mark:dir(rtl) { transform: translateX(50%); }
|
|
65
68
|
.mark::before { content: ""; display: block; width: 1px; height: 0.25rem; margin: 0 auto 0.125rem; background: var(--ray-border); }
|
|
66
69
|
`;
|
|
67
70
|
|
|
@@ -137,7 +140,7 @@ export class RaySlider extends FieldControl {
|
|
|
137
140
|
const v = typeof m === "number" ? m : m.value;
|
|
138
141
|
const span = document.createElement("span");
|
|
139
142
|
span.className = "mark";
|
|
140
|
-
span.style.
|
|
143
|
+
span.style.insetInlineStart = `${((v - min) / (max - min || 1)) * 100}%`;
|
|
141
144
|
span.textContent = typeof m === "number" ? String(m) : (m.label ?? String(m.value));
|
|
142
145
|
box.append(span);
|
|
143
146
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// pane. With `autosave="key"` the size survives reloads (localStorage).
|
|
8
8
|
|
|
9
9
|
import { RayElement, css, define } from "../../core/core.js";
|
|
10
|
+
import { isRtl, inlineKey } from "../../core/direction.js";
|
|
10
11
|
|
|
11
12
|
const styles = css`
|
|
12
13
|
:host {
|
|
@@ -34,13 +35,13 @@ const styles = css`
|
|
|
34
35
|
position: absolute;
|
|
35
36
|
top: 0;
|
|
36
37
|
bottom: 0;
|
|
37
|
-
left: 50%;
|
|
38
|
+
left: 50%; /* physical */
|
|
38
39
|
width: 1px;
|
|
39
40
|
background: var(--ray-border);
|
|
40
41
|
transform: translateX(-50%);
|
|
41
42
|
transition: background-color var(--ray-duration-fast) var(--ray-easing-standard), width var(--ray-duration-fast) var(--ray-easing-standard);
|
|
42
43
|
}
|
|
43
|
-
:host([orientation="vertical"]) .divider::before { top: 50%; bottom: auto;
|
|
44
|
+
:host([orientation="vertical"]) .divider::before { top: 50%; bottom: auto; inset-inline: 0; width: auto; height: 1px; transform: translateY(-50%); }
|
|
44
45
|
:host(:not([disabled])) .divider:hover::before,
|
|
45
46
|
.divider:focus-visible::before,
|
|
46
47
|
:host([data-dragging]) .divider::before { width: 3px; background: var(--ray-primary); }
|
|
@@ -182,13 +183,16 @@ export class RaySplit extends RayElement {
|
|
|
182
183
|
this.setAttribute("data-dragging", "");
|
|
183
184
|
const rect = this.getBoundingClientRect();
|
|
184
185
|
const vertical = this._vertical();
|
|
185
|
-
|
|
186
|
+
// The start pane's size: right to left it is on the right.
|
|
187
|
+
const rtl = !vertical && isRtl(this);
|
|
188
|
+
const at = (ev) => (vertical ? ev.clientY - rect.top : rtl ? rect.right - ev.clientX : ev.clientX - rect.left);
|
|
189
|
+
const move = (ev) => this._resizeTo(at(ev), false);
|
|
186
190
|
const up = (ev) => {
|
|
187
191
|
this.divider.removeEventListener("pointermove", move);
|
|
188
192
|
this.divider.removeEventListener("pointerup", up);
|
|
189
193
|
this.divider.removeEventListener("pointercancel", up);
|
|
190
194
|
this.removeAttribute("data-dragging");
|
|
191
|
-
this._resizeTo(
|
|
195
|
+
this._resizeTo(at(ev), true);
|
|
192
196
|
};
|
|
193
197
|
this.divider.addEventListener("pointermove", move);
|
|
194
198
|
this.divider.addEventListener("pointerup", up);
|
|
@@ -202,9 +206,11 @@ export class RaySplit extends RayElement {
|
|
|
202
206
|
const current = this.collapsed ? 0 : this.size;
|
|
203
207
|
const less = vertical ? "ArrowUp" : "ArrowLeft";
|
|
204
208
|
const more = vertical ? "ArrowDown" : "ArrowRight";
|
|
209
|
+
// Right to left, ArrowLeft moves the divider away from the start pane.
|
|
210
|
+
const key = vertical ? e.key : inlineKey(e.key, this);
|
|
205
211
|
let next = null;
|
|
206
|
-
if (
|
|
207
|
-
else if (
|
|
212
|
+
if (key === less) next = current - step;
|
|
213
|
+
else if (key === more) next = current + step;
|
|
208
214
|
else if (e.key === "Home") next = this.min ?? 0;
|
|
209
215
|
else if (e.key === "End") next = this.max ?? this._total();
|
|
210
216
|
else if (e.key === "Enter") {
|
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
|
|
|
@@ -30,7 +30,7 @@ const stepperStyles = css`
|
|
|
30
30
|
padding: 0;
|
|
31
31
|
font: inherit;
|
|
32
32
|
color: inherit;
|
|
33
|
-
text-align:
|
|
33
|
+
text-align: start;
|
|
34
34
|
background: none;
|
|
35
35
|
border: 0;
|
|
36
36
|
border-radius: var(--ray-radius-md);
|
|
@@ -58,7 +58,7 @@ const stepperStyles = css`
|
|
|
58
58
|
.step[data-state="active"] .indicator { color: var(--_light-color); background: var(--ray-background); border-color: var(--_filled); }
|
|
59
59
|
.step[data-state="done"] .indicator { color: var(--_contrast); background: var(--_filled); border-color: var(--_filled); }
|
|
60
60
|
.step[data-error] .indicator { color: #fff; background: var(--ray-red-filled); border-color: var(--ray-red-filled); }
|
|
61
|
-
.spinner { width: 50%; height: 50%; border: 2px solid currentColor; border-
|
|
61
|
+
.spinner { width: 50%; height: 50%; border: 2px solid currentColor; border-inline-end-color: transparent; border-radius: 50%; animation: ray-step-spin 0.8s linear infinite; }
|
|
62
62
|
@keyframes ray-step-spin { to { rotate: 1turn; } }
|
|
63
63
|
.text { display: grid; min-width: 0; }
|
|
64
64
|
.label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--ray-font-size-sm); font-weight: var(--ray-font-weight-medium); }
|
|
@@ -69,7 +69,7 @@ const stepperStyles = css`
|
|
|
69
69
|
.connector { flex: 1; min-width: var(--ray-space-4); height: 2px; border-radius: 1px; background: var(--ray-border); transition: background-color var(--ray-duration-base); }
|
|
70
70
|
.step[data-state="done"] .connector { background: var(--_filled); }
|
|
71
71
|
.step:last-child .connector { display: none; }
|
|
72
|
-
:host([orientation="vertical"]) .connector { flex: none; width: 2px; min-width: 0; height: auto; min-height: var(--ray-space-6); margin: var(--ray-space-1)
|
|
72
|
+
:host([orientation="vertical"]) .connector { flex: none; width: 2px; min-width: 0; height: auto; min-height: var(--ray-space-6); margin-block: var(--ray-space-1); margin-inline-start: calc(var(--_icon) / 2 - 1px); }
|
|
73
73
|
/* Narrow (labels-below or a narrow stepper): the labels under the numbers. */
|
|
74
74
|
:host([icon-position="top"]) .step { align-items: flex-start; }
|
|
75
75
|
:host([icon-position="top"]) .head { flex-direction: column; align-items: center; text-align: center; gap: var(--ray-space-1); }
|
|
@@ -91,7 +91,7 @@ export class RayStepper extends RayElement {
|
|
|
91
91
|
static properties = {
|
|
92
92
|
active: { type: Number, reflect: true, default: 0 },
|
|
93
93
|
orientation: { type: String, reflect: true, default: "horizontal" },
|
|
94
|
-
iconPosition: { type: String, reflect: true, attribute: "icon-position", default: "
|
|
94
|
+
iconPosition: { type: String, reflect: true, attribute: "icon-position", default: "start" },
|
|
95
95
|
allowStepSelect: { type: Boolean, reflect: true, attribute: "allow-step-select" },
|
|
96
96
|
linear: { type: Boolean, default: true },
|
|
97
97
|
size: { type: String, reflect: true, default: "md" },
|
|
@@ -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");
|
|
@@ -18,7 +18,7 @@ const styles = css`
|
|
|
18
18
|
.thumb {
|
|
19
19
|
position: absolute;
|
|
20
20
|
top: 50%;
|
|
21
|
-
|
|
21
|
+
inset-inline-start: 2px;
|
|
22
22
|
width: calc(var(--_s) - 6px);
|
|
23
23
|
height: calc(var(--_s) - 6px);
|
|
24
24
|
background: var(--ray-white);
|
|
@@ -26,12 +26,12 @@ const styles = css`
|
|
|
26
26
|
border-radius: var(--_r);
|
|
27
27
|
box-shadow: var(--ray-shadow-xs);
|
|
28
28
|
transform: translateY(-50%);
|
|
29
|
-
transition:
|
|
29
|
+
transition: inset-inline-start var(--ray-duration-base) var(--ray-easing-standard);
|
|
30
30
|
}
|
|
31
31
|
.control:checked + .indicator { background: var(--_filled); border-color: var(--_filled); }
|
|
32
32
|
/* The thumb takes the text color of the filled shade: white on dark fills,
|
|
33
33
|
black on light ones (yellow, or "dark" in the dark scheme). */
|
|
34
|
-
.control:checked + .indicator .thumb {
|
|
34
|
+
.control:checked + .indicator .thumb { inset-inline-start: calc(100% - var(--_s) + 4px); border-color: transparent; background: var(--_contrast); }
|
|
35
35
|
/* Disabled: a muted track (gray when on, like a disabled checkbox) and a
|
|
36
36
|
plain thumb that stays visible on it, on its side. */
|
|
37
37
|
.control:disabled + .indicator .thumb { background: var(--ray-background); border-color: var(--ray-border); box-shadow: none; }
|
|
@@ -27,14 +27,14 @@ const styles = css`
|
|
|
27
27
|
.scroll { max-height: var(--ray-table-max-height, none); overflow: auto; }
|
|
28
28
|
:host([with-border]) .scroll { border: 1px solid var(--ray-border); border-radius: var(--ray-radius-default); }
|
|
29
29
|
table { width: 100%; border-collapse: separate; border-spacing: 0; font-variant-numeric: tabular-nums; }
|
|
30
|
-
caption { padding: var(--_py) var(--_px); text-align:
|
|
31
|
-
th, td { padding: var(--_py) var(--_px); text-align:
|
|
32
|
-
:host([with-column-borders]) th + th, :host([with-column-borders]) td + td { border-
|
|
30
|
+
caption { padding: var(--_py) var(--_px); text-align: start; font-weight: var(--ray-font-weight-semibold); }
|
|
31
|
+
th, td { padding: var(--_py) var(--_px); text-align: start; vertical-align: middle; border-bottom: 1px solid var(--ray-border); }
|
|
32
|
+
:host([with-column-borders]) th + th, :host([with-column-borders]) td + td { border-inline-start: 1px solid var(--ray-border); }
|
|
33
33
|
tbody tr:last-child td { border-bottom: 0; }
|
|
34
34
|
:host(:not([with-border])) tbody tr:last-child td { border-bottom: 1px solid var(--ray-border); }
|
|
35
35
|
th { font-size: var(--ray-font-size-xs); font-weight: var(--ray-font-weight-semibold); color: var(--ray-muted-foreground); white-space: nowrap; background: var(--ray-background); }
|
|
36
36
|
:host([sticky-header]) thead th { position: sticky; top: 0; z-index: 1; box-shadow: 0 1px 0 var(--ray-border); }
|
|
37
|
-
[data-align="right"] { text-align: right; }
|
|
37
|
+
[data-align="right"] { text-align: right; } /* physical */
|
|
38
38
|
[data-align="center"] { text-align: center; }
|
|
39
39
|
:host([striped]) tbody tr:nth-child(even) td { background: color-mix(in srgb, var(--ray-muted) 60%, transparent); }
|
|
40
40
|
tbody tr:hover td { background: var(--ray-default-hover); }
|
|
@@ -61,13 +61,13 @@ const styles = css`
|
|
|
61
61
|
th[aria-sort="ascending"] .sort, th[aria-sort="descending"] .sort { color: var(--ray-foreground); }
|
|
62
62
|
th[aria-sort="ascending"] .sort svg, th[aria-sort="descending"] .sort svg { opacity: 1; }
|
|
63
63
|
th[aria-sort="descending"] .sort svg { transform: rotate(180deg); }
|
|
64
|
-
.check { width: 1%; padding-
|
|
64
|
+
.check { width: 1%; padding-inline-end: 0; }
|
|
65
65
|
.check input { width: 1rem; height: 1rem; margin: 0; accent-color: var(--ray-primary); cursor: pointer; vertical-align: middle; }
|
|
66
66
|
.empty td { padding: var(--ray-space-8) var(--_px); text-align: center; color: var(--ray-muted-foreground); }
|
|
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 = {
|
package/components/tabs/tabs.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// panel share a `value`.
|
|
6
6
|
|
|
7
7
|
import { RayElement, css, define, uid } from "../../core/core.js";
|
|
8
|
+
import { inlineKey } from "../../core/direction.js";
|
|
8
9
|
import { applyColor } from "../../core/styles.js";
|
|
9
10
|
|
|
10
11
|
const tabsStyles = css`
|
|
@@ -24,9 +25,9 @@ const tabsStyles = css`
|
|
|
24
25
|
}
|
|
25
26
|
.list::-webkit-scrollbar { display: none; }
|
|
26
27
|
.list ::slotted(ray-tab) { flex: none; }
|
|
27
|
-
:host([orientation="vertical"]) .list { flex-direction: column; overflow-x: visible; box-shadow: none; border-
|
|
28
|
+
:host([orientation="vertical"]) .list { flex-direction: column; overflow-x: visible; box-shadow: none; border-inline-end: 2px solid var(--ray-border); }
|
|
28
29
|
:host([variant="outline"]) .list { --_line: 1px; }
|
|
29
|
-
:host([variant="outline"][orientation="vertical"]) .list { border-
|
|
30
|
+
:host([variant="outline"][orientation="vertical"]) .list { border-inline-end-width: 1px; }
|
|
30
31
|
:host([variant="pills"]) .list { gap: var(--ray-space-1); border: 0; box-shadow: none; }
|
|
31
32
|
:host([grow]) .list ::slotted(ray-tab) { flex: 1 0 auto; }
|
|
32
33
|
.panels { flex: 1; min-width: 0; padding-top: var(--ray-space-3); }
|
|
@@ -114,9 +115,11 @@ export class RayTabs extends RayElement {
|
|
|
114
115
|
const vertical = this.orientation === "vertical";
|
|
115
116
|
const next = vertical ? "ArrowDown" : "ArrowRight";
|
|
116
117
|
const prev = vertical ? "ArrowUp" : "ArrowLeft";
|
|
118
|
+
// Right to left, ArrowLeft is the next tab.
|
|
119
|
+
const key = vertical ? e.key : inlineKey(e.key, this);
|
|
117
120
|
let target = null;
|
|
118
|
-
if (
|
|
119
|
-
else if (
|
|
121
|
+
if (key === next) target = tabs[(i + 1) % tabs.length];
|
|
122
|
+
else if (key === prev) target = tabs[(i - 1 + tabs.length) % tabs.length];
|
|
120
123
|
else if (e.key === "Home") target = tabs[0];
|
|
121
124
|
else if (e.key === "End") target = tabs[tabs.length - 1];
|
|
122
125
|
else if ((e.key === "Enter" || e.key === " ") && this.activation === "manual") {
|
|
@@ -157,14 +160,14 @@ const tabStyles = css`
|
|
|
157
160
|
:host([data-variant="default"]) { border-bottom: 2px solid transparent; }
|
|
158
161
|
:host([data-variant="default"]:hover:not([data-active])) { border-bottom-color: var(--ray-border); background: var(--ray-muted); }
|
|
159
162
|
:host([data-variant="default"][data-active]) { border-bottom-color: var(--_filled, var(--ray-primary)); }
|
|
160
|
-
:host([data-variant="default"][data-vertical]) { margin: 0 -2px
|
|
161
|
-
:host([data-variant="default"][data-vertical][data-active]) { border-
|
|
163
|
+
:host([data-variant="default"][data-vertical]) { margin: 0; margin-inline-end: -2px; justify-content: flex-start; border-bottom: 0; border-inline-end: 2px solid transparent; }
|
|
164
|
+
:host([data-variant="default"][data-vertical][data-active]) { border-inline-end-color: var(--_filled, var(--ray-primary)); }
|
|
162
165
|
|
|
163
166
|
/* outline */
|
|
164
167
|
:host([data-variant="outline"]) { border: 1px solid transparent; border-radius: var(--ray-radius-default) var(--ray-radius-default) 0 0; }
|
|
165
168
|
:host([data-variant="outline"][data-active]) { border-color: var(--ray-border); border-bottom-color: var(--ray-background); background: var(--ray-background); }
|
|
166
|
-
:host([data-variant="outline"][data-vertical]) { margin: 0 -1px
|
|
167
|
-
:host([data-variant="outline"][data-vertical][data-active]) { border-bottom-color: var(--ray-border); border-
|
|
169
|
+
:host([data-variant="outline"][data-vertical]) { margin: 0; margin-inline-end: -1px; justify-content: flex-start; border-radius: 0; border-start-start-radius: var(--ray-radius-default); border-end-start-radius: var(--ray-radius-default); }
|
|
170
|
+
:host([data-variant="outline"][data-vertical][data-active]) { border-bottom-color: var(--ray-border); border-inline-end-color: var(--ray-background); }
|
|
168
171
|
|
|
169
172
|
/* pills */
|
|
170
173
|
:host([data-variant="pills"]) { border-radius: var(--ray-radius-default); }
|
|
@@ -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();
|
|
@@ -22,9 +22,14 @@ const itemStyles = css`
|
|
|
22
22
|
}
|
|
23
23
|
:host([hidden]) { display: none; }
|
|
24
24
|
:host([data-last]) { padding-bottom: 0; }
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
:host([data-align="
|
|
25
|
+
/* The line at the end: end, or left/right as sides of the screen (the
|
|
26
|
+
grid's columns follow the writing direction). */
|
|
27
|
+
:host([data-align="end"]), :host([data-align="right"]:dir(ltr)), :host([data-align="left"]:dir(rtl)) {
|
|
28
|
+
grid-template-columns: minmax(0, 1fr) var(--_bullet);
|
|
29
|
+
text-align: end;
|
|
30
|
+
}
|
|
31
|
+
:host([data-align="end"]) .bullet-col, :host([data-align="right"]:dir(ltr)) .bullet-col, :host([data-align="left"]:dir(rtl)) .bullet-col { grid-column: 2; grid-row: 1; }
|
|
32
|
+
:host([data-align="end"]) .body, :host([data-align="right"]:dir(ltr)) .body, :host([data-align="left"]:dir(rtl)) .body { grid-column: 1; grid-row: 1; }
|
|
28
33
|
.bullet-col { position: relative; display: flex; justify-content: center; }
|
|
29
34
|
.bullet {
|
|
30
35
|
position: relative;
|
|
@@ -47,12 +52,12 @@ const itemStyles = css`
|
|
|
47
52
|
position: absolute;
|
|
48
53
|
top: var(--_bullet);
|
|
49
54
|
bottom: calc(-1 * var(--ray-space-6));
|
|
50
|
-
left: calc(50% - var(--_line) / 2);
|
|
55
|
+
left: calc(50% - var(--_line) / 2); /* physical */
|
|
51
56
|
width: 0;
|
|
52
|
-
border-left: var(--_line) var(--_line-style, solid) var(--ray-border);
|
|
57
|
+
border-left: var(--_line) var(--_line-style, solid) var(--ray-border); /* physical */
|
|
53
58
|
}
|
|
54
59
|
:host([data-last]) .line { display: none; }
|
|
55
|
-
:host([data-line-reached]) .line { border-left-color: var(--_filled); }
|
|
60
|
+
:host([data-line-reached]) .line { border-left-color: var(--_filled); } /* physical */
|
|
56
61
|
:host([line-variant="dashed"]) .line { --_line-style: dashed; }
|
|
57
62
|
:host([line-variant="dotted"]) .line { --_line-style: dotted; }
|
|
58
63
|
.body { min-width: 0; padding-top: calc((var(--_bullet) - 1.4em) / 2); }
|
|
@@ -67,7 +72,7 @@ const itemStyles = css`
|
|
|
67
72
|
export class RayTimeline extends RayElement {
|
|
68
73
|
static properties = {
|
|
69
74
|
active: { type: Number, reflect: true, default: -1 },
|
|
70
|
-
align: { type: String, reflect: true, default: "
|
|
75
|
+
align: { type: String, reflect: true, default: "start" },
|
|
71
76
|
bulletSize: { type: String, attribute: "bullet-size" },
|
|
72
77
|
lineWidth: { type: Number, attribute: "line-width" },
|
|
73
78
|
color: { type: String, reflect: true },
|
|
@@ -106,7 +111,7 @@ export class RayTimeline extends RayElement {
|
|
|
106
111
|
item.toggleAttribute("data-reached", active >= 0 && k <= active);
|
|
107
112
|
item.toggleAttribute("data-line-reached", active >= 0 && (this.reverseActive ? k <= active && k > 0 : k < active));
|
|
108
113
|
item.toggleAttribute("data-last", i === n - 1);
|
|
109
|
-
item.setAttribute("data-align",
|
|
114
|
+
item.setAttribute("data-align", ["end", "left", "right"].includes(this.align) ? this.align : "start");
|
|
110
115
|
});
|
|
111
116
|
}
|
|
112
117
|
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// </ray-toggle-group>
|
|
14
14
|
|
|
15
15
|
import { css, define } from "../../core/core.js";
|
|
16
|
+
import { inlineKey } from "../../core/direction.js";
|
|
16
17
|
import { RayButtonGroup } from "../button-group/button-group.js";
|
|
17
18
|
import "../toggle/toggle.js";
|
|
18
19
|
|
|
@@ -113,14 +114,15 @@ export class RayToggleGroup extends RayButtonGroup {
|
|
|
113
114
|
const vertical = this.orientation === "vertical";
|
|
114
115
|
const next = vertical ? "ArrowDown" : "ArrowRight";
|
|
115
116
|
const prev = vertical ? "ArrowUp" : "ArrowLeft";
|
|
116
|
-
|
|
117
|
+
const key = vertical ? e.key : inlineKey(e.key, this);
|
|
118
|
+
if (![next, prev, "Home", "End"].includes(key)) return;
|
|
117
119
|
const items = this.toggles.filter((t) => !t.disabled);
|
|
118
120
|
if (items.length === 0) return;
|
|
119
121
|
const current = items.findIndex((t) => t.matches(":focus-within"));
|
|
120
122
|
let i = current;
|
|
121
|
-
if (
|
|
122
|
-
else if (
|
|
123
|
-
else if (
|
|
123
|
+
if (key === "Home") i = 0;
|
|
124
|
+
else if (key === "End") i = items.length - 1;
|
|
125
|
+
else if (key === next) i = (current + 1) % items.length;
|
|
124
126
|
else i = (current - 1 + items.length) % items.length;
|
|
125
127
|
e.preventDefault();
|
|
126
128
|
this.#rove(items[i]);
|
package/core/core.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// the theme API. No dependencies; loads as a plain ES module (also from
|
|
3
3
|
// ray://app in a std/ui window).
|
|
4
4
|
|
|
5
|
-
import { getIcon, onIconsChange } from "./icons.js";
|
|
5
|
+
import { getIcon, mirrorsInRtl, 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
|
|
|
@@ -59,6 +59,8 @@ export const hostStyles = css`
|
|
|
59
59
|
height: var(--ray-icon-size, 1em);
|
|
60
60
|
stroke-width: var(--ray-icon-stroke, 2);
|
|
61
61
|
}
|
|
62
|
+
/* Right to left, the roles that point along the text. */
|
|
63
|
+
:where([data-ray-icon-mirror]):dir(rtl) > svg { transform: scaleX(-1); }
|
|
62
64
|
.focus-ring:focus-visible {
|
|
63
65
|
outline: var(--ray-focus-ring-width) solid var(--ray-ring);
|
|
64
66
|
outline-offset: var(--ray-focus-ring-offset);
|
|
@@ -313,12 +315,16 @@ export class RayElement extends HTMLElement {
|
|
|
313
315
|
* Protected. Fills every [data-ray-icon="name"] under `root` (default: the
|
|
314
316
|
* shadow root) with that icon of the registry (setIcons). Components call
|
|
315
317
|
* 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
|
|
318
|
+
* (the value components use to hide an icon) leaves the slot empty;
|
|
319
|
+
* data-ray-icon-filled asks for the filled version (the outline without one).
|
|
320
|
+
* A role that points along the text (ui.next…) gets data-ray-icon-mirror,
|
|
321
|
+
* which mirrors it under dir="rtl".
|
|
317
322
|
*/
|
|
318
323
|
paintIcons(root = this.shadowRoot) {
|
|
319
324
|
for (const slot of root.querySelectorAll("[data-ray-icon]")) {
|
|
320
325
|
const name = slot.getAttribute("data-ray-icon");
|
|
321
|
-
slot.innerHTML = name && name !== "none" ? getIcon(name) : "";
|
|
326
|
+
slot.innerHTML = name && name !== "none" ? getIcon(name, { filled: slot.hasAttribute("data-ray-icon-filled") }) : "";
|
|
327
|
+
slot.toggleAttribute("data-ray-icon-mirror", mirrorsInRtl(name));
|
|
322
328
|
}
|
|
323
329
|
}
|
|
324
330
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// The writing direction, for what CSS does not mirror by itself: arrow keys
|
|
2
|
+
// and pointer or scroll coordinates. Read when used (not cached), so a
|
|
3
|
+
// change of dir takes effect at once.
|
|
4
|
+
|
|
5
|
+
/** Whether `el` lays out right to left (dir="rtl" on it or an ancestor). */
|
|
6
|
+
export function isRtl(el) {
|
|
7
|
+
return getComputedStyle(el).direction === "rtl";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* An arrow key as the reading direction sees it. Right to left, ArrowLeft
|
|
12
|
+
* moves forward, so it comes back as "ArrowRight", and ArrowRight as
|
|
13
|
+
* "ArrowLeft"; any other key as it is.
|
|
14
|
+
* if (inlineKey(e.key, this) === "ArrowRight") this.next();
|
|
15
|
+
*/
|
|
16
|
+
export function inlineKey(key, el) {
|
|
17
|
+
if ((key === "ArrowLeft" || key === "ArrowRight") && isRtl(el)) return key === "ArrowLeft" ? "ArrowRight" : "ArrowLeft";
|
|
18
|
+
return key;
|
|
19
|
+
}
|