@raylang/elements 0.0.0-stage → 0.3.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 (95) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +4 -0
  3. package/README.md +12 -2
  4. package/base.css +111 -0
  5. package/components/accordion/accordion.js +197 -0
  6. package/components/alert/alert.js +112 -0
  7. package/components/app-bar/app-bar.js +168 -0
  8. package/components/app-shell/app-shell.js +198 -0
  9. package/components/aspect-ratio/aspect-ratio.js +59 -0
  10. package/components/attachment/attachment.js +333 -0
  11. package/components/avatar/avatar.js +96 -0
  12. package/components/badge/badge.js +63 -0
  13. package/components/blockquote/blockquote.js +128 -0
  14. package/components/bottom-nav/bottom-nav.js +309 -0
  15. package/components/breadcrumbs/breadcrumbs.js +175 -0
  16. package/components/button/button.js +283 -0
  17. package/components/button-group/button-group.js +72 -0
  18. package/components/card/card.js +73 -0
  19. package/components/carousel/carousel.js +435 -0
  20. package/components/center/center.js +26 -0
  21. package/components/checkbox/checkbox.js +152 -0
  22. package/components/checkbox-group/checkbox-group.js +152 -0
  23. package/components/code/code.js +574 -0
  24. package/components/color-picker/color-picker.js +775 -0
  25. package/components/combobox/combobox.js +563 -0
  26. package/components/command-palette/command-palette.js +510 -0
  27. package/components/container/container.js +41 -0
  28. package/components/date-picker/date-picker.js +594 -0
  29. package/components/dialog/dialog.js +329 -0
  30. package/components/drawer/drawer.js +139 -0
  31. package/components/empty-state/empty-state.js +66 -0
  32. package/components/file-upload/file-upload.js +589 -0
  33. package/components/grid/grid.js +96 -0
  34. package/components/group/group.js +40 -0
  35. package/components/icon/icon.js +58 -0
  36. package/components/icon-button/icon-button.js +30 -0
  37. package/components/indicator/indicator.js +112 -0
  38. package/components/input/input.js +85 -0
  39. package/components/kbd/kbd.js +60 -0
  40. package/components/menu/menu.js +296 -0
  41. package/components/nav-link/nav-link.js +159 -0
  42. package/components/notification/notification.js +591 -0
  43. package/components/number-input/number-input.js +241 -0
  44. package/components/pagination/pagination.js +194 -0
  45. package/components/password-input/password-input.js +85 -0
  46. package/components/path-input/path-input.js +137 -0
  47. package/components/popover/popover.js +85 -0
  48. package/components/progress/progress.js +109 -0
  49. package/components/radio/radio.js +253 -0
  50. package/components/scroll-area/scroll-area.js +158 -0
  51. package/components/search/search.js +157 -0
  52. package/components/segmented-control/segmented-control.js +190 -0
  53. package/components/select/select.js +506 -0
  54. package/components/separator/separator.js +58 -0
  55. package/components/sidebar/sidebar.js +252 -0
  56. package/components/skeleton/skeleton.js +90 -0
  57. package/components/slider/slider.js +155 -0
  58. package/components/spinner/spinner.js +39 -0
  59. package/components/split/split.js +221 -0
  60. package/components/stack/stack.js +46 -0
  61. package/components/stat/stat.js +79 -0
  62. package/components/statusbar/statusbar.js +110 -0
  63. package/components/stepper/stepper.js +239 -0
  64. package/components/switch/switch.js +45 -0
  65. package/components/table/table.js +328 -0
  66. package/components/tabs/tabs.js +224 -0
  67. package/components/textarea/textarea.js +105 -0
  68. package/components/theme-toggle/theme-toggle.js +78 -0
  69. package/components/timeline/timeline.js +150 -0
  70. package/components/toggle/toggle.js +130 -0
  71. package/components/toggle-group/toggle-group.js +216 -0
  72. package/components/tooltip/tooltip.js +101 -0
  73. package/core/charts.js +107 -0
  74. package/core/color.js +110 -0
  75. package/core/core.js +556 -0
  76. package/core/dismiss.js +69 -0
  77. package/core/floating.js +195 -0
  78. package/core/focus.js +70 -0
  79. package/core/form.js +235 -0
  80. package/core/host.js +200 -0
  81. package/core/hotkey.js +40 -0
  82. package/core/icons.js +104 -0
  83. package/core/motion.js +73 -0
  84. package/core/position.js +91 -0
  85. package/core/raylang.js +167 -0
  86. package/core/scroll.js +58 -0
  87. package/core/styles.js +211 -0
  88. package/core.js +16 -0
  89. package/custom-elements.json +18545 -0
  90. package/index.js +74 -0
  91. package/package.json +312 -4
  92. package/ray-elements.d.ts +2570 -0
  93. package/tokens.css +796 -0
  94. package/tokens.json +2083 -0
  95. package/vscode.html-custom-data.json +5499 -0
@@ -0,0 +1,252 @@
1
+ // <ray-sidebar>: the side navigation of an app shell (shadcn's Sidebar).
2
+ // Header, content and footer slots; on wide screens a panel that can
3
+ // collapse to an icon rail (`collapsed`); below `breakpoint` an off-canvas
4
+ // drawer (`open`): a modal <dialog> sliding in from its side, with focus
5
+ // trapped, Escape and backdrop click closing it.
6
+ //
7
+ // toggle() does the right thing for the screen: collapse/expand on wide
8
+ // screens, open/close the drawer on narrow ones — wire it to a menu button.
9
+ //
10
+ // <ray-sidebar-group label="Projects">: a titled section of nav links; the
11
+ // title hides when the sidebar collapses.
12
+
13
+ import { RayElement, css, define } from "../../core/core.js";
14
+ import { overlayStyles } from "../../core/styles.js";
15
+ import { play, playAll } from "../../core/motion.js";
16
+
17
+ const styles = css`
18
+ :host {
19
+ --_w: 16rem;
20
+ --_rail: 3.5rem;
21
+ display: block;
22
+ flex: none;
23
+ width: var(--_w);
24
+ height: 100%;
25
+ min-height: 0;
26
+ transition: width var(--ray-duration-base) var(--ray-easing-standard);
27
+ }
28
+ :host([collapsed]:not([data-mobile])) { width: var(--_rail); }
29
+ :host([data-mobile]) { width: 0; }
30
+ .panel {
31
+ box-sizing: border-box;
32
+ display: flex;
33
+ flex-direction: column;
34
+ width: 100%;
35
+ height: 100%;
36
+ overflow: hidden;
37
+ color: var(--ray-foreground);
38
+ background: var(--ray-sidebar, color-mix(in srgb, var(--ray-muted) 70%, var(--ray-background)));
39
+ border-inline-end: 1px solid var(--ray-border);
40
+ }
41
+ :host([side="right"]) .panel { border-inline-end: 0; border-inline-start: 1px solid var(--ray-border); }
42
+ :host([variant="floating"]:not([data-mobile])) { padding: var(--ray-space-2); box-sizing: border-box; }
43
+ :host([variant="floating"]) .panel { border: 1px solid var(--ray-border); border-radius: var(--ray-radius-lg); box-shadow: var(--ray-shadow-sm); }
44
+ .header, .footer { flex: none; padding: var(--ray-space-2); }
45
+ .header:not([data-filled]), .footer:not([data-filled]) { display: none; }
46
+ .content { flex: 1; min-height: 0; overflow-x: hidden; overflow-y: auto; padding: var(--ray-space-2); }
47
+
48
+ /* Narrow screens: the panel lives in a modal dialog at the edge. */
49
+ dialog {
50
+ --_from: -100%;
51
+ position: fixed;
52
+ inset: 0 auto 0 0;
53
+ box-sizing: border-box;
54
+ width: min(var(--_w), calc(100vw - 3rem));
55
+ max-width: none;
56
+ height: 100dvh;
57
+ max-height: none;
58
+ margin: 0;
59
+ padding: 0;
60
+ border: 0;
61
+ background: transparent;
62
+ overflow: visible;
63
+ }
64
+ :host([side="right"]) dialog { inset: 0 0 0 auto; }
65
+ dialog[open] { display: block; animation: ray-sidebar-in var(--ray-duration-slow) var(--ray-easing-out); }
66
+ dialog .panel { box-shadow: var(--ray-shadow-xl); background: var(--ray-card); }
67
+ @keyframes ray-sidebar-in { from { transform: translateX(var(--_from)); } }
68
+ :host([side="right"]) dialog { --_from: 100%; }
69
+ dialog::backdrop { animation: ray-fade-in var(--ray-duration-base) var(--ray-easing-out); }
70
+ @keyframes ray-fade-in { from { opacity: 0; } }
71
+ `;
72
+
73
+ export class RaySidebar extends RayElement {
74
+ static properties = {
75
+ collapsed: { type: Boolean, reflect: true },
76
+ open: { type: Boolean, reflect: true },
77
+ side: { type: String, reflect: true, default: "left" },
78
+ variant: { type: String, reflect: true, default: "sidebar" },
79
+ width: { type: String },
80
+ collapsedWidth: { type: String },
81
+ breakpoint: { type: String, default: "48em" },
82
+ label: { type: String, default: "Sidebar" },
83
+ };
84
+ static styles = [overlayStyles, styles];
85
+ static template = `
86
+ <div class="panel" part="panel">
87
+ <div class="header" part="header"><slot name="header"></slot></div>
88
+ <nav class="content" part="content"><slot></slot></nav>
89
+ <div class="footer" part="footer"><slot name="footer"></slot></div>
90
+ </div>
91
+ <dialog part="drawer"></dialog>`;
92
+
93
+ #mq = null;
94
+ #exit = null;
95
+
96
+ constructor() {
97
+ super();
98
+ this.panel = this.$(".panel");
99
+ this.dialog = this.$("dialog");
100
+ for (const name of ["header", "footer"]) {
101
+ const slot = this.$(`slot[name="${name}"]`);
102
+ slot.addEventListener("slotchange", () => slot.parentElement.toggleAttribute("data-filled", slot.assignedNodes().length > 0));
103
+ }
104
+ this.dialog.addEventListener("cancel", (e) => {
105
+ e.preventDefault();
106
+ this.open = false;
107
+ });
108
+ this.dialog.addEventListener("close", () => {
109
+ if (this.open) this.commitProperty("open", false);
110
+ });
111
+ // A click on the backdrop (outside the panel) closes the drawer.
112
+ this.dialog.addEventListener("click", (e) => {
113
+ if (e.target === this.dialog) this.open = false;
114
+ });
115
+ // Following a link closes the drawer.
116
+ this.addEventListener("click", (e) => {
117
+ if (!this.mobile || !this.open) return;
118
+ const link = e.composedPath().find((n) => n.localName === "a" && n.href);
119
+ if (link) this.open = false;
120
+ });
121
+ // New nav links and groups pick up the collapsed state.
122
+ this.$(".content slot").addEventListener("slotchange", () => this._syncCollapsed());
123
+ }
124
+
125
+ connectedCallback() {
126
+ super.connectedCallback();
127
+ this._watch();
128
+ }
129
+
130
+ disconnectedCallback() {
131
+ super.disconnectedCallback();
132
+ this.#mq?.removeEventListener("change", this._onMedia);
133
+ this.#mq = null;
134
+ if (this.dialog.open) this.dialog.close();
135
+ }
136
+
137
+ /** Whether the sidebar is in its narrow-screen (drawer) mode. */
138
+ get mobile() {
139
+ return this.hasAttribute("data-mobile");
140
+ }
141
+
142
+ /** Collapses/expands on wide screens; opens/closes the drawer on narrow ones. */
143
+ toggle() {
144
+ if (this.mobile) this.open = !this.open;
145
+ else this.collapsed = !this.collapsed;
146
+ this.emit("ray-toggle", { collapsed: Boolean(this.collapsed), open: Boolean(this.open), mobile: this.mobile });
147
+ }
148
+
149
+ _watch() {
150
+ this.#mq?.removeEventListener("change", this._onMedia);
151
+ this.#mq = matchMedia(`(max-width: ${this.breakpoint || "48em"})`);
152
+ this._onMedia = () => this._setMobile(this.#mq.matches);
153
+ this.#mq.addEventListener("change", this._onMedia);
154
+ this._setMobile(this.#mq.matches);
155
+ }
156
+
157
+ _setMobile(on) {
158
+ this.toggleAttribute("data-mobile", on);
159
+ if (on) {
160
+ if (this.panel.parentNode !== this.dialog) this.dialog.append(this.panel);
161
+ if (this.open && !this.dialog.open) this._show();
162
+ } else {
163
+ if (this.dialog.open) this.dialog.close();
164
+ if (this.panel.parentNode !== this.shadowRoot) this.shadowRoot.prepend(this.panel);
165
+ if (this.open) this.commitProperty("open", false);
166
+ }
167
+ this._syncCollapsed();
168
+ }
169
+
170
+ update(changed) {
171
+ if (this.width) this.style.setProperty("--_w", this.width);
172
+ else this.style.removeProperty("--_w");
173
+ if (this.collapsedWidth) this.style.setProperty("--_rail", this.collapsedWidth);
174
+ else this.style.removeProperty("--_rail");
175
+ this.$(".content").setAttribute("aria-label", this.label ?? "Sidebar");
176
+ if (changed.has("breakpoint") && this.isConnected) this._watch();
177
+ if (changed.has("collapsed")) this._syncCollapsed();
178
+ if (changed.has("open") && this.mobile) {
179
+ if (this.open && this.#exit) {
180
+ this.#exit.cancel();
181
+ this.#exit = null;
182
+ } else if (this.open && !this.dialog.open) {
183
+ this._show();
184
+ } else if (!this.open && this.dialog.open && !this.#exit) {
185
+ this._leave();
186
+ }
187
+ }
188
+ }
189
+
190
+ _show() {
191
+ this.dialog.showModal();
192
+ this.emit("ray-open");
193
+ }
194
+
195
+ async _leave() {
196
+ const to = this.side === "right" ? "translateX(100%)" : "translateX(-100%)";
197
+ const exit = playAll([
198
+ play(this.dialog, [{ transform: "none" }, { transform: to }]),
199
+ play(this.dialog, [{ opacity: 1 }, { opacity: 0 }], { pseudoElement: "::backdrop" }),
200
+ ]);
201
+ this.#exit = exit;
202
+ await exit.finished;
203
+ if (this.#exit !== exit) return;
204
+ this.#exit = null;
205
+ if (!this.open && this.dialog.open) this.dialog.close();
206
+ exit.cancel();
207
+ this.emit("ray-close");
208
+ }
209
+
210
+ // The rail shows icons only: nav links and group titles follow.
211
+ _syncCollapsed() {
212
+ const rail = Boolean(this.collapsed) && !this.mobile;
213
+ for (const el of this.querySelectorAll("ray-nav-link")) el.iconOnly = rail;
214
+ for (const el of this.querySelectorAll("ray-sidebar-group")) el.toggleAttribute("collapsed", rail);
215
+ }
216
+ }
217
+
218
+ const groupStyles = css`
219
+ :host { display: block; }
220
+ :host(:not(:first-child)) { margin-top: var(--ray-space-3); }
221
+ .label {
222
+ display: block;
223
+ height: 1.75rem;
224
+ padding: 0 var(--ray-space-3);
225
+ font-size: var(--ray-font-size-xs);
226
+ font-weight: var(--ray-font-weight-medium);
227
+ line-height: 1.75rem;
228
+ color: var(--ray-muted-foreground);
229
+ white-space: nowrap;
230
+ overflow: hidden;
231
+ transition: opacity var(--ray-duration-fast) var(--ray-easing-standard);
232
+ }
233
+ .label:empty { display: none; }
234
+ /* Collapsed: the title fades but keeps its height, so icons do not jump. */
235
+ :host([collapsed]) .label { opacity: 0; }
236
+ .items { display: grid; gap: 2px; }
237
+ `;
238
+
239
+ export class RaySidebarGroup extends RayElement {
240
+ static properties = {
241
+ label: { type: String },
242
+ };
243
+ static styles = [groupStyles];
244
+ static template = `<div class="label" part="label" id="label"></div><div class="items" part="items" role="group" aria-labelledby="label"><slot></slot></div>`;
245
+
246
+ update() {
247
+ this.$(".label").textContent = this.label ?? "";
248
+ }
249
+ }
250
+
251
+ define("ray-sidebar", RaySidebar);
252
+ define("ray-sidebar-group", RaySidebarGroup);
@@ -0,0 +1,90 @@
1
+ // <ray-skeleton>: a placeholder shown while content loads (Mantine's
2
+ // Skeleton). A shimmering block, a circle, or a few text lines; with the
3
+ // `loaded` attribute it shows its slotted content instead.
4
+
5
+ import { RayElement, css, define } from "../../core/core.js";
6
+
7
+ const RADII = ["xs", "sm", "md", "lg", "xl", "full"];
8
+
9
+ const styles = css`
10
+ :host {
11
+ display: block;
12
+ --_w: 100%;
13
+ --_h: 1rem;
14
+ --_r: var(--ray-radius-sm);
15
+ /* Visible on the page and on cards in both schemes; the sweep moves
16
+ toward the background color. */
17
+ --_base: color-mix(in srgb, var(--ray-muted) 45%, var(--ray-border));
18
+ --_shine: color-mix(in srgb, var(--_base) 45%, var(--ray-background));
19
+ }
20
+ :host([hidden]) { display: none; }
21
+ :host([circle]) { --_r: 50%; --_w: 2.5rem; }
22
+ :host([circle]) { width: max-content; }
23
+ .bones { display: grid; gap: var(--ray-space-2); width: var(--_w); }
24
+ .bone {
25
+ height: var(--_h);
26
+ border-radius: var(--_r);
27
+ background: var(--_base);
28
+ }
29
+ :host([circle]) .bones { width: var(--_w); }
30
+ :host([circle]) .bone { width: var(--_w); height: var(--_w); }
31
+ :host([lines]) .bone:last-child:not(:first-child) { width: 70%; }
32
+ /* animation: shimmer (default) | pulse | none. Not "animate": that would
33
+ shadow Element.prototype.animate(). */
34
+ :host(:not([animation])) .bone, :host([animation="shimmer"]) .bone {
35
+ background: linear-gradient(90deg, var(--_base) 25%, var(--_shine) 50%, var(--_base) 75%) 0 0 / 300% 100%;
36
+ animation: ray-skeleton-shine 1.6s var(--ray-easing-standard) infinite;
37
+ }
38
+ :host([animation="pulse"]) .bone { animation: ray-skeleton-pulse 1.6s var(--ray-easing-standard) infinite; }
39
+ @keyframes ray-skeleton-shine { from { background-position: 100% 0; } to { background-position: 0 0; } }
40
+ @keyframes ray-skeleton-pulse { 50% { opacity: 0.45; } }
41
+ @media (prefers-reduced-motion: reduce) {
42
+ .bone { animation: none !important; background: var(--_base) !important; }
43
+ }
44
+ .content { display: none; }
45
+ :host([loaded]) .bones { display: none; }
46
+ :host([loaded]) .content { display: contents; }
47
+ `;
48
+
49
+ const length = (v) => (/^-?\d+(\.\d+)?$/.test(String(v)) ? `${v}px` : String(v));
50
+
51
+ export class RaySkeleton extends RayElement {
52
+ static properties = {
53
+ width: { type: String },
54
+ height: { type: String },
55
+ radius: { type: String, reflect: true },
56
+ circle: { type: Boolean, reflect: true },
57
+ lines: { type: Number, reflect: true },
58
+ animation: { type: String, reflect: true },
59
+ loaded: { type: Boolean, reflect: true },
60
+ };
61
+ static styles = [styles];
62
+ static template = `<div class="bones" part="skeleton" aria-hidden="true"></div><div class="content"><slot></slot></div>`;
63
+
64
+ update() {
65
+ const s = this.style;
66
+ // A circle's diameter: width, else height.
67
+ const w = this.width || (this.circle ? this.height : "");
68
+ if (w) s.setProperty("--_w", length(w));
69
+ else s.removeProperty("--_w");
70
+ if (this.height) s.setProperty("--_h", length(this.height));
71
+ else s.removeProperty("--_h");
72
+ if (this.radius && !this.circle) s.setProperty("--_r", RADII.includes(this.radius) ? `var(--ray-radius-${this.radius})` : length(this.radius));
73
+ else s.removeProperty("--_r");
74
+ const n = this.circle ? 1 : Math.max(1, Math.floor(this.lines || 1));
75
+ const bones = this.$(".bones");
76
+ if (bones.children.length !== n) {
77
+ bones.replaceChildren(...Array.from({ length: n }, () => {
78
+ const b = document.createElement("div");
79
+ b.className = "bone";
80
+ b.setAttribute("part", "bone");
81
+ return b;
82
+ }));
83
+ }
84
+ // Screen readers: busy while the placeholder stands in for the content.
85
+ if (this.loaded) this.removeAttribute("aria-busy");
86
+ else this.setAttribute("aria-busy", "true");
87
+ }
88
+ }
89
+
90
+ define("ray-skeleton", RaySkeleton);
@@ -0,0 +1,155 @@
1
+ // <ray-slider>: a range input (native, so keyboard, touch and screen readers
2
+ // work as usual) with label, description, value display and marks.
3
+ // Form-associated.
4
+
5
+ import { css, define } from "../../core/core.js";
6
+ import { applyColor, sizeStyles } from "../../core/styles.js";
7
+ import { FieldControl, fieldStyles } from "../../core/form.js";
8
+
9
+ const styles = css`
10
+ :host {
11
+ --_track: 0.375rem;
12
+ --_thumb: 1.125rem;
13
+ }
14
+ :host([size="xs"]) { --_track: 0.25rem; --_thumb: 0.875rem; }
15
+ :host([size="md"]) { --_track: 0.5rem; --_thumb: 1.375rem; }
16
+ :host([size="lg"]) { --_track: 0.625rem; --_thumb: 1.625rem; }
17
+ :host([size="xl"]) { --_track: 0.75rem; --_thumb: 1.875rem; }
18
+ .head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--ray-space-2); }
19
+ .value { font-size: calc(var(--_fs) - 0.0625rem); font-variant-numeric: tabular-nums; color: var(--ray-muted-foreground); }
20
+ .value[hidden] { display: none; }
21
+ .root { position: relative; padding: calc((var(--_thumb) - var(--_track)) / 2) 0; }
22
+ input {
23
+ display: block;
24
+ width: 100%;
25
+ height: var(--_thumb);
26
+ margin: calc((var(--_track) - var(--_thumb)) / 2) 0;
27
+ background: transparent;
28
+ -webkit-appearance: none;
29
+ appearance: none;
30
+ cursor: pointer;
31
+ }
32
+ input:disabled { cursor: not-allowed; }
33
+ input::-webkit-slider-runnable-track {
34
+ height: var(--_track);
35
+ border-radius: var(--ray-radius-full);
36
+ background: linear-gradient(to right, var(--_filled) var(--_pct), var(--ray-muted) var(--_pct));
37
+ box-shadow: inset 0 0 0 1px var(--ray-border);
38
+ }
39
+ input::-moz-range-track { height: var(--_track); border-radius: var(--ray-radius-full); background: var(--ray-muted); }
40
+ input::-moz-range-progress { height: var(--_track); border-radius: var(--ray-radius-full); background: var(--_filled); }
41
+ input::-webkit-slider-thumb {
42
+ -webkit-appearance: none;
43
+ width: var(--_thumb);
44
+ height: var(--_thumb);
45
+ margin-top: calc((var(--_track) - var(--_thumb)) / 2);
46
+ background: var(--ray-white);
47
+ border: 3px solid var(--_filled);
48
+ border-radius: 50%;
49
+ box-shadow: var(--ray-shadow-xs);
50
+ }
51
+ input::-moz-range-thumb {
52
+ width: var(--_thumb);
53
+ height: var(--_thumb);
54
+ background: var(--ray-white);
55
+ border: 3px solid var(--_filled);
56
+ border-radius: 50%;
57
+ }
58
+ input:focus-visible { outline: none; }
59
+ input:focus-visible::-webkit-slider-thumb { outline: var(--ray-focus-ring-width) solid var(--ray-ring); outline-offset: 2px; }
60
+ input:focus-visible::-moz-range-thumb { outline: var(--ray-focus-ring-width) solid var(--ray-ring); outline-offset: 2px; }
61
+ input:disabled { --_filled: var(--ray-border); }
62
+ .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
+ .marks:empty { display: none; }
64
+ .mark { position: absolute; transform: translateX(-50%); white-space: nowrap; }
65
+ .mark::before { content: ""; display: block; width: 1px; height: 0.25rem; margin: 0 auto 0.125rem; background: var(--ray-border); }
66
+ `;
67
+
68
+ export class RaySlider extends FieldControl {
69
+ static properties = {
70
+ ...FieldControl.fieldProperties,
71
+ value: { type: Number },
72
+ min: { type: Number, default: 0 },
73
+ max: { type: Number, default: 100 },
74
+ step: { type: Number, default: 1 },
75
+ color: { type: String, reflect: true },
76
+ showValue: { type: Boolean, reflect: true },
77
+ unit: { type: String },
78
+ marks: { attribute: false },
79
+ };
80
+ static styles = [sizeStyles, fieldStyles, styles];
81
+ static template = `
82
+ <div class="head">
83
+ <label class="label" part="label" for="control" hidden><span class="label-text"></span><span class="required" aria-hidden="true" hidden> *</span></label>
84
+ <span class="value" part="value" aria-hidden="true" hidden></span>
85
+ </div>
86
+ <div class="description" part="description" id="description" hidden></div>
87
+ <div class="root" part="root"><input id="control" type="range" part="input"></div>
88
+ <div class="marks" part="marks"></div>
89
+ <div class="error" part="error" id="error" hidden></div>`;
90
+
91
+ wire() {
92
+ this._default = this.getAttribute("value");
93
+ this.control.addEventListener("input", () => {
94
+ this.commitProperty("value", Number(this.control.value));
95
+ this._commit();
96
+ });
97
+ this.control.addEventListener("change", (e) => this.forward(e));
98
+ }
99
+
100
+ update(changed) {
101
+ const c = this.control;
102
+ c.min = String(this.min ?? 0);
103
+ c.max = String(this.max ?? 100);
104
+ c.step = String(this.step ?? 1);
105
+ if (this.value === undefined) this.commitProperty("value", Number(c.value));
106
+ if (Number(c.value) !== this.value) c.value = String(this.value);
107
+ applyColor(this.$(".root"), this.color);
108
+ this.syncField();
109
+ if (changed.has("marks") || changed.has("min") || changed.has("max") || !this._marked) this._renderMarks();
110
+ this._commit();
111
+ }
112
+
113
+ _commit() {
114
+ const c = this.control;
115
+ const min = Number(c.min);
116
+ const max = Number(c.max);
117
+ const pct = max > min ? ((Number(c.value) - min) / (max - min)) * 100 : 0;
118
+ c.style.setProperty("--_pct", `${pct}%`);
119
+ const text = `${c.value}${this.unit ?? ""}`;
120
+ const value = this.$(".value");
121
+ value.hidden = !this.showValue;
122
+ value.textContent = text;
123
+ if (this.unit) c.setAttribute("aria-valuetext", text);
124
+ else c.removeAttribute("aria-valuetext");
125
+ this.internals.setFormValue(c.value);
126
+ this.syncValidity();
127
+ }
128
+
129
+ _renderMarks() {
130
+ this._marked = true;
131
+ const box = this.$(".marks");
132
+ box.replaceChildren();
133
+ if (!Array.isArray(this.marks)) return;
134
+ const min = this.min ?? 0;
135
+ const max = this.max ?? 100;
136
+ for (const m of this.marks) {
137
+ const v = typeof m === "number" ? m : m.value;
138
+ const span = document.createElement("span");
139
+ span.className = "mark";
140
+ span.style.left = `${((v - min) / (max - min || 1)) * 100}%`;
141
+ span.textContent = typeof m === "number" ? String(m) : (m.label ?? String(m.value));
142
+ box.append(span);
143
+ }
144
+ }
145
+
146
+ formResetCallback() {
147
+ this.value = this._default === null ? undefined : Number(this._default);
148
+ }
149
+
150
+ formStateRestoreCallback(state) {
151
+ this.value = Number(state);
152
+ }
153
+ }
154
+
155
+ define("ray-slider", RaySlider);
@@ -0,0 +1,39 @@
1
+ // <ray-spinner>: an indeterminate loading indicator (Mantine's Loader).
2
+
3
+ import { RayElement, css, define } from "../../core/core.js";
4
+ import { applyColor, loaderStyles, srOnlyStyles } from "../../core/styles.js";
5
+
6
+ const styles = css`
7
+ :host {
8
+ display: inline-flex;
9
+ vertical-align: middle;
10
+ --_loader: 2.25rem;
11
+ }
12
+ :host([size="xs"]) { --_loader: 1.125rem; }
13
+ :host([size="sm"]) { --_loader: 1.375rem; }
14
+ :host([size="lg"]) { --_loader: 2.75rem; }
15
+ :host([size="xl"]) { --_loader: 3.625rem; }
16
+ .loader { color: var(--_filled); }
17
+ `;
18
+
19
+ export class RaySpinner extends RayElement {
20
+ static properties = {
21
+ color: { type: String, reflect: true },
22
+ size: { type: String, reflect: true, default: "md" },
23
+ label: { type: String, default: "Loading" },
24
+ };
25
+ static styles = [loaderStyles, srOnlyStyles, styles];
26
+ static template = `<span class="loader" part="spinner"></span><span class="sr-only"></span>`;
27
+
28
+ connectedCallback() {
29
+ super.connectedCallback();
30
+ if (!this.hasAttribute("role")) this.setAttribute("role", "status");
31
+ }
32
+
33
+ update() {
34
+ applyColor(this.$(".loader"), this.color);
35
+ this.$(".sr-only").textContent = this.label;
36
+ }
37
+ }
38
+
39
+ define("ray-spinner", RaySpinner);