@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,435 @@
1
+ // <ray-carousel>: slides in a scroll-snap viewport (Mantine's Carousel,
2
+ // shadcn's Carousel). Native scrolling does the moving — touch swipe,
3
+ // trackpads, scroll wheels — and the component adds previous/next controls,
4
+ // indicators, keyboard, loop and autoplay on top.
5
+ //
6
+ // Any child element is a slide. With several slides in view (slide-size
7
+ // "50%", "18rem"…) the positions are the distinct snap points the viewport
8
+ // can reach: `index`, the indicators and next()/prev() count those.
9
+
10
+ import { RayElement, css, define } from "../../core/core.js";
11
+ import { motionAllowed } from "../../core/motion.js";
12
+
13
+ const styles = css`
14
+ :host {
15
+ display: block;
16
+ position: relative;
17
+ /* Inside grid/flex parents: never wider than them (the slides scroll). */
18
+ min-width: 0;
19
+ --_size: 100%;
20
+ --_gap: var(--ray-space-4);
21
+ }
22
+ :host([orientation="vertical"]) { height: 20rem; }
23
+ .root { position: relative; display: flex; flex-direction: column; gap: var(--ray-space-3); height: 100%; }
24
+ .stage { position: relative; flex: 1; min-width: 0; min-height: 0; }
25
+ .viewport {
26
+ display: flex;
27
+ gap: var(--_gap);
28
+ height: 100%;
29
+ overflow-x: auto;
30
+ overflow-y: hidden;
31
+ scroll-snap-type: x mandatory;
32
+ overscroll-behavior-x: contain;
33
+ scrollbar-width: none;
34
+ border-radius: var(--ray-radius-default);
35
+ outline: none;
36
+ }
37
+ .viewport::-webkit-scrollbar { display: none; }
38
+ .viewport:focus-visible { outline: var(--ray-focus-ring-width) solid var(--ray-ring); outline-offset: 2px; }
39
+ :host([orientation="vertical"]) .viewport {
40
+ flex-direction: column;
41
+ overflow-x: hidden;
42
+ overflow-y: auto;
43
+ scroll-snap-type: y mandatory;
44
+ overscroll-behavior-y: contain;
45
+ }
46
+ ::slotted(*) {
47
+ flex: 0 0 var(--_size);
48
+ min-width: 0;
49
+ min-height: 0;
50
+ box-sizing: border-box;
51
+ scroll-snap-align: start;
52
+ }
53
+ :host([align="center"]) ::slotted(*) { scroll-snap-align: center; }
54
+
55
+ .control {
56
+ position: absolute;
57
+ top: 50%;
58
+ z-index: 1;
59
+ display: inline-flex;
60
+ align-items: center;
61
+ justify-content: center;
62
+ width: 2rem;
63
+ height: 2rem;
64
+ padding: 0;
65
+ color: var(--ray-popover-foreground);
66
+ background: var(--ray-popover);
67
+ border: 1px solid var(--ray-border);
68
+ border-radius: var(--ray-radius-full);
69
+ box-shadow: var(--ray-shadow-sm);
70
+ cursor: pointer;
71
+ transform: translateY(-50%);
72
+ transition: opacity var(--ray-duration-fast) var(--ray-easing-standard), background var(--ray-duration-fast) var(--ray-easing-standard);
73
+ }
74
+ .control:hover:not(:disabled) { background: var(--ray-accent); }
75
+ .control:disabled { opacity: 0; pointer-events: none; }
76
+ .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); }
80
+ :host([orientation="vertical"]) .prev { top: var(--ray-space-2); }
81
+ :host([orientation="vertical"]) .next { bottom: var(--ray-space-2); }
82
+ :host([hide-controls]) .control { display: none; }
83
+
84
+ .footer { display: flex; align-items: center; justify-content: center; gap: var(--ray-space-2); min-height: 1.5rem; }
85
+ .footer[hidden] { display: none; }
86
+ .indicators { display: flex; align-items: center; gap: 0.125rem; }
87
+ :host([hide-indicators]) .indicators { display: none; }
88
+ .dot {
89
+ display: inline-flex;
90
+ align-items: center;
91
+ justify-content: center;
92
+ /* 24px: the minimum target (WCAG 2.5.8); the dot drawn inside is smaller. */
93
+ width: 1.5rem;
94
+ height: 1.5rem;
95
+ padding: 0;
96
+ background: none;
97
+ border: 0;
98
+ border-radius: var(--ray-radius-full);
99
+ cursor: pointer;
100
+ }
101
+ .dot::before {
102
+ content: "";
103
+ width: 0.5rem;
104
+ height: 0.5rem;
105
+ border-radius: var(--ray-radius-full);
106
+ background: color-mix(in srgb, var(--ray-foreground) 25%, transparent);
107
+ transition: width var(--ray-duration-base) var(--ray-easing-standard), background var(--ray-duration-base) var(--ray-easing-standard);
108
+ }
109
+ .dot:hover::before { background: color-mix(in srgb, var(--ray-foreground) 45%, transparent); }
110
+ .dot[aria-current="true"] { width: 1.75rem; }
111
+ .dot[aria-current="true"]::before { width: 1.25rem; background: var(--ray-primary); }
112
+ .dot:focus-visible, .play:focus-visible { outline: var(--ray-focus-ring-width) solid var(--ray-ring); outline-offset: 1px; }
113
+ .play {
114
+ display: inline-flex;
115
+ align-items: center;
116
+ justify-content: center;
117
+ width: 1.5rem;
118
+ height: 1.5rem;
119
+ padding: 0;
120
+ color: var(--ray-muted-foreground);
121
+ background: none;
122
+ border: 0;
123
+ border-radius: var(--ray-radius-full);
124
+ cursor: pointer;
125
+ }
126
+ .play:hover { color: var(--ray-foreground); background: var(--ray-accent); }
127
+ .play[hidden] { display: none; }
128
+ .play svg { width: 0.875rem; height: 0.875rem; }
129
+ `;
130
+
131
+ const PAUSE_ICON = `<span data-ray-icon="pause" aria-hidden="true"></span>`;
132
+
133
+ const reduced = typeof matchMedia === "function" ? matchMedia("(prefers-reduced-motion: reduce)") : null;
134
+
135
+ export class RayCarousel extends RayElement {
136
+ static properties = {
137
+ index: { type: Number, default: 0 },
138
+ label: { type: String, default: "Carousel" },
139
+ slideSize: { type: String, attribute: "slide-size" },
140
+ slideGap: { type: String, attribute: "slide-gap" },
141
+ align: { type: String, reflect: true, default: "start" },
142
+ orientation: { type: String, reflect: true, default: "horizontal" },
143
+ loop: { type: Boolean, reflect: true },
144
+ autoplay: { type: Number, default: 0 },
145
+ hideControls: { type: Boolean, reflect: true },
146
+ hideIndicators: { type: Boolean, reflect: true },
147
+ };
148
+ static styles = [styles];
149
+ static template = `
150
+ <div class="root" part="root" role="region" aria-roledescription="carousel">
151
+ <div class="stage">
152
+ <div class="viewport" part="viewport" tabindex="0" aria-live="polite"><slot></slot></div>
153
+ <button class="control prev" part="previous" type="button" aria-label="Previous slide"><span data-ray-icon="chevron-left" aria-hidden="true"></span></button>
154
+ <button class="control next" part="next" type="button" aria-label="Next slide"><span data-ray-icon="chevron-right" aria-hidden="true"></span></button>
155
+ </div>
156
+ <div class="footer" part="footer">
157
+ <button class="play" part="play" type="button" hidden></button>
158
+ <div class="indicators" part="indicators"></div>
159
+ </div>
160
+ </div>`;
161
+
162
+ #snaps = [0];
163
+ #scrollTimer = 0;
164
+ #autoTimer = 0;
165
+ #hover = false;
166
+ #focus = false;
167
+ // Reduced motion: autoplay starts paused (the play button starts it).
168
+ #userPaused = Boolean(reduced?.matches);
169
+ #resize = null;
170
+
171
+ constructor() {
172
+ super();
173
+ this.viewport = this.$(".viewport");
174
+ this._slot = this.$("slot");
175
+ this._slot.addEventListener("slotchange", () => this._measure());
176
+ this.viewport.addEventListener("scroll", () => {
177
+ // No rAF in some hosts: settle the position with a short timeout.
178
+ clearTimeout(this.#scrollTimer);
179
+ this.#scrollTimer = setTimeout(() => this._track(), 50);
180
+ }, { passive: true });
181
+ this.viewport.addEventListener("scrollend", () => this._track());
182
+ this.$(".prev").addEventListener("click", () => this.prev());
183
+ this.$(".next").addEventListener("click", () => this.next());
184
+ this.$(".play").addEventListener("click", () => {
185
+ this.#userPaused = !this.#userPaused;
186
+ this._schedule();
187
+ });
188
+ this.$(".indicators").addEventListener("click", (e) => {
189
+ const dot = e.target.closest?.(".dot");
190
+ if (dot) this.goTo(Number(dot.dataset.index));
191
+ });
192
+ this.addEventListener("keydown", (e) => this._onKey(e));
193
+ this.addEventListener("pointerenter", () => { this.#hover = true; this._schedule(); });
194
+ this.addEventListener("pointerleave", () => { this.#hover = false; this._schedule(); });
195
+ this.addEventListener("focusin", () => { this.#focus = true; this._schedule(); });
196
+ this.addEventListener("focusout", (e) => {
197
+ if (!this.contains(e.relatedTarget) && !this.shadowRoot.contains(e.relatedTarget)) {
198
+ this.#focus = false;
199
+ this._schedule();
200
+ }
201
+ });
202
+ this._onVisibility = () => this._schedule();
203
+ }
204
+
205
+ connectedCallback() {
206
+ super.connectedCallback();
207
+ document.addEventListener("visibilitychange", this._onVisibility);
208
+ if (typeof ResizeObserver === "function" && !this.#resize) {
209
+ this.#resize = new ResizeObserver(() => this._measure());
210
+ this.#resize.observe(this.viewport);
211
+ }
212
+ queueMicrotask(() => this._measure());
213
+ }
214
+
215
+ disconnectedCallback() {
216
+ super.disconnectedCallback();
217
+ document.removeEventListener("visibilitychange", this._onVisibility);
218
+ this.#resize?.disconnect();
219
+ this.#resize = null;
220
+ clearTimeout(this.#autoTimer);
221
+ clearTimeout(this.#scrollTimer);
222
+ }
223
+
224
+ /** The slide elements (the assigned children). */
225
+ get slides() {
226
+ return this._slot.assignedElements();
227
+ }
228
+
229
+ /** Number of positions (snap points): one per slide unless several fit in view. */
230
+ get count() {
231
+ return this.#snaps.length;
232
+ }
233
+
234
+ update(changed) {
235
+ const root = this.$(".root");
236
+ root.setAttribute("aria-label", this.label || "Carousel");
237
+ if (this.slideSize) this.style.setProperty("--_size", this.slideSize);
238
+ else this.style.removeProperty("--_size");
239
+ if (this.slideGap) this.style.setProperty("--_gap", this.slideGap);
240
+ else this.style.removeProperty("--_gap");
241
+ if (changed.has("slideSize") || changed.has("slideGap") || changed.has("align") || changed.has("orientation")) {
242
+ queueMicrotask(() => this._measure());
243
+ }
244
+ if (changed.has("index") && !this._tracking) this._go(this.index, false);
245
+ if (changed.has("autoplay")) this._schedule();
246
+ this._paint();
247
+ }
248
+
249
+ /** Goes to the next position (wraps with `loop`). */
250
+ next() {
251
+ const i = this._current();
252
+ if (i < this.count - 1) this.goTo(i + 1);
253
+ else if (this.loop) this.goTo(0);
254
+ }
255
+
256
+ /** Goes to the previous position (wraps with `loop`). */
257
+ prev() {
258
+ const i = this._current();
259
+ if (i > 0) this.goTo(i - 1);
260
+ else if (this.loop) this.goTo(this.count - 1);
261
+ }
262
+
263
+ /** Scrolls to position `index` (smoothly unless `instant` or reduced motion). */
264
+ /** Moves to a position (not scrollTo(): that is Element.prototype.scrollTo). */
265
+ goTo(index, { instant = false } = {}) {
266
+ this._go(index, true, instant);
267
+ }
268
+
269
+ // --- internals ------------------------------------------------------------
270
+
271
+ _vertical() {
272
+ return this.orientation === "vertical";
273
+ }
274
+
275
+ _current() {
276
+ return Math.min(Math.max(0, this.index ?? 0), this.count - 1);
277
+ }
278
+
279
+ // The snap points: each slide's aligned position, clamped to what the
280
+ // viewport can reach, without duplicates.
281
+ _measure() {
282
+ const vp = this.viewport;
283
+ const v = this._vertical();
284
+ const slides = this.slides;
285
+ const total = slides.length;
286
+ slides.forEach((s, i) => {
287
+ if (!s.hasAttribute("role")) s.setAttribute("role", "group");
288
+ s.setAttribute("aria-roledescription", "slide");
289
+ if (!s.hasAttribute("aria-label") || s.dataset.rayAutoLabel !== undefined) {
290
+ s.setAttribute("aria-label", `${i + 1} of ${total}`);
291
+ s.dataset.rayAutoLabel = "";
292
+ }
293
+ });
294
+ const vr = vp.getBoundingClientRect();
295
+ const size = v ? vp.clientHeight : vp.clientWidth;
296
+ const max = Math.max(0, v ? vp.scrollHeight - vp.clientHeight : vp.scrollWidth - vp.clientWidth);
297
+ const scroll = v ? vp.scrollTop : vp.scrollLeft;
298
+ const snaps = [];
299
+ for (const s of slides) {
300
+ const r = s.getBoundingClientRect();
301
+ const start = (v ? r.top - vr.top : r.left - vr.left) + scroll;
302
+ const length = v ? r.height : r.width;
303
+ const pos = this.align === "center" ? start - (size - length) / 2 : start;
304
+ const clamped = Math.round(Math.min(max, Math.max(0, pos)));
305
+ // A position only a sliver past the previous one (the end clamp, gaps)
306
+ // is the same stop: keep the later, so the end stays reachable.
307
+ if (snaps.length && clamped - snaps[snaps.length - 1] < Math.max(2, length / 3)) snaps[snaps.length - 1] = clamped;
308
+ else snaps.push(clamped);
309
+ }
310
+ this.#snaps = snaps.length ? snaps : [0];
311
+ this._renderDots();
312
+ this._track();
313
+ }
314
+
315
+ _renderDots() {
316
+ const box = this.$(".indicators");
317
+ const n = this.count;
318
+ if (box.children.length !== n) {
319
+ box.replaceChildren(
320
+ ...Array.from({ length: n }, (_, i) => {
321
+ const b = document.createElement("button");
322
+ b.type = "button";
323
+ b.className = "dot";
324
+ b.setAttribute("part", "indicator");
325
+ b.dataset.index = String(i);
326
+ b.setAttribute("aria-label", `Go to slide ${i + 1}`);
327
+ return b;
328
+ }),
329
+ );
330
+ }
331
+ this.$(".footer").hidden = (n < 2 || this.hideIndicators) && !(this.autoplay > 0);
332
+ }
333
+
334
+ // The position closest to the current scroll offset.
335
+ _track() {
336
+ const vp = this.viewport;
337
+ const scroll = this._vertical() ? vp.scrollTop : vp.scrollLeft;
338
+ let best = 0;
339
+ for (let i = 1; i < this.#snaps.length; i++) {
340
+ if (Math.abs(this.#snaps[i] - scroll) < Math.abs(this.#snaps[best] - scroll)) best = i;
341
+ }
342
+ this._setIndex(best);
343
+ this._paint();
344
+ }
345
+
346
+ _setIndex(i) {
347
+ if (i === (this.index ?? 0)) return;
348
+ this._tracking = true;
349
+ this.commitProperty("index", i);
350
+ queueMicrotask(() => { this._tracking = false; });
351
+ this.dispatchEvent(new Event("change", { bubbles: true }));
352
+ }
353
+
354
+ _go(index, user, instant = false) {
355
+ if (!Number.isFinite(index)) return;
356
+ const i = Math.min(Math.max(0, Math.round(index)), this.count - 1);
357
+ const pos = this.#snaps[i] ?? 0;
358
+ const smooth = user && !instant && !reduced?.matches && motionAllowed(this);
359
+ const opts = { behavior: smooth ? "smooth" : "instant" };
360
+ if (this._vertical()) this.viewport.scrollTo({ top: pos, ...opts });
361
+ else this.viewport.scrollTo({ left: pos, ...opts });
362
+ this._setIndex(i);
363
+ this._paint();
364
+ this._schedule();
365
+ }
366
+
367
+ _paint() {
368
+ const i = this._current();
369
+ const n = this.count;
370
+ this.$(".prev").disabled = !this.loop && i <= 0;
371
+ this.$(".next").disabled = !this.loop && i >= n - 1;
372
+ for (const d of this.$(".indicators").children) {
373
+ if (Number(d.dataset.index) === i) d.setAttribute("aria-current", "true");
374
+ else d.removeAttribute("aria-current");
375
+ }
376
+ this._paintPlay();
377
+ this.$(".footer").hidden = (n < 2 || this.hideIndicators) && !(this.autoplay > 0);
378
+ }
379
+
380
+ // Autoplay runs unless paused by the user (or by reduced motion), hovered,
381
+ // focused, or the page is hidden.
382
+ _running() {
383
+ if (!(this.autoplay > 0) || this.count < 2 || this.#userPaused) return false;
384
+ return !this.#hover && !this.#focus && document.visibilityState !== "hidden";
385
+ }
386
+
387
+ _schedule() {
388
+ clearTimeout(this.#autoTimer);
389
+ this._paintPlay();
390
+ if (!this._running()) return;
391
+ this.#autoTimer = setTimeout(() => {
392
+ const i = this._current();
393
+ this.goTo(i < this.count - 1 ? i + 1 : 0);
394
+ }, this.autoplay);
395
+ }
396
+
397
+ // The pause/play button reflects the user's choice (hover and focus pause
398
+ // only for a moment); the live region is off while slides move by themselves.
399
+ _paintPlay() {
400
+ const play = this.$(".play");
401
+ const auto = this.autoplay > 0;
402
+ play.hidden = !auto;
403
+ const paused = this.#userPaused;
404
+ if (auto && play.dataset.state !== (paused ? "paused" : "playing")) {
405
+ play.dataset.state = paused ? "paused" : "playing";
406
+ play.innerHTML = paused ? `<span data-ray-icon="play" aria-hidden="true"></span>` : PAUSE_ICON;
407
+ this.paintIcons(play);
408
+ play.setAttribute("aria-label", paused ? "Start autoplay" : "Pause autoplay");
409
+ }
410
+ this.viewport.setAttribute("aria-live", auto && this._running() ? "off" : "polite");
411
+ }
412
+
413
+ _onKey(e) {
414
+ const t = e.composedPath()[0];
415
+ if (t && (t.isContentEditable || ["input", "textarea", "select"].includes(t.localName))) return;
416
+ const v = this._vertical();
417
+ const back = v ? "ArrowUp" : "ArrowLeft";
418
+ const fwd = v ? "ArrowDown" : "ArrowRight";
419
+ if (e.key === fwd) {
420
+ e.preventDefault();
421
+ this.next();
422
+ } else if (e.key === back) {
423
+ e.preventDefault();
424
+ this.prev();
425
+ } else if (e.key === "Home") {
426
+ e.preventDefault();
427
+ this.goTo(0);
428
+ } else if (e.key === "End") {
429
+ e.preventDefault();
430
+ this.goTo(this.count - 1);
431
+ }
432
+ }
433
+ }
434
+
435
+ define("ray-carousel", RayCarousel);
@@ -0,0 +1,26 @@
1
+ // <ray-center>: its content centered on both axes (Mantine's Center). It
2
+ // fills its parent's width; give it a height (or let a flex/grid parent
3
+ // stretch it) to center vertically. `inline` makes it as wide as its content.
4
+
5
+ import { RayElement, css, define } from "../../core/core.js";
6
+
7
+ const styles = css`
8
+ :host {
9
+ display: flex;
10
+ align-items: center;
11
+ justify-content: center;
12
+ min-width: 0;
13
+ }
14
+ :host([inline]) { display: inline-flex; }
15
+ :host([hidden]) { display: none; }
16
+ `;
17
+
18
+ export class RayCenter extends RayElement {
19
+ static properties = {
20
+ inline: { type: Boolean, reflect: true },
21
+ };
22
+ static styles = [styles];
23
+ static template = `<slot></slot>`;
24
+ }
25
+
26
+ define("ray-center", RayCenter);
@@ -0,0 +1,152 @@
1
+ // <ray-checkbox>: a checkbox with label (attribute or content), description
2
+ // and error. Submits `value` (default "on") under `name` when checked.
3
+
4
+ import { css, define } from "../../core/core.js";
5
+ import { applyColor, sizeStyles, radiusStyles } from "../../core/styles.js";
6
+ import { FieldControl, fieldStyles } from "../../core/form.js";
7
+
8
+ export const checkStyles = css`
9
+ :host {
10
+ display: block;
11
+ --_s: 1.25rem;
12
+ }
13
+ :host([size="xs"]) { --_s: 1rem; }
14
+ :host([size="md"]) { --_s: 1.5rem; }
15
+ :host([size="lg"]) { --_s: 1.875rem; }
16
+ :host([size="xl"]) { --_s: 2.25rem; }
17
+ .root {
18
+ display: flex;
19
+ align-items: flex-start;
20
+ gap: calc(var(--_s) * 0.6);
21
+ }
22
+ :host([label-position="left"]) .root { flex-direction: row-reverse; justify-content: flex-end; }
23
+ .inner {
24
+ position: relative;
25
+ flex: none;
26
+ display: inline-flex;
27
+ width: var(--_iw, var(--_s));
28
+ height: var(--_s);
29
+ }
30
+ .control {
31
+ position: absolute;
32
+ inset: 0;
33
+ z-index: 1;
34
+ width: 100%;
35
+ height: 100%;
36
+ margin: 0;
37
+ opacity: 0;
38
+ cursor: pointer;
39
+ }
40
+ .control:disabled { cursor: not-allowed; }
41
+ .indicator {
42
+ display: inline-flex;
43
+ align-items: center;
44
+ justify-content: center;
45
+ width: 100%;
46
+ height: 100%;
47
+ color: var(--_contrast);
48
+ background: var(--ray-background);
49
+ border: 1px solid var(--ray-input);
50
+ border-radius: var(--_r);
51
+ transition: background-color var(--ray-duration-fast) var(--ray-easing-standard), border-color var(--ray-duration-fast) var(--ray-easing-standard);
52
+ }
53
+ .control:focus-visible + .indicator {
54
+ outline: var(--ray-focus-ring-width) solid var(--ray-ring);
55
+ outline-offset: var(--ray-focus-ring-offset);
56
+ }
57
+ .control:disabled + .indicator { background: var(--ray-muted); border-color: var(--ray-border); }
58
+ .root[data-invalid] .indicator { border-color: var(--ray-destructive); }
59
+ .body { display: flex; flex-direction: column; justify-content: center; min-height: var(--_s); }
60
+ .label { margin: 0; cursor: pointer; font-weight: var(--ray-font-weight-regular); line-height: var(--ray-line-height-sm); }
61
+ .description, .error { margin: var(--ray-space-1) 0 0; }
62
+ :host([disabled]) .label { color: var(--ray-muted-foreground); cursor: not-allowed; }
63
+ `;
64
+
65
+ const styles = css`
66
+ :host { --_r: var(--ray-radius-sm); }
67
+ .indicator { --ray-icon-stroke: 3; }
68
+ .check, .dash { display: none; }
69
+ .check svg, .dash svg { width: calc(var(--_s) * 0.65); height: calc(var(--_s) * 0.65); }
70
+ .control:checked + .indicator,
71
+ .control:indeterminate + .indicator { background: var(--_filled); border-color: var(--_filled); }
72
+ .control:checked:not(:indeterminate) + .indicator .check { display: inline-flex; }
73
+ .control:indeterminate + .indicator .dash { display: inline-flex; }
74
+ :host([variant="outline"]) .control:checked + .indicator,
75
+ :host([variant="outline"]) .control:indeterminate + .indicator { background: var(--ray-background); color: var(--_filled); }
76
+ .control:disabled:checked + .indicator,
77
+ .control:disabled:indeterminate + .indicator { background: var(--ray-border); border-color: var(--ray-border); color: var(--ray-muted-foreground); }
78
+ `;
79
+
80
+ export function checkTemplate(indicator, inputAttrs = "") {
81
+ return `
82
+ <div class="root" part="root">
83
+ <span class="inner">
84
+ <input type="checkbox" id="control" class="control" part="input" ${inputAttrs}>
85
+ ${indicator}
86
+ </span>
87
+ <span class="body">
88
+ <label class="label" part="label" for="control" hidden><span class="label-text"></span><slot></slot><span class="required" aria-hidden="true" hidden> *</span></label>
89
+ <div class="description" part="description" id="description" hidden></div>
90
+ <div class="error" part="error" id="error" hidden></div>
91
+ </span>
92
+ </div>`;
93
+ }
94
+
95
+ export class RayCheckbox extends FieldControl {
96
+ static properties = {
97
+ ...FieldControl.fieldProperties,
98
+ checked: { type: Boolean, attribute: false },
99
+ indeterminate: { type: Boolean, reflect: true },
100
+ value: { type: String, reflect: true, default: "on" },
101
+ color: { type: String, reflect: true },
102
+ variant: { type: String, reflect: true, default: "filled" },
103
+ labelPosition: { type: String, reflect: true, default: "right" },
104
+ };
105
+ static styles = [sizeStyles, radiusStyles, fieldStyles, checkStyles, styles];
106
+ static template = checkTemplate(`<span class="indicator" part="indicator" aria-hidden="true"><span class="check" data-ray-icon="check"></span><span class="dash" data-ray-icon="minus"></span></span>`);
107
+
108
+ wire() {
109
+ // The `checked` attribute is the default state, like on <input>.
110
+ this._default = this.hasAttribute("checked");
111
+ if (!this.hasValue("checked")) this.commitProperty("checked", this._default);
112
+ this.control.addEventListener("change", (e) => {
113
+ this.commitProperty("checked", this.control.checked);
114
+ if (this.indeterminate) this.indeterminate = false;
115
+ this._commit();
116
+ this.forward(e);
117
+ });
118
+ const slot = this.$(".label slot");
119
+ slot.addEventListener("slotchange", () => this.requestUpdate());
120
+ }
121
+
122
+ update() {
123
+ const c = this.control;
124
+ c.checked = Boolean(this.checked);
125
+ c.indeterminate = Boolean(this.indeterminate);
126
+ if (this.indeterminate) c.setAttribute("aria-checked", "mixed");
127
+ else c.removeAttribute("aria-checked");
128
+ applyColor(this.$(".root"), this.color);
129
+ this.syncField();
130
+ // The label shows for the attribute or for slotted content.
131
+ const slotted = this.$(".label slot").assignedNodes().some((n) => n.nodeType === 1 || n.textContent.trim());
132
+ this.$(".label").hidden = !(this.label || slotted);
133
+ this.$(".root").toggleAttribute("data-invalid", Boolean(this.error));
134
+ this._commit();
135
+ }
136
+
137
+ _commit() {
138
+ this.internals.setFormValue(this.checked ? (this.value ?? "on") : null);
139
+ this.syncValidity();
140
+ }
141
+
142
+ formResetCallback() {
143
+ this.checked = this._default;
144
+ this.indeterminate = false;
145
+ }
146
+
147
+ formStateRestoreCallback(state) {
148
+ this.checked = state !== null && state !== undefined;
149
+ }
150
+ }
151
+
152
+ define("ray-checkbox", RayCheckbox);