@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.
Files changed (61) hide show
  1. package/base.css +1 -1
  2. package/check/check.d.ts +2 -0
  3. package/check/check.js +18 -4
  4. package/check/cli.js +10 -6
  5. package/components/accordion/accordion.js +8 -5
  6. package/components/alert/alert.js +3 -2
  7. package/components/attachment/attachment.js +5 -6
  8. package/components/avatar/avatar.js +1 -1
  9. package/components/blockquote/blockquote.js +2 -2
  10. package/components/bottom-nav/bottom-nav.js +5 -3
  11. package/components/breadcrumbs/breadcrumbs.js +1 -1
  12. package/components/carousel/carousel.js +26 -14
  13. package/components/checkbox/checkbox.js +6 -3
  14. package/components/checkbox-group/checkbox-group.js +1 -1
  15. package/components/code/code.js +9 -7
  16. package/components/color-picker/color-picker.js +6 -6
  17. package/components/combobox/combobox.js +5 -4
  18. package/components/command-palette/command-palette.js +2 -2
  19. package/components/date-picker/date-picker.js +9 -7
  20. package/components/dialog/dialog.js +2 -2
  21. package/components/drawer/drawer.js +28 -10
  22. package/components/file-upload/file-upload.js +1 -1
  23. package/components/icon/icon.js +9 -1
  24. package/components/indicator/indicator.js +2 -1
  25. package/components/menu/menu.js +1 -1
  26. package/components/nav-link/nav-link.js +6 -3
  27. package/components/notification/notification.js +16 -13
  28. package/components/number-input/number-input.js +1 -1
  29. package/components/pagination/pagination.js +4 -4
  30. package/components/password-input/password-input.js +2 -2
  31. package/components/progress/progress.js +2 -2
  32. package/components/radio/radio.js +4 -2
  33. package/components/scroll-area/scroll-area.js +14 -11
  34. package/components/search/search.js +5 -5
  35. package/components/segmented-control/segmented-control.js +5 -3
  36. package/components/select/select.js +5 -5
  37. package/components/separator/separator.js +8 -3
  38. package/components/sidebar/sidebar.js +14 -6
  39. package/components/slider/slider.js +5 -2
  40. package/components/split/split.js +12 -6
  41. package/components/stat/stat.js +1 -1
  42. package/components/step-dots/step-dots.js +11 -2
  43. package/components/stepper/stepper.js +5 -5
  44. package/components/switch/switch.js +3 -3
  45. package/components/table/table.js +6 -6
  46. package/components/tabs/tabs.js +11 -8
  47. package/components/theme-toggle/theme-toggle.js +2 -2
  48. package/components/timeline/timeline.js +13 -8
  49. package/components/toggle-group/toggle-group.js +6 -4
  50. package/core/core.js +10 -4
  51. package/core/direction.js +19 -0
  52. package/core/icons.js +107 -8
  53. package/core/position.js +11 -3
  54. package/core/styles.js +1 -1
  55. package/core.js +1 -0
  56. package/custom-elements.json +151 -79
  57. package/llms-full.txt +75 -31
  58. package/llms.txt +32 -3
  59. package/package.json +1 -1
  60. package/ray-elements.d.ts +56 -39
  61. 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-left: var(--_w) var(--_style) var(--_color); }
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
- :host([label-position="left"]) .labeled::before { flex: 0 0 var(--ray-space-3); }
31
- :host([label-position="right"]) .labeled::after { flex: 0 0 var(--ray-space-3); }
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
- :host([side="right"]) .panel { border-inline-end: 0; border-inline-start: 1px solid var(--ray-border); }
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 auto 0 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
- :host([side="right"]) dialog { inset: 0 0 0 auto; }
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: "left" },
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.side === "right" ? "translateX(100%)" : "translateX(-100%)";
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.left = `${((v - min) / (max - min || 1)) * 100}%`;
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; left: 0; right: 0; width: auto; height: 1px; transform: translateY(-50%); }
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
- const move = (ev) => this._resizeTo(vertical ? ev.clientY - rect.top : ev.clientX - rect.left, false);
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(vertical ? ev.clientY - rect.top : ev.clientX - rect.left, true);
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 (e.key === less) next = current - step;
207
- else if (e.key === more) next = current + step;
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") {
@@ -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: "arrow-up-right", down: "arrow-down-right", flat: "minus" };
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
- const noun = this.label || "Step";
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 : done ? `${count} of ${count}, done` : `${noun} ${value + 1} of ${count}`;
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: left;
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-right-color: transparent; border-radius: 50%; animation: ray-step-spin 0.8s linear infinite; }
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) 0 var(--ray-space-1) calc(var(--_icon) / 2 - 1px); }
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: "left" },
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 || "check"));
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
- left: 2px;
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: left var(--ray-duration-base) var(--ray-easing-standard);
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 { left: calc(100% - var(--_s) + 4px); border-color: transparent; background: var(--_contrast); }
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: left; font-weight: var(--ray-font-weight-semibold); }
31
- th, td { padding: var(--_py) var(--_px); text-align: left; vertical-align: middle; border-bottom: 1px solid var(--ray-border); }
32
- :host([with-column-borders]) th + th, :host([with-column-borders]) td + td { border-left: 1px solid var(--ray-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-right: 0; }
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="chevron-up" aria-hidden="true"></span>`;
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 = {
@@ -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-right: 2px solid var(--ray-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-right-width: 1px; }
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 (e.key === next) target = tabs[(i + 1) % tabs.length];
119
- else if (e.key === prev) target = tabs[(i - 1 + tabs.length) % tabs.length];
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 0 0; justify-content: flex-start; border-bottom: 0; border-right: 2px solid transparent; }
161
- :host([data-variant="default"][data-vertical][data-active]) { border-right-color: var(--_filled, var(--ray-primary)); }
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 0 0; justify-content: flex-start; border-radius: var(--ray-radius-default) 0 0 var(--ray-radius-default); }
167
- :host([data-variant="outline"][data-vertical][data-active]) { border-bottom-color: var(--ray-border); border-right-color: var(--ray-background); }
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: "monitor", light: "sun", dark: "moon" };
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="monitor" aria-hidden="true"></span></button>`;
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
- :host([data-align="right"]) { grid-template-columns: minmax(0, 1fr) var(--_bullet); text-align: right; }
26
- :host([data-align="right"]) .bullet-col { grid-column: 2; grid-row: 1; }
27
- :host([data-align="right"]) .body { grid-column: 1; grid-row: 1; }
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: "left" },
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", this.align === "right" ? "right" : "left");
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
- if (![next, prev, "Home", "End"].includes(e.key)) return;
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 (e.key === "Home") i = 0;
122
- else if (e.key === "End") i = items.length - 1;
123
- else if (e.key === next) i = (current + 1) % items.length;
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
+ }