@raylang/elements 0.7.0 → 0.9.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/components/accordion/accordion.js +7 -4
- package/components/alert/alert.js +2 -1
- package/components/attachment/attachment.js +4 -5
- package/components/blockquote/blockquote.js +2 -2
- package/components/bottom-nav/bottom-nav.js +3 -3
- package/components/carousel/carousel.js +22 -10
- package/components/checkbox/checkbox.js +5 -2
- package/components/checkbox-group/checkbox-group.js +1 -1
- package/components/code/code.js +16 -11
- package/components/color-picker/color-picker.js +5 -5
- package/components/combobox/combobox.js +2 -1
- package/components/command-palette/command-palette.js +17 -2
- package/components/date-picker/date-picker.js +5 -3
- package/components/dialog/dialog.js +1 -1
- package/components/doc-tabs/doc-tabs.js +255 -0
- package/components/drawer/drawer.js +27 -9
- package/components/icon/icon.js +4 -1
- package/components/icon-button/icon-button.js +4 -0
- package/components/indicator/indicator.js +2 -1
- package/components/input/input.js +13 -1
- package/components/menu/menu.js +43 -8
- package/components/mode-input/mode-input.js +221 -0
- package/components/nav-link/nav-link.js +5 -2
- package/components/notification/notification.js +15 -12
- 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 +3 -3
- package/components/segmented-control/segmented-control.js +5 -3
- package/components/select/select.js +3 -3
- package/components/separator/separator.js +8 -3
- package/components/sidebar/sidebar.js +154 -11
- package/components/slider/slider.js +5 -2
- package/components/split/split.js +25 -7
- package/components/stepper/stepper.js +4 -4
- package/components/switch/switch.js +3 -3
- package/components/table/table.js +5 -5
- package/components/tabs/tabs.js +55 -14
- package/components/textarea/textarea.js +13 -1
- package/components/timeline/timeline.js +13 -8
- package/components/toggle-group/toggle-group.js +6 -4
- package/core/complete.js +252 -0
- package/core/core.js +6 -1
- package/core/direction.js +19 -0
- package/core/icons.js +12 -0
- package/core/position.js +11 -3
- package/core/scroll.js +85 -1
- package/core/styles.js +1 -1
- package/core/textlayer.js +214 -0
- package/core.js +1 -0
- package/custom-elements.json +1487 -143
- package/index.js +2 -0
- package/llms-full.txt +224 -28
- package/llms.txt +26 -4
- package/package.json +9 -1
- package/ray-elements.d.ts +190 -31
- package/vscode.html-custom-data.json +477 -22
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// <ray-mode-input>: a text field with a menu of modes on one side, like
|
|
2
|
+
// ray-password-input's toggle but with any number of choices. Choosing a
|
|
3
|
+
// mode applies its attributes to the field (type, placeholder, inputmode,
|
|
4
|
+
// pattern… whatever a ray-input takes), so one control says both what a
|
|
5
|
+
// value is and the value: "a file at this path", "text", "an amount in €".
|
|
6
|
+
//
|
|
7
|
+
// A ray-input underneath: label, description, error, sizes, forms, the
|
|
8
|
+
// left/right slots and validation work the same.
|
|
9
|
+
//
|
|
10
|
+
// <ray-mode-input mode="text">
|
|
11
|
+
// <option value="text" label="Text"></option>
|
|
12
|
+
// <option value="file" label="File" data-icon="file" data-set-placeholder="Path to a file"></option>
|
|
13
|
+
// </ray-mode-input>
|
|
14
|
+
//
|
|
15
|
+
// or from script: `el.options = [{ value, label, icon, hint, description,
|
|
16
|
+
// disabled, attrs: { placeholder: "…" } }]`.
|
|
17
|
+
//
|
|
18
|
+
// Choosing fires a cancelable ray-mode-change ({ mode, previous, option });
|
|
19
|
+
// cancelling it keeps the previous mode. Alt+ArrowDown in the field opens
|
|
20
|
+
// the menu. The menu is a ray-menu of radio items, native with native-menu.
|
|
21
|
+
|
|
22
|
+
import { css, define } from "../../core/core.js";
|
|
23
|
+
import { fieldTemplate } from "../../core/form.js";
|
|
24
|
+
import { RayInput } from "../input/input.js";
|
|
25
|
+
import "../menu/menu.js";
|
|
26
|
+
|
|
27
|
+
const styles = css`
|
|
28
|
+
.mode-menu { display: contents; }
|
|
29
|
+
.trigger {
|
|
30
|
+
display: inline-flex;
|
|
31
|
+
flex: none;
|
|
32
|
+
align-items: center;
|
|
33
|
+
gap: 0.25em;
|
|
34
|
+
height: max(1.5rem, calc(var(--_h) * 0.7));
|
|
35
|
+
min-width: max(1.5rem, calc(var(--_h) * 0.7));
|
|
36
|
+
margin-inline: calc(var(--_px) * 0.35);
|
|
37
|
+
padding: 0 0.35em;
|
|
38
|
+
font: inherit;
|
|
39
|
+
font-size: var(--_fs);
|
|
40
|
+
color: var(--ray-muted-foreground);
|
|
41
|
+
white-space: nowrap;
|
|
42
|
+
background: transparent;
|
|
43
|
+
border: 0;
|
|
44
|
+
border-radius: var(--ray-radius-sm);
|
|
45
|
+
cursor: pointer;
|
|
46
|
+
}
|
|
47
|
+
.trigger:hover, .trigger[aria-expanded="true"] { color: var(--ray-foreground); background: var(--ray-default-hover); }
|
|
48
|
+
.trigger:disabled { cursor: not-allowed; opacity: 0.6; }
|
|
49
|
+
.trigger [data-ray-icon] svg { width: 1.1em; height: 1.1em; }
|
|
50
|
+
.trigger .chevron svg { width: 0.85em; height: 0.85em; }
|
|
51
|
+
.trigger .text:empty, .trigger .icon:empty { display: none; }
|
|
52
|
+
/* At the start unless position="end". */
|
|
53
|
+
:host(:not([position="end"])) .mode-box { order: -1; }
|
|
54
|
+
.item-description { display: block; font-size: 0.85em; color: var(--ray-muted-foreground); }
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
// An option from an <option> child: label, data-icon, data-hint,
|
|
58
|
+
// data-description, disabled, and every data-set-<attr> as an attribute
|
|
59
|
+
// for the field.
|
|
60
|
+
function optionOf(node) {
|
|
61
|
+
const attrs = {};
|
|
62
|
+
for (const a of node.attributes) {
|
|
63
|
+
if (a.name.startsWith("data-set-")) attrs[a.name.slice(9)] = a.value;
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
value: node.value,
|
|
67
|
+
label: node.getAttribute("label") ?? node.textContent.trim(),
|
|
68
|
+
icon: node.dataset.icon || "",
|
|
69
|
+
hint: node.dataset.hint || "",
|
|
70
|
+
description: node.dataset.description || "",
|
|
71
|
+
disabled: node.disabled,
|
|
72
|
+
attrs,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class RayModeInput extends RayInput {
|
|
77
|
+
static properties = {
|
|
78
|
+
...RayInput.properties,
|
|
79
|
+
mode: { type: String, reflect: true },
|
|
80
|
+
options: { attribute: false },
|
|
81
|
+
display: { type: String, reflect: true },
|
|
82
|
+
position: { type: String, reflect: true, default: "start" },
|
|
83
|
+
menuLabel: { type: String, default: "Mode" },
|
|
84
|
+
nativeMenu: { type: Boolean, reflect: true },
|
|
85
|
+
};
|
|
86
|
+
static styles = [...RayInput.styles, styles];
|
|
87
|
+
static template = fieldTemplate(
|
|
88
|
+
`<input id="control" class="control" part="input">` +
|
|
89
|
+
`<span class="mode-box"><ray-menu class="mode-menu" placement="bottom-start" part="menu">` +
|
|
90
|
+
`<button type="button" slot="trigger" class="trigger focus-ring" part="trigger">` +
|
|
91
|
+
`<span class="icon" part="trigger-icon"></span><span class="text" part="trigger-label"></span>` +
|
|
92
|
+
`<span class="chevron" data-ray-icon="ui.expand" aria-hidden="true"></span>` +
|
|
93
|
+
`</button></ray-menu></span>`,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// The field's own value of each attribute a mode changed, to give it back.
|
|
97
|
+
#saved = new Map();
|
|
98
|
+
#applied = null;
|
|
99
|
+
|
|
100
|
+
/** The modes in use: the `options` property, else the <option> children. */
|
|
101
|
+
get modes() {
|
|
102
|
+
if (Array.isArray(this.options)) return this.options.map((o) => ({ attrs: {}, ...o, value: String(o.value) }));
|
|
103
|
+
return [...this.querySelectorAll(":scope > option")].map(optionOf);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The chosen mode (the first when `mode` names none). */
|
|
107
|
+
get currentOption() {
|
|
108
|
+
const all = this.modes;
|
|
109
|
+
return all.find((o) => o.value === this.mode) ?? all[0] ?? null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
wire() {
|
|
113
|
+
super.wire();
|
|
114
|
+
const menu = this.$(".mode-menu");
|
|
115
|
+
// Keep the caret in the field while the pointer opens the menu.
|
|
116
|
+
this.$(".trigger").addEventListener("pointerdown", (e) => e.preventDefault());
|
|
117
|
+
menu.addEventListener("ray-select", (e) => {
|
|
118
|
+
e.stopPropagation();
|
|
119
|
+
this.choose(e.detail.value);
|
|
120
|
+
this.control.focus();
|
|
121
|
+
});
|
|
122
|
+
this.control.addEventListener("keydown", (e) => {
|
|
123
|
+
if (e.altKey && e.key === "ArrowDown") {
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
if (menu.native) menu.popupNative();
|
|
126
|
+
else menu.open = true;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
// <option> children may come, go or change (the field's own attributes are not modes).
|
|
130
|
+
new MutationObserver((records) => {
|
|
131
|
+
if (records.some((r) => r.type === "childList" || r.target !== this)) this.requestUpdate();
|
|
132
|
+
}).observe(this, { childList: true, subtree: true, attributes: true, characterData: true });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Switches to the mode `value` as the user would: fires ray-mode-change, which may cancel it. */
|
|
136
|
+
choose(value) {
|
|
137
|
+
const option = this.modes.find((o) => o.value === value);
|
|
138
|
+
const previous = this.currentOption;
|
|
139
|
+
if (!option || option.disabled || option.value === previous?.value) {
|
|
140
|
+
this.requestUpdate();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
this.mode = value;
|
|
144
|
+
const go = this.emit("ray-mode-change", { mode: value, previous: previous?.value ?? null, option }, { cancelable: true });
|
|
145
|
+
if (!go) this.mode = previous?.value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// The chosen mode's attributes on the field; the previous mode's taken back.
|
|
149
|
+
#apply(option) {
|
|
150
|
+
if (option && this.#applied === option.value) return;
|
|
151
|
+
for (const [name, value] of this.#saved) {
|
|
152
|
+
if (value === null) this.removeAttribute(name);
|
|
153
|
+
else this.setAttribute(name, value);
|
|
154
|
+
}
|
|
155
|
+
this.#saved.clear();
|
|
156
|
+
for (const [name, value] of Object.entries(option?.attrs ?? {})) {
|
|
157
|
+
this.#saved.set(name, this.getAttribute(name));
|
|
158
|
+
this.setAttribute(name, value);
|
|
159
|
+
}
|
|
160
|
+
this.#applied = option?.value ?? null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
update(changed) {
|
|
164
|
+
const option = this.currentOption;
|
|
165
|
+
// The setter, so the mode is reflected (CSS can read [mode="file"]).
|
|
166
|
+
if (option && this.mode !== option.value) this.mode = option.value;
|
|
167
|
+
this.#apply(option);
|
|
168
|
+
super.update(changed);
|
|
169
|
+
this.#renderMenu(option);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
#renderMenu(option) {
|
|
173
|
+
const menu = this.$(".mode-menu");
|
|
174
|
+
const trigger = this.$(".trigger");
|
|
175
|
+
menu.native = Boolean(this.nativeMenu);
|
|
176
|
+
menu.placement = this.position === "end" ? "bottom-end" : "bottom-start";
|
|
177
|
+
const display = this.display || (option?.icon ? "icon" : "label");
|
|
178
|
+
trigger.querySelector(".text").textContent = display === "icon" || !option ? "" : option.hint || option.label;
|
|
179
|
+
const icon = trigger.querySelector(".icon");
|
|
180
|
+
const iconName = display === "label" || !option?.icon ? "" : option.icon;
|
|
181
|
+
if ((icon.dataset.rayIcon ?? "") !== iconName) {
|
|
182
|
+
icon.replaceChildren();
|
|
183
|
+
if (iconName) icon.dataset.rayIcon = iconName;
|
|
184
|
+
else delete icon.dataset.rayIcon;
|
|
185
|
+
}
|
|
186
|
+
// The trigger is named with the current mode: "Mode: File".
|
|
187
|
+
const name = option ? `${this.menuLabel ?? "Mode"}: ${option.label}` : this.menuLabel ?? "Mode";
|
|
188
|
+
trigger.setAttribute("aria-label", name);
|
|
189
|
+
trigger.title = name;
|
|
190
|
+
trigger.disabled = this.effectivelyDisabled || Boolean(this.readonly);
|
|
191
|
+
|
|
192
|
+
// The menu: one radio item per mode, the current one checked.
|
|
193
|
+
const all = this.modes;
|
|
194
|
+
// A native menu takes the item's whole text as its title: no description there.
|
|
195
|
+
const describe = !this.nativeMenu;
|
|
196
|
+
const key = JSON.stringify([describe, all.map((o) => [o.value, o.label, o.description, Boolean(o.disabled)])]);
|
|
197
|
+
if (menu.dataset.key !== key) {
|
|
198
|
+
menu.dataset.key = key;
|
|
199
|
+
for (const old of menu.querySelectorAll("ray-menu-item")) old.remove();
|
|
200
|
+
for (const o of all) {
|
|
201
|
+
const item = document.createElement("ray-menu-item");
|
|
202
|
+
item.type = "radio";
|
|
203
|
+
item.name = "mode";
|
|
204
|
+
item.value = o.value;
|
|
205
|
+
item.disabled = Boolean(o.disabled);
|
|
206
|
+
item.append(o.label);
|
|
207
|
+
if (o.description && describe) {
|
|
208
|
+
const d = document.createElement("span");
|
|
209
|
+
d.className = "item-description";
|
|
210
|
+
d.textContent = o.description;
|
|
211
|
+
item.append(d);
|
|
212
|
+
}
|
|
213
|
+
menu.append(item);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
for (const item of menu.querySelectorAll("ray-menu-item")) item.checked = item.value === option?.value;
|
|
217
|
+
this.paintIcons(this.shadowRoot);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
define("ray-mode-input", RayModeInput);
|
|
@@ -17,7 +17,7 @@ const styles = css`
|
|
|
17
17
|
font: inherit;
|
|
18
18
|
font-size: var(--ray-font-size-sm);
|
|
19
19
|
color: var(--ray-foreground);
|
|
20
|
-
text-align:
|
|
20
|
+
text-align: start;
|
|
21
21
|
text-decoration: none;
|
|
22
22
|
background: transparent;
|
|
23
23
|
border: 0;
|
|
@@ -43,6 +43,8 @@ const styles = css`
|
|
|
43
43
|
.chevron svg { width: 0.875rem; height: 0.875rem; }
|
|
44
44
|
:host([data-parent]) .chevron { display: inline-flex; }
|
|
45
45
|
:host([opened]) .chevron { transform: rotate(90deg); }
|
|
46
|
+
/* Right to left the chevron is mirrored (it points left): it turns the other way. */
|
|
47
|
+
:host([opened]) .chevron:dir(rtl) { transform: rotate(-90deg); }
|
|
46
48
|
/* Children expand and collapse: a 0fr → 1fr grid row animates the height
|
|
47
49
|
in both directions without measuring it (like the accordion). */
|
|
48
50
|
.children {
|
|
@@ -58,7 +60,7 @@ const styles = css`
|
|
|
58
60
|
:host([icon-only]) .body, :host([icon-only]) .chevron, :host([icon-only]) .section.right { display: none; }
|
|
59
61
|
:host([icon-only]) .children { display: none; }
|
|
60
62
|
/* 2px of room so the focus ring of a child is not clipped. */
|
|
61
|
-
.children-inner { min-height: 0; overflow: hidden; margin: -2px 0; padding: 2px
|
|
63
|
+
.children-inner { min-height: 0; overflow: hidden; margin: -2px 0; padding-block: 2px; padding-inline-start: var(--ray-space-5); }
|
|
62
64
|
`;
|
|
63
65
|
|
|
64
66
|
const INNER = `
|
|
@@ -153,6 +155,7 @@ export class RayNavLink extends RayElement {
|
|
|
153
155
|
});
|
|
154
156
|
this.shadowRoot.prepend(el);
|
|
155
157
|
this._el = el;
|
|
158
|
+
this.paintIcons(el);
|
|
156
159
|
}
|
|
157
160
|
}
|
|
158
161
|
|
|
@@ -17,7 +17,8 @@ const notificationStyles = css`
|
|
|
17
17
|
display: flex;
|
|
18
18
|
align-items: flex-start;
|
|
19
19
|
gap: var(--ray-space-3);
|
|
20
|
-
padding: var(--ray-space-3)
|
|
20
|
+
padding: var(--ray-space-3);
|
|
21
|
+
padding-inline-start: var(--ray-space-4);
|
|
21
22
|
overflow: hidden;
|
|
22
23
|
color: var(--ray-popover-foreground);
|
|
23
24
|
background: var(--ray-popover);
|
|
@@ -29,7 +30,7 @@ const notificationStyles = css`
|
|
|
29
30
|
/* Behind the front toast of a collapsed stack: only its edge shows. */
|
|
30
31
|
.card > * { transition: opacity var(--ray-duration-base) var(--ray-easing-standard); }
|
|
31
32
|
:host([data-behind]) .card > * { opacity: 0; }
|
|
32
|
-
.card[data-icon], .card[data-loading], .card[data-glyph] { padding-
|
|
33
|
+
.card[data-icon], .card[data-loading], .card[data-glyph] { padding-inline-start: var(--ray-space-3); }
|
|
33
34
|
.icon {
|
|
34
35
|
display: none;
|
|
35
36
|
flex: none;
|
|
@@ -201,14 +202,16 @@ const toasterStyles = css`
|
|
|
201
202
|
:host(:not(:popover-open)) { display: none; }
|
|
202
203
|
:host([position^="top"]) { top: 0; }
|
|
203
204
|
:host([position^="bottom"]), :host(:not([position])) { bottom: 0; }
|
|
204
|
-
|
|
205
|
-
:host([position$="
|
|
206
|
-
:host([position$="
|
|
205
|
+
/* -start/-end follow the writing direction; -left/-right are sides of the screen. */
|
|
206
|
+
:host([position$="-end"]), :host(:not([position])) { inset-inline-end: 0; }
|
|
207
|
+
:host([position$="-start"]) { inset-inline-start: 0; }
|
|
208
|
+
:host([position$="right"]) { right: 0; } /* physical */
|
|
209
|
+
:host([position$="left"]) { left: 0; } /* physical */
|
|
210
|
+
:host([position$="center"]) { left: 50%; transform: translateX(-50%); } /* physical */
|
|
207
211
|
/* Toasts are placed by layout(): absolute, stacked from the edge. */
|
|
208
212
|
::slotted(*) {
|
|
209
213
|
position: absolute;
|
|
210
|
-
|
|
211
|
-
right: var(--ray-space-4);
|
|
214
|
+
inset-inline: var(--ray-space-4);
|
|
212
215
|
pointer-events: auto;
|
|
213
216
|
touch-action: pan-y;
|
|
214
217
|
transform-origin: 50% 100%;
|
|
@@ -222,10 +225,10 @@ const toasterStyles = css`
|
|
|
222
225
|
::slotted([data-swiping]) { transition: none; }
|
|
223
226
|
/* Phones: full width at the bottom (or top), wherever the desktop position is. */
|
|
224
227
|
@media (max-width: 40em) {
|
|
225
|
-
:host { width: auto;
|
|
228
|
+
:host { width: auto; inset-inline: 0; transform: none; padding: var(--ray-space-3); }
|
|
226
229
|
:host([position^="top"]) { top: 0; bottom: auto; }
|
|
227
230
|
:host(:not([position^="top"])) { bottom: 0; top: auto; }
|
|
228
|
-
::slotted(*) {
|
|
231
|
+
::slotted(*) { inset-inline: var(--ray-space-3); }
|
|
229
232
|
:host(:not([position^="top"])) ::slotted(*) { bottom: var(--ray-space-3); }
|
|
230
233
|
:host([position^="top"]) ::slotted(*) { top: var(--ray-space-3); }
|
|
231
234
|
}
|
|
@@ -272,7 +275,7 @@ const SHRINK = 0.05;
|
|
|
272
275
|
|
|
273
276
|
export class RayToaster extends RayElement {
|
|
274
277
|
static properties = {
|
|
275
|
-
position: { type: String, reflect: true, default: "bottom-
|
|
278
|
+
position: { type: String, reflect: true, default: "bottom-end" },
|
|
276
279
|
limit: { type: Number, default: 5 },
|
|
277
280
|
// Toasts visible in the collapsed stack (the rest wait, invisible).
|
|
278
281
|
visible: { type: Number, default: 3 },
|
|
@@ -358,7 +361,7 @@ export class RayToaster extends RayElement {
|
|
|
358
361
|
/** Places every toast: a stack (collapsed) or a list (expanded). */
|
|
359
362
|
layout() {
|
|
360
363
|
const toasts = this.toasts;
|
|
361
|
-
const top = (this.position ?? "bottom-
|
|
364
|
+
const top = (this.position ?? "bottom-end").startsWith("top");
|
|
362
365
|
const dir = top ? 1 : -1;
|
|
363
366
|
const heights = toasts.map((el) => el.shadowRoot?.querySelector(".card")?.offsetHeight || el.offsetHeight || 0);
|
|
364
367
|
const front = heights[0] ?? 0;
|
|
@@ -529,7 +532,7 @@ export function notify(options = {}) {
|
|
|
529
532
|
el._onClose = options.onClose;
|
|
530
533
|
t.append(el);
|
|
531
534
|
// Enter from the edge: start off-screen, then let layout() transition it in.
|
|
532
|
-
const top = (t.position ?? "bottom-
|
|
535
|
+
const top = (t.position ?? "bottom-end").startsWith("top");
|
|
533
536
|
el.style.transition = "none";
|
|
534
537
|
el.style.opacity = "0";
|
|
535
538
|
el.style.transform = `translateY(${top ? -100 : 100}%)`;
|
|
@@ -42,7 +42,7 @@ const styles = css`
|
|
|
42
42
|
.value {
|
|
43
43
|
flex: none;
|
|
44
44
|
min-width: 2.75em;
|
|
45
|
-
text-align:
|
|
45
|
+
text-align: end;
|
|
46
46
|
font-size: var(--ray-font-size-xs);
|
|
47
47
|
font-variant-numeric: tabular-nums;
|
|
48
48
|
color: var(--ray-muted-foreground);
|
|
@@ -59,7 +59,7 @@ const styles = css`
|
|
|
59
59
|
width: 40%;
|
|
60
60
|
animation: ray-indeterminate 1.4s var(--ray-easing-standard) infinite;
|
|
61
61
|
}
|
|
62
|
-
@keyframes ray-indeterminate { from {
|
|
62
|
+
@keyframes ray-indeterminate { from { inset-inline-start: -40%; } to { inset-inline-start: 100%; } }
|
|
63
63
|
`;
|
|
64
64
|
|
|
65
65
|
export class RayProgress extends RayElement {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// plans, presets, shipping methods. Both can share a group.
|
|
9
9
|
|
|
10
10
|
import { RayElement, css, define } from "../../core/core.js";
|
|
11
|
+
import { inlineKey } from "../../core/direction.js";
|
|
11
12
|
import { applyColor, sizeStyles } from "../../core/styles.js";
|
|
12
13
|
import { FormControl, fieldStyles } from "../../core/form.js";
|
|
13
14
|
|
|
@@ -136,9 +137,10 @@ export class RayRadioGroup extends FormControl {
|
|
|
136
137
|
const radios = this.radios.filter((r) => !r.disabled);
|
|
137
138
|
const i = radios.indexOf(e.target.closest?.(RADIOS));
|
|
138
139
|
if (i < 0) return;
|
|
140
|
+
const key = inlineKey(e.key, this);
|
|
139
141
|
let next = null;
|
|
140
|
-
if (
|
|
141
|
-
else if (
|
|
142
|
+
if (key === "ArrowDown" || key === "ArrowRight") next = radios[(i + 1) % radios.length];
|
|
143
|
+
else if (key === "ArrowUp" || key === "ArrowLeft") next = radios[(i - 1 + radios.length) % radios.length];
|
|
142
144
|
else if (e.key === " ") next = radios[i];
|
|
143
145
|
if (!next) return;
|
|
144
146
|
e.preventDefault();
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// Give it a size: height / max-height (or CSS, or a flex/grid parent).
|
|
8
8
|
|
|
9
9
|
import { RayElement, css, define } from "../../core/core.js";
|
|
10
|
+
import { isRtl } from "../../core/direction.js";
|
|
10
11
|
|
|
11
12
|
const styles = css`
|
|
12
13
|
:host {
|
|
@@ -47,12 +48,14 @@ const styles = css`
|
|
|
47
48
|
transition: opacity var(--ray-duration-fast);
|
|
48
49
|
}
|
|
49
50
|
.edge[data-show] { opacity: 1; }
|
|
50
|
-
.top, .bottom {
|
|
51
|
-
.
|
|
51
|
+
.top, .bottom { inset-inline: 0; height: 0.75rem; }
|
|
52
|
+
.start, .end { top: 0; bottom: 0; width: 0.75rem; }
|
|
52
53
|
.top { top: 0; background: linear-gradient(var(--_shadow), transparent); }
|
|
53
54
|
.bottom { bottom: 0; background: linear-gradient(transparent, var(--_shadow)); }
|
|
54
|
-
.
|
|
55
|
-
.
|
|
55
|
+
.start { inset-inline-start: 0; background: linear-gradient(to right, var(--_shadow), transparent); } /* physical */
|
|
56
|
+
.end { inset-inline-end: 0; background: linear-gradient(to left, var(--_shadow), transparent); } /* physical */
|
|
57
|
+
.start:dir(rtl) { background: linear-gradient(to left, var(--_shadow), transparent); } /* physical */
|
|
58
|
+
.end:dir(rtl) { background: linear-gradient(to right, var(--_shadow), transparent); } /* physical */
|
|
56
59
|
:host([no-shadows]) .edge { display: none; }
|
|
57
60
|
`;
|
|
58
61
|
|
|
@@ -70,7 +73,7 @@ export class RayScrollArea extends RayElement {
|
|
|
70
73
|
static template = `
|
|
71
74
|
<div class="viewport" part="viewport" tabindex="0"><slot></slot></div>
|
|
72
75
|
<span class="edge top" part="shadow"></span><span class="edge bottom" part="shadow"></span>
|
|
73
|
-
<span class="edge
|
|
76
|
+
<span class="edge start" part="shadow"></span><span class="edge end" part="shadow"></span>`;
|
|
74
77
|
|
|
75
78
|
#ro = null;
|
|
76
79
|
#atEnd = false;
|
|
@@ -115,15 +118,15 @@ export class RayScrollArea extends RayElement {
|
|
|
115
118
|
this._edges();
|
|
116
119
|
}
|
|
117
120
|
|
|
118
|
-
/** Scrolls to the start (top
|
|
121
|
+
/** Scrolls to the start (the top, or the left; the right right to left). */
|
|
119
122
|
scrollToStart({ behavior = "smooth" } = {}) {
|
|
120
123
|
this.viewport.scrollTo({ top: 0, left: 0, behavior });
|
|
121
124
|
}
|
|
122
125
|
|
|
123
|
-
/** Scrolls to the end (bottom, or right when horizontal). */
|
|
126
|
+
/** Scrolls to the end (the bottom, or the right when horizontal; the left right to left). */
|
|
124
127
|
scrollToEnd({ behavior = "smooth" } = {}) {
|
|
125
128
|
const v = this.viewport;
|
|
126
|
-
if (this.orientation === "horizontal") v.scrollTo({ left: v.scrollWidth, behavior });
|
|
129
|
+
if (this.orientation === "horizontal") v.scrollTo({ left: isRtl(this) ? -v.scrollWidth : v.scrollWidth, behavior });
|
|
127
130
|
else v.scrollTo({ top: v.scrollHeight, behavior });
|
|
128
131
|
}
|
|
129
132
|
|
|
@@ -144,13 +147,13 @@ export class RayScrollArea extends RayElement {
|
|
|
144
147
|
const x = Math.abs(v.scrollLeft);
|
|
145
148
|
show("top", v.scrollTop > 1);
|
|
146
149
|
show("bottom", maxY > 1 && v.scrollTop < maxY - 1);
|
|
147
|
-
show("
|
|
148
|
-
show("
|
|
150
|
+
show("start", x > 1);
|
|
151
|
+
show("end", maxX > 1 && x < maxX - 1);
|
|
149
152
|
const horizontal = this.orientation === "horizontal";
|
|
150
153
|
const left = horizontal ? maxX - x : maxY - v.scrollTop;
|
|
151
154
|
const scrollable = horizontal ? maxX > 1 : maxY > 1;
|
|
152
155
|
const atEnd = scrollable && left <= (this.threshold ?? 48);
|
|
153
|
-
if (atEnd && !this.#atEnd) this.emit("ray-reach-end", { edge: horizontal ? "
|
|
156
|
+
if (atEnd && !this.#atEnd) this.emit("ray-reach-end", { edge: !horizontal ? "bottom" : isRtl(this) ? "left" : "right" });
|
|
154
157
|
this.#atEnd = atEnd;
|
|
155
158
|
}
|
|
156
159
|
}
|
|
@@ -14,14 +14,14 @@ const styles = css`
|
|
|
14
14
|
:host(:not([variant])) .box { background: var(--ray-muted); border-color: transparent; }
|
|
15
15
|
.lead {
|
|
16
16
|
display: inline-flex;
|
|
17
|
-
padding-
|
|
17
|
+
padding-inline-start: calc(var(--_px) * 0.66);
|
|
18
18
|
color: var(--ray-muted-foreground);
|
|
19
19
|
pointer-events: none;
|
|
20
20
|
}
|
|
21
21
|
.lead svg { width: 1.1em; height: 1.1em; }
|
|
22
|
-
.control { padding-
|
|
22
|
+
.control { padding-inline-start: calc(var(--_px) * 0.5); }
|
|
23
23
|
.control::-webkit-search-cancel-button, .control::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }
|
|
24
|
-
.tail { display: inline-flex; align-items: center; gap: var(--ray-space-1); padding-
|
|
24
|
+
.tail { display: inline-flex; align-items: center; gap: var(--ray-space-1); padding-inline-end: calc(var(--_px) * 0.4); }
|
|
25
25
|
.clear {
|
|
26
26
|
display: inline-flex;
|
|
27
27
|
align-items: center;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// <option> elements or the `data` property. Form-associated.
|
|
4
4
|
|
|
5
5
|
import { css, define } from "../../core/core.js";
|
|
6
|
+
import { inlineKey } from "../../core/direction.js";
|
|
6
7
|
import { applyColor, sizeStyles, radiusStyles } from "../../core/styles.js";
|
|
7
8
|
import { FormControl } from "../../core/form.js";
|
|
8
9
|
|
|
@@ -22,7 +23,7 @@ const styles = css`
|
|
|
22
23
|
.indicator {
|
|
23
24
|
position: absolute;
|
|
24
25
|
top: 0;
|
|
25
|
-
left: 0;
|
|
26
|
+
left: 0; /* physical */
|
|
26
27
|
z-index: 0;
|
|
27
28
|
box-sizing: border-box;
|
|
28
29
|
background: var(--ray-raised);
|
|
@@ -170,9 +171,10 @@ export class RaySegmentedControl extends FormControl {
|
|
|
170
171
|
_key(e) {
|
|
171
172
|
const enabled = this._buttons.filter((b) => !b.disabled);
|
|
172
173
|
const i = enabled.findIndex((b) => b.dataset.value === this.value);
|
|
174
|
+
const key = inlineKey(e.key, this);
|
|
173
175
|
let next = null;
|
|
174
|
-
if (
|
|
175
|
-
else if (
|
|
176
|
+
if (key === "ArrowRight" || key === "ArrowDown") next = enabled[(i + 1) % enabled.length];
|
|
177
|
+
else if (key === "ArrowLeft" || key === "ArrowUp") next = enabled[(i - 1 + enabled.length) % enabled.length];
|
|
176
178
|
if (!next) return;
|
|
177
179
|
e.preventDefault();
|
|
178
180
|
this._choose(next.dataset.value, true);
|
|
@@ -30,8 +30,8 @@ const styles = css`
|
|
|
30
30
|
align-items: center;
|
|
31
31
|
width: 100%;
|
|
32
32
|
min-width: 0;
|
|
33
|
-
padding-
|
|
34
|
-
text-align:
|
|
33
|
+
padding-inline-end: calc(var(--_h) * 0.8);
|
|
34
|
+
text-align: start;
|
|
35
35
|
color: inherit;
|
|
36
36
|
background: transparent;
|
|
37
37
|
border: 0;
|
|
@@ -43,7 +43,7 @@ const styles = css`
|
|
|
43
43
|
.trigger[data-placeholder] .value { color: var(--ray-muted-foreground); }
|
|
44
44
|
.chevron {
|
|
45
45
|
position: absolute;
|
|
46
|
-
|
|
46
|
+
inset-inline-end: 0;
|
|
47
47
|
display: inline-flex;
|
|
48
48
|
align-items: center;
|
|
49
49
|
justify-content: center;
|
|
@@ -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
|
|