@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,594 @@
1
+ // <ray-calendar>: a month grid to pick a day (WAI-ARIA grid pattern: roving
2
+ // tabindex, arrows by day and week, Home/End, PageUp/PageDown by month,
3
+ // Shift+PageUp/PageDown by year, Enter/Space to select).
4
+ //
5
+ // <ray-date-picker>: a form field showing the chosen date, with a calendar in
6
+ // a dropdown. A hidden native <input type="date"> (#control) stays the model:
7
+ // value, required, min/max validity and form participation work like a native
8
+ // one. The dropdown is a manual [popover] (top layer) with light dismiss
9
+ // (core/dismiss.js) and an exit animation (core/motion.js).
10
+ //
11
+ // Dates are ISO strings ("2026-10-06") handled as plain calendar days (UTC
12
+ // arithmetic), so no time zone shifts a day.
13
+
14
+ import { RayElement, css, define } from "../../core/core.js";
15
+ import { sizeStyles, radiusStyles } from "../../core/styles.js";
16
+ import { place, autoUpdate } from "../../core/position.js";
17
+ import { dismissable } from "../../core/dismiss.js";
18
+ import { play, POP_OUT } from "../../core/motion.js";
19
+ import { FieldControl, fieldStyles, boxStyles, fieldTemplate } from "../../core/form.js";
20
+
21
+ // --- plain calendar days ------------------------------------------------------
22
+
23
+ const ISO = /^(\d{4})-(\d{2})-(\d{2})$/;
24
+
25
+ /** "2026-10-06" -> a UTC Date at midnight, or null. */
26
+ function parse(iso) {
27
+ const m = ISO.exec(String(iso ?? ""));
28
+ if (!m) return null;
29
+ const d = new Date(Date.UTC(+m[1], +m[2] - 1, +m[3]));
30
+ return d.getUTCMonth() === +m[2] - 1 ? d : null;
31
+ }
32
+
33
+ /** A UTC Date -> "2026-10-06". */
34
+ function iso(d) {
35
+ return d.toISOString().slice(0, 10);
36
+ }
37
+
38
+ function addDays(d, n) {
39
+ const r = new Date(d);
40
+ r.setUTCDate(r.getUTCDate() + n);
41
+ return r;
42
+ }
43
+
44
+ /** Same day `n` months later, clamped to the month's last day (Jan 31 + 1 = Feb 28). */
45
+ function addMonths(d, n) {
46
+ const y = d.getUTCFullYear();
47
+ const m = d.getUTCMonth() + n;
48
+ const last = new Date(Date.UTC(y, m + 1, 0)).getUTCDate();
49
+ return new Date(Date.UTC(y, m, Math.min(d.getUTCDate(), last)));
50
+ }
51
+
52
+ function today() {
53
+ const now = new Date();
54
+ return new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()));
55
+ }
56
+
57
+ function localeOf(el) {
58
+ return el.locale || el.closest("[lang]")?.getAttribute("lang") || navigator.language || "en";
59
+ }
60
+
61
+ /** The first day of the week for a locale (0 = Sunday … 6 = Saturday). */
62
+ function firstDayOf(locale) {
63
+ try {
64
+ const loc = new Intl.Locale(locale);
65
+ const info = typeof loc.getWeekInfo === "function" ? loc.getWeekInfo() : loc.weekInfo;
66
+ if (info?.firstDay) return info.firstDay % 7;
67
+ } catch {
68
+ // An invalid locale tag: fall back below.
69
+ }
70
+ return 1;
71
+ }
72
+
73
+ // --- ray-calendar ---------------------------------------------------------------
74
+
75
+ const calendarStyles = css`
76
+ :host {
77
+ display: inline-block;
78
+ width: fit-content;
79
+ --_cell: var(--_h);
80
+ font-size: var(--_fs);
81
+ color: var(--ray-foreground);
82
+ }
83
+ .header { display: flex; align-items: center; justify-content: space-between; gap: var(--ray-space-2); margin-bottom: var(--ray-space-2); }
84
+ .caption { flex: 1; text-align: center; font-weight: var(--ray-font-weight-medium); white-space: nowrap; }
85
+ .nav {
86
+ display: inline-flex;
87
+ align-items: center;
88
+ justify-content: center;
89
+ width: calc(var(--_cell) * 0.85);
90
+ height: calc(var(--_cell) * 0.85);
91
+ padding: 0;
92
+ color: var(--ray-muted-foreground);
93
+ background: transparent;
94
+ border: 1px solid var(--ray-border);
95
+ border-radius: var(--ray-radius-sm);
96
+ cursor: pointer;
97
+ }
98
+ .nav:hover { color: var(--ray-foreground); background: var(--ray-default-hover); }
99
+ .nav svg { width: 1em; height: 1em; }
100
+ table { border-collapse: collapse; }
101
+ th {
102
+ width: var(--_cell);
103
+ height: calc(var(--_cell) * 0.8);
104
+ padding: 0;
105
+ font-size: var(--ray-font-size-xs);
106
+ font-weight: var(--ray-font-weight-normal, 400);
107
+ color: var(--ray-muted-foreground);
108
+ }
109
+ td { padding: 1px; text-align: center; }
110
+ .day {
111
+ width: var(--_cell);
112
+ height: var(--_cell);
113
+ padding: 0;
114
+ font: inherit;
115
+ color: inherit;
116
+ font-variant-numeric: tabular-nums;
117
+ background: transparent;
118
+ border: 0;
119
+ border-radius: var(--ray-radius-sm);
120
+ cursor: pointer;
121
+ }
122
+ .day:hover:not([aria-disabled="true"]) { background: var(--ray-accent); color: var(--ray-accent-foreground); }
123
+ .day[data-outside] { color: var(--ray-muted-foreground); }
124
+ .day[aria-current="date"] { background: var(--ray-accent); font-weight: var(--ray-font-weight-semibold); }
125
+ .day[data-selected],
126
+ .day[data-selected]:hover {
127
+ color: var(--ray-primary-foreground);
128
+ background: var(--ray-primary);
129
+ font-weight: var(--ray-font-weight-semibold);
130
+ }
131
+ .day[aria-disabled="true"] { opacity: 0.35; cursor: not-allowed; }
132
+ .day:focus-visible { outline: var(--ray-focus-ring-width) solid var(--ray-ring); outline-offset: 1px; }
133
+ `;
134
+
135
+ export class RayCalendar extends RayElement {
136
+ static properties = {
137
+ value: { type: String },
138
+ min: { type: String },
139
+ max: { type: String },
140
+ locale: { type: String },
141
+ firstDay: { type: Number },
142
+ size: { type: String, reflect: true, default: "sm" },
143
+ };
144
+ static styles = [sizeStyles, calendarStyles];
145
+ static template = `
146
+ <div class="header" part="header">
147
+ <button class="nav prev focus-ring" part="nav" type="button" data-ray-icon="chevron-left"></button>
148
+ <div class="caption" part="caption" id="caption" aria-live="polite"></div>
149
+ <button class="nav next focus-ring" part="nav" type="button" data-ray-icon="chevron-right"></button>
150
+ </div>
151
+ <table role="grid" aria-labelledby="caption" part="grid"><thead><tr></tr></thead><tbody></tbody></table>`;
152
+
153
+ /** The day the keyboard focus is on (roving tabindex). */
154
+ #focus = null;
155
+
156
+ constructor() {
157
+ super();
158
+ this.$(".prev").addEventListener("click", () => this._moveFocus(addMonths(this._focusDate(), -1), false));
159
+ this.$(".next").addEventListener("click", () => this._moveFocus(addMonths(this._focusDate(), 1), false));
160
+ const body = this.$("tbody");
161
+ body.addEventListener("click", (e) => {
162
+ const b = e.target.closest?.(".day");
163
+ if (b) this._select(b.dataset.date);
164
+ });
165
+ body.addEventListener("keydown", (e) => this._onKey(e));
166
+ }
167
+
168
+ update(changed) {
169
+ if (changed.has("value") || !this.#focus) {
170
+ const v = parse(this.value);
171
+ if (v) this.#focus = v;
172
+ }
173
+ this._render();
174
+ }
175
+
176
+ /** Focuses the day the keyboard is on (the selected day, else today). */
177
+ focusDay() {
178
+ this._render();
179
+ this.$('.day[tabindex="0"]')?.focus();
180
+ }
181
+
182
+ /** The day the keyboard is on, as ISO. */
183
+ get focusedDate() {
184
+ return iso(this._focusDate());
185
+ }
186
+
187
+ _focusDate() {
188
+ return this.#focus ?? parse(this.value) ?? today();
189
+ }
190
+
191
+ _disabled(d) {
192
+ const min = parse(this.min);
193
+ const max = parse(this.max);
194
+ return Boolean((min && d < min) || (max && d > max));
195
+ }
196
+
197
+ _render() {
198
+ const locale = localeOf(this);
199
+ const focus = this._focusDate();
200
+ const year = focus.getUTCFullYear();
201
+ const month = focus.getUTCMonth();
202
+ const first = this.firstDay ?? firstDayOf(locale);
203
+ this.$(".caption").textContent = new Intl.DateTimeFormat(locale, { month: "long", year: "numeric", timeZone: "UTC" }).format(focus);
204
+ const monthName = new Intl.DateTimeFormat(locale, { month: "long", timeZone: "UTC" });
205
+ this.$(".prev").setAttribute("aria-label", `Previous month, ${monthName.format(addMonths(focus, -1))}`);
206
+ this.$(".next").setAttribute("aria-label", `Next month, ${monthName.format(addMonths(focus, 1))}`);
207
+
208
+ // Weekday headers: 2023-01-01 was a Sunday.
209
+ const short = new Intl.DateTimeFormat(locale, { weekday: "short", timeZone: "UTC" });
210
+ const long = new Intl.DateTimeFormat(locale, { weekday: "long", timeZone: "UTC" });
211
+ const head = this.$("thead tr");
212
+ head.replaceChildren();
213
+ for (let i = 0; i < 7; i++) {
214
+ const d = new Date(Date.UTC(2023, 0, 1 + ((first + i) % 7)));
215
+ const th = document.createElement("th");
216
+ th.scope = "col";
217
+ th.abbr = long.format(d);
218
+ th.textContent = short.format(d);
219
+ head.append(th);
220
+ }
221
+
222
+ // Six weeks, starting on the first day of the week on or before the 1st.
223
+ const start = new Date(Date.UTC(year, month, 1));
224
+ const offset = (start.getUTCDay() - first + 7) % 7;
225
+ let day = addDays(start, -offset);
226
+ const label = new Intl.DateTimeFormat(locale, { dateStyle: "full", timeZone: "UTC" });
227
+ const selected = this.value && parse(this.value) ? this.value : "";
228
+ const now = iso(today());
229
+ const focusIso = iso(focus);
230
+ const body = this.$("tbody");
231
+ body.replaceChildren();
232
+ for (let w = 0; w < 6; w++) {
233
+ const tr = document.createElement("tr");
234
+ for (let i = 0; i < 7; i++) {
235
+ const id = iso(day);
236
+ const td = document.createElement("td");
237
+ td.setAttribute("role", "gridcell");
238
+ const b = document.createElement("button");
239
+ b.type = "button";
240
+ b.className = "day";
241
+ b.setAttribute("part", "day");
242
+ b.dataset.date = id;
243
+ b.textContent = String(day.getUTCDate());
244
+ b.setAttribute("aria-label", label.format(day));
245
+ b.tabIndex = id === focusIso ? 0 : -1;
246
+ if (day.getUTCMonth() !== month) b.toggleAttribute("data-outside", true);
247
+ if (id === now) b.setAttribute("aria-current", "date");
248
+ const sel = id === selected;
249
+ // aria-selected belongs to the gridcell (a button cannot have it).
250
+ td.setAttribute("aria-selected", String(sel));
251
+ b.toggleAttribute("data-selected", sel);
252
+ if (this._disabled(day)) b.setAttribute("aria-disabled", "true");
253
+ td.append(b);
254
+ tr.append(td);
255
+ day = addDays(day, 1);
256
+ }
257
+ body.append(tr);
258
+ }
259
+ }
260
+
261
+ _moveFocus(d, focus = true) {
262
+ this.#focus = d;
263
+ this._render();
264
+ if (focus) this.$('.day[tabindex="0"]')?.focus();
265
+ }
266
+
267
+ _onKey(e) {
268
+ const d = this._focusDate();
269
+ const first = this.firstDay ?? firstDayOf(localeOf(this));
270
+ let next = null;
271
+ switch (e.key) {
272
+ case "ArrowLeft": next = addDays(d, -1); break;
273
+ case "ArrowRight": next = addDays(d, 1); break;
274
+ case "ArrowUp": next = addDays(d, -7); break;
275
+ case "ArrowDown": next = addDays(d, 7); break;
276
+ case "Home": next = addDays(d, -((d.getUTCDay() - first + 7) % 7)); break;
277
+ case "End": next = addDays(d, 6 - ((d.getUTCDay() - first + 7) % 7)); break;
278
+ case "PageUp": next = addMonths(d, e.shiftKey ? -12 : -1); break;
279
+ case "PageDown": next = addMonths(d, e.shiftKey ? 12 : 1); break;
280
+ case "Enter":
281
+ case " ":
282
+ e.preventDefault();
283
+ this._select(iso(d));
284
+ return;
285
+ default:
286
+ return;
287
+ }
288
+ e.preventDefault();
289
+ this._moveFocus(next);
290
+ }
291
+
292
+ _select(id) {
293
+ const d = parse(id);
294
+ if (!d || this._disabled(d)) return;
295
+ this.#focus = d;
296
+ const changed = this.value !== id;
297
+ this.commitProperty("value", id);
298
+ this._render();
299
+ this.$('.day[tabindex="0"]')?.focus();
300
+ if (!changed) return;
301
+ this.dispatchEvent(new Event("change", { bubbles: true }));
302
+ }
303
+ }
304
+
305
+ define("ray-calendar", RayCalendar);
306
+
307
+ // --- ray-date-picker ------------------------------------------------------------
308
+
309
+ const pickerStyles = css`
310
+ .native {
311
+ position: absolute;
312
+ inset: 0;
313
+ width: 100%;
314
+ height: 100%;
315
+ opacity: 0;
316
+ pointer-events: none;
317
+ }
318
+ .trigger {
319
+ display: flex;
320
+ align-items: center;
321
+ gap: var(--ray-space-2);
322
+ width: 100%;
323
+ min-width: 0;
324
+ text-align: left;
325
+ color: inherit;
326
+ background: transparent;
327
+ border: 0;
328
+ cursor: pointer;
329
+ }
330
+ .trigger:disabled { cursor: not-allowed; }
331
+ .trigger:focus-visible { outline: none; }
332
+ .icon { display: inline-flex; color: var(--ray-muted-foreground); }
333
+ .icon svg { width: 1.1em; height: 1.1em; }
334
+ .value { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
335
+ .trigger[data-placeholder] .value { color: var(--ray-muted-foreground); }
336
+ .clear {
337
+ display: inline-flex;
338
+ flex: none;
339
+ align-items: center;
340
+ justify-content: center;
341
+ width: calc(var(--_h) * 0.6);
342
+ height: calc(var(--_h) * 0.6);
343
+ margin-right: calc(var(--_px) * 0.4);
344
+ padding: 0;
345
+ color: var(--ray-muted-foreground);
346
+ background: transparent;
347
+ border: 0;
348
+ border-radius: var(--ray-radius-sm);
349
+ cursor: pointer;
350
+ }
351
+ .clear:hover { color: var(--ray-foreground); background: var(--ray-default-hover); }
352
+ .clear svg { width: 0.9em; height: 0.9em; }
353
+ .clear[hidden] { display: none; }
354
+ .panel {
355
+ box-sizing: border-box;
356
+ margin: 0;
357
+ padding: var(--ray-space-3);
358
+ color: var(--ray-popover-foreground);
359
+ background: var(--ray-popover);
360
+ border: 1px solid var(--ray-border);
361
+ border-radius: var(--ray-radius-md);
362
+ box-shadow: var(--ray-shadow-md);
363
+ }
364
+ .panel:popover-open { animation: ray-pop-in var(--ray-duration-fast) var(--ray-easing-out); }
365
+ @keyframes ray-pop-in { from { opacity: 0; transform: translateY(-2px); } }
366
+ `;
367
+
368
+ export class RayDatePicker extends FieldControl {
369
+ static properties = {
370
+ ...FieldControl.fieldProperties,
371
+ value: { type: String },
372
+ min: { type: String },
373
+ max: { type: String },
374
+ placeholder: { type: String, default: "Pick a date" },
375
+ format: { type: String, default: "medium" },
376
+ locale: { type: String },
377
+ firstDay: { type: Number },
378
+ clearable: { type: Boolean, reflect: true },
379
+ open: { type: Boolean, reflect: true },
380
+ };
381
+ static styles = [sizeStyles, radiusStyles, fieldStyles, boxStyles, pickerStyles];
382
+ static template = fieldTemplate(
383
+ `<input id="control" class="native" part="input" type="date" tabindex="-1" aria-hidden="true">` +
384
+ `<button type="button" id="trigger" class="control trigger" part="trigger" aria-haspopup="dialog" aria-expanded="false" aria-controls="panel">` +
385
+ `<span class="icon" aria-hidden="true" data-ray-icon="calendar"></span><span class="value" id="value"></span></button>` +
386
+ `<button type="button" class="clear focus-ring" part="clear" aria-label="Clear date" hidden><span data-ray-icon="close" aria-hidden="true"></span></button>`,
387
+ { sections: false },
388
+ ) + `<div class="panel" id="panel" part="dropdown" popover="manual" role="dialog" aria-label="Choose a date"><ray-calendar part="calendar" exportparts="day, caption, nav, grid"></ray-calendar></div>`;
389
+
390
+ #undismiss = null;
391
+ #exit = null;
392
+ #stopTracking = null;
393
+
394
+ constructor() {
395
+ super();
396
+ /** The button that shows the date and opens the calendar. */
397
+ this.trigger = this.$("#trigger");
398
+ /** The dropdown ([popover]) holding the calendar. */
399
+ this.panel = this.$("#panel");
400
+ /** The <ray-calendar> in the dropdown. */
401
+ this.calendar = this.$("ray-calendar");
402
+ }
403
+
404
+ wire() {
405
+ const label = this.$("label.label");
406
+ label.id = "label";
407
+ label.htmlFor = "trigger";
408
+ this.trigger.addEventListener("click", () => this.toggle());
409
+ this.trigger.addEventListener("keydown", (e) => {
410
+ if (e.key === "ArrowDown" && !this.open) {
411
+ e.preventDefault();
412
+ this.toggle(true);
413
+ }
414
+ });
415
+ this.$(".clear").addEventListener("click", () => {
416
+ this._set("");
417
+ this.trigger.focus();
418
+ });
419
+ this.calendar.addEventListener("change", (e) => {
420
+ // The picker reports its own change: keep the calendar's inside.
421
+ e.stopPropagation();
422
+ this._set(this.calendar.value);
423
+ this.open = false;
424
+ this.trigger.focus();
425
+ });
426
+ this.panel.addEventListener("toggle", (e) => this._onToggle(e.newState === "open"));
427
+ this._default = this.getAttribute("value") ?? "";
428
+ }
429
+
430
+ disconnectedCallback() {
431
+ super.disconnectedCallback();
432
+ this.#stopTracking?.();
433
+ this.#stopTracking = null;
434
+ this.#undismiss?.();
435
+ this.#undismiss = null;
436
+ }
437
+
438
+ update(changed) {
439
+ const c = this.control;
440
+ c.min = this.min ?? "";
441
+ c.max = this.max ?? "";
442
+ const v = this.value ?? "";
443
+ if (c.value !== v) c.value = v;
444
+ // An invalid ISO string leaves the native input empty: mirror that.
445
+ if (this.value && c.value !== this.value) this.commitProperty("value", c.value);
446
+ const cal = this.calendar;
447
+ cal.value = c.value;
448
+ cal.min = this.min;
449
+ cal.max = this.max;
450
+ cal.locale = this.locale;
451
+ cal.firstDay = this.firstDay;
452
+ cal.size = this.size === "xs" ? "xs" : this.size === "lg" || this.size === "xl" ? "md" : "sm";
453
+ this.syncField();
454
+ this._commit();
455
+ this._paint();
456
+ if (changed.has("open")) this._setOpen(Boolean(this.open));
457
+ }
458
+
459
+ /** Label, description, error and ARIA go to the button the user operates. */
460
+ syncField() {
461
+ super.syncField();
462
+ const c = this.control;
463
+ const t = this.trigger;
464
+ for (const a of ["aria-describedby", "aria-invalid", "aria-label"]) {
465
+ if (c.hasAttribute(a)) t.setAttribute(a, c.getAttribute(a));
466
+ else t.removeAttribute(a);
467
+ }
468
+ c.removeAttribute("aria-label");
469
+ if (this.label) t.setAttribute("aria-labelledby", "label value");
470
+ else t.removeAttribute("aria-labelledby");
471
+ t.disabled = this.effectivelyDisabled;
472
+ if (t.disabled && this.open) this.open = false;
473
+ }
474
+
475
+ /** Validity of the model, anchored on the visible button. */
476
+ syncValidity() {
477
+ const c = this.control;
478
+ if (c.validity.valid) this.internals.setValidity({});
479
+ else this.internals.setValidity(c.validity, c.validationMessage, this.trigger ?? c);
480
+ }
481
+
482
+ /** The chosen date formatted for display ("" when none). */
483
+ get displayValue() {
484
+ const d = parse(this.control.value);
485
+ if (!d) return "";
486
+ const style = ["short", "medium", "long", "full"].includes(this.format) ? this.format : "medium";
487
+ return new Intl.DateTimeFormat(localeOf(this), { dateStyle: style, timeZone: "UTC" }).format(d);
488
+ }
489
+
490
+ _paint() {
491
+ const shown = this.displayValue;
492
+ this.$(".value").textContent = shown || (this.placeholder ?? "");
493
+ this.trigger.toggleAttribute("data-placeholder", !shown);
494
+ this.$(".clear").hidden = !this.clearable || !shown || this.effectivelyDisabled;
495
+ }
496
+
497
+ _commit() {
498
+ this.internals.setFormValue(this.control.value || null);
499
+ this.syncValidity();
500
+ }
501
+
502
+ // A change made by the user: the model, then input + change.
503
+ _set(v) {
504
+ if (this.control.value === v) return;
505
+ this.control.value = v;
506
+ this.commitProperty("value", this.control.value);
507
+ this._commit();
508
+ this._paint();
509
+ this.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
510
+ this.dispatchEvent(new Event("change", { bubbles: true }));
511
+ }
512
+
513
+ /** Opens or closes the calendar. */
514
+ toggle(force) {
515
+ if (this.trigger.disabled) return;
516
+ this.open = force ?? !this.open;
517
+ }
518
+
519
+ _setOpen(open) {
520
+ const p = this.panel;
521
+ if (!this.isConnected) return;
522
+ const shown = p.matches(":popover-open");
523
+ if (open) {
524
+ // Reopened while animating out: stay open.
525
+ this.#exit?.cancel();
526
+ this.#exit = null;
527
+ p.style.pointerEvents = "";
528
+ if (!shown) {
529
+ try {
530
+ p.showPopover();
531
+ } catch {
532
+ this.commitProperty("open", false);
533
+ return;
534
+ }
535
+ }
536
+ this.#undismiss ??= dismissable({
537
+ panel: p,
538
+ owner: this,
539
+ trigger: this.trigger,
540
+ close: (reason) => {
541
+ this.open = false;
542
+ if (reason === "escape") this.trigger.focus();
543
+ },
544
+ });
545
+ } else if (shown && !this.#exit) {
546
+ this._leave();
547
+ }
548
+ }
549
+
550
+ // Animate the dropdown out, then really hide it (unless reopened).
551
+ async _leave() {
552
+ const p = this.panel;
553
+ this.#undismiss?.();
554
+ this.#undismiss = null;
555
+ this.trigger.setAttribute("aria-expanded", "false");
556
+ const exit = play(p, POP_OUT, { speed: "fast" });
557
+ this.#exit = exit;
558
+ p.style.pointerEvents = "none";
559
+ await exit.finished;
560
+ p.style.pointerEvents = "";
561
+ if (this.#exit !== exit) return;
562
+ this.#exit = null;
563
+ if (!this.open && p.matches(":popover-open")) p.hidePopover();
564
+ exit.cancel();
565
+ }
566
+
567
+ _onToggle(open) {
568
+ this.trigger.setAttribute("aria-expanded", String(open));
569
+ if (open) {
570
+ const box = this.$(".box");
571
+ const update = () => place(box.getBoundingClientRect(), this.panel, { placement: "bottom-start", offset: 4 });
572
+ update();
573
+ this.#stopTracking?.();
574
+ this.#stopTracking = autoUpdate(box, this.panel, update);
575
+ this.calendar.value = this.control.value;
576
+ this.calendar.focusDay();
577
+ } else {
578
+ this.#stopTracking?.();
579
+ this.#stopTracking = null;
580
+ }
581
+ if (Boolean(this.open) !== open) this.commitProperty("open", open);
582
+ this.emit(open ? "ray-open" : "ray-close");
583
+ }
584
+
585
+ formResetCallback() {
586
+ this.value = this._default;
587
+ }
588
+
589
+ formStateRestoreCallback(state) {
590
+ this.value = String(state ?? "");
591
+ }
592
+ }
593
+
594
+ define("ray-date-picker", RayDatePicker);