@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,506 @@
1
+ // <ray-select>: a select with its own dropdown. Options come from child
2
+ // <option>/<optgroup> elements or from the `data` property
3
+ // (["a", "b"] or [{ value, label, disabled }]).
4
+ //
5
+ // A hidden native <select> (#control) stays the model: value, required,
6
+ // validity and form participation work exactly like a native one. What the
7
+ // user sees and operates is a combobox button and a listbox in a [popover]
8
+ // (top layer: above dialogs, never clipped), following the WAI-ARIA
9
+ // select-only combobox pattern: focus stays on the button, the active option
10
+ // is aria-activedescendant.
11
+
12
+ import { css, define } from "../../core/core.js";
13
+ import { sizeStyles, radiusStyles } from "../../core/styles.js";
14
+ import { place, autoUpdate } from "../../core/position.js";
15
+ import { dismissable } from "../../core/dismiss.js";
16
+ import { play, POP_OUT } from "../../core/motion.js";
17
+ import { FieldControl, fieldStyles, boxStyles, fieldTemplate } from "../../core/form.js";
18
+
19
+ const styles = css`
20
+ .native {
21
+ position: absolute;
22
+ inset: 0;
23
+ width: 100%;
24
+ height: 100%;
25
+ opacity: 0;
26
+ pointer-events: none;
27
+ }
28
+ .trigger {
29
+ display: flex;
30
+ align-items: center;
31
+ width: 100%;
32
+ min-width: 0;
33
+ padding-right: calc(var(--_h) * 0.8);
34
+ text-align: left;
35
+ color: inherit;
36
+ background: transparent;
37
+ border: 0;
38
+ cursor: pointer;
39
+ }
40
+ .trigger:disabled { cursor: not-allowed; }
41
+ .trigger:focus-visible { outline: none; }
42
+ .value { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
43
+ .trigger[data-placeholder] .value { color: var(--ray-muted-foreground); }
44
+ .chevron {
45
+ position: absolute;
46
+ right: 0;
47
+ display: inline-flex;
48
+ align-items: center;
49
+ justify-content: center;
50
+ width: calc(var(--_h) * 0.8);
51
+ height: 100%;
52
+ color: var(--ray-muted-foreground);
53
+ pointer-events: none;
54
+ transition: transform var(--ray-duration-fast) var(--ray-easing-standard);
55
+ }
56
+ .trigger[aria-expanded="true"] ~ .chevron { transform: rotate(180deg); }
57
+ .chevron svg { width: 1em; height: 1em; }
58
+ .options { display: none; }
59
+
60
+ .listbox {
61
+ box-sizing: border-box;
62
+ max-height: min(18rem, calc(100vh - 16px));
63
+ overflow: auto;
64
+ margin: 0;
65
+ padding: var(--ray-space-1);
66
+ color: var(--ray-popover-foreground);
67
+ background: var(--ray-popover);
68
+ border: 1px solid var(--ray-border);
69
+ border-radius: var(--ray-radius-default);
70
+ box-shadow: var(--ray-shadow-md);
71
+ font-size: var(--_fs);
72
+ overscroll-behavior: contain;
73
+ }
74
+ .listbox:popover-open { animation: ray-pop-in var(--ray-duration-fast) var(--ray-easing-out); }
75
+ @keyframes ray-pop-in { from { opacity: 0; transform: translateY(-2px); } }
76
+ .option {
77
+ display: flex;
78
+ align-items: center;
79
+ gap: var(--ray-space-2);
80
+ min-height: calc(var(--_h) - 0.25rem);
81
+ padding: 0 var(--ray-space-2);
82
+ border-radius: var(--ray-radius-sm);
83
+ cursor: pointer;
84
+ user-select: none;
85
+ }
86
+ /* Not .label: that class is the field's own label (fieldStyles). */
87
+ .option .text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
88
+ .option .mark { display: inline-flex; width: 1em; height: 1em; color: var(--_filled, var(--ray-primary)); visibility: hidden; }
89
+ .option .mark svg { width: 1em; height: 1em; }
90
+ .option[aria-selected="true"] { font-weight: var(--ray-font-weight-medium); }
91
+ .option[aria-selected="true"] .mark { visibility: visible; }
92
+ .option[data-active] { background: var(--ray-accent); color: var(--ray-accent-foreground); }
93
+ .option[aria-disabled="true"] { color: var(--ray-muted-foreground); cursor: not-allowed; opacity: 0.6; }
94
+ .group-label {
95
+ padding: var(--ray-space-2) var(--ray-space-2) var(--ray-space-1);
96
+ font-size: var(--ray-font-size-xs);
97
+ font-weight: var(--ray-font-weight-medium);
98
+ color: var(--ray-muted-foreground);
99
+ }
100
+ .group + .group { margin-top: var(--ray-space-1); padding-top: var(--ray-space-1); border-top: 1px solid var(--ray-border); }
101
+ .empty { padding: var(--ray-space-2); color: var(--ray-muted-foreground); }
102
+ `;
103
+
104
+ const CHEVRON = `<span class="chevron" aria-hidden="true" data-ray-icon="chevron-down"></span>`;
105
+
106
+ export class RaySelect extends FieldControl {
107
+ static properties = {
108
+ ...FieldControl.fieldProperties,
109
+ value: { type: String },
110
+ placeholder: { type: String },
111
+ data: { attribute: false },
112
+ open: { type: Boolean, reflect: true },
113
+ };
114
+ static styles = [sizeStyles, radiusStyles, fieldStyles, boxStyles, styles];
115
+ static template = fieldTemplate(
116
+ `<select id="control" class="native" part="select" tabindex="-1" aria-hidden="true"></select>` +
117
+ `<button type="button" id="trigger" class="control trigger" part="trigger" role="combobox" aria-haspopup="listbox" aria-expanded="false" aria-controls="listbox"><span class="value"></span></button>${CHEVRON}`,
118
+ { sections: false },
119
+ ) + `<div class="listbox" id="listbox" part="dropdown" role="listbox" popover="manual" tabindex="-1"></div><div class="options"><slot></slot></div>`;
120
+
121
+ #active = -1;
122
+ #items = [];
123
+ #stopTracking = null;
124
+ #typed = "";
125
+ #typedAt = 0;
126
+ #undismiss = null;
127
+ #exit = null;
128
+
129
+ constructor() {
130
+ super();
131
+ /** The combobox button the user operates. */
132
+ this.trigger = this.$("#trigger");
133
+ /** The dropdown ([popover] listbox). */
134
+ this.listbox = this.$("#listbox");
135
+ }
136
+
137
+ wire() {
138
+ const label = this.$("label.label");
139
+ label.id = "label";
140
+ label.htmlFor = "trigger";
141
+ this.$(".value").id = "value";
142
+
143
+ // Option changes only: the host's own attributes do not affect the list.
144
+ this._observer = new MutationObserver((records) => {
145
+ if (records.some((r) => !(r.type === "attributes" && r.target === this))) this._renderOptions();
146
+ });
147
+ this._observer.observe(this, { childList: true, subtree: true, attributes: true, characterData: true });
148
+ // The model's own change (also what tests and scripts can drive).
149
+ this.control.addEventListener("change", () => this._changed());
150
+
151
+ this.trigger.addEventListener("click", () => this.toggle());
152
+ this.trigger.addEventListener("keydown", (e) => this._onKey(e));
153
+ this.listbox.addEventListener("toggle", (e) => this._onToggle(e.newState === "open"));
154
+ this.listbox.addEventListener("pointermove", (e) => {
155
+ const i = this.#items.findIndex((o) => o.el === e.target.closest?.(".option"));
156
+ if (i >= 0 && !this.#items[i].disabled) this._activate(i, false);
157
+ });
158
+ this.listbox.addEventListener("click", (e) => {
159
+ const i = this.#items.findIndex((o) => o.el === e.target.closest?.(".option"));
160
+ if (i >= 0) this._choose(i);
161
+ });
162
+ // Clicks on the option list must not steal focus from the button.
163
+ this.listbox.addEventListener("pointerdown", (e) => e.preventDefault());
164
+
165
+ this._renderOptions();
166
+ this._default = this.getAttribute("value") ?? this.control.value;
167
+ }
168
+
169
+ disconnectedCallback() {
170
+ super.disconnectedCallback();
171
+ this.#stopTracking?.();
172
+ this.#stopTracking = null;
173
+ this.#undismiss?.();
174
+ this.#undismiss = null;
175
+ }
176
+
177
+ update(changed) {
178
+ if (changed.has("data") || changed.has("placeholder")) this._renderOptions();
179
+ const c = this.control;
180
+ if (this.value !== undefined && c.value !== this.value) c.value = this.value;
181
+ this.syncField();
182
+ this._commit();
183
+ this._paintValue();
184
+ if (changed.has("open")) this._setOpen(Boolean(this.open));
185
+ }
186
+
187
+ /** Label, description, error and ARIA go to the button the user operates. */
188
+ syncField() {
189
+ super.syncField();
190
+ const c = this.control;
191
+ const t = this.trigger;
192
+ for (const a of ["aria-describedby", "aria-invalid", "aria-label"]) {
193
+ if (c.hasAttribute(a)) t.setAttribute(a, c.getAttribute(a));
194
+ else t.removeAttribute(a);
195
+ }
196
+ c.removeAttribute("aria-label");
197
+ if (this.label) t.setAttribute("aria-labelledby", "label value");
198
+ else t.removeAttribute("aria-labelledby");
199
+ t.setAttribute("aria-required", String(Boolean(this.required)));
200
+ t.disabled = this.effectivelyDisabled;
201
+ if (t.disabled && this.open) this.open = false;
202
+ }
203
+
204
+ /** Validity of the model, anchored on the visible button. */
205
+ syncValidity() {
206
+ const c = this.control;
207
+ if (c.validity.valid) this.internals.setValidity({});
208
+ else this.internals.setValidity(c.validity, c.validationMessage, this.trigger ?? c);
209
+ }
210
+
211
+ // Rebuilds the model <select> from `data` or the light-DOM options, then the listbox.
212
+ _renderOptions() {
213
+ const c = this.control;
214
+ const keep = this.value ?? c.value;
215
+ c.replaceChildren();
216
+ if (this.placeholder) {
217
+ const p = new Option(this.placeholder, "");
218
+ p.disabled = true;
219
+ p.hidden = true;
220
+ p.dataset.placeholder = "";
221
+ c.append(p);
222
+ }
223
+ if (Array.isArray(this.data)) {
224
+ for (const item of this.data) {
225
+ const o = typeof item === "string" ? new Option(item, item) : new Option(item.label ?? item.value, item.value);
226
+ if (typeof item === "object" && item.disabled) o.disabled = true;
227
+ c.append(o);
228
+ }
229
+ } else {
230
+ for (const child of this.children) {
231
+ if (child.localName === "option" || child.localName === "optgroup") c.append(child.cloneNode(true));
232
+ }
233
+ }
234
+ // Keep the current value, else the placeholder (if any) or a selected child.
235
+ if (keep !== undefined && [...c.options].some((o) => o.value === keep && o.dataset.placeholder === undefined)) {
236
+ c.value = keep;
237
+ } else if (this.placeholder && ![...this.children].some((o) => o.selected && o.hasAttribute("selected"))) {
238
+ c.value = "";
239
+ }
240
+ if (this.value === undefined || c.value !== this.value) this.commitProperty("value", c.value);
241
+ this._commit();
242
+ this._renderList();
243
+ this._paintValue();
244
+ }
245
+
246
+ // The listbox mirrors the model: one .option per real option, grouped by optgroup.
247
+ _renderList() {
248
+ const lb = this.listbox;
249
+ if (!lb) return;
250
+ lb.replaceChildren();
251
+ this.#items = [];
252
+ let n = 0;
253
+ const optionEl = (o) => {
254
+ const el = document.createElement("div");
255
+ el.className = "option";
256
+ el.id = `option-${n++}`;
257
+ el.setAttribute("role", "option");
258
+ el.setAttribute("part", "option");
259
+ // A disabled optgroup disables its options; the model <select> itself
260
+ // is disabled while the host is, which must not stick to the options.
261
+ const group = o.parentElement?.localName === "optgroup" ? o.parentElement : null;
262
+ const disabled = o.disabled || Boolean(group?.disabled);
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>`;
265
+ el.firstChild.textContent = o.label || o.text;
266
+ this.#items.push({ el, value: o.value, disabled, text: (o.label || o.text).trim().toLowerCase() });
267
+ return el;
268
+ };
269
+ for (const child of this.control.children) {
270
+ if (child.localName === "optgroup") {
271
+ const group = document.createElement("div");
272
+ group.className = "group";
273
+ group.setAttribute("role", "group");
274
+ const gl = document.createElement("div");
275
+ gl.className = "group-label";
276
+ gl.id = `group-${n}`;
277
+ gl.textContent = child.label;
278
+ group.setAttribute("aria-labelledby", gl.id);
279
+ group.append(gl, ...[...child.children].map(optionEl));
280
+ lb.append(group);
281
+ } else if (child.dataset.placeholder === undefined) {
282
+ lb.append(optionEl(child));
283
+ }
284
+ }
285
+ if (!this.#items.length) {
286
+ const empty = document.createElement("div");
287
+ empty.className = "empty";
288
+ empty.textContent = "No options";
289
+ lb.append(empty);
290
+ }
291
+ this.paintIcons(lb);
292
+ this._paintSelected();
293
+ }
294
+
295
+ _paintSelected() {
296
+ const v = this.control.value;
297
+ const placeholder = this.control.selectedOptions[0]?.dataset.placeholder !== undefined;
298
+ for (const it of this.#items) it.el.setAttribute("aria-selected", String(!placeholder && it.value === v));
299
+ }
300
+
301
+ _paintValue() {
302
+ const t = this.trigger;
303
+ if (!t) return;
304
+ const o = this.control.selectedOptions[0];
305
+ const isPlaceholder = !o || o.dataset.placeholder !== undefined;
306
+ this.$(".value").textContent = o ? o.label || o.text : this.placeholder ?? "";
307
+ t.toggleAttribute("data-placeholder", isPlaceholder);
308
+ this._paintSelected();
309
+ }
310
+
311
+ _commit() {
312
+ this.internals.setFormValue(this.control.value || null);
313
+ this.syncValidity();
314
+ }
315
+
316
+ // After the model changed by the user (listbox, typeahead) or a script.
317
+ _changed() {
318
+ this.commitProperty("value", this.control.value);
319
+ this._commit();
320
+ this._paintValue();
321
+ this.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
322
+ this.dispatchEvent(new Event("change", { bubbles: true }));
323
+ }
324
+
325
+ _choose(i) {
326
+ const it = this.#items[i];
327
+ if (!it || it.disabled) return;
328
+ this.open = false;
329
+ if (this.control.value !== it.value) {
330
+ this.control.value = it.value;
331
+ this._changed();
332
+ }
333
+ this.trigger.focus();
334
+ }
335
+
336
+ _activate(i, scroll = true) {
337
+ this.#items[this.#active]?.el.removeAttribute("data-active");
338
+ this.#active = i;
339
+ const it = this.#items[i];
340
+ if (!it) {
341
+ this.trigger.removeAttribute("aria-activedescendant");
342
+ return;
343
+ }
344
+ it.el.setAttribute("data-active", "");
345
+ this.trigger.setAttribute("aria-activedescendant", it.el.id);
346
+ if (scroll) it.el.scrollIntoView({ block: "nearest" });
347
+ }
348
+
349
+ // The next enabled option from `from` in direction `dir` (clamped at the ends).
350
+ _step(from, dir) {
351
+ for (let i = from + dir; i >= 0 && i < this.#items.length; i += dir) {
352
+ if (!this.#items[i].disabled) return i;
353
+ }
354
+ return from;
355
+ }
356
+
357
+ _edge(dir) {
358
+ return dir > 0 ? this._step(-1, 1) : this._step(this.#items.length, -1);
359
+ }
360
+
361
+ _selectedIndex() {
362
+ return this.#items.findIndex((it) => it.value === this.control.value && it.el.getAttribute("aria-selected") === "true");
363
+ }
364
+
365
+ // Typeahead: the next option starting with what was typed in the last 500 ms.
366
+ _typeahead(ch) {
367
+ const now = Date.now();
368
+ this.#typed = now - this.#typedAt > 500 ? ch : this.#typed + ch;
369
+ this.#typedAt = now;
370
+ const start = this.open ? this.#active : this._selectedIndex();
371
+ const n = this.#items.length;
372
+ const same = this.#typed.split("").every((c) => c === this.#typed[0]);
373
+ const query = same ? this.#typed[0] : this.#typed;
374
+ for (let k = 1; k <= n; k++) {
375
+ const i = (Math.max(start, 0) + (same || this.#typed.length === 1 ? k : k - 1)) % n;
376
+ const it = this.#items[i];
377
+ if (!it.disabled && it.text.startsWith(query)) return i;
378
+ }
379
+ return -1;
380
+ }
381
+
382
+ _onKey(e) {
383
+ if (this.trigger.disabled) return;
384
+ const open = Boolean(this.open);
385
+ const k = e.key;
386
+ if (!open) {
387
+ if (k === "ArrowDown" || k === "ArrowUp" || k === "Enter" || k === " ") {
388
+ e.preventDefault();
389
+ this.open = true;
390
+ const sel = this._selectedIndex();
391
+ this._activate(sel >= 0 ? sel : this._edge(k === "ArrowUp" ? -1 : 1));
392
+ } else if (k.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
393
+ // Closed: typing selects, like a native select.
394
+ const i = this._typeahead(k.toLowerCase());
395
+ if (i >= 0 && this.#items[i].value !== this.control.value) {
396
+ this.control.value = this.#items[i].value;
397
+ this._changed();
398
+ }
399
+ }
400
+ return;
401
+ }
402
+ if (k === "ArrowDown" || k === "ArrowUp") {
403
+ e.preventDefault();
404
+ if (e.altKey && k === "ArrowUp") return this._choose(this.#active);
405
+ this._activate(this.#active < 0 ? this._edge(k === "ArrowDown" ? 1 : -1) : this._step(this.#active, k === "ArrowDown" ? 1 : -1));
406
+ } else if (k === "Home" || k === "End" || k === "PageUp" || k === "PageDown") {
407
+ e.preventDefault();
408
+ this._activate(this._edge(k === "Home" || k === "PageUp" ? 1 : -1));
409
+ } else if (k === "Enter" || k === " ") {
410
+ e.preventDefault();
411
+ this._choose(this.#active);
412
+ } else if (k === "Escape") {
413
+ e.preventDefault();
414
+ this.open = false;
415
+ } else if (k === "Tab") {
416
+ this.open = false;
417
+ } else if (k.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
418
+ const i = this._typeahead(k.toLowerCase());
419
+ if (i >= 0) this._activate(i);
420
+ }
421
+ }
422
+
423
+ /** Opens or closes the dropdown. */
424
+ toggle(force) {
425
+ if (this.trigger.disabled) return;
426
+ this.open = force ?? !this.open;
427
+ }
428
+
429
+ _setOpen(open) {
430
+ const lb = this.listbox;
431
+ if (!lb || !this.isConnected) return;
432
+ const shown = lb.matches(":popover-open");
433
+ if (open) {
434
+ // Reopened while animating out: stay open.
435
+ this.#exit?.cancel();
436
+ this.#exit = null;
437
+ lb.style.pointerEvents = "";
438
+ if (!shown) {
439
+ try {
440
+ lb.showPopover();
441
+ } catch {
442
+ this.commitProperty("open", false);
443
+ return;
444
+ }
445
+ }
446
+ this.#undismiss ??= dismissable({ panel: lb, owner: this, trigger: this.trigger, close: () => { this.open = false; } });
447
+ } else if (shown && !this.#exit) {
448
+ this._leave();
449
+ }
450
+ }
451
+
452
+ // Animate the dropdown out, then really hide it (unless reopened).
453
+ async _leave() {
454
+ const lb = this.listbox;
455
+ this.#undismiss?.();
456
+ this.#undismiss = null;
457
+ this.trigger.setAttribute("aria-expanded", "false");
458
+ const exit = play(lb, POP_OUT, { speed: "fast" });
459
+ this.#exit = exit;
460
+ lb.style.pointerEvents = "none";
461
+ await exit.finished;
462
+ lb.style.pointerEvents = "";
463
+ if (this.#exit !== exit) return;
464
+ this.#exit = null;
465
+ if (!this.open && lb.matches(":popover-open")) lb.hidePopover();
466
+ exit.cancel();
467
+ }
468
+
469
+ _onToggle(open) {
470
+ this.trigger.setAttribute("aria-expanded", String(open));
471
+ if (open) {
472
+ const box = this.$(".box");
473
+ const update = () => place(box.getBoundingClientRect(), this.listbox, { placement: "bottom-start", offset: 4, matchWidth: true });
474
+ update();
475
+ this.#stopTracking?.();
476
+ this.#stopTracking = autoUpdate(box, this.listbox, update);
477
+ if (this.#active < 0) {
478
+ const sel = this._selectedIndex();
479
+ this._activate(sel >= 0 ? sel : this._edge(1));
480
+ } else {
481
+ this.#items[this.#active]?.el.scrollIntoView({ block: "nearest" });
482
+ }
483
+ } else {
484
+ this.#stopTracking?.();
485
+ this.#stopTracking = null;
486
+ this._activate(-1);
487
+ }
488
+ if (Boolean(this.open) !== open) this.commitProperty("open", open);
489
+ this.dispatchEvent(new CustomEvent(open ? "ray-open" : "ray-close", { bubbles: true, composed: true }));
490
+ }
491
+
492
+ /** The option objects of the model select. */
493
+ get options() {
494
+ return this.control.options;
495
+ }
496
+
497
+ formResetCallback() {
498
+ this.value = this._default;
499
+ }
500
+
501
+ formStateRestoreCallback(state) {
502
+ this.value = String(state ?? "");
503
+ }
504
+ }
505
+
506
+ define("ray-select", RaySelect);
@@ -0,0 +1,58 @@
1
+ // <ray-separator>: a horizontal or vertical rule, optionally with a label
2
+ // (Mantine's Divider).
3
+
4
+ import { RayElement, css, define } from "../../core/core.js";
5
+
6
+ const styles = css`
7
+ :host {
8
+ display: block;
9
+ --_color: var(--ray-border);
10
+ --_w: 1px;
11
+ --_style: solid;
12
+ }
13
+ :host([size="sm"]) { --_w: 2px; }
14
+ :host([size="md"]) { --_w: 3px; }
15
+ :host([size="lg"]) { --_w: 4px; }
16
+ :host([size="xl"]) { --_w: 5px; }
17
+ :host([variant="dashed"]) { --_style: dashed; }
18
+ :host([variant="dotted"]) { --_style: dotted; }
19
+ :host([orientation="vertical"]) { display: inline-block; align-self: stretch; height: auto; min-height: 1em; vertical-align: middle; }
20
+ .line { border-top: var(--_w) var(--_style) var(--_color); }
21
+ :host([orientation="vertical"]) .line { height: 100%; border-top: 0; border-left: var(--_w) var(--_style) var(--_color); }
22
+ .labeled {
23
+ display: flex;
24
+ align-items: center;
25
+ gap: var(--ray-space-2);
26
+ font-size: var(--ray-font-size-xs);
27
+ color: var(--ray-muted-foreground);
28
+ }
29
+ .labeled::before, .labeled::after { content: ""; flex: 1; border-top: var(--_w) var(--_style) var(--_color); }
30
+ :host([label-position="left"]) .labeled::before { flex: 0 0 var(--ray-space-3); }
31
+ :host([label-position="right"]) .labeled::after { flex: 0 0 var(--ray-space-3); }
32
+ [hidden] { display: none !important; }
33
+ `;
34
+
35
+ export class RaySeparator extends RayElement {
36
+ static properties = {
37
+ orientation: { type: String, reflect: true, default: "horizontal" },
38
+ label: { type: String, reflect: true },
39
+ labelPosition: { type: String, reflect: true, default: "center" },
40
+ size: { type: String, reflect: true, default: "xs" },
41
+ variant: { type: String, reflect: true, default: "solid" },
42
+ };
43
+ static styles = [styles];
44
+ static template = `<div class="line" part="line"></div><div class="labeled" part="label" hidden></div>`;
45
+
46
+ update() {
47
+ const vertical = this.orientation === "vertical";
48
+ this.setAttribute("role", "separator");
49
+ this.setAttribute("aria-orientation", vertical ? "vertical" : "horizontal");
50
+ const labeled = this.$(".labeled");
51
+ const showLabel = Boolean(this.label) && !vertical;
52
+ labeled.hidden = !showLabel;
53
+ labeled.textContent = this.label ?? "";
54
+ this.$(".line").hidden = showLabel;
55
+ }
56
+ }
57
+
58
+ define("ray-separator", RaySeparator);