@raylang/elements 0.7.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 (49) hide show
  1. package/base.css +1 -1
  2. package/components/accordion/accordion.js +7 -4
  3. package/components/alert/alert.js +2 -1
  4. package/components/attachment/attachment.js +4 -5
  5. package/components/blockquote/blockquote.js +2 -2
  6. package/components/bottom-nav/bottom-nav.js +3 -3
  7. package/components/carousel/carousel.js +22 -10
  8. package/components/checkbox/checkbox.js +5 -2
  9. package/components/checkbox-group/checkbox-group.js +1 -1
  10. package/components/code/code.js +8 -6
  11. package/components/color-picker/color-picker.js +5 -5
  12. package/components/combobox/combobox.js +2 -1
  13. package/components/command-palette/command-palette.js +1 -1
  14. package/components/date-picker/date-picker.js +5 -3
  15. package/components/dialog/dialog.js +1 -1
  16. package/components/drawer/drawer.js +27 -9
  17. package/components/icon/icon.js +4 -1
  18. package/components/indicator/indicator.js +2 -1
  19. package/components/menu/menu.js +1 -1
  20. package/components/nav-link/nav-link.js +5 -2
  21. package/components/notification/notification.js +15 -12
  22. package/components/progress/progress.js +2 -2
  23. package/components/radio/radio.js +4 -2
  24. package/components/scroll-area/scroll-area.js +14 -11
  25. package/components/search/search.js +3 -3
  26. package/components/segmented-control/segmented-control.js +5 -3
  27. package/components/select/select.js +3 -3
  28. package/components/separator/separator.js +8 -3
  29. package/components/sidebar/sidebar.js +14 -6
  30. package/components/slider/slider.js +5 -2
  31. package/components/split/split.js +12 -6
  32. package/components/stepper/stepper.js +4 -4
  33. package/components/switch/switch.js +3 -3
  34. package/components/table/table.js +5 -5
  35. package/components/tabs/tabs.js +11 -8
  36. package/components/timeline/timeline.js +13 -8
  37. package/components/toggle-group/toggle-group.js +6 -4
  38. package/core/core.js +6 -1
  39. package/core/direction.js +19 -0
  40. package/core/icons.js +12 -0
  41. package/core/position.js +11 -3
  42. package/core/styles.js +1 -1
  43. package/core.js +1 -0
  44. package/custom-elements.json +80 -62
  45. package/llms-full.txt +41 -17
  46. package/llms.txt +20 -2
  47. package/package.json +1 -1
  48. package/ray-elements.d.ts +32 -25
  49. package/vscode.html-custom-data.json +80 -18
package/base.css CHANGED
@@ -84,7 +84,7 @@
84
84
  }
85
85
  :where(.ray-table) :where(th, td) {
86
86
  padding: var(--ray-space-2) var(--ray-space-3);
87
- text-align: left;
87
+ text-align: start;
88
88
  vertical-align: middle;
89
89
  border-bottom: 1px solid var(--ray-border);
90
90
  }
@@ -17,7 +17,7 @@ export class RayAccordion extends RayElement {
17
17
  value: { type: String },
18
18
  multiple: { type: Boolean, reflect: true },
19
19
  variant: { type: String, reflect: true, default: "default" },
20
- chevronPosition: { type: String, reflect: true, default: "right" },
20
+ chevronPosition: { type: String, reflect: true, default: "end" },
21
21
  };
22
22
  static styles = [accordionStyles];
23
23
  static template = `<div class="items" part="items"><slot></slot></div>`;
@@ -52,7 +52,7 @@ export class RayAccordion extends RayElement {
52
52
  items.forEach((item, i) => {
53
53
  item._setOpen(open.includes(item.value));
54
54
  item.dataset.variant = this.variant || "default";
55
- item.dataset.chevron = this.chevronPosition || "right";
55
+ item.dataset.chevron = this.chevronPosition || "end";
56
56
  item.toggleAttribute("data-first", i === 0);
57
57
  item.toggleAttribute("data-last", i === items.length - 1);
58
58
  });
@@ -104,7 +104,7 @@ const itemStyles = css`
104
104
  font-size: var(--ray-font-size-md);
105
105
  font-weight: var(--ray-font-weight-medium);
106
106
  color: var(--ray-foreground);
107
- text-align: left;
107
+ text-align: start;
108
108
  background: transparent;
109
109
  border: 0;
110
110
  border-radius: inherit;
@@ -116,7 +116,10 @@ const itemStyles = css`
116
116
  .label { display: grid; flex: 1; gap: 0.125rem; min-width: 0; }
117
117
  .description { font-size: var(--ray-font-size-sm); font-weight: var(--ray-font-weight-normal, 400); color: var(--ray-muted-foreground); }
118
118
  .description:empty { display: none; }
119
- :host([data-chevron="left"]) .chevron { order: -1; }
119
+ /* The chevron before the label: start, or left/right as sides of the screen. */
120
+ :host([data-chevron="start"]) .chevron,
121
+ :host([data-chevron="left"]) .chevron:dir(ltr),
122
+ :host([data-chevron="right"]) .chevron:dir(rtl) { order: -1; }
120
123
  .chevron { display: inline-flex; color: var(--ray-muted-foreground); transition: transform var(--ray-duration-base) var(--ray-easing-standard); }
121
124
  .chevron svg { width: 1rem; height: 1rem; }
122
125
  :host([data-open]) .chevron { transform: rotate(180deg); }
@@ -34,7 +34,8 @@ const styles = css`
34
34
  justify-content: center;
35
35
  width: 1.5rem;
36
36
  height: 1.5rem;
37
- margin: -0.125rem -0.375rem 0 0;
37
+ margin: -0.125rem 0 0;
38
+ margin-inline-end: -0.375rem;
38
39
  padding: 0;
39
40
  color: inherit;
40
41
  background: transparent;
@@ -25,7 +25,7 @@ const styles = css`
25
25
  width: 16rem;
26
26
  max-width: 100%;
27
27
  padding: var(--ray-space-2);
28
- padding-right: var(--ray-space-3);
28
+ padding-inline-end: var(--ray-space-3);
29
29
  color: var(--ray-foreground);
30
30
  background: var(--ray-card);
31
31
  border: 1px solid var(--ray-border);
@@ -53,7 +53,7 @@ const styles = css`
53
53
  .badge {
54
54
  position: absolute;
55
55
  bottom: 3px;
56
- left: 50%;
56
+ left: 50%; /* physical */
57
57
  transform: translateX(-50%);
58
58
  padding: 0 0.25rem;
59
59
  font-size: 0.5625rem;
@@ -82,8 +82,7 @@ const styles = css`
82
82
  :host([status="error"]) .meta { color: var(--ray-destructive); }
83
83
  .bar {
84
84
  position: absolute;
85
- left: 0;
86
- right: 0;
85
+ inset-inline: 0;
87
86
  bottom: 0;
88
87
  height: 2px;
89
88
  background: var(--ray-muted);
@@ -131,7 +130,7 @@ const styles = css`
131
130
  :host([variant="card"]) .remove {
132
131
  position: absolute;
133
132
  top: calc(var(--ray-space-2) + 4px);
134
- right: calc(var(--ray-space-2) + 4px);
133
+ inset-inline-end: calc(var(--ray-space-2) + 4px);
135
134
  background: var(--ray-card);
136
135
  box-shadow: var(--ray-shadow-xs);
137
136
  opacity: 1;
@@ -22,7 +22,7 @@ const styles = css`
22
22
  :host([hide-edge]) { --_edge: 0px; }
23
23
  figure { position: relative; margin: 0; }
24
24
  /* Room for the mark, so it does not cover what is above or beside. */
25
- :host([data-icon]) figure { padding: calc(var(--_icon) / 2) 0 0 calc(var(--_icon) / 2); }
25
+ :host([data-icon]) figure { padding-block-start: calc(var(--_icon) / 2); padding-inline-start: calc(var(--_icon) / 2); }
26
26
  blockquote {
27
27
  margin: 0;
28
28
  padding: var(--_p) calc(var(--_p) * 1.25);
@@ -43,7 +43,7 @@ const styles = css`
43
43
  .icon {
44
44
  position: absolute;
45
45
  top: 0;
46
- left: 0;
46
+ inset-inline-start: 0;
47
47
  display: grid;
48
48
  place-items: center;
49
49
  width: var(--_icon);
@@ -40,7 +40,7 @@ const navStyles = css`
40
40
  grid-auto-columns: minmax(0, 1fr);
41
41
  grid-auto-flow: column;
42
42
  align-items: stretch;
43
- padding: var(--ray-space-1) max(var(--ray-space-1), env(safe-area-inset-right, 0px)) max(var(--ray-space-1), env(safe-area-inset-bottom, 0px)) max(var(--ray-space-1), env(safe-area-inset-left, 0px));
43
+ padding: var(--ray-space-1) max(var(--ray-space-1), env(safe-area-inset-right, 0px)) max(var(--ray-space-1), env(safe-area-inset-bottom, 0px)) max(var(--ray-space-1), env(safe-area-inset-left, 0px)); /* physical */
44
44
  background: var(--ray-bottom-nav, var(--ray-background));
45
45
  border-top: 1px solid var(--ray-border);
46
46
  }
@@ -109,7 +109,7 @@ const itemStyles = css`
109
109
  .badge {
110
110
  position: absolute;
111
111
  top: 0;
112
- left: calc(50% + 0.375rem);
112
+ inset-inline-start: calc(50% + 0.375rem);
113
113
  min-width: 1rem;
114
114
  height: 1rem;
115
115
  padding: 0 0.25rem;
@@ -125,7 +125,7 @@ const itemStyles = css`
125
125
  box-sizing: content-box;
126
126
  }
127
127
  .badge:empty:not([data-dot]) { display: none; }
128
- .badge[data-dot] { min-width: 0; width: 0.5rem; height: 0.5rem; padding: 0; top: 0.25rem; left: calc(50% + 0.5rem); }
128
+ .badge[data-dot] { min-width: 0; width: 0.5rem; height: 0.5rem; padding: 0; top: 0.25rem; inset-inline-start: calc(50% + 0.5rem); }
129
129
  .label {
130
130
  max-width: 100%;
131
131
  overflow: hidden;
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { RayElement, css, define } from "../../core/core.js";
11
11
  import { motionAllowed } from "../../core/motion.js";
12
+ import { isRtl, inlineKey } from "../../core/direction.js";
12
13
 
13
14
  const styles = css`
14
15
  :host {
@@ -74,9 +75,11 @@ const styles = css`
74
75
  .control:hover:not(:disabled) { background: var(--ray-accent); }
75
76
  .control:disabled { opacity: 0; pointer-events: none; }
76
77
  .control svg { width: 1rem; height: 1rem; }
77
- .prev { left: var(--ray-space-2); }
78
- .next { right: var(--ray-space-2); }
79
- :host([orientation="vertical"]) .control { top: auto; left: 50%; transform: translateX(-50%) rotate(90deg); }
78
+ .prev { inset-inline-start: var(--ray-space-2); }
79
+ .next { inset-inline-end: var(--ray-space-2); }
80
+ :host([orientation="vertical"]) .control { top: auto; left: 50%; transform: translateX(-50%) rotate(90deg); } /* physical */
81
+ /* Up and down do not depend on the writing direction: the turned arrows are not mirrored. */
82
+ :host([orientation="vertical"]) .control [data-ray-icon] > svg { transform: none; }
80
83
  :host([orientation="vertical"]) .prev { top: var(--ray-space-2); }
81
84
  :host([orientation="vertical"]) .next { bottom: var(--ray-space-2); }
82
85
  :host([hide-controls]) .control { display: none; }
@@ -272,6 +275,14 @@ export class RayCarousel extends RayElement {
272
275
  return this.orientation === "vertical";
273
276
  }
274
277
 
278
+ // The scroll offset from the start. Right to left, scrollLeft runs from 0
279
+ // (the start, on the right) down to negative values.
280
+ _scroll() {
281
+ const vp = this.viewport;
282
+ if (this._vertical()) return vp.scrollTop;
283
+ return isRtl(this) ? -vp.scrollLeft : vp.scrollLeft;
284
+ }
285
+
275
286
  _current() {
276
287
  return Math.min(Math.max(0, this.index ?? 0), this.count - 1);
277
288
  }
@@ -294,11 +305,12 @@ export class RayCarousel extends RayElement {
294
305
  const vr = vp.getBoundingClientRect();
295
306
  const size = v ? vp.clientHeight : vp.clientWidth;
296
307
  const max = Math.max(0, v ? vp.scrollHeight - vp.clientHeight : vp.scrollWidth - vp.clientWidth);
297
- const scroll = v ? vp.scrollTop : vp.scrollLeft;
308
+ const scroll = this._scroll();
309
+ const rtl = !v && isRtl(this);
298
310
  const snaps = [];
299
311
  for (const s of slides) {
300
312
  const r = s.getBoundingClientRect();
301
- const start = (v ? r.top - vr.top : r.left - vr.left) + scroll;
313
+ const start = (v ? r.top - vr.top : rtl ? vr.right - r.right : r.left - vr.left) + scroll;
302
314
  const length = v ? r.height : r.width;
303
315
  const pos = this.align === "center" ? start - (size - length) / 2 : start;
304
316
  const clamped = Math.round(Math.min(max, Math.max(0, pos)));
@@ -333,8 +345,7 @@ export class RayCarousel extends RayElement {
333
345
 
334
346
  // The position closest to the current scroll offset.
335
347
  _track() {
336
- const vp = this.viewport;
337
- const scroll = this._vertical() ? vp.scrollTop : vp.scrollLeft;
348
+ const scroll = this._scroll();
338
349
  let best = 0;
339
350
  for (let i = 1; i < this.#snaps.length; i++) {
340
351
  if (Math.abs(this.#snaps[i] - scroll) < Math.abs(this.#snaps[best] - scroll)) best = i;
@@ -358,7 +369,7 @@ export class RayCarousel extends RayElement {
358
369
  const smooth = user && !instant && !reduced?.matches && motionAllowed(this);
359
370
  const opts = { behavior: smooth ? "smooth" : "instant" };
360
371
  if (this._vertical()) this.viewport.scrollTo({ top: pos, ...opts });
361
- else this.viewport.scrollTo({ left: pos, ...opts });
372
+ else this.viewport.scrollTo({ left: isRtl(this) ? -pos : pos, ...opts });
362
373
  this._setIndex(i);
363
374
  this._paint();
364
375
  this._schedule();
@@ -416,10 +427,11 @@ export class RayCarousel extends RayElement {
416
427
  const v = this._vertical();
417
428
  const back = v ? "ArrowUp" : "ArrowLeft";
418
429
  const fwd = v ? "ArrowDown" : "ArrowRight";
419
- if (e.key === fwd) {
430
+ const key = v ? e.key : inlineKey(e.key, this);
431
+ if (key === fwd) {
420
432
  e.preventDefault();
421
433
  this.next();
422
- } else if (e.key === back) {
434
+ } else if (key === back) {
423
435
  e.preventDefault();
424
436
  this.prev();
425
437
  } else if (e.key === "Home") {
@@ -19,7 +19,10 @@ export const checkStyles = css`
19
19
  align-items: flex-start;
20
20
  gap: calc(var(--_s) * 0.6);
21
21
  }
22
- :host([label-position="left"]) .root { flex-direction: row-reverse; justify-content: flex-end; }
22
+ /* The label before the box: start, or left/right as sides of the screen. */
23
+ :host([label-position="start"]) .root,
24
+ :host([label-position="left"]) .root:dir(ltr),
25
+ :host([label-position="right"]) .root:dir(rtl) { flex-direction: row-reverse; justify-content: flex-end; }
23
26
  .inner {
24
27
  position: relative;
25
28
  flex: none;
@@ -100,7 +103,7 @@ export class RayCheckbox extends FieldControl {
100
103
  value: { type: String, reflect: true, default: "on" },
101
104
  color: { type: String, reflect: true },
102
105
  variant: { type: String, reflect: true, default: "filled" },
103
- labelPosition: { type: String, reflect: true, default: "right" },
106
+ labelPosition: { type: String, reflect: true, default: "end" },
104
107
  };
105
108
  static styles = [sizeStyles, radiusStyles, fieldStyles, checkStyles, styles];
106
109
  static template = checkTemplate(`<span class="indicator" part="indicator" aria-hidden="true"><span class="check" data-ray-icon="ui.checked"></span><span class="dash" data-ray-icon="ui.indeterminate"></span></span>`);
@@ -15,7 +15,7 @@ const styles = css`
15
15
  .items { display: flex; flex-direction: column; gap: var(--ray-space-3); margin-top: var(--ray-space-2); }
16
16
  :host([orientation="horizontal"]) .items { flex-direction: row; flex-wrap: wrap; gap: var(--ray-space-3) var(--ray-space-5); }
17
17
  .label { margin-bottom: 0; }
18
- .count { margin-left: var(--ray-space-1); font-weight: 400; color: var(--ray-muted-foreground); font-variant-numeric: tabular-nums; }
18
+ .count { margin-inline-start: var(--ray-space-1); font-weight: 400; color: var(--ray-muted-foreground); font-variant-numeric: tabular-nums; }
19
19
  `;
20
20
 
21
21
  /** "a, b" (an attribute) or an array, as an array of values. */
@@ -293,7 +293,8 @@ const styles = css`
293
293
  align-items: center;
294
294
  gap: var(--ray-space-2);
295
295
  min-height: 2.5rem;
296
- padding: 0 var(--ray-space-2) 0 var(--ray-space-4);
296
+ padding-block: 0;
297
+ padding-inline: var(--ray-space-4) var(--ray-space-2);
297
298
  font-size: var(--ray-font-size-xs);
298
299
  color: var(--ray-muted-foreground);
299
300
  border-bottom: 1px solid var(--ray-border);
@@ -325,12 +326,13 @@ const styles = css`
325
326
  .copy-label { display: none; }
326
327
  .copy[data-copied] .copy-label { display: inline; }
327
328
  /* Without a header, the button floats in the top-end corner. */
328
- .copy.floating { position: absolute; z-index: 1; top: var(--ray-space-2); right: var(--ray-space-2); background: var(--ray-code-background); opacity: 0; transition: opacity var(--ray-duration-fast); }
329
+ .copy.floating { position: absolute; z-index: 1; top: var(--ray-space-2); inset-inline-end: var(--ray-space-2); background: var(--ray-code-background); opacity: 0; transition: opacity var(--ray-duration-fast); }
329
330
  .frame:hover .copy.floating, .copy.floating:focus-visible, .copy.floating[data-copied] { opacity: 1; }
330
331
  @media (hover: none) { .copy.floating { opacity: 1; } }
331
332
  .copy[hidden] { display: none; }
332
333
 
333
- .scroller { overflow: auto; max-height: var(--_max, none); outline: none; }
334
+ /* Code reads left to right, in a right-to-left page too. */
335
+ .scroller { direction: ltr; overflow: auto; max-height: var(--_max, none); outline: none; }
334
336
  .scroller:focus-visible { box-shadow: inset 0 0 0 2px var(--ray-ring); }
335
337
  :host([expanded]) .scroller { max-height: none; }
336
338
  pre { margin: 0; padding: var(--ray-space-3) 0; font-family: var(--ray-font-mono); font-size: var(--_fs); line-height: var(--_lh); tab-size: 2; }
@@ -339,19 +341,19 @@ const styles = css`
339
341
  .line { display: block; padding: 0 var(--ray-space-4); white-space: pre; border-inline-start: 2px solid transparent; }
340
342
  :host([wrap]) .line { white-space: pre-wrap; overflow-wrap: anywhere; }
341
343
  .line:empty::after { content: " "; }
342
- :host([line-numbers]) .line { counter-increment: line; padding-left: 0; }
344
+ :host([line-numbers]) .line { counter-increment: line; padding-inline-start: 0; }
343
345
  :host([line-numbers]) .line::before {
344
346
  content: counter(line);
345
347
  display: inline-block;
346
348
  width: calc(var(--_digits, 2) * 1ch + var(--ray-space-6));
347
349
  padding-inline-end: var(--ray-space-4);
348
350
  box-sizing: border-box;
349
- text-align: right;
351
+ text-align: end;
350
352
  color: var(--ray-code-comment);
351
353
  opacity: 0.7;
352
354
  user-select: none;
353
355
  }
354
- :host([line-numbers][wrap]) .line { padding-left: calc(var(--_digits, 2) * 1ch + var(--ray-space-6)); text-indent: calc((var(--_digits, 2) * 1ch + var(--ray-space-6)) * -1); }
356
+ :host([line-numbers][wrap]) .line { padding-inline-start: calc(var(--_digits, 2) * 1ch + var(--ray-space-6)); text-indent: calc((var(--_digits, 2) * 1ch + var(--ray-space-6)) * -1); }
355
357
  .line[data-highlight] { background: color-mix(in srgb, var(--ray-primary) 12%, transparent); border-inline-start-color: var(--ray-primary); }
356
358
  .line[data-diff="add"] { color: var(--ray-code-added); background: color-mix(in srgb, var(--ray-code-added) 12%, transparent); }
357
359
  .line[data-diff="remove"] { color: var(--ray-code-removed); background: color-mix(in srgb, var(--ray-code-removed) 12%, transparent); }
@@ -172,7 +172,7 @@ const pickerStyles = css`
172
172
  height: calc(var(--_w) * 0.62);
173
173
  background:
174
174
  linear-gradient(to top, #000, transparent),
175
- linear-gradient(to right, #fff, hsl(var(--_hue) 100% 50%));
175
+ linear-gradient(to right, #fff, hsl(var(--_hue) 100% 50%)); /* physical */
176
176
  border-radius: var(--ray-radius-sm);
177
177
  cursor: crosshair;
178
178
  touch-action: none;
@@ -180,7 +180,7 @@ const pickerStyles = css`
180
180
  :host([full-width]) .area { height: auto; aspect-ratio: 5 / 3; }
181
181
  .area-thumb {
182
182
  position: absolute;
183
- left: var(--_s);
183
+ left: var(--_s); /* physical */
184
184
  top: calc(100% - var(--_v));
185
185
  width: var(--_thumb);
186
186
  height: var(--_thumb);
@@ -209,13 +209,13 @@ const pickerStyles = css`
209
209
  inset: 0 calc(var(--_thumb) / -2);
210
210
  border-radius: inherit;
211
211
  }
212
- .hue::before { background: linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00); }
213
- .alpha::before { background: linear-gradient(to right, transparent, var(--_solid)), ${CHECKER}; }
212
+ .hue::before { background: linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00); } /* physical */
213
+ .alpha::before { background: linear-gradient(to right, transparent, var(--_solid)), ${CHECKER}; } /* physical */
214
214
  :host(:not([data-alpha])) .alpha { display: none; }
215
215
  .slider-thumb {
216
216
  position: absolute;
217
217
  top: 50%;
218
- left: var(--_pos);
218
+ left: var(--_pos); /* physical */
219
219
  width: var(--_thumb);
220
220
  height: var(--_thumb);
221
221
  box-sizing: border-box;
@@ -30,7 +30,8 @@ const styles = css`
30
30
  max-width: 100%;
31
31
  height: calc(var(--_h) - 0.75rem);
32
32
  min-height: 1.375rem;
33
- padding: 0 0.125rem 0 0.5rem;
33
+ padding-block: 0;
34
+ padding-inline: 0.5rem 0.125rem;
34
35
  font-size: calc(var(--_fs) - 0.0625rem);
35
36
  color: var(--ray-foreground);
36
37
  background: var(--ray-muted);
@@ -110,7 +110,7 @@ const styles = css`
110
110
  .hint b {
111
111
  display: inline-block;
112
112
  min-width: 1.25rem;
113
- margin-right: var(--ray-space-1);
113
+ margin-inline-end: var(--ray-space-1);
114
114
  padding: 0 0.3rem;
115
115
  font-family: var(--ray-font-mono);
116
116
  font-size: 0.6875rem;
@@ -12,6 +12,7 @@
12
12
  // arithmetic), so no time zone shifts a day.
13
13
 
14
14
  import { RayElement, css, define } from "../../core/core.js";
15
+ import { inlineKey } from "../../core/direction.js";
15
16
  import { sizeStyles, radiusStyles } from "../../core/styles.js";
16
17
  import { place, autoUpdate } from "../../core/position.js";
17
18
  import { dismissable } from "../../core/dismiss.js";
@@ -268,7 +269,8 @@ export class RayCalendar extends RayElement {
268
269
  const d = this._focusDate();
269
270
  const first = this.firstDay ?? firstDayOf(localeOf(this));
270
271
  let next = null;
271
- switch (e.key) {
272
+ // The grid follows the writing direction: right to left, ArrowLeft is the next day.
273
+ switch (inlineKey(e.key, this)) {
272
274
  case "ArrowLeft": next = addDays(d, -1); break;
273
275
  case "ArrowRight": next = addDays(d, 1); break;
274
276
  case "ArrowUp": next = addDays(d, -7); break;
@@ -321,7 +323,7 @@ const pickerStyles = css`
321
323
  gap: var(--ray-space-2);
322
324
  width: 100%;
323
325
  min-width: 0;
324
- text-align: left;
326
+ text-align: start;
325
327
  color: inherit;
326
328
  background: transparent;
327
329
  border: 0;
@@ -340,7 +342,7 @@ const pickerStyles = css`
340
342
  justify-content: center;
341
343
  width: calc(var(--_h) * 0.6);
342
344
  height: calc(var(--_h) * 0.6);
343
- margin-right: calc(var(--_px) * 0.4);
345
+ margin-inline-end: calc(var(--_px) * 0.4);
344
346
  padding: 0;
345
347
  color: var(--ray-muted-foreground);
346
348
  background: transparent;
@@ -66,7 +66,7 @@ const styles = css`
66
66
  justify-content: center;
67
67
  width: var(--ray-control-height-xs);
68
68
  height: var(--ray-control-height-xs);
69
- margin-left: auto;
69
+ margin-inline-start: auto;
70
70
  padding: 0;
71
71
  color: var(--ray-muted-foreground);
72
72
  background: transparent;
@@ -32,24 +32,41 @@ const styles = css`
32
32
  dialog[open] { display: flex; flex-direction: column; }
33
33
  dialog::backdrop { animation: ray-fade-in var(--ray-duration-base) var(--ray-easing-out); }
34
34
 
35
- :host(:not([position])) dialog, :host([position="right"]) dialog {
36
- inset: var(--_inset) var(--_inset) var(--_inset) auto;
35
+ /* position: start/end follow the writing direction, left/right are sides
36
+ of the screen. */
37
+ dialog {
38
+ inset-block: var(--_inset);
39
+ inset-inline: auto var(--_inset);
37
40
  width: min(var(--_size), calc(100vw - 2 * var(--_inset)));
38
41
  height: calc(100dvh - 2 * var(--_inset));
39
- border-left-width: 1px;
42
+ border-inline-start-width: 1px;
43
+ --_from: translateX(100%);
44
+ }
45
+ dialog:dir(rtl) { --_from: translateX(-100%); }
46
+ :host([position="start"]) dialog {
47
+ inset-inline: var(--_inset) auto;
48
+ border-inline-start-width: 0;
49
+ border-inline-end-width: 1px;
50
+ --_from: translateX(-100%);
51
+ }
52
+ :host([position="start"]) dialog:dir(rtl) { --_from: translateX(100%); }
53
+ :host([position="right"]) dialog {
54
+ inset: var(--_inset) var(--_inset) var(--_inset) auto; /* physical */
55
+ border-width: 0;
56
+ border-left-width: 1px; /* physical */
40
57
  --_from: translateX(100%);
41
58
  }
42
59
  :host([position="left"]) dialog {
43
- inset: var(--_inset) auto var(--_inset) var(--_inset);
44
- width: min(var(--_size), calc(100vw - 2 * var(--_inset)));
45
- height: calc(100dvh - 2 * var(--_inset));
46
- border-right-width: 1px;
60
+ inset: var(--_inset) auto var(--_inset) var(--_inset); /* physical */
61
+ border-width: 0;
62
+ border-right-width: 1px; /* physical */
47
63
  --_from: translateX(-100%);
48
64
  }
49
65
  :host([position="top"]) dialog {
50
66
  inset: var(--_inset) var(--_inset) auto var(--_inset);
51
67
  width: calc(100vw - 2 * var(--_inset));
52
68
  height: min(var(--_size), calc(100dvh - 2 * var(--_inset)));
69
+ border-width: 0;
53
70
  border-bottom-width: 1px;
54
71
  --_from: translateY(-100%);
55
72
  }
@@ -57,6 +74,7 @@ const styles = css`
57
74
  inset: auto var(--_inset) var(--_inset) var(--_inset);
58
75
  width: calc(100vw - 2 * var(--_inset));
59
76
  height: min(var(--_size), calc(100dvh - 2 * var(--_inset)));
77
+ border-width: 0;
60
78
  border-top-width: 1px;
61
79
  --_from: translateY(100%);
62
80
  }
@@ -83,7 +101,7 @@ const styles = css`
83
101
  justify-content: center;
84
102
  width: var(--ray-control-height-xs);
85
103
  height: var(--ray-control-height-xs);
86
- margin-left: auto;
104
+ margin-inline-start: auto;
87
105
  padding: 0;
88
106
  color: var(--ray-muted-foreground);
89
107
  background: transparent;
@@ -101,7 +119,7 @@ const styles = css`
101
119
  export class RayDrawer extends RayDialog {
102
120
  static properties = {
103
121
  ...RayDialog.properties,
104
- position: { type: String, reflect: true, default: "right" },
122
+ position: { type: String, reflect: true, default: "end" },
105
123
  size: { type: String, reflect: true, default: "md" },
106
124
  modeless: { type: Boolean, reflect: true },
107
125
  floating: { type: Boolean, reflect: true },
@@ -1,7 +1,8 @@
1
1
  // <ray-icon name="…">: an icon of the registry (setIcons), so the app's own
2
2
  // markup uses the same set as the components. Decorative unless `label`.
3
3
  // `filled` draws the icon's filled version (a state: active, selected,
4
- // favorite), or the outline one when it has none.
4
+ // favorite), or the outline one when it has none. `mirror` mirrors it right
5
+ // to left (a back arrow), as the roles that point along the text (ui.next…) are.
5
6
 
6
7
  import { RayElement, css, define } from "../../core/core.js";
7
8
 
@@ -13,6 +14,7 @@ const styles = css`
13
14
  size) must not reach the icon. size= overrides it inline. */
14
15
  :host { --_size: 1em; display: inline-flex; vertical-align: -0.125em; line-height: 1; }
15
16
  .icon svg { width: var(--_size); height: var(--_size); }
17
+ :host([mirror]) .icon:dir(rtl) > svg { transform: scaleX(-1); }
16
18
  `;
17
19
 
18
20
  export class RayIcon extends RayElement {
@@ -23,6 +25,7 @@ export class RayIcon extends RayElement {
23
25
  color: { type: String, reflect: true },
24
26
  stroke: { type: Number },
25
27
  filled: { type: Boolean, reflect: true },
28
+ mirror: { type: Boolean, reflect: true },
26
29
  };
27
30
  static styles = [styles];
28
31
  static template = `<span class="icon" part="icon" data-ray-icon=""></span>`;
@@ -21,7 +21,7 @@ const styles = css`
21
21
  position: absolute;
22
22
  z-index: 1;
23
23
  top: var(--_top);
24
- left: var(--_left);
24
+ inset-inline-start: var(--_left);
25
25
  display: inline-flex;
26
26
  align-items: center;
27
27
  justify-content: center;
@@ -34,6 +34,7 @@ const styles = css`
34
34
  transform: translate(-50%, -50%);
35
35
  pointer-events: none;
36
36
  }
37
+ .indicator:dir(rtl) { transform: translate(50%, -50%); }
37
38
  .indicator[data-label] {
38
39
  height: auto;
39
40
  min-height: var(--_size);
@@ -230,7 +230,7 @@ const itemStyles = css`
230
230
  .section { display: inline-flex; align-items: center; color: var(--ray-muted-foreground); }
231
231
  :host([color]) .section { color: inherit; }
232
232
  .section:not([data-filled]) { display: none; }
233
- .right { margin-left: var(--ray-space-6); font-size: var(--ray-font-size-xs); }
233
+ .right { margin-inline-start: var(--ray-space-6); font-size: var(--ray-font-size-xs); }
234
234
  ::slotted(svg) { width: 1rem; height: 1rem; }
235
235
  `;
236
236
 
@@ -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: left;
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 0 2px var(--ray-space-5); }
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) var(--ray-space-3) var(--ray-space-3) var(--ray-space-4);
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-left: var(--ray-space-3); }
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
- :host([position$="right"]), :host(:not([position])) { right: 0; }
205
- :host([position$="left"]) { left: 0; }
206
- :host([position$="center"]) { left: 50%; transform: translateX(-50%); }
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
- left: var(--ray-space-4);
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; left: 0; right: 0; transform: none; padding: var(--ray-space-3); }
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(*) { left: var(--ray-space-3); right: var(--ray-space-3); }
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-right" },
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-right").startsWith("top");
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-right").startsWith("top");
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}%)`;