@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,85 @@
1
+ // <ray-popover>: a floating panel anchored to a trigger (slot="trigger"),
2
+ // toggled by clicking it; closes on Escape and outside clicks.
3
+
4
+ import { css, define } from "../../core/core.js";
5
+ import { Floating, floatingStyles } from "../../core/floating.js";
6
+ import { focusFirst, tabbables } from "../../core/focus.js";
7
+
8
+ const styles = css`
9
+ /* Defaults on the host, so an ancestor's private --_p / --_width do not leak in. */
10
+ :host { --_p: var(--ray-space-md); --_width: max-content; }
11
+ .panel { padding: var(--_p); width: var(--_width); }
12
+ :host([padding="0"]) { --_p: 0; }
13
+ :host([padding="xs"]) { --_p: var(--ray-space-xs); }
14
+ :host([padding="sm"]) { --_p: var(--ray-space-sm); }
15
+ :host([padding="lg"]) { --_p: var(--ray-space-lg); }
16
+ `;
17
+
18
+ export class RayPopover extends Floating {
19
+ static properties = {
20
+ ...Floating.properties,
21
+ width: { type: String },
22
+ padding: { type: String, reflect: true },
23
+ matchWidth: { type: Boolean, reflect: true },
24
+ trapFocus: { type: Boolean, reflect: true },
25
+ };
26
+ static styles = [floatingStyles, styles];
27
+ static template = `<slot name="trigger"></slot><div class="panel" part="dropdown" role="dialog"><slot></slot></div>`;
28
+
29
+ wireTrigger(t) {
30
+ t.addEventListener("click", () => {
31
+ if (!this.open && this.justClosed()) return;
32
+ this.toggle();
33
+ });
34
+ }
35
+
36
+ update(changed) {
37
+ if (this.width) this.style.setProperty("--_width", /^\d+$/.test(this.width) ? `${this.width}px` : this.width);
38
+ else this.style.removeProperty("--_width");
39
+ const label = this.getAttribute("aria-label");
40
+ if (label) this.panel.setAttribute("aria-label", label);
41
+ super.update(changed);
42
+ }
43
+
44
+ placeOptions() {
45
+ return { matchWidth: Boolean(this.matchWidth) };
46
+ }
47
+
48
+ toggled(open) {
49
+ if (open) {
50
+ if (this.trapFocus) focusFirst(this, { lightOnly: true });
51
+ this._onKey = (e) => this._keydown(e);
52
+ this.panel.addEventListener("keydown", this._onKey);
53
+ } else {
54
+ this.panel.removeEventListener("keydown", this._onKey);
55
+ // Return focus to the trigger if it was inside the panel.
56
+ if (this.focusWasInside) this.trigger?.focus();
57
+ }
58
+ }
59
+
60
+ // With trap-focus, Tab cycles inside the panel.
61
+ _keydown(e) {
62
+ if (e.key !== "Tab" || !this.trapFocus) return;
63
+ const items = tabbables(this, { lightOnly: true }).filter((el) => el !== this.trigger && !this.trigger?.contains(el));
64
+ if (!items.length) return;
65
+ const active = deepActive();
66
+ const first = items[0];
67
+ const last = items[items.length - 1];
68
+ const at = (el) => el === active || el.shadowRoot?.contains(active) || el.contains(active);
69
+ if (e.shiftKey && at(first)) {
70
+ e.preventDefault();
71
+ last.focus();
72
+ } else if (!e.shiftKey && at(last)) {
73
+ e.preventDefault();
74
+ first.focus();
75
+ }
76
+ }
77
+ }
78
+
79
+ function deepActive() {
80
+ let a = document.activeElement;
81
+ while (a?.shadowRoot?.activeElement) a = a.shadowRoot.activeElement;
82
+ return a;
83
+ }
84
+
85
+ define("ray-popover", RayPopover);
@@ -0,0 +1,109 @@
1
+ // <ray-progress>: a determinate or indeterminate progress bar.
2
+
3
+ import { RayElement, css, define } from "../../core/core.js";
4
+ import { applyColor, radiusStyles } from "../../core/styles.js";
5
+
6
+ const styles = css`
7
+ :host {
8
+ display: block;
9
+ --_h: 0.5rem;
10
+ --_r: var(--ray-radius-full);
11
+ }
12
+ :host([size="xs"]) { --_h: 0.1875rem; }
13
+ :host([size="sm"]) { --_h: 0.3125rem; }
14
+ :host([size="lg"]) { --_h: 0.75rem; }
15
+ :host([size="xl"]) { --_h: 1rem; }
16
+ .row { display: flex; align-items: center; gap: var(--ray-space-2); }
17
+ .track {
18
+ flex: 1;
19
+ position: relative;
20
+ height: var(--_h);
21
+ overflow: hidden;
22
+ background: var(--ray-muted);
23
+ border-radius: var(--_r);
24
+ box-shadow: inset 0 0 0 1px var(--ray-border);
25
+ }
26
+ .bar {
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: center;
30
+ height: 100%;
31
+ width: var(--_pct, 0%);
32
+ background: var(--_filled);
33
+ border-radius: var(--_r);
34
+ color: var(--_contrast);
35
+ font-size: max(0.625rem, calc(var(--_h) * 0.7));
36
+ font-weight: var(--ray-font-weight-semibold);
37
+ line-height: 1;
38
+ white-space: nowrap;
39
+ transition: width var(--ray-duration-slow) var(--ray-easing-standard);
40
+ }
41
+ /* show-value on small bars: the value beside the track, not squeezed inside it. */
42
+ .value {
43
+ flex: none;
44
+ min-width: 2.75em;
45
+ text-align: right;
46
+ font-size: var(--ray-font-size-xs);
47
+ font-variant-numeric: tabular-nums;
48
+ color: var(--ray-muted-foreground);
49
+ }
50
+ .value:empty { display: none; }
51
+ :host([striped]) .bar {
52
+ background-image: linear-gradient(45deg, rgb(255 255 255 / 0.18) 25%, transparent 25%, transparent 50%, rgb(255 255 255 / 0.18) 50%, rgb(255 255 255 / 0.18) 75%, transparent 75%);
53
+ background-size: 1rem 1rem;
54
+ }
55
+ :host([animated]) .bar { animation: ray-stripes 1s linear infinite; }
56
+ @keyframes ray-stripes { from { background-position: 1rem 0; } to { background-position: 0 0; } }
57
+ .track[data-indeterminate] .bar {
58
+ position: absolute;
59
+ width: 40%;
60
+ animation: ray-indeterminate 1.4s var(--ray-easing-standard) infinite;
61
+ }
62
+ @keyframes ray-indeterminate { from { left: -40%; } to { left: 100%; } }
63
+ `;
64
+
65
+ export class RayProgress extends RayElement {
66
+ static properties = {
67
+ value: { type: Number },
68
+ max: { type: Number, default: 100 },
69
+ color: { type: String, reflect: true },
70
+ size: { type: String, reflect: true, default: "md" },
71
+ radius: { type: String, reflect: true },
72
+ striped: { type: Boolean, reflect: true },
73
+ animated: { type: Boolean, reflect: true },
74
+ label: { type: String },
75
+ showValue: { type: Boolean, reflect: true },
76
+ };
77
+ static styles = [radiusStyles, styles];
78
+ static template = `<div class="row"><div class="track" part="track"><div class="bar" part="bar"></div></div><span class="value" part="value" aria-hidden="true"></span></div>`;
79
+
80
+ constructor() {
81
+ super();
82
+ this.internals = this.attachInternals();
83
+ this.internals.role = "progressbar";
84
+ }
85
+
86
+ update() {
87
+ const track = this.$(".track");
88
+ const bar = this.$(".bar");
89
+ applyColor(track, this.color);
90
+ const max = this.max > 0 ? this.max : 100;
91
+ const indeterminate = this.value === undefined || this.value === null || Number.isNaN(this.value);
92
+ track.toggleAttribute("data-indeterminate", indeterminate);
93
+ const pct = indeterminate ? 0 : Math.min(100, Math.max(0, (this.value / max) * 100));
94
+ track.style.setProperty("--_pct", `${pct}%`);
95
+ const text = this.showValue && !indeterminate ? `${Math.round(pct)}%` : "";
96
+ // Inside the bar only when it is tall enough to hold text (lg, xl).
97
+ const inside = this.size === "lg" || this.size === "xl";
98
+ bar.textContent = inside ? text : "";
99
+ this.$(".value").textContent = inside ? "" : text;
100
+ const i = this.internals;
101
+ i.ariaValueMin = "0";
102
+ i.ariaValueMax = String(max);
103
+ i.ariaValueNow = indeterminate ? null : String(this.value);
104
+ i.ariaValueText = indeterminate ? null : `${Math.round(pct)}%`;
105
+ i.ariaLabel = this.label || this.getAttribute("aria-label") || "Progress";
106
+ }
107
+ }
108
+
109
+ define("ray-progress", RayProgress);
@@ -0,0 +1,253 @@
1
+ // <ray-radio-group> + <ray-radio>: one choice among options.
2
+ //
3
+ // Native radios cannot group across shadow roots, so the group owns the
4
+ // state: it is the form control (value submitted under `name`), keeps one
5
+ // radio checked, and moves focus and selection with the arrow keys (roving
6
+ // tabindex), like native radios.
7
+
8
+ import { RayElement, css, define } from "../../core/core.js";
9
+ import { applyColor, sizeStyles } from "../../core/styles.js";
10
+ import { FormControl, fieldStyles } from "../../core/form.js";
11
+
12
+ const groupStyles = css`
13
+ :host { --_rs: 1.25rem; }
14
+ :host([size="xs"]) { --_rs: 1rem; }
15
+ :host([size="md"]) { --_rs: 1.5rem; }
16
+ :host([size="lg"]) { --_rs: 1.875rem; }
17
+ :host([size="xl"]) { --_rs: 2.25rem; }
18
+ .items {
19
+ display: flex;
20
+ flex-direction: column;
21
+ gap: var(--ray-space-3);
22
+ margin-top: var(--ray-space-2);
23
+ }
24
+ :host([orientation="horizontal"]) .items { flex-direction: row; flex-wrap: wrap; gap: var(--ray-space-5); }
25
+ .label { margin-bottom: 0; }
26
+ `;
27
+
28
+ export class RayRadioGroup extends FormControl {
29
+ static delegatesFocus = false;
30
+ static properties = {
31
+ name: { type: String, reflect: true },
32
+ value: { type: String },
33
+ label: { type: String },
34
+ description: { type: String },
35
+ error: { type: String },
36
+ required: { type: Boolean, reflect: true },
37
+ disabled: { type: Boolean, reflect: true },
38
+ orientation: { type: String, reflect: true, default: "vertical" },
39
+ size: { type: String, reflect: true, default: "sm" },
40
+ color: { type: String, reflect: true },
41
+ variant: { type: String, reflect: true, default: "filled" },
42
+ descriptionPosition: { type: String, reflect: true, default: "above" },
43
+ };
44
+ static styles = [sizeStyles, fieldStyles, groupStyles];
45
+ static template = `
46
+ <div class="label" part="label" id="label" hidden><span class="label-text"></span><span class="required" aria-hidden="true" hidden> *</span></div>
47
+ <div class="description" part="description" id="description" hidden></div>
48
+ <div class="items" part="items"><slot></slot></div>
49
+ <div class="error" part="error" id="error" hidden></div>`;
50
+
51
+ constructor() {
52
+ super();
53
+ this.internals.role = "radiogroup";
54
+ this.addEventListener("keydown", (e) => this._keydown(e));
55
+ this.addEventListener("click", (e) => {
56
+ const radio = e.target.closest?.("ray-radio");
57
+ if (radio && this.radios.includes(radio)) this._choose(radio, true);
58
+ });
59
+ }
60
+
61
+ connectedCallback() {
62
+ super.connectedCallback();
63
+ if (!this._wired) {
64
+ this._wired = true;
65
+ this.$("slot").addEventListener("slotchange", () => this.requestUpdate());
66
+ this._default = this.getAttribute("value");
67
+ }
68
+ }
69
+
70
+ /** The radios of this group (direct or nested, not in a nested group). */
71
+ get radios() {
72
+ return [...this.querySelectorAll("ray-radio")].filter((r) => r.closest("ray-radio-group") === this);
73
+ }
74
+
75
+ update() {
76
+ const radios = this.radios;
77
+ // A radio marked `checked` in the markup sets the initial value.
78
+ if (this.value === undefined) {
79
+ const marked = radios.find((r) => r.hasAttribute("checked"));
80
+ if (marked) {
81
+ this.commitProperty("value", marked.value);
82
+ if (this._default === null) this._default = marked.value;
83
+ }
84
+ }
85
+ const disabled = this.effectivelyDisabled;
86
+ const current = radios.find((r) => r.value === this.value);
87
+ const focusable = current && !current.disabled && !disabled ? current : radios.find((r) => !r.disabled);
88
+ for (const r of radios) {
89
+ r._setChecked(r === current);
90
+ r._setGroupDisabled(disabled);
91
+ r.toggleAttribute("data-outline", (r.variant || this.variant) === "outline");
92
+ r.tabIndex = r === focusable && !disabled ? 0 : -1;
93
+ }
94
+
95
+ this.$("#label").hidden = !this.label;
96
+ this.$(".label-text").textContent = this.label ?? "";
97
+ this.$(".required").hidden = !this.required;
98
+ const desc = this.$("#description");
99
+ desc.hidden = !this.description;
100
+ desc.textContent = this.description ?? "";
101
+ const err = this.$("#error");
102
+ err.hidden = !this.error;
103
+ err.textContent = this.error ?? "";
104
+ this.internals.ariaLabel = this.label || this.getAttribute("aria-label") || null;
105
+ this.internals.ariaRequired = this.required ? "true" : "false";
106
+ this.internals.ariaDisabled = disabled ? "true" : "false";
107
+ this.internals.ariaInvalid = this.error ? "true" : "false";
108
+ // Inherited by the radios (custom properties cross into the light DOM children).
109
+ applyColor(this, this.color);
110
+
111
+ this.internals.setFormValue(this.value ?? null);
112
+ if (this.required && !this.value) {
113
+ this.internals.setValidity({ valueMissing: true }, "Please select one of these options.", radios[0]);
114
+ } else {
115
+ this.internals.setValidity({});
116
+ }
117
+ }
118
+
119
+ _choose(radio, user) {
120
+ if (radio.disabled || this.effectivelyDisabled) return;
121
+ const changed = this.value !== radio.value;
122
+ this.value = radio.value;
123
+ radio.focus();
124
+ if (user && changed) {
125
+ this.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
126
+ this.dispatchEvent(new Event("change", { bubbles: true }));
127
+ }
128
+ }
129
+
130
+ _keydown(e) {
131
+ const radios = this.radios.filter((r) => !r.disabled);
132
+ const i = radios.indexOf(e.target.closest?.("ray-radio"));
133
+ if (i < 0) return;
134
+ let next = null;
135
+ if (e.key === "ArrowDown" || e.key === "ArrowRight") next = radios[(i + 1) % radios.length];
136
+ else if (e.key === "ArrowUp" || e.key === "ArrowLeft") next = radios[(i - 1 + radios.length) % radios.length];
137
+ else if (e.key === " ") next = radios[i];
138
+ if (!next) return;
139
+ e.preventDefault();
140
+ this._choose(next, true);
141
+ }
142
+
143
+ formResetCallback() {
144
+ this.value = this._default ?? undefined;
145
+ }
146
+
147
+ formStateRestoreCallback(state) {
148
+ this.value = state ?? undefined;
149
+ }
150
+ }
151
+
152
+ const radioStyles = css`
153
+ :host {
154
+ display: block;
155
+ --_s: var(--_rs, 1.25rem);
156
+ outline: none;
157
+ }
158
+ :host([size="xs"]) { --_s: 1rem; }
159
+ :host([size="sm"]) { --_s: 1.25rem; }
160
+ :host([size="md"]) { --_s: 1.5rem; }
161
+ :host([size="lg"]) { --_s: 1.875rem; }
162
+ :host([size="xl"]) { --_s: 2.25rem; }
163
+ .root { display: flex; align-items: flex-start; gap: calc(var(--_s) * 0.6); cursor: pointer; }
164
+ .indicator {
165
+ flex: none;
166
+ display: inline-flex;
167
+ align-items: center;
168
+ justify-content: center;
169
+ width: var(--_s);
170
+ height: var(--_s);
171
+ background: var(--ray-background);
172
+ border: 1px solid var(--ray-input);
173
+ border-radius: 50%;
174
+ transition: background-color var(--ray-duration-fast) var(--ray-easing-standard), border-color var(--ray-duration-fast) var(--ray-easing-standard);
175
+ }
176
+ .dot { width: 40%; height: 40%; border-radius: 50%; background: var(--_contrast, var(--ray-primary-contrast)); transform: scale(0); transition: transform var(--ray-duration-fast) var(--ray-easing-standard); }
177
+ :host([data-checked]) .indicator { background: var(--_filled, var(--ray-primary-filled)); border-color: var(--_filled, var(--ray-primary-filled)); }
178
+ :host([data-checked]) .dot { transform: scale(1); }
179
+ :host([data-outline][data-checked]) .indicator { background: var(--ray-background); }
180
+ :host([data-outline]) .dot { background: var(--_filled, var(--ray-primary-filled)); width: 50%; height: 50%; }
181
+ :host(:focus-visible) .indicator { outline: var(--ray-focus-ring-width) solid var(--ray-ring); outline-offset: var(--ray-focus-ring-offset); }
182
+ :host([data-disabled]) .root { cursor: not-allowed; }
183
+ :host([data-disabled]) .indicator { background: var(--ray-muted); border-color: var(--ray-border); }
184
+ :host([data-disabled][data-checked]) .indicator { background: var(--ray-border); }
185
+ :host([data-disabled]) .label { color: var(--ray-muted-foreground); }
186
+ .body { display: flex; flex-direction: column; justify-content: center; min-height: var(--_s); }
187
+ .label { font-size: var(--_fs, var(--ray-font-size-sm)); line-height: var(--ray-line-height-sm); }
188
+ .description { margin-top: var(--ray-space-1); font-size: calc(var(--_fs, var(--ray-font-size-sm)) - 0.125rem); color: var(--ray-muted-foreground); }
189
+ [hidden] { display: none !important; }
190
+ `;
191
+
192
+ export class RayRadio extends RayElement {
193
+ static properties = {
194
+ value: { type: String, reflect: true, default: "" },
195
+ label: { type: String },
196
+ description: { type: String },
197
+ disabled: { type: Boolean, reflect: true },
198
+ color: { type: String, reflect: true },
199
+ size: { type: String, reflect: true },
200
+ variant: { type: String, reflect: true },
201
+ };
202
+ static styles = [radioStyles];
203
+ static template = `
204
+ <div class="root" part="root">
205
+ <span class="indicator" part="indicator"><span class="dot"></span></span>
206
+ <span class="body">
207
+ <span class="label" part="label"><span class="label-text"></span><slot></slot></span>
208
+ <span class="description" part="description" hidden></span>
209
+ </span>
210
+ </div>`;
211
+
212
+ constructor() {
213
+ super();
214
+ this.internals = this.attachInternals();
215
+ this.internals.role = "radio";
216
+ this._checked = false;
217
+ this._groupDisabled = false;
218
+ }
219
+
220
+ /** Whether this radio is the group's choice (read-only: set the group's value). */
221
+ get checked() {
222
+ return this._checked;
223
+ }
224
+
225
+ _setChecked(on) {
226
+ this._checked = on;
227
+ this.toggleAttribute("data-checked", on);
228
+ this.internals.ariaChecked = on ? "true" : "false";
229
+ }
230
+
231
+ _setGroupDisabled(on) {
232
+ if (this._groupDisabled === on) return;
233
+ this._groupDisabled = on;
234
+ this.requestUpdate();
235
+ }
236
+
237
+ update(changed) {
238
+ // The group derives checked/tabindex from values and disabled states.
239
+ if (changed.has("value") || changed.has("disabled") || changed.has("variant")) this.closest("ray-radio-group")?.requestUpdate();
240
+ const disabled = Boolean(this.disabled || this._groupDisabled);
241
+ this.toggleAttribute("data-disabled", disabled);
242
+ this.internals.ariaDisabled = disabled ? "true" : "false";
243
+ this.$(".label-text").textContent = this.label ?? "";
244
+ const desc = this.$(".description");
245
+ desc.hidden = !this.description;
246
+ desc.textContent = this.description ?? "";
247
+ if (this.color) applyColor(this.$(".root"), this.color);
248
+ if (!this.closest("ray-radio-group") && !this.hasAttribute("tabindex")) this.tabIndex = 0;
249
+ }
250
+ }
251
+
252
+ define("ray-radio-group", RayRadioGroup);
253
+ define("ray-radio", RayRadio);
@@ -0,0 +1,158 @@
1
+ // <ray-scroll-area>: a box that scrolls its content (Mantine's ScrollArea),
2
+ // with the native scrolling (wheel, touch, keyboard, momentum) and a thin
3
+ // scrollbar painted with the tokens. Edge shadows tell there is more
4
+ // content that way; ray-reach-end fires when the end comes into view (load
5
+ // the next page there).
6
+ //
7
+ // Give it a size: height / max-height (or CSS, or a flex/grid parent).
8
+
9
+ import { RayElement, css, define } from "../../core/core.js";
10
+
11
+ const styles = css`
12
+ :host {
13
+ --_thumb: color-mix(in srgb, var(--ray-foreground) 28%, transparent);
14
+ --_shadow: color-mix(in srgb, var(--ray-foreground) 14%, transparent);
15
+ position: relative;
16
+ display: block;
17
+ min-width: 0;
18
+ min-height: 0;
19
+ overflow: hidden;
20
+ }
21
+ :host([hidden]) { display: none; }
22
+ .viewport {
23
+ box-sizing: border-box;
24
+ width: 100%;
25
+ height: 100%;
26
+ max-height: inherit;
27
+ overflow: auto;
28
+ overscroll-behavior: contain;
29
+ scrollbar-width: thin;
30
+ scrollbar-color: var(--_thumb) transparent;
31
+ outline: none;
32
+ }
33
+ .viewport:focus-visible { box-shadow: inset 0 0 0 var(--ray-focus-ring-width) var(--ray-ring); }
34
+ :host([orientation="vertical"]) .viewport { overflow-x: hidden; }
35
+ :host([orientation="horizontal"]) .viewport { overflow-y: hidden; }
36
+ /* hover: the thumb shows while the pointer is over the area (or it scrolls). */
37
+ :host([type="hover"]) .viewport { scrollbar-color: transparent transparent; transition: scrollbar-color var(--ray-duration-base); }
38
+ :host([type="hover"]:hover) .viewport, :host([type="hover"][data-scrolling]) .viewport, :host([type="hover"]:focus-within) .viewport { scrollbar-color: var(--_thumb) transparent; }
39
+ :host([type="never"]) .viewport { scrollbar-width: none; }
40
+ :host([type="never"]) .viewport::-webkit-scrollbar { display: none; }
41
+
42
+ .edge {
43
+ position: absolute;
44
+ z-index: 1;
45
+ pointer-events: none;
46
+ opacity: 0;
47
+ transition: opacity var(--ray-duration-fast);
48
+ }
49
+ .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; }
52
+ .top { top: 0; background: linear-gradient(var(--_shadow), transparent); }
53
+ .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); }
56
+ :host([no-shadows]) .edge { display: none; }
57
+ `;
58
+
59
+ export class RayScrollArea extends RayElement {
60
+ static properties = {
61
+ height: { type: String },
62
+ maxHeight: { type: String, attribute: "max-height" },
63
+ orientation: { type: String, reflect: true, default: "vertical" },
64
+ type: { type: String, reflect: true, default: "auto" },
65
+ noShadows: { type: Boolean, reflect: true, attribute: "no-shadows" },
66
+ threshold: { type: Number, default: 48 },
67
+ label: { type: String },
68
+ };
69
+ static styles = [styles];
70
+ static template = `
71
+ <div class="viewport" part="viewport" tabindex="0"><slot></slot></div>
72
+ <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>`;
74
+
75
+ #ro = null;
76
+ #atEnd = false;
77
+ #idle = 0;
78
+
79
+ constructor() {
80
+ super();
81
+ /** The element that scrolls. */
82
+ this.viewport = this.$(".viewport");
83
+ this.viewport.addEventListener("scroll", () => this._onScroll(), { passive: true });
84
+ // Content that grows or shrinks moves the edges too.
85
+ this.$("slot").addEventListener("slotchange", () => {
86
+ for (const el of this.children) this.#ro?.observe(el);
87
+ this._edges();
88
+ });
89
+ }
90
+
91
+ connectedCallback() {
92
+ super.connectedCallback();
93
+ this.#ro = new ResizeObserver(() => this._edges());
94
+ this.#ro.observe(this.viewport);
95
+ for (const el of this.children) this.#ro.observe(el);
96
+ }
97
+
98
+ disconnectedCallback() {
99
+ super.disconnectedCallback();
100
+ this.#ro?.disconnect();
101
+ this.#ro = null;
102
+ }
103
+
104
+ update() {
105
+ this.style.height = this.height ?? "";
106
+ this.style.maxHeight = this.maxHeight ?? "";
107
+ // Scrollable content is focusable (keyboard scrolling); name it when asked.
108
+ if (this.label) {
109
+ this.viewport.setAttribute("role", "region");
110
+ this.viewport.setAttribute("aria-label", this.label);
111
+ } else {
112
+ this.viewport.removeAttribute("role");
113
+ this.viewport.removeAttribute("aria-label");
114
+ }
115
+ this._edges();
116
+ }
117
+
118
+ /** Scrolls to the start (top/left). */
119
+ scrollToStart({ behavior = "smooth" } = {}) {
120
+ this.viewport.scrollTo({ top: 0, left: 0, behavior });
121
+ }
122
+
123
+ /** Scrolls to the end (bottom, or right when horizontal). */
124
+ scrollToEnd({ behavior = "smooth" } = {}) {
125
+ const v = this.viewport;
126
+ if (this.orientation === "horizontal") v.scrollTo({ left: v.scrollWidth, behavior });
127
+ else v.scrollTo({ top: v.scrollHeight, behavior });
128
+ }
129
+
130
+ _onScroll() {
131
+ this.setAttribute("data-scrolling", "");
132
+ clearTimeout(this.#idle);
133
+ this.#idle = setTimeout(() => this.removeAttribute("data-scrolling"), 800);
134
+ this._edges();
135
+ }
136
+
137
+ // The edge shadows, and ray-reach-end once per arrival at the end.
138
+ _edges() {
139
+ const v = this.viewport;
140
+ if (!v) return;
141
+ const show = (cls, on) => this.$(`.${cls}`).toggleAttribute("data-show", on);
142
+ const maxY = v.scrollHeight - v.clientHeight;
143
+ const maxX = v.scrollWidth - v.clientWidth;
144
+ const x = Math.abs(v.scrollLeft);
145
+ show("top", v.scrollTop > 1);
146
+ show("bottom", maxY > 1 && v.scrollTop < maxY - 1);
147
+ show("left", x > 1);
148
+ show("right", maxX > 1 && x < maxX - 1);
149
+ const horizontal = this.orientation === "horizontal";
150
+ const left = horizontal ? maxX - x : maxY - v.scrollTop;
151
+ const scrollable = horizontal ? maxX > 1 : maxY > 1;
152
+ const atEnd = scrollable && left <= (this.threshold ?? 48);
153
+ if (atEnd && !this.#atEnd) this.emit("ray-reach-end", { edge: horizontal ? "right" : "bottom" });
154
+ this.#atEnd = atEnd;
155
+ }
156
+ }
157
+
158
+ define("ray-scroll-area", RayScrollArea);