@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
@@ -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";
@@ -144,9 +145,9 @@ export class RayCalendar extends RayElement {
144
145
  static styles = [sizeStyles, calendarStyles];
145
146
  static template = `
146
147
  <div class="header" part="header">
147
- <button class="nav prev focus-ring" part="nav" type="button" data-ray-icon="chevron-left"></button>
148
+ <button class="nav prev focus-ring" part="nav" type="button" data-ray-icon="ui.previous"></button>
148
149
  <div class="caption" part="caption" id="caption" aria-live="polite"></div>
149
- <button class="nav next focus-ring" part="nav" type="button" data-ray-icon="chevron-right"></button>
150
+ <button class="nav next focus-ring" part="nav" type="button" data-ray-icon="ui.next"></button>
150
151
  </div>
151
152
  <table role="grid" aria-labelledby="caption" part="grid"><thead><tr></tr></thead><tbody></tbody></table>`;
152
153
 
@@ -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;
@@ -382,8 +384,8 @@ export class RayDatePicker extends FieldControl {
382
384
  static template = fieldTemplate(
383
385
  `<input id="control" class="native" part="input" type="date" tabindex="-1" aria-hidden="true">` +
384
386
  `<button type="button" id="trigger" class="control trigger" part="trigger" aria-haspopup="dialog" aria-expanded="false" aria-controls="panel">` +
385
- `<span class="icon" aria-hidden="true" data-ray-icon="calendar"></span><span class="value" id="value"></span></button>` +
386
- `<button type="button" class="clear focus-ring" part="clear" aria-label="Clear date" hidden><span data-ray-icon="close" aria-hidden="true"></span></button>`,
387
+ `<span class="icon" aria-hidden="true" data-ray-icon="ui.calendar"></span><span class="value" id="value"></span></button>` +
388
+ `<button type="button" class="clear focus-ring" part="clear" aria-label="Clear date" hidden><span data-ray-icon="ui.clear" aria-hidden="true"></span></button>`,
387
389
  { sections: false },
388
390
  ) + `<div class="panel" id="panel" part="dropdown" popover="manual" role="dialog" aria-label="Choose a date"><ray-calendar part="calendar" exportparts="day, caption, nav, grid"></ray-calendar></div>`;
389
391
 
@@ -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;
@@ -106,7 +106,7 @@ export class RayDialog extends RayElement {
106
106
  <dialog part="dialog" aria-labelledby="title">
107
107
  <header class="header" part="header">
108
108
  <div class="heading"><h2 class="title" id="title" part="title"></h2><p class="description" id="description" part="description" hidden></p></div>
109
- <div class="actions"><slot name="header"></slot><button class="close focus-ring" part="close" type="button" aria-label="Close"><span data-ray-icon="close" aria-hidden="true"></span></button></div>
109
+ <div class="actions"><slot name="header"></slot><button class="close focus-ring" part="close" type="button" aria-label="Close"><span data-ray-icon="ui.close" aria-hidden="true"></span></button></div>
110
110
  </header>
111
111
  <div class="body" part="body"><slot></slot></div>
112
112
  <footer class="footer" part="footer"><slot name="footer"></slot></footer>
@@ -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 },
@@ -111,7 +129,7 @@ export class RayDrawer extends RayDialog {
111
129
  <dialog part="drawer" aria-labelledby="title">
112
130
  <header class="header" part="header">
113
131
  <div class="heading"><h2 class="title" id="title" part="title"></h2><p class="description" id="description" part="description" hidden></p></div>
114
- <div class="actions"><slot name="header"></slot><button class="close focus-ring" part="close" type="button" aria-label="Close"><span data-ray-icon="close" aria-hidden="true"></span></button></div>
132
+ <div class="actions"><slot name="header"></slot><button class="close focus-ring" part="close" type="button" aria-label="Close"><span data-ray-icon="ui.close" aria-hidden="true"></span></button></div>
115
133
  </header>
116
134
  <div class="body" part="body"><slot></slot></div>
117
135
  <footer class="footer" part="footer"><slot name="footer"></slot></footer>
@@ -233,7 +233,7 @@ export class RayFileUpload extends FormControl {
233
233
  <span class="label" part="label" id="label" hidden><span class="label-text"></span><span class="required" aria-hidden="true" hidden> *</span></span>
234
234
  <div class="description" part="description" id="description" hidden></div>
235
235
  <button type="button" class="zone" part="dropzone" aria-labelledby="label title hint pill" aria-describedby="description error">
236
- <span class="glyph" part="icon"><slot name="icon"><span data-ray-icon="upload"></span></slot></span>
236
+ <span class="glyph" part="icon"><slot name="icon"><span data-ray-icon="ui.upload"></span></slot></span>
237
237
  <span class="copy">
238
238
  <span class="title" id="title"></span>
239
239
  <span class="hint" id="hint" part="hint"></span>
@@ -1,5 +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
+ // `filled` draws the icon's filled version (a state: active, selected,
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.
3
6
 
4
7
  import { RayElement, css, define } from "../../core/core.js";
5
8
 
@@ -11,6 +14,7 @@ const styles = css`
11
14
  size) must not reach the icon. size= overrides it inline. */
12
15
  :host { --_size: 1em; display: inline-flex; vertical-align: -0.125em; line-height: 1; }
13
16
  .icon svg { width: var(--_size); height: var(--_size); }
17
+ :host([mirror]) .icon:dir(rtl) > svg { transform: scaleX(-1); }
14
18
  `;
15
19
 
16
20
  export class RayIcon extends RayElement {
@@ -20,6 +24,8 @@ export class RayIcon extends RayElement {
20
24
  label: { type: String },
21
25
  color: { type: String, reflect: true },
22
26
  stroke: { type: Number },
27
+ filled: { type: Boolean, reflect: true },
28
+ mirror: { type: Boolean, reflect: true },
23
29
  };
24
30
  static styles = [styles];
25
31
  static template = `<span class="icon" part="icon" data-ray-icon=""></span>`;
@@ -31,8 +37,10 @@ export class RayIcon extends RayElement {
31
37
 
32
38
  update() {
33
39
  const icon = this.$(".icon");
34
- if (icon.getAttribute("data-ray-icon") !== (this.name ?? "")) {
40
+ const filled = Boolean(this.filled);
41
+ if (icon.getAttribute("data-ray-icon") !== (this.name ?? "") || icon.hasAttribute("data-ray-icon-filled") !== filled) {
35
42
  icon.setAttribute("data-ray-icon", this.name ?? "");
43
+ icon.toggleAttribute("data-ray-icon-filled", filled);
36
44
  this.paintIcons();
37
45
  }
38
46
  // xs…xl from the scale, or any CSS length.
@@ -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,14 +60,14 @@ 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 = `
65
67
  <span class="section" part="left-section"><slot name="left"></slot></span>
66
68
  <span class="body"><span class="label" part="label"></span><span class="description" part="description"></span></span>
67
69
  <span class="section right" part="right-section"><slot name="right"></slot></span>
68
- <span class="chevron" aria-hidden="true" data-ray-icon="chevron-right"></span>`;
70
+ <span class="chevron" aria-hidden="true" data-ray-icon="ui.disclosure"></span>`;
69
71
 
70
72
  export class RayNavLink extends RayElement {
71
73
  static delegatesFocus = true;
@@ -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;
@@ -150,7 +151,7 @@ export class RayNotification extends RayElement {
150
151
  <div class="message" part="message"><slot></slot></div>
151
152
  </div>
152
153
  <button class="action focus-ring" part="action" type="button" hidden></button>
153
- <button class="close focus-ring" part="close" type="button" aria-label="Close notification"><span data-ray-icon="close" aria-hidden="true"></span></button>
154
+ <button class="close focus-ring" part="close" type="button" aria-label="Close notification"><span data-ray-icon="ui.close" aria-hidden="true"></span></button>
154
155
  </div>`;
155
156
 
156
157
  constructor() {
@@ -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}%)`;
@@ -115,7 +115,7 @@ export class RayNumberInput extends FieldControl {
115
115
  static styles = [sizeStyles, radiusStyles, fieldStyles, boxStyles, styles];
116
116
  static template = fieldTemplate(
117
117
  `<input id="control" class="control" part="input" role="spinbutton" inputmode="decimal" autocomplete="off" spellcheck="false">` +
118
- `<span class="steppers" part="controls"><button type="button" class="step up" part="increment" tabindex="-1" aria-label="Increment"><span data-ray-icon="chevron-up"></span></button><button type="button" class="step down" part="decrement" tabindex="-1" aria-label="Decrement"><span data-ray-icon="chevron-down"></span></button></span>`,
118
+ `<span class="steppers" part="controls"><button type="button" class="step up" part="increment" tabindex="-1" aria-label="Increment"><span data-ray-icon="ui.increment"></span></button><button type="button" class="step down" part="decrement" tabindex="-1" aria-label="Decrement"><span data-ray-icon="ui.decrement"></span></button></span>`,
119
119
  );
120
120
 
121
121
  #typing = false;
@@ -139,15 +139,15 @@ export class RayPagination extends RayElement {
139
139
  const items = [];
140
140
  const control = (role, target, icon, label, off) => ({ kind: "control", role, target, icon, label, off });
141
141
  if (!this.hideControls) {
142
- if (this.withEdges) items.push(control("first", 1, "chevrons-left", "First page", page <= 1));
143
- items.push(control("previous", page - 1, "chevron-left", "Previous page", page <= 1));
142
+ if (this.withEdges) items.push(control("first", 1, "ui.first", "First page", page <= 1));
143
+ items.push(control("previous", page - 1, "ui.previous", "Previous page", page <= 1));
144
144
  }
145
145
  for (const it of paginationRange(total, page, this.siblings ?? 1, this.boundaries ?? 1)) {
146
146
  items.push(it === "dots" ? { kind: "dots" } : { kind: "page", target: it });
147
147
  }
148
148
  if (!this.hideControls) {
149
- items.push(control("next", page + 1, "chevron-right", "Next page", page >= total));
150
- if (this.withEdges) items.push(control("last", total, "chevrons-right", "Last page", page >= total));
149
+ items.push(control("next", page + 1, "ui.next", "Next page", page >= total));
150
+ if (this.withEdges) items.push(control("last", total, "ui.last", "Last page", page >= total));
151
151
  }
152
152
  list.replaceChildren(...items.map((it) => this._item(it, page)));
153
153
  this.paintIcons(list);
@@ -43,7 +43,7 @@ export class RayPasswordInput extends RayInput {
43
43
  static styles = [...RayInput.styles, styles];
44
44
  static template = fieldTemplate(
45
45
  `<input id="control" class="control" part="input" type="password" autocomplete="current-password" spellcheck="false" autocapitalize="off">` +
46
- `<button type="button" class="toggle focus-ring" part="toggle" aria-pressed="false"><span data-ray-icon="eye" aria-hidden="true"></span></button>`,
46
+ `<button type="button" class="toggle focus-ring" part="toggle" aria-pressed="false"><span data-ray-icon="ui.show-password" aria-hidden="true"></span></button>`,
47
47
  );
48
48
 
49
49
  wire() {
@@ -69,7 +69,7 @@ export class RayPasswordInput extends RayInput {
69
69
  toggle.title = toggle.getAttribute("aria-label");
70
70
  toggle.disabled = this.effectivelyDisabled;
71
71
  const icon = toggle.firstElementChild;
72
- const name = this.visible ? "eye-off" : "eye";
72
+ const name = this.visible ? "ui.hide-password" : "ui.show-password";
73
73
  if (icon.dataset.rayIcon !== name) {
74
74
  icon.dataset.rayIcon = name;
75
75
  this.paintIcons(toggle);
@@ -42,7 +42,7 @@ const styles = css`
42
42
  .value {
43
43
  flex: none;
44
44
  min-width: 2.75em;
45
- text-align: right;
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 { left: -40%; } to { left: 100%; } }
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 (e.key === "ArrowDown" || e.key === "ArrowRight") next = radios[(i + 1) % radios.length];
141
- else if (e.key === "ArrowUp" || e.key === "ArrowLeft") next = radios[(i - 1 + radios.length) % radios.length];
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 { left: 0; right: 0; height: 0.75rem; }
51
- .left, .right { top: 0; bottom: 0; width: 0.75rem; }
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
- .left { left: 0; background: linear-gradient(to right, var(--_shadow), transparent); }
55
- .right { right: 0; background: linear-gradient(to left, var(--_shadow), transparent); }
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 left" part="shadow"></span><span class="edge right" part="shadow"></span>`;
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/left). */
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("left", x > 1);
148
- show("right", maxX > 1 && x < maxX - 1);
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 ? "right" : "bottom" });
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-left: calc(var(--_px) * 0.66);
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-left: calc(var(--_px) * 0.5); }
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-right: calc(var(--_px) * 0.4); }
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;
@@ -62,9 +62,9 @@ export class RaySearch extends RayInput {
62
62
  };
63
63
  static styles = [...RayInput.styles, styles];
64
64
  static template = fieldTemplate(
65
- `<span class="lead" part="icon" aria-hidden="true" data-ray-icon="search"></span>` +
65
+ `<span class="lead" part="icon" aria-hidden="true" data-ray-icon="ui.search"></span>` +
66
66
  `<input id="control" class="control" part="input" type="search" enterkeyhint="search" autocomplete="off" spellcheck="false">` +
67
- `<span class="tail"><button class="clear focus-ring" part="clear" type="button" aria-label="Clear search" tabindex="-1" hidden><span data-ray-icon="close" aria-hidden="true"></span></button>` +
67
+ `<span class="tail"><button class="clear focus-ring" part="clear" type="button" aria-label="Clear search" tabindex="-1" hidden><span data-ray-icon="ui.clear" aria-hidden="true"></span></button>` +
68
68
  `<kbd class="shortcut" part="shortcut" aria-hidden="true" hidden></kbd></span>`,
69
69
  );
70
70
 
@@ -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 (e.key === "ArrowRight" || e.key === "ArrowDown") next = enabled[(i + 1) % enabled.length];
175
- else if (e.key === "ArrowLeft" || e.key === "ArrowUp") next = enabled[(i - 1 + enabled.length) % enabled.length];
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-right: calc(var(--_h) * 0.8);
34
- text-align: left;
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
- right: 0;
46
+ inset-inline-end: 0;
47
47
  display: inline-flex;
48
48
  align-items: center;
49
49
  justify-content: center;
@@ -101,7 +101,7 @@ const styles = css`
101
101
  .empty { padding: var(--ray-space-2); color: var(--ray-muted-foreground); }
102
102
  `;
103
103
 
104
- const CHEVRON = `<span class="chevron" aria-hidden="true" data-ray-icon="chevron-down"></span>`;
104
+ const CHEVRON = `<span class="chevron" aria-hidden="true" data-ray-icon="ui.expand"></span>`;
105
105
 
106
106
  export class RaySelect extends FieldControl {
107
107
  static properties = {
@@ -261,7 +261,7 @@ export class RaySelect extends FieldControl {
261
261
  const group = o.parentElement?.localName === "optgroup" ? o.parentElement : null;
262
262
  const disabled = o.disabled || Boolean(group?.disabled);
263
263
  el.setAttribute("aria-disabled", String(disabled));
264
- el.innerHTML = `<span class="text"></span><span class="mark" aria-hidden="true" data-ray-icon="check"></span>`;
264
+ el.innerHTML = `<span class="text"></span><span class="mark" aria-hidden="true" data-ray-icon="ui.checked"></span>`;
265
265
  el.firstChild.textContent = o.label || o.text;
266
266
  this.#items.push({ el, value: o.value, disabled, text: (o.label || o.text).trim().toLowerCase() });
267
267
  return el;