@alacris/ui 0.0.0 → 0.1.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 (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -1
  3. package/package.json +75 -3
  4. package/src/components/base.js +46 -0
  5. package/src/components/ui-accordion-item.js +157 -0
  6. package/src/components/ui-accordion.js +58 -0
  7. package/src/components/ui-alert.js +168 -0
  8. package/src/components/ui-app-bar.js +119 -0
  9. package/src/components/ui-autocomplete.js +371 -0
  10. package/src/components/ui-avatar.js +104 -0
  11. package/src/components/ui-backdrop.js +66 -0
  12. package/src/components/ui-badge.js +95 -0
  13. package/src/components/ui-bottom-app-bar.js +78 -0
  14. package/src/components/ui-bottom-nav.js +83 -0
  15. package/src/components/ui-breadcrumbs.js +100 -0
  16. package/src/components/ui-button-group.js +49 -0
  17. package/src/components/ui-button.js +143 -0
  18. package/src/components/ui-card.js +86 -0
  19. package/src/components/ui-carousel-item.js +53 -0
  20. package/src/components/ui-carousel.js +301 -0
  21. package/src/components/ui-checkbox.js +165 -0
  22. package/src/components/ui-chip-set.js +90 -0
  23. package/src/components/ui-chip.js +226 -0
  24. package/src/components/ui-container.js +59 -0
  25. package/src/components/ui-date-picker.js +774 -0
  26. package/src/components/ui-dialog.js +179 -0
  27. package/src/components/ui-divider.js +58 -0
  28. package/src/components/ui-drawer.js +169 -0
  29. package/src/components/ui-fab-menu.js +122 -0
  30. package/src/components/ui-fab.js +117 -0
  31. package/src/components/ui-icon-button.js +112 -0
  32. package/src/components/ui-icon.js +64 -0
  33. package/src/components/ui-list-item.js +166 -0
  34. package/src/components/ui-list.js +38 -0
  35. package/src/components/ui-loading-indicator.js +74 -0
  36. package/src/components/ui-menu-item.js +117 -0
  37. package/src/components/ui-menu.js +192 -0
  38. package/src/components/ui-nav-item.js +138 -0
  39. package/src/components/ui-nav-rail.js +111 -0
  40. package/src/components/ui-option.js +85 -0
  41. package/src/components/ui-pagination.js +173 -0
  42. package/src/components/ui-progress.js +100 -0
  43. package/src/components/ui-radio-group.js +104 -0
  44. package/src/components/ui-radio.js +143 -0
  45. package/src/components/ui-rating.js +126 -0
  46. package/src/components/ui-search.js +316 -0
  47. package/src/components/ui-select.js +418 -0
  48. package/src/components/ui-sheet.js +205 -0
  49. package/src/components/ui-side-sheet.js +221 -0
  50. package/src/components/ui-skeleton.js +89 -0
  51. package/src/components/ui-slider.js +266 -0
  52. package/src/components/ui-snackbar.js +233 -0
  53. package/src/components/ui-spinner.js +101 -0
  54. package/src/components/ui-split-button.js +158 -0
  55. package/src/components/ui-stack.js +37 -0
  56. package/src/components/ui-step.js +91 -0
  57. package/src/components/ui-stepper.js +86 -0
  58. package/src/components/ui-surface.js +51 -0
  59. package/src/components/ui-switch.js +153 -0
  60. package/src/components/ui-tab-panel.js +54 -0
  61. package/src/components/ui-tab.js +116 -0
  62. package/src/components/ui-table-footer.js +141 -0
  63. package/src/components/ui-table-toolbar.js +219 -0
  64. package/src/components/ui-table.js +735 -0
  65. package/src/components/ui-tabs.js +178 -0
  66. package/src/components/ui-text-field.js +319 -0
  67. package/src/components/ui-text.js +36 -0
  68. package/src/components/ui-time-picker.js +753 -0
  69. package/src/components/ui-toggle-button.js +170 -0
  70. package/src/components/ui-toggle-group.js +111 -0
  71. package/src/components/ui-toolbar.js +64 -0
  72. package/src/components/ui-tooltip.js +148 -0
  73. package/src/index.js +121 -0
  74. package/src/motion/animate.js +145 -0
  75. package/src/motion/flip.js +46 -0
  76. package/src/motion/index.js +4 -0
  77. package/src/motion/presence.js +104 -0
  78. package/src/motion/ripple.js +108 -0
  79. package/src/theme/apply-theme.js +142 -0
  80. package/src/theme/create-theme.js +70 -0
  81. package/src/theme/index.js +5 -0
  82. package/src/tokens/color.js +237 -0
  83. package/src/tokens/index.js +14 -0
  84. package/src/tokens/sys.js +76 -0
  85. package/src/tokens/system.js +114 -0
  86. package/src/tokens/typography.js +189 -0
  87. package/src/util/focus.js +89 -0
  88. package/src/util/form.js +69 -0
  89. package/src/util/icons.js +161 -0
  90. package/src/util/keys.js +110 -0
  91. package/src/util/position.js +102 -0
  92. package/src/util/table.js +230 -0
  93. package/types/elements.d.ts +78 -0
  94. package/types/index.d.ts +168 -0
@@ -0,0 +1,774 @@
1
+ // <ui-date-picker> — Material date picker: a text-field-style control that
2
+ // opens a calendar. Docked (default) commits on day click; modal confirms
3
+ // with OK / Cancel.
4
+ //
5
+ // <ui-date-picker label="Event" value=${date}
6
+ // @change=${(e) => date(e.detail.value)}></ui-date-picker>
7
+ //
8
+ // `value` is an ISO date string (YYYY-MM-DD), or '' for none. Typing an
9
+ // ISO or locale-formatted date into the field commits on blur / Enter.
10
+ // Set `range` to pick a start and end; `change` then reports
11
+ // `{ start, end, value }` where `value` is `start/end`.
12
+ //
13
+ // @prop {string} label=''
14
+ // @prop {string} value='' — ISO date (YYYY-MM-DD); range: start/end
15
+ // @prop {boolean} range=false — pick a start and end date
16
+ // @prop {string} start='' — range start ISO
17
+ // @prop {string} end='' — range end ISO
18
+ // @prop {string} variant='filled' — filled | outlined
19
+ // @prop {string} presentation='docked' — docked | modal
20
+ // @prop {string} min='' — inclusive ISO lower bound
21
+ // @prop {string} max='' — inclusive ISO upper bound
22
+ // @prop {string} locale='' — BCP 47 tag; empty uses the runtime locale
23
+ // @prop {boolean} disabled=false
24
+ // @prop {boolean} required=false
25
+ // @prop {string} name='' — form participation
26
+ // @prop {string} placeholder=''
27
+ // @event change — committed; detail: { value } or { start, end, value } when range
28
+ // @event input — field keystroke; detail: { value } (the raw text)
29
+ // @event open — calendar visible (after the enter animation)
30
+ // @event close — calendar removed (after the exit animation)
31
+ // @part field, input, label, panel, day
32
+ // @vars see `t` below (`themeVars.names`)
33
+
34
+ import { define, html, css, vars, computed, signal, effect, onCleanup } from '@alacris/core';
35
+ import { sys } from '../tokens/sys.js';
36
+ import { base } from './base.js';
37
+ import { formBind } from '../util/form.js';
38
+ import { presence } from '../motion/presence.js';
39
+ import { animate, fx } from '../motion/animate.js';
40
+ import { autoUpdate } from '../util/position.js';
41
+ import { focusTrap, scrollLock } from '../util/focus.js';
42
+ import './ui-icon-button.js';
43
+ import './ui-button.js';
44
+
45
+ const ISO = /^(\d{4})-(\d{2})-(\d{2})$/;
46
+
47
+ const pad = (n) => String(n).padStart(2, '0');
48
+ const toISO = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
49
+ const parseISO = (s) => {
50
+ const m = ISO.exec(s || '');
51
+ if (!m) return null;
52
+ const d = new Date(+m[1], +m[2] - 1, +m[3]);
53
+ return (d.getFullYear() === +m[1] && d.getMonth() === +m[2] - 1 && d.getDate() === +m[3]) ? d : null;
54
+ };
55
+ const loc = (locale) => locale || undefined;
56
+ const formatDate = (iso, locale) => {
57
+ const d = parseISO(iso);
58
+ return d ? new Intl.DateTimeFormat(loc(locale), { dateStyle: 'medium' }).format(d) : '';
59
+ };
60
+ const formatRange = (start, end, locale) => {
61
+ const a = formatDate(start, locale);
62
+ const b = formatDate(end, locale);
63
+ if (a && b) return `${a} – ${b}`;
64
+ return a || b || '';
65
+ };
66
+ const monthTitle = (d, locale) =>
67
+ new Intl.DateTimeFormat(loc(locale), { month: 'long', year: 'numeric' }).format(d);
68
+ const weekdays = (locale) => {
69
+ const fmt = new Intl.DateTimeFormat(loc(locale), { weekday: 'narrow' });
70
+ return Array.from({ length: 7 }, (_, i) => fmt.format(new Date(2026, 7, 9 + i)));
71
+ };
72
+ const parseTyped = (text) => {
73
+ const t = (text || '').trim();
74
+ if (!t) return '';
75
+ if (parseISO(t)) return t;
76
+ const d = new Date(t);
77
+ return Number.isNaN(d.getTime()) ? null : toISO(d);
78
+ };
79
+ const startOfMonth = (d) => new Date(d.getFullYear(), d.getMonth(), 1);
80
+ const todayISO = () => toISO(new Date());
81
+ const monthCells = (view, selected, min, max, today, rangeStart = '', rangeEnd = '') => {
82
+ const y = view.getFullYear();
83
+ const m = view.getMonth();
84
+ const start = new Date(y, m, 1 - new Date(y, m, 1).getDay());
85
+ const cells = [];
86
+ for (let i = 0; i < 42; i++) {
87
+ const d = new Date(start.getFullYear(), start.getMonth(), start.getDate() + i);
88
+ const iso = toISO(d);
89
+ const isStart = !!rangeStart && iso === rangeStart;
90
+ const isEnd = !!rangeEnd && iso === rangeEnd;
91
+ cells.push({
92
+ iso,
93
+ day: d.getDate(),
94
+ inMonth: d.getMonth() === m,
95
+ selected: isStart || isEnd || (!rangeStart && iso === selected),
96
+ rangeStart: isStart,
97
+ rangeEnd: isEnd,
98
+ inRange: !!(rangeStart && rangeEnd && iso >= rangeStart && iso <= rangeEnd),
99
+ today: iso === today,
100
+ disabled: !!(min && iso < min) || !!(max && iso > max),
101
+ });
102
+ }
103
+ return cells;
104
+ };
105
+
106
+ const t = vars('ui-date-picker', {
107
+ bg: sys.color.surfaceContainerHighest,
108
+ fg: sys.color.onSurface,
109
+ labelFg: sys.color.onSurfaceVariant,
110
+ accent: sys.color.primary,
111
+ onAccent: sys.color.onPrimary,
112
+ outlineColor: sys.color.outline,
113
+ radius: sys.radius.xs,
114
+ font: sys.type.bodyLg,
115
+ height: '56px',
116
+ panelBg: sys.color.surfaceContainerHigh,
117
+ panelRadius: sys.radius.xl,
118
+ dayRadius: sys.radius.full,
119
+ todayFg: sys.color.primary,
120
+ mutedFg: sys.color.onSurfaceVariant,
121
+ rangeBg: sys.color.secondaryContainer,
122
+ rangeFg: sys.color.onSecondaryContainer,
123
+ scrim: `color-mix(in srgb, ${sys.color.scrim} 32%, transparent)`,
124
+ });
125
+
126
+ const styles = css`
127
+ :host { display: block; inline-size: 240px; }
128
+ :host([range]) { inline-size: 320px; }
129
+ .range { inline-size: 100%; }
130
+ .range .field { min-inline-size: 320px; }
131
+ .root { display: block; position: relative; }
132
+ .field {
133
+ position: relative;
134
+ display: flex;
135
+ align-items: center;
136
+ gap: ${sys.space(1)};
137
+ min-block-size: calc(${t.height} + var(--ui-density, 0) * 4px);
138
+ padding-inline: ${sys.space(4)} ${sys.space(1)};
139
+ border-radius: ${t.radius};
140
+ font: ${t.font};
141
+ letter-spacing: ${sys.tracking.bodyLg};
142
+ color: ${t.fg};
143
+ cursor: text;
144
+ --ui-icon-size: 1.5rem;
145
+ }
146
+ .filled .field {
147
+ background: ${t.bg};
148
+ border-start-start-radius: ${t.radius};
149
+ border-start-end-radius: ${t.radius};
150
+ border-end-start-radius: 0;
151
+ border-end-end-radius: 0;
152
+ }
153
+ .filled .field::after {
154
+ content: '';
155
+ position: absolute;
156
+ inset-inline: 0;
157
+ inset-block-end: 0;
158
+ block-size: 1px;
159
+ background: ${sys.color.onSurfaceVariant};
160
+ transition: block-size ${sys.duration.short2} ${sys.easing.standard},
161
+ background-color ${sys.duration.short2} ${sys.easing.standard};
162
+ }
163
+ .filled:focus-within .field::after,
164
+ .filled.open .field::after { block-size: 2px; background: ${t.accent}; }
165
+ .filled .field::before {
166
+ content: '';
167
+ position: absolute; inset: 0; pointer-events: none;
168
+ background: ${sys.color.onSurface};
169
+ opacity: 0;
170
+ border-radius: inherit;
171
+ transition: opacity ${sys.duration.short2} ${sys.easing.standard};
172
+ }
173
+ .filled:hover:not(:focus-within):not(.open):not(.disabled) .field::before {
174
+ opacity: ${sys.state.hover};
175
+ }
176
+ .filled:hover:not(:focus-within):not(.open):not(.disabled) .field::after {
177
+ background: ${sys.color.onSurface};
178
+ }
179
+
180
+ fieldset {
181
+ position: absolute;
182
+ inset: -6px 0 0;
183
+ margin: 0;
184
+ padding: 0 calc(${sys.space(3)} - 2px);
185
+ min-inline-size: 0;
186
+ box-sizing: border-box;
187
+ appearance: none;
188
+ border: 1px solid ${t.outlineColor};
189
+ border-radius: ${t.radius};
190
+ pointer-events: none;
191
+ transition: border-color ${sys.duration.short2} ${sys.easing.standard},
192
+ border-width ${sys.duration.short2} ${sys.easing.standard};
193
+ }
194
+ legend {
195
+ float: unset;
196
+ display: block;
197
+ width: max-content;
198
+ padding: 0;
199
+ margin: 0;
200
+ margin-inline-start: 0;
201
+ white-space: nowrap;
202
+ overflow: hidden;
203
+ font: ${t.font};
204
+ font-size: 0.75em;
205
+ letter-spacing: calc(${sys.tracking.bodyLg} * 0.75);
206
+ visibility: hidden;
207
+ max-inline-size: 0.01px;
208
+ height: 12px;
209
+ line-height: 12px;
210
+ transition: max-inline-size ${sys.duration.short2} ${sys.easing.standard};
211
+ }
212
+ legend span {
213
+ padding-inline: ${sys.space(1)} calc(${sys.space(1)} - 0.5px);
214
+ display: inline-block;
215
+ opacity: 0;
216
+ visibility: visible;
217
+ }
218
+ .outlined.floating legend {
219
+ max-inline-size: 100%;
220
+ transition: max-inline-size ${sys.duration.short3} ${sys.easing.standard};
221
+ }
222
+ .outlined:focus-within fieldset,
223
+ .outlined.open fieldset { border-width: 2px; border-color: ${t.accent}; }
224
+ .root:hover:not(:focus-within):not(.open) fieldset { border-color: ${sys.color.onSurface}; }
225
+
226
+ .label {
227
+ position: absolute;
228
+ inset-inline-start: ${sys.space(4)};
229
+ inset-block-start: 50%;
230
+ translate: 0 -50%;
231
+ color: ${t.labelFg};
232
+ pointer-events: none;
233
+ transform-origin: 0 50%;
234
+ transition: translate ${sys.duration.short3} ${sys.easing.standard},
235
+ scale ${sys.duration.short3} ${sys.easing.standard},
236
+ color ${sys.duration.short2} ${sys.easing.standard};
237
+ }
238
+ .filled.floating .label { translate: 0 calc(-50% - 16px); scale: 0.75; }
239
+ .outlined.floating .label {
240
+ translate: 0 calc(-50% - (${t.height} + var(--ui-density, 0) * 4px) / 2);
241
+ scale: 0.75;
242
+ }
243
+ :focus-within .label, .open .label { color: ${t.accent}; }
244
+
245
+ input {
246
+ flex: 1;
247
+ min-inline-size: 0;
248
+ margin: 0;
249
+ border: none;
250
+ outline: none;
251
+ appearance: none;
252
+ background: transparent;
253
+ font: inherit;
254
+ letter-spacing: inherit;
255
+ color: inherit;
256
+ padding: 0;
257
+ }
258
+ .filled.has-label input { padding-block-start: 18px; }
259
+ input::placeholder { color: ${t.labelFg}; opacity: 0; }
260
+ .floating input::placeholder { opacity: 1; }
261
+
262
+ .panel {
263
+ position: fixed;
264
+ z-index: ${sys.z.modal};
265
+ padding: ${sys.space(3)} ${sys.space(3)} ${sys.space(4)};
266
+ background: ${t.panelBg};
267
+ border-radius: ${t.panelRadius};
268
+ box-shadow: ${sys.elevation[3]};
269
+ color: ${t.fg};
270
+ overflow: auto;
271
+ }
272
+ .overlay {
273
+ position: fixed;
274
+ inset: 0;
275
+ z-index: ${sys.z.modal};
276
+ display: grid;
277
+ place-items: center;
278
+ }
279
+ .scrim { position: absolute; inset: 0; background: ${t.scrim}; }
280
+ @keyframes ui-date-picker-in {
281
+ from { transform: scale(0.8); }
282
+ to { transform: none; }
283
+ }
284
+ .modal-surface {
285
+ animation: ui-date-picker-in ${sys.duration.medium2} ${sys.easing.emphasizedDecelerate};
286
+ position: relative;
287
+ display: flex;
288
+ flex-direction: column;
289
+ gap: ${sys.space(2)};
290
+ padding: ${sys.space(6)} ${sys.space(3)} ${sys.space(3)};
291
+ background: ${t.panelBg};
292
+ border-radius: ${t.panelRadius};
293
+ box-shadow: ${sys.elevation[3]};
294
+ color: ${t.fg};
295
+ min-inline-size: min(360px, calc(100vw - 48px));
296
+ max-inline-size: calc(100vw - 48px);
297
+ }
298
+ .headline {
299
+ padding-inline: ${sys.space(3)};
300
+ font: ${sys.type.labelMd};
301
+ letter-spacing: ${sys.tracking.labelMd};
302
+ color: ${t.mutedFg};
303
+ }
304
+ .picked {
305
+ padding-inline: ${sys.space(3)};
306
+ padding-block-end: ${sys.space(2)};
307
+ font: ${sys.type.headlineMd};
308
+ letter-spacing: ${sys.tracking.headlineMd};
309
+ }
310
+ .actions {
311
+ display: flex;
312
+ justify-content: flex-end;
313
+ gap: ${sys.space(2)};
314
+ padding-inline: ${sys.space(1)};
315
+ }
316
+
317
+ .cal-header {
318
+ display: flex;
319
+ align-items: center;
320
+ gap: ${sys.space(1)};
321
+ padding-inline: ${sys.space(2)};
322
+ min-block-size: 48px;
323
+ }
324
+ .month {
325
+ flex: 1;
326
+ font: ${sys.type.titleSm};
327
+ letter-spacing: ${sys.tracking.titleSm};
328
+ }
329
+ .weekdays, .cal-row {
330
+ display: grid;
331
+ grid-template-columns: repeat(7, 40px);
332
+ justify-content: center;
333
+ }
334
+ .days {
335
+ display: flex;
336
+ flex-direction: column;
337
+ align-items: center;
338
+ }
339
+ .weekday {
340
+ display: grid;
341
+ place-items: center;
342
+ block-size: 40px;
343
+ font: ${sys.type.labelSm};
344
+ letter-spacing: ${sys.tracking.labelSm};
345
+ color: ${t.mutedFg};
346
+ }
347
+ .day {
348
+ position: relative;
349
+ isolation: isolate;
350
+ display: grid;
351
+ place-items: center;
352
+ inline-size: 40px;
353
+ block-size: 40px;
354
+ margin: 0;
355
+ padding: 0;
356
+ border: none;
357
+ outline: none;
358
+ appearance: none;
359
+ background: transparent;
360
+ font: ${sys.type.bodySm};
361
+ letter-spacing: ${sys.tracking.bodySm};
362
+ color: ${t.fg};
363
+ cursor: pointer;
364
+ }
365
+ .day .text {
366
+ position: relative;
367
+ z-index: 2;
368
+ }
369
+ .day .layer {
370
+ position: absolute; inset: 0; z-index: 3;
371
+ border-radius: ${t.dayRadius}; background: currentColor; opacity: 0;
372
+ transition: opacity ${sys.duration.short2} ${sys.easing.standard};
373
+ }
374
+ .day:hover .layer { opacity: ${sys.state.hover}; }
375
+ .day:active .layer { opacity: ${sys.state.pressed}; }
376
+ .day:focus-visible { outline: ${sys.focus.ring}; outline-offset: -2px; }
377
+ .day.outside { color: ${t.mutedFg}; }
378
+
379
+ /* In-range connector track */
380
+ .day.in-range { color: ${t.rangeFg}; }
381
+ .day.in-range::before {
382
+ content: '';
383
+ position: absolute;
384
+ inset: 0;
385
+ background: ${t.rangeBg};
386
+ z-index: 0;
387
+ pointer-events: none;
388
+ }
389
+ .day.in-range.range-start::before {
390
+ inset-inline-start: 50%;
391
+ inset-inline-end: 0;
392
+ }
393
+ .day.in-range.range-end::before {
394
+ inset-inline-start: 0;
395
+ inset-inline-end: 50%;
396
+ }
397
+ .day.in-range:nth-child(7n + 1)::before {
398
+ border-start-start-radius: ${t.dayRadius};
399
+ border-end-start-radius: ${t.dayRadius};
400
+ }
401
+ .day.in-range:nth-child(7n)::before {
402
+ border-start-end-radius: ${t.dayRadius};
403
+ border-end-end-radius: ${t.dayRadius};
404
+ }
405
+ .day.in-range.range-start.range-end::before { display: none; }
406
+
407
+ /* Selected circular badge */
408
+ .day.selected {
409
+ color: ${t.onAccent};
410
+ font-weight: 500;
411
+ }
412
+ .day.selected::after {
413
+ content: '';
414
+ position: absolute;
415
+ inset: 0;
416
+ border-radius: ${t.dayRadius};
417
+ background: ${t.accent};
418
+ z-index: 1;
419
+ pointer-events: none;
420
+ transition: background-color ${sys.duration.short4} ${sys.easing.standard};
421
+ }
422
+
423
+ /* Today outline indicator */
424
+ .day.today:not(.selected) {
425
+ color: ${t.todayFg};
426
+ }
427
+ .day.today:not(.selected)::after {
428
+ content: '';
429
+ position: absolute;
430
+ inset: 0;
431
+ border-radius: ${t.dayRadius};
432
+ border: 1px solid ${t.todayFg};
433
+ box-sizing: border-box;
434
+ z-index: 1;
435
+ pointer-events: none;
436
+ }
437
+
438
+ .day:disabled {
439
+ color: color-mix(in srgb, ${sys.color.onSurface} calc(${sys.state.disabledContent} * 100%), transparent);
440
+ cursor: default;
441
+ pointer-events: none;
442
+ }
443
+
444
+ .disabled { opacity: ${sys.state.disabledContent}; pointer-events: none; }
445
+ `;
446
+
447
+ define('ui-date-picker', {
448
+ formAssociated: true,
449
+ props: {
450
+ label: '', value: '', range: false, start: '', end: '',
451
+ variant: 'filled', presentation: 'docked',
452
+ min: '', max: '', locale: '', disabled: false, required: false,
453
+ name: '', placeholder: '',
454
+ },
455
+ styles: [base, styles],
456
+ setup(p, host) {
457
+ const { label, value, range, start, end, variant, presentation, min, max, locale, disabled, required, name, placeholder } = p;
458
+ formBind(host, { name, value, disabled });
459
+
460
+ const open = signal(false);
461
+ const focused = signal(false);
462
+ const text = signal('');
463
+ const viewMonth = signal(startOfMonth(parseISO(value() || start()) || new Date()));
464
+ const draft = signal(value());
465
+ const draftStart = signal(start());
466
+ const draftEnd = signal(end());
467
+ const rangeAnchor = signal('');
468
+ let fieldEl = null;
469
+ let modalSurfaceEl = null;
470
+ let stopAuto = null;
471
+ let releaseTrap = null;
472
+ let unlock = null;
473
+
474
+ effect(() => {
475
+ if (range()) {
476
+ const next = [start(), end()].filter(Boolean).join('/');
477
+ if (value.peek() !== next) value.set(next);
478
+ text.set(formatRange(start(), end(), locale()) || next);
479
+ const d = parseISO(start());
480
+ if (d && !open()) viewMonth.set(startOfMonth(d));
481
+ return;
482
+ }
483
+ const v = value();
484
+ text.set(formatDate(v, locale()) || v);
485
+ const d = parseISO(v);
486
+ if (d && !open()) viewMonth.set(startOfMonth(d));
487
+ });
488
+
489
+ const floating = computed(() => focused() || text() !== '' || placeholder() !== '' || open());
490
+ const cls = computed(() =>
491
+ ['root', variant(), range() && 'range', floating() && 'floating', open() && 'open',
492
+ disabled() && 'disabled', label() && 'has-label'].filter(Boolean).join(' '));
493
+ const selected = computed(() => (presentation() === 'modal' && open() ? draft() : value()));
494
+ const liveStart = computed(() => (range() && open() ? draftStart() : start()));
495
+ const liveEnd = computed(() => (range() && open() ? draftEnd() : end()));
496
+ const cells = computed(() =>
497
+ monthCells(viewMonth(), selected(), min(), max(), todayISO(),
498
+ range() ? liveStart() : '', range() ? liveEnd() : ''));
499
+ const title = computed(() => monthTitle(viewMonth(), locale()));
500
+ const heads = computed(() => weekdays(locale()));
501
+ const pickedLabel = computed(() => range()
502
+ ? (formatRange(liveStart(), liveEnd(), locale()) || 'Selected dates')
503
+ : (formatDate(selected(), locale()) || 'Selected date'));
504
+
505
+ const outOfRange = (iso) => !!(min() && iso < min()) || !!(max() && iso > max());
506
+
507
+ const commit = (iso, { close = true } = {}) => {
508
+ if (iso !== value()) {
509
+ value.set(iso);
510
+ host.emit('change', { value: iso });
511
+ }
512
+ text.set(formatDate(iso, locale()) || iso);
513
+ if (close) closePanel();
514
+ };
515
+ const commitRange = (s, e, { close = true } = {}) => {
516
+ start.set(s);
517
+ end.set(e);
518
+ const joined = [s, e].filter(Boolean).join('/');
519
+ value.set(joined);
520
+ host.emit('change', { start: s, end: e, value: joined });
521
+ text.set(formatRange(s, e, locale()) || joined);
522
+ if (close) closePanel();
523
+ };
524
+
525
+ let lastPicked = null;
526
+ const pick = (iso) => {
527
+ if (!iso || outOfRange(iso) || iso === lastPicked) return;
528
+ lastPicked = iso;
529
+ setTimeout(() => { lastPicked = null; }, 0);
530
+ if (range()) {
531
+ const anchor = rangeAnchor();
532
+ if (!anchor || iso === anchor) {
533
+ rangeAnchor.set(iso);
534
+ draftStart.set(iso);
535
+ draftEnd.set('');
536
+ text.set(formatDate(iso, locale()) || iso);
537
+ return;
538
+ }
539
+ let a = anchor;
540
+ let b = iso;
541
+ if (b < a) { a = iso; b = anchor; }
542
+ rangeAnchor.set('');
543
+ draftStart.set(a);
544
+ draftEnd.set(b);
545
+ if (presentation() !== 'modal') commitRange(a, b);
546
+ return;
547
+ }
548
+ if (presentation() === 'modal') { draft.set(iso); return; }
549
+ commit(iso);
550
+ };
551
+
552
+ const openPanel = () => {
553
+ if (disabled() || open()) return;
554
+ const d = parseISO(range() ? start() : value()) || new Date();
555
+ viewMonth.set(startOfMonth(d));
556
+ draft.set(value());
557
+ draftStart.set(start());
558
+ draftEnd.set(end());
559
+ rangeAnchor.set(range() && start() && !end() ? start() : '');
560
+ open.set(true);
561
+ };
562
+ const closePanel = () => open.set(false);
563
+ const toggle = (e) => {
564
+ e?.stopPropagation();
565
+ open() ? closePanel() : openPanel();
566
+ };
567
+
568
+ const shiftMonth = (delta) => {
569
+ const v = viewMonth();
570
+ viewMonth.set(new Date(v.getFullYear(), v.getMonth() + delta, 1));
571
+ };
572
+
573
+ const onInput = (e) => {
574
+ text.set(e.target.value);
575
+ host.emit('input', { value: e.target.value });
576
+ };
577
+ const onBlur = () => {
578
+ focused.set(false);
579
+ if (range()) {
580
+ text.set(formatRange(start(), end(), locale()) || value());
581
+ return;
582
+ }
583
+ const parsed = parseTyped(text());
584
+ if (parsed === null) {
585
+ text.set(formatDate(value(), locale()) || value());
586
+ return;
587
+ }
588
+ if (parsed !== value() && !outOfRange(parsed)) commit(parsed, { close: false });
589
+ else text.set(formatDate(value(), locale()) || value());
590
+ };
591
+ const onKeydown = (e) => {
592
+ if (e.key === 'ArrowDown' && e.altKey) { e.preventDefault(); openPanel(); }
593
+ else if (e.key === 'F4') { e.preventDefault(); toggle(); }
594
+ else if (e.key === 'Enter') {
595
+ e.preventDefault();
596
+ const parsed = parseTyped(text());
597
+ if (parsed !== null && !outOfRange(parsed)) commit(parsed);
598
+ } else if (e.key === 'Escape' && open()) {
599
+ e.preventDefault();
600
+ closePanel();
601
+ }
602
+ };
603
+
604
+ effect(() => {
605
+ if (!open()) return;
606
+ const onDoc = (e) => {
607
+ if (e.composedPath().includes(host)) return;
608
+ closePanel();
609
+ };
610
+ const onEsc = (e) => { if (e.key === 'Escape') closePanel(); };
611
+ document.addEventListener('pointerdown', onDoc);
612
+ document.addEventListener('keydown', onEsc, true);
613
+ return () => {
614
+ document.removeEventListener('pointerdown', onDoc);
615
+ document.removeEventListener('keydown', onEsc, true);
616
+ };
617
+ });
618
+ effect(() => {
619
+ if (open() && presentation() === 'modal') {
620
+ unlock = scrollLock();
621
+ queueMicrotask(() => { if (open() && !releaseTrap) releaseTrap = focusTrap(host); });
622
+ } else {
623
+ if (modalSurfaceEl?.isConnected) {
624
+ animate(modalSurfaceEl, fx.scaleOut, { duration: 'short4', easing: 'emphasizedAccelerate' });
625
+ }
626
+ releaseTrap?.(); releaseTrap = null;
627
+ unlock?.(); unlock = null;
628
+ }
629
+ });
630
+ effect(() => {
631
+ if (!open() && stopAuto) { stopAuto(); stopAuto = null; }
632
+ });
633
+ onCleanup(() => { stopAuto?.(); releaseTrap?.(); unlock?.(); });
634
+
635
+ const dayClassFrom = (cell) => [
636
+ 'day',
637
+ !cell.inMonth && 'outside',
638
+ cell.selected && 'selected',
639
+ cell.today && 'today',
640
+ cell.inRange && 'in-range',
641
+ cell.rangeStart && 'range-start',
642
+ cell.rangeEnd && 'range-end',
643
+ ].filter(Boolean).join(' ');
644
+ const dayButton = (cell) => html`
645
+ <button type="button" part="day" role="gridcell"
646
+ class=${dayClassFrom(cell)}
647
+ data-iso=${cell.iso}
648
+ aria-selected=${cell.selected ? 'true' : 'false'}
649
+ ?disabled=${cell.disabled}
650
+ @click=${() => pick(cell.iso)}>
651
+ <span class="layer" aria-hidden="true"></span>
652
+ <span class="text">${cell.day}</span>
653
+ </button>`;
654
+ const calGrid = () => {
655
+ const all = cells();
656
+ const rows = [];
657
+ for (let r = 0; r < all.length; r += 7) {
658
+ rows.push(html`<div class="cal-row" role="row">${all.slice(r, r + 7).map(dayButton)}</div>`);
659
+ }
660
+ return html`
661
+ <div class="cal" role="grid" aria-label=${() => title()}>
662
+ <div class="weekdays" role="row">${() => heads().map((w) => html`<span class="weekday" role="columnheader">${w}</span>`)}</div>
663
+ <div class="days">${rows}</div>
664
+ </div>`;
665
+ };
666
+
667
+ // Presence mounts the grid in a nested owner. A setup-level paint keeps
668
+ // selected / in-range classes in sync even if a row binding does not.
669
+ effect(() => {
670
+ const a = range() ? liveStart() : '';
671
+ const b = range() ? liveEnd() : '';
672
+ const sel = selected();
673
+ const rng = range();
674
+ if (!open()) return;
675
+ const paint = () => {
676
+ const root = host.shadowRoot;
677
+ if (!root) return;
678
+ for (const btn of root.querySelectorAll('.day')) {
679
+ const iso = btn.getAttribute('data-iso');
680
+ if (!iso) continue;
681
+ const isStart = !!(rng && a && iso === a);
682
+ const isEnd = !!(rng && b && iso === b);
683
+ const isSel = !!(isStart || isEnd || (!rng && iso === sel) || (!a && iso === sel));
684
+ btn.classList.toggle('selected', isSel);
685
+ btn.classList.toggle('in-range', !!(a && b && iso >= a && iso <= b));
686
+ btn.classList.toggle('range-start', isStart);
687
+ btn.classList.toggle('range-end', isEnd);
688
+ btn.setAttribute('aria-selected', isSel ? 'true' : 'false');
689
+ }
690
+ };
691
+ paint();
692
+ queueMicrotask(paint);
693
+ });
694
+
695
+ const panelRef = (el) => {
696
+ stopAuto?.();
697
+ if (presentation() !== 'modal') {
698
+ stopAuto = autoUpdate(el, fieldEl, { placement: 'bottom-start', offset: 4 });
699
+ }
700
+ };
701
+
702
+ const dockedView = () => html`
703
+ <div class="panel" part="panel" role="dialog" aria-label=${() => label() || 'Choose date'}
704
+ ref=${panelRef}>
705
+ <div class="cal-header">
706
+ <span class="month">${title}</span>
707
+ <ui-icon-button icon="chevron-left" label="Previous month" @click=${() => shiftMonth(-1)}></ui-icon-button>
708
+ <ui-icon-button icon="chevron-right" label="Next month" @click=${() => shiftMonth(1)}></ui-icon-button>
709
+ </div>
710
+ ${calGrid}
711
+ </div>`;
712
+
713
+ const modalView = () => html`
714
+ <div class="overlay">
715
+ <div class="scrim" aria-hidden="true" @click=${closePanel}></div>
716
+ <div class="modal-surface" part="panel" role="dialog" aria-modal="true"
717
+ aria-label=${() => label() || 'Choose date'}
718
+ ref=${(el) => (modalSurfaceEl = el)}>
719
+ <div class="headline">${() => (range() ? 'Select dates' : 'Select date')}</div>
720
+ <div class="picked">${pickedLabel}</div>
721
+ <div class="cal-header">
722
+ <span class="month">${title}</span>
723
+ <ui-icon-button icon="chevron-left" label="Previous month" @click=${() => shiftMonth(-1)}></ui-icon-button>
724
+ <ui-icon-button icon="chevron-right" label="Next month" @click=${() => shiftMonth(1)}></ui-icon-button>
725
+ </div>
726
+ ${calGrid}
727
+ <div class="actions">
728
+ <ui-button variant="text" @click=${closePanel}>Cancel</ui-button>
729
+ <ui-button variant="text" @click=${() => range() ? commitRange(draftStart(), draftEnd()) : commit(draft())}>OK</ui-button>
730
+ </div>
731
+ </div>
732
+ </div>`;
733
+
734
+ return html`
735
+ <div class=${cls}>
736
+ <div class="field" part="field" ref=${(el) => (fieldEl = el)}>
737
+ ${() => (variant() === 'outlined'
738
+ ? html`<fieldset aria-hidden="true"><legend><span>${label}${() => (required() ? ' *' : '')}</span></legend></fieldset>`
739
+ : null)}
740
+ ${() => (label() ? html`<span class="label" part="label" id="field-label">${label}${() => (required() ? ' *' : '')}</span>` : null)}
741
+ <input part="input" .value=${text}
742
+ placeholder=${() => placeholder() || null}
743
+ ?disabled=${disabled} ?required=${required}
744
+ aria-labelledby=${() => (label() ? 'field-label' : null)}
745
+ aria-label=${() => (label() ? null : (placeholder() || 'Date'))}
746
+ aria-haspopup="dialog" aria-expanded=${() => String(open())}
747
+ autocomplete="off"
748
+ @input=${onInput} @focus=${() => focused.set(true)} @blur=${onBlur}
749
+ @keydown=${onKeydown}>
750
+ <ui-icon-button icon="calendar" label=${() => (open() ? 'Close calendar' : 'Open calendar')}
751
+ @click=${toggle}></ui-icon-button>
752
+ </div>
753
+ ${presence(() => open() && presentation() !== 'modal', dockedView, {
754
+ enter: fx.scaleIn,
755
+ exit: fx.scaleOut,
756
+ enterDuration: 'short4',
757
+ exitDuration: 'short4',
758
+ onEntered: () => host.emit('open'),
759
+ onExited: () => host.emit('close'),
760
+ })}
761
+ ${presence(() => open() && presentation() === 'modal', modalView, {
762
+ enter: fx.fadeIn,
763
+ exit: fx.fadeOut,
764
+ enterDuration: 'medium2',
765
+ exitDuration: 'short4',
766
+ onEntered: () => host.emit('open'),
767
+ onExited: () => host.emit('close'),
768
+ })}
769
+ </div>`;
770
+ },
771
+ });
772
+
773
+ export const tag = 'ui-date-picker';
774
+ export const themeVars = t;