snice 1.14.3 → 2.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 (185) hide show
  1. package/bin/templates/base/tsconfig.json +5 -4
  2. package/components/accordion/demo.html +403 -0
  3. package/components/accordion/snice-accordion-item.css +85 -0
  4. package/components/accordion/snice-accordion-item.ts +226 -0
  5. package/components/accordion/snice-accordion.css +31 -0
  6. package/components/accordion/snice-accordion.ts +182 -0
  7. package/components/accordion/snice-accordion.types.ts +32 -0
  8. package/components/alert/demo.html +445 -0
  9. package/components/alert/snice-alert.css +195 -0
  10. package/components/alert/snice-alert.ts +141 -0
  11. package/components/alert/snice-alert.types.ts +12 -0
  12. package/components/avatar/demo.html +598 -0
  13. package/components/avatar/snice-avatar.css +131 -0
  14. package/components/avatar/snice-avatar.ts +136 -0
  15. package/components/avatar/snice-avatar.types.ts +13 -0
  16. package/components/badge/demo.html +523 -0
  17. package/components/badge/snice-badge.css +161 -0
  18. package/components/badge/snice-badge.ts +117 -0
  19. package/components/badge/snice-badge.types.ts +16 -0
  20. package/components/breadcrumbs/demo.html +404 -0
  21. package/components/breadcrumbs/snice-breadcrumbs.css +133 -0
  22. package/components/breadcrumbs/snice-breadcrumbs.ts +191 -0
  23. package/components/breadcrumbs/snice-breadcrumbs.types.ts +26 -0
  24. package/components/breadcrumbs/snice-crumb.ts +26 -0
  25. package/components/button/demo.html +42 -0
  26. package/components/button/snice-button.css +230 -0
  27. package/components/button/snice-button.ts +169 -0
  28. package/components/button/snice-button.types.ts +25 -0
  29. package/components/card/demo.html +525 -0
  30. package/components/card/snice-card.css +140 -0
  31. package/components/card/snice-card.ts +102 -0
  32. package/components/card/snice-card.types.ts +10 -0
  33. package/components/checkbox/demo.html +253 -0
  34. package/components/checkbox/snice-checkbox.css +164 -0
  35. package/components/checkbox/snice-checkbox.ts +223 -0
  36. package/components/checkbox/snice-checkbox.types.ts +22 -0
  37. package/components/chip/demo.html +383 -0
  38. package/components/chip/snice-chip.css +195 -0
  39. package/components/chip/snice-chip.ts +139 -0
  40. package/components/chip/snice-chip.types.ts +15 -0
  41. package/components/date-picker/README.md +233 -0
  42. package/components/date-picker/demo.html +191 -0
  43. package/components/date-picker/snice-date-picker.css +330 -0
  44. package/components/date-picker/snice-date-picker.ts +777 -0
  45. package/components/date-picker/snice-date-picker.types.ts +83 -0
  46. package/components/divider/demo.html +233 -0
  47. package/components/divider/snice-divider.css +155 -0
  48. package/components/divider/snice-divider.ts +69 -0
  49. package/components/divider/snice-divider.types.ts +15 -0
  50. package/components/drawer/demo.html +328 -0
  51. package/components/drawer/snice-drawer.css +476 -0
  52. package/components/drawer/snice-drawer.ts +287 -0
  53. package/components/drawer/snice-drawer.types.ts +17 -0
  54. package/components/global.d.ts +14 -0
  55. package/components/input/demo.html +303 -0
  56. package/components/input/snice-input.css +257 -0
  57. package/components/input/snice-input.ts +442 -0
  58. package/components/input/snice-input.types.ts +59 -0
  59. package/components/input/test.html +77 -0
  60. package/components/layout/README.md +260 -0
  61. package/components/layout/demo.html +538 -0
  62. package/components/layout/snice-layout-blog.css +129 -0
  63. package/components/layout/snice-layout-blog.ts +48 -0
  64. package/components/layout/snice-layout-card.css +104 -0
  65. package/components/layout/snice-layout-card.ts +35 -0
  66. package/components/layout/snice-layout-centered.css +51 -0
  67. package/components/layout/snice-layout-centered.ts +22 -0
  68. package/components/layout/snice-layout-dashboard.css +98 -0
  69. package/components/layout/snice-layout-dashboard.ts +45 -0
  70. package/components/layout/snice-layout-fullscreen.css +72 -0
  71. package/components/layout/snice-layout-fullscreen.ts +34 -0
  72. package/components/layout/snice-layout-landing.css +92 -0
  73. package/components/layout/snice-layout-landing.ts +47 -0
  74. package/components/layout/snice-layout-minimal.css +16 -0
  75. package/components/layout/snice-layout-minimal.ts +19 -0
  76. package/components/layout/snice-layout-sidebar.css +117 -0
  77. package/components/layout/snice-layout-sidebar.ts +48 -0
  78. package/components/layout/snice-layout-split.css +103 -0
  79. package/components/layout/snice-layout-split.ts +29 -0
  80. package/components/layout/snice-layout.css +72 -0
  81. package/components/layout/snice-layout.ts +35 -0
  82. package/components/layout/snice-layout.types.ts +5 -0
  83. package/components/login/demo-auth-controller.ts +185 -0
  84. package/components/login/demo.html +470 -0
  85. package/components/login/snice-login.css +204 -0
  86. package/components/login/snice-login.ts +337 -0
  87. package/components/login/snice-login.types.ts +34 -0
  88. package/components/modal/demo.html +291 -0
  89. package/components/modal/snice-modal.css +203 -0
  90. package/components/modal/snice-modal.ts +233 -0
  91. package/components/modal/snice-modal.types.ts +21 -0
  92. package/components/pagination/demo.html +395 -0
  93. package/components/pagination/snice-pagination.ts +333 -0
  94. package/components/pagination/snice-pagination.types.ts +21 -0
  95. package/components/progress/demo.html +510 -0
  96. package/components/progress/snice-progress.css +267 -0
  97. package/components/progress/snice-progress.ts +247 -0
  98. package/components/progress/snice-progress.types.ts +19 -0
  99. package/components/radio/demo.html +287 -0
  100. package/components/radio/snice-radio.css +171 -0
  101. package/components/radio/snice-radio.ts +218 -0
  102. package/components/radio/snice-radio.types.ts +21 -0
  103. package/components/select/demo.html +511 -0
  104. package/components/select/snice-option.ts +52 -0
  105. package/components/select/snice-option.types.ts +14 -0
  106. package/components/select/snice-select.css +392 -0
  107. package/components/select/snice-select.ts +796 -0
  108. package/components/select/snice-select.types.ts +55 -0
  109. package/components/skeleton/demo.html +514 -0
  110. package/components/skeleton/snice-skeleton.css +109 -0
  111. package/components/skeleton/snice-skeleton.ts +126 -0
  112. package/components/skeleton/snice-skeleton.types.ts +11 -0
  113. package/components/switch/demo.html +284 -0
  114. package/components/switch/snice-switch.css +221 -0
  115. package/components/switch/snice-switch.ts +229 -0
  116. package/components/switch/snice-switch.types.ts +23 -0
  117. package/components/symbols.ts +23 -0
  118. package/components/table/demo-table-controller.ts +100 -0
  119. package/components/table/demo.html +480 -0
  120. package/components/table/snice-cell-boolean.ts +112 -0
  121. package/components/table/snice-cell-date.ts +210 -0
  122. package/components/table/snice-cell-duration.ts +91 -0
  123. package/components/table/snice-cell-filesize.ts +90 -0
  124. package/components/table/snice-cell-number.ts +165 -0
  125. package/components/table/snice-cell-progress.ts +83 -0
  126. package/components/table/snice-cell-rating.ts +82 -0
  127. package/components/table/snice-cell-sparkline.ts +253 -0
  128. package/components/table/snice-cell-text.ts +125 -0
  129. package/components/table/snice-cell.css +296 -0
  130. package/components/table/snice-cell.ts +473 -0
  131. package/components/table/snice-column.ts +353 -0
  132. package/components/table/snice-header.css +243 -0
  133. package/components/table/snice-header.ts +261 -0
  134. package/components/table/snice-progress.ts +66 -0
  135. package/components/table/snice-rating.ts +45 -0
  136. package/components/table/snice-row.css +255 -0
  137. package/components/table/snice-row.ts +331 -0
  138. package/components/table/snice-table.css +241 -0
  139. package/components/table/snice-table.ts +737 -0
  140. package/components/table/snice-table.types.ts +158 -0
  141. package/components/tabs/demo.html +487 -0
  142. package/components/tabs/snice-tab-panel.css +264 -0
  143. package/components/tabs/snice-tab-panel.ts +47 -0
  144. package/components/tabs/snice-tab.css +96 -0
  145. package/components/tabs/snice-tab.ts +65 -0
  146. package/components/tabs/snice-tabs.css +189 -0
  147. package/components/tabs/snice-tabs.ts +332 -0
  148. package/components/tabs/snice-tabs.types.ts +28 -0
  149. package/components/theme/theme.css +234 -0
  150. package/components/toast/demo.html +329 -0
  151. package/components/toast/snice-toast-container.ts +256 -0
  152. package/components/toast/snice-toast.css +213 -0
  153. package/components/toast/snice-toast.ts +276 -0
  154. package/components/toast/snice-toast.types.ts +35 -0
  155. package/components/tooltip/demo.html +350 -0
  156. package/components/tooltip/snice-tooltip-portal.css +79 -0
  157. package/components/tooltip/snice-tooltip.css +117 -0
  158. package/components/tooltip/snice-tooltip.ts +612 -0
  159. package/components/tooltip/snice-tooltip.types.ts +32 -0
  160. package/components/transitions.ts +94 -0
  161. package/components/tsconfig.json +18 -0
  162. package/dist/index.cjs +441 -329
  163. package/dist/index.cjs.map +1 -1
  164. package/dist/index.cjs.min.map +1 -1
  165. package/dist/index.esm.js +441 -329
  166. package/dist/index.esm.js.map +1 -1
  167. package/dist/index.esm.min.js +3 -3
  168. package/dist/index.esm.min.js.map +1 -1
  169. package/dist/index.iife.js +441 -329
  170. package/dist/index.iife.js.map +1 -1
  171. package/dist/index.iife.min.js +3 -3
  172. package/dist/index.iife.min.js.map +1 -1
  173. package/dist/symbols.esm.js +1 -1
  174. package/dist/transitions.esm.js +1 -1
  175. package/dist/types/controller.d.ts +1 -1
  176. package/dist/types/element.d.ts +10 -10
  177. package/dist/types/events.d.ts +2 -2
  178. package/dist/types/index.d.ts +1 -1
  179. package/dist/types/observe.d.ts +1 -1
  180. package/dist/types/request-response.d.ts +2 -3
  181. package/dist/types/router.d.ts +1 -1
  182. package/package.json +9 -3
  183. package/dist/index.cjs.min +0 -15
  184. package/dist/symbols.cjs +0 -103
  185. package/dist/transitions.cjs +0 -219
@@ -0,0 +1,777 @@
1
+ import { element, property, query, on, watch, dispatch, ready } from 'snice';
2
+ import css from './snice-date-picker.css?inline';
3
+ import type { DatePickerSize, DatePickerVariant, DateFormat, SniceDatePickerElement, DatePickerValue } from './snice-date-picker.types';
4
+
5
+ @element('snice-date-picker')
6
+ export class SniceDatePicker extends HTMLElement implements SniceDatePickerElement {
7
+ @property({ reflect: true })
8
+ size: DatePickerSize = 'medium';
9
+
10
+ @property({ reflect: true })
11
+ variant: DatePickerVariant = 'outlined';
12
+
13
+ @property({ reflect: true })
14
+ value = '';
15
+
16
+ // Track input separately to prevent cursor jumps during typing
17
+ private inputValue = '';
18
+
19
+ @property({ reflect: true })
20
+ format: DateFormat = 'mm/dd/yyyy';
21
+
22
+ @property({ reflect: true })
23
+ placeholder = '';
24
+
25
+ @property({ reflect: true })
26
+ label = '';
27
+
28
+ @property({ attribute: 'helper-text', reflect: true })
29
+ helperText = '';
30
+
31
+ @property({ attribute: 'error-text', reflect: true })
32
+ errorText = '';
33
+
34
+ @property({ type: Boolean, reflect: true })
35
+ disabled = false;
36
+
37
+ @property({ type: Boolean, reflect: true })
38
+ readonly = false;
39
+
40
+ @property({ type: Boolean, reflect: true })
41
+ required = false;
42
+
43
+ @property({ type: Boolean, reflect: true })
44
+ invalid = false;
45
+
46
+ @property({ type: Boolean, reflect: true })
47
+ clearable = false;
48
+
49
+ @property({ reflect: true })
50
+ min = '';
51
+
52
+ @property({ reflect: true })
53
+ max = '';
54
+
55
+ @property({ reflect: true })
56
+ name = '';
57
+
58
+ @property({ type: Boolean, attribute: 'show-calendar', reflect: true })
59
+ showCalendar = false;
60
+
61
+ @property({ type: Number, attribute: 'first-day-of-week', reflect: true })
62
+ firstDayOfWeek = 0; // 0 = Sunday
63
+
64
+ @query('.input')
65
+ input?: HTMLInputElement;
66
+
67
+ @query('.calendar')
68
+ calendar?: HTMLElement;
69
+
70
+ @query('.clear-button')
71
+ clearButton?: HTMLButtonElement;
72
+
73
+ @query('.calendar-toggle')
74
+ calendarToggle?: HTMLButtonElement;
75
+
76
+ @query('.day--selected')
77
+ selectedDayButton?: HTMLButtonElement;
78
+
79
+ @query('.day:not(.day--empty):not(.day--disabled)')
80
+ firstDayButton?: HTMLButtonElement;
81
+
82
+ private selectedDate: Date | null = null;
83
+ private viewDate = new Date();
84
+
85
+ private monthNames = [
86
+ 'January', 'February', 'March', 'April', 'May', 'June',
87
+ 'July', 'August', 'September', 'October', 'November', 'December'
88
+ ];
89
+
90
+ private dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
91
+
92
+ css() {
93
+ return css;
94
+ }
95
+
96
+ html() {
97
+ return /*html*/`
98
+ <div class="date-picker-wrapper">
99
+ ${this.label ? /*html*/`
100
+ <label class="label ${this.required ? 'label--required' : ''}">
101
+ ${this.label}
102
+ </label>
103
+ ` : ''}
104
+
105
+ <div class="input-container">
106
+ <input
107
+ class="input
108
+ input--${this.size}
109
+ input--${this.variant}
110
+ ${this.invalid ? 'input--invalid' : ''}
111
+ ${this.clearable ? 'input--clearable' : ''}"
112
+ type="text"
113
+ value="${this.inputValue || this.getFormattedValue()}"
114
+ placeholder="${this.placeholder || this.getPlaceholderForFormat()}"
115
+ ${this.disabled ? 'disabled' : ''}
116
+ ${this.readonly ? 'readonly' : ''}
117
+ ${this.required ? 'required' : ''}
118
+ ${this.name ? `name="${this.name}"` : ''}
119
+ part="input"
120
+ autocomplete="off"
121
+ />
122
+
123
+ <button
124
+ class="calendar-toggle"
125
+ type="button"
126
+ aria-label="Open calendar"
127
+ tabindex="-1"
128
+ part="calendar-toggle"
129
+ ${this.disabled ? 'disabled' : ''}
130
+ >
131
+ <svg viewBox="0 0 24 24" width="18" height="18">
132
+ <path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7z" fill="currentColor"/>
133
+ </svg>
134
+ </button>
135
+
136
+ <button
137
+ class="clear-button"
138
+ type="button"
139
+ aria-label="Clear"
140
+ tabindex="-1"
141
+ part="clear"
142
+ style="display: none;"
143
+ >
144
+ <svg viewBox="0 0 24 24" width="16" height="16">
145
+ <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" fill="currentColor"/>
146
+ </svg>
147
+ </button>
148
+
149
+ <div class="calendar" part="calendar" hidden>
150
+ <div class="calendar-header">
151
+ <button class="nav-button" type="button" data-nav="prev-month" aria-label="Previous month">
152
+ <svg viewBox="0 0 24 24" width="20" height="20">
153
+ <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" fill="currentColor"/>
154
+ </svg>
155
+ </button>
156
+
157
+ <div class="calendar-title">
158
+ <button class="month-button" type="button" data-nav="month-picker">
159
+ ${this.monthNames[this.viewDate.getMonth()]} ${this.viewDate.getFullYear()}
160
+ </button>
161
+ </div>
162
+
163
+ <button class="nav-button" type="button" data-nav="next-month" aria-label="Next month">
164
+ <svg viewBox="0 0 24 24" width="20" height="20">
165
+ <path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" fill="currentColor"/>
166
+ </svg>
167
+ </button>
168
+ </div>
169
+
170
+ <div class="calendar-weekdays">
171
+ ${this.getDayHeaders()}
172
+ </div>
173
+
174
+ <div class="calendar-days">
175
+ ${this.getDaysGrid()}
176
+ </div>
177
+
178
+ <div class="calendar-footer">
179
+ <button class="today-button" type="button" data-nav="today">
180
+ Today
181
+ </button>
182
+ </div>
183
+ </div>
184
+ </div>
185
+
186
+ ${this.errorText ? /*html*/`
187
+ <span class="error-text" part="error-text">${this.errorText}</span>
188
+ ` : this.helperText ? /*html*/`
189
+ <span class="helper-text" part="helper-text">${this.helperText}</span>
190
+ ` : /*html*/`
191
+ <span class="helper-text" part="helper-text">&nbsp;</span>
192
+ `}
193
+ </div>
194
+ `;
195
+ }
196
+
197
+ @ready()
198
+ init() {
199
+ this.parseInitialValue();
200
+ this.updateClearButton();
201
+ this.setupCalendarClickOutside();
202
+
203
+ if (this.input) {
204
+ this.input.disabled = this.disabled;
205
+ this.input.readOnly = this.readonly;
206
+ this.input.required = this.required;
207
+
208
+ if (this.invalid) {
209
+ this.input.setAttribute('aria-invalid', 'true');
210
+ this.input.classList.add('input--invalid');
211
+ }
212
+ }
213
+ }
214
+
215
+ private parseInitialValue() {
216
+ if (this.value) {
217
+ const date = this.parseDate(this.value);
218
+ if (date) {
219
+ this.selectedDate = date;
220
+ this.viewDate = new Date(date);
221
+ this.inputValue = this.formatDate(date);
222
+ } else {
223
+ this.inputValue = this.value;
224
+ }
225
+ }
226
+ }
227
+
228
+ private parseDate(dateString: string): Date | null {
229
+ if (!dateString) return null;
230
+
231
+ if (this.format === 'mmmm dd, yyyy') {
232
+ const monthNameRegex = /^([A-Za-z]+)\s+(\d{1,2}),\s+(\d{4})$/;
233
+ const match = dateString.match(monthNameRegex);
234
+ if (match) {
235
+ const [, monthName, day, year] = match;
236
+ const monthIndex = this.monthNames.findIndex(m => m.toLowerCase() === monthName.toLowerCase());
237
+ if (monthIndex >= 0) {
238
+ const date = new Date(parseInt(year), monthIndex, parseInt(day));
239
+ if (!isNaN(date.getTime())) {
240
+ return date;
241
+ }
242
+ }
243
+ }
244
+ return null;
245
+ }
246
+
247
+ let date = new Date(dateString);
248
+ if (!isNaN(date.getTime())) {
249
+ return date;
250
+ }
251
+
252
+ const parts = dateString.split(/[-\/]/);
253
+ if (parts.length !== 3) return null;
254
+
255
+ let year: number, month: number, day: number;
256
+
257
+ switch (this.format) {
258
+ case 'mm/dd/yyyy':
259
+ case 'mm-dd-yyyy':
260
+ [month, day, year] = parts.map(Number);
261
+ break;
262
+ case 'dd/mm/yyyy':
263
+ case 'dd-mm-yyyy':
264
+ [day, month, year] = parts.map(Number);
265
+ break;
266
+ case 'yyyy-mm-dd':
267
+ case 'yyyy/mm/dd':
268
+ [year, month, day] = parts.map(Number);
269
+ break;
270
+ default:
271
+ return null;
272
+ }
273
+
274
+ if (year && month && day) {
275
+ date = new Date(year, month - 1, day);
276
+ if (!isNaN(date.getTime())) {
277
+ return date;
278
+ }
279
+ }
280
+
281
+ return null;
282
+ }
283
+
284
+ private formatDate(date: Date): string {
285
+ if (!date) return '';
286
+
287
+ const year = date.getFullYear();
288
+ const month = date.getMonth() + 1;
289
+ const day = date.getDate();
290
+
291
+ const mm = month.toString().padStart(2, '0');
292
+ const dd = day.toString().padStart(2, '0');
293
+ const yyyy = year.toString();
294
+
295
+ switch (this.format) {
296
+ case 'mm/dd/yyyy':
297
+ return `${mm}/${dd}/${yyyy}`;
298
+ case 'dd/mm/yyyy':
299
+ return `${dd}/${mm}/${yyyy}`;
300
+ case 'yyyy-mm-dd':
301
+ return `${yyyy}-${mm}-${dd}`;
302
+ case 'yyyy/mm/dd':
303
+ return `${yyyy}/${mm}/${dd}`;
304
+ case 'dd-mm-yyyy':
305
+ return `${dd}-${mm}-${yyyy}`;
306
+ case 'mm-dd-yyyy':
307
+ return `${mm}-${dd}-${yyyy}`;
308
+ case 'mmmm dd, yyyy':
309
+ return `${this.monthNames[date.getMonth()]} ${dd}, ${yyyy}`;
310
+ default:
311
+ return `${mm}/${dd}/${yyyy}`;
312
+ }
313
+ }
314
+
315
+ private getFormattedValue(): string {
316
+ return this.selectedDate ? this.formatDate(this.selectedDate) : this.value;
317
+ }
318
+
319
+ private getPlaceholderForFormat(): string {
320
+ switch (this.format) {
321
+ case 'mm/dd/yyyy':
322
+ return 'MM/DD/YYYY';
323
+ case 'dd/mm/yyyy':
324
+ return 'DD/MM/YYYY';
325
+ case 'yyyy-mm-dd':
326
+ return 'YYYY-MM-DD';
327
+ case 'yyyy/mm/dd':
328
+ return 'YYYY/MM/DD';
329
+ case 'dd-mm-yyyy':
330
+ return 'DD-MM-YYYY';
331
+ case 'mm-dd-yyyy':
332
+ return 'MM-DD-YYYY';
333
+ case 'mmmm dd, yyyy':
334
+ return 'Month DD, YYYY';
335
+ default:
336
+ return 'MM/DD/YYYY';
337
+ }
338
+ }
339
+
340
+ private getDayHeaders(): string {
341
+ const days = [...this.dayNames];
342
+ // Rotate array based on firstDayOfWeek (0=Sunday, 1=Monday, etc.)
343
+ for (let i = 0; i < this.firstDayOfWeek; i++) {
344
+ days.push(days.shift()!);
345
+ }
346
+ return days.map(day => `<div class="weekday">${day}</div>`).join('');
347
+ }
348
+
349
+ private getDaysGrid(): string {
350
+ const year = this.viewDate.getFullYear();
351
+ const month = this.viewDate.getMonth();
352
+
353
+ // First day of the month
354
+ const firstDay = new Date(year, month, 1);
355
+ // Last day of the month
356
+ const lastDay = new Date(year, month + 1, 0);
357
+
358
+ // Calculate starting position based on first day of week preference
359
+ let startingDayOfWeek = firstDay.getDay() - this.firstDayOfWeek;
360
+ if (startingDayOfWeek < 0) startingDayOfWeek += 7;
361
+
362
+ const daysInMonth = lastDay.getDate();
363
+ const today = new Date();
364
+ const isToday = (date: Date) =>
365
+ date.getDate() === today.getDate() &&
366
+ date.getMonth() === today.getMonth() &&
367
+ date.getFullYear() === today.getFullYear();
368
+
369
+ const isSelected = (date: Date) =>
370
+ this.selectedDate &&
371
+ date.getDate() === this.selectedDate.getDate() &&
372
+ date.getMonth() === this.selectedDate.getMonth() &&
373
+ date.getFullYear() === this.selectedDate.getFullYear();
374
+
375
+ const isDisabled = (date: Date) => {
376
+ if (this.min) {
377
+ const minDate = this.parseDate(this.min);
378
+ if (minDate && date < minDate) return true;
379
+ }
380
+ if (this.max) {
381
+ const maxDate = this.parseDate(this.max);
382
+ if (maxDate && date > maxDate) return true;
383
+ }
384
+ return false;
385
+ };
386
+
387
+ let html = '';
388
+
389
+ // Empty cells for days before month starts
390
+ for (let i = 0; i < startingDayOfWeek; i++) {
391
+ html += '<div class="day day--empty"></div>';
392
+ }
393
+
394
+ // Days of the month
395
+ for (let day = 1; day <= daysInMonth; day++) {
396
+ const date = new Date(year, month, day);
397
+ const classes = ['day'];
398
+
399
+ if (isToday(date)) classes.push('day--today');
400
+ if (isSelected(date)) classes.push('day--selected');
401
+ if (isDisabled(date)) classes.push('day--disabled');
402
+
403
+ html += `
404
+ <button
405
+ class="${classes.join(' ')}"
406
+ type="button"
407
+ data-date="${year}-${(month + 1).toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}"
408
+ ${isDisabled(date) ? 'disabled' : ''}
409
+ aria-label="${this.formatDate(date)}"
410
+ >
411
+ ${day}
412
+ </button>
413
+ `;
414
+ }
415
+
416
+ return html;
417
+ }
418
+
419
+ private updateInputValue() {
420
+ if (this.input && document.activeElement !== this.input) {
421
+ const displayValue = this.getFormattedValue();
422
+ this.input.value = displayValue;
423
+ this.inputValue = displayValue;
424
+ }
425
+ this.updateClearButton();
426
+ }
427
+
428
+ private isCompleteDate(value: string): boolean {
429
+ if (this.format === 'mmmm dd, yyyy') {
430
+ return /^[A-Za-z]+\s+\d{1,2},\s+\d{4}$/.test(value);
431
+ }
432
+
433
+ const separators = (value.match(/[\/\-]/g) || []).length;
434
+ return separators >= 2 && value.length >= 8;
435
+ }
436
+
437
+ private updateClearButton() {
438
+ if (this.clearButton && this.clearable) {
439
+ const shouldShow = this.selectedDate && !this.disabled && !this.readonly;
440
+ this.clearButton.style.display = shouldShow ? '' : 'none';
441
+ this.clearButton.classList.toggle('clear-button--visible', !!shouldShow);
442
+ }
443
+ }
444
+
445
+ private updateCalendarGrid() {
446
+ const calendarDays = this.calendar?.querySelector('.calendar-days');
447
+ const calendarTitle = this.calendar?.querySelector('.month-button');
448
+
449
+ if (calendarDays) {
450
+ calendarDays.innerHTML = this.getDaysGrid();
451
+ }
452
+
453
+ if (calendarTitle) {
454
+ calendarTitle.textContent = `${this.monthNames[this.viewDate.getMonth()]} ${this.viewDate.getFullYear()}`;
455
+ }
456
+ }
457
+
458
+ private setupCalendarClickOutside() {
459
+ document.addEventListener('click', (e) => {
460
+ if (!this.contains(e.target as Node) && this.showCalendar) {
461
+ this.close();
462
+ }
463
+ });
464
+ }
465
+
466
+ @on('input', '.input')
467
+ handleInput(e: Event) {
468
+ const target = e.target as HTMLInputElement;
469
+
470
+ this.inputValue = target.value;
471
+
472
+ const date = this.parseDate(target.value);
473
+ if (date && this.isCompleteDate(target.value)) {
474
+ this.selectedDate = date;
475
+ this.viewDate = new Date(date);
476
+ if (this.showCalendar) {
477
+ this.updateCalendarGrid();
478
+ }
479
+ }
480
+
481
+ this.updateClearButton();
482
+ this.dispatchInputEvent();
483
+ }
484
+
485
+ @on('change', '.input')
486
+ handleChange(e: Event) {
487
+ const target = e.target as HTMLInputElement;
488
+
489
+ const date = this.parseDate(target.value);
490
+ if (date) {
491
+ this.selectedDate = date;
492
+ this.value = this.formatDate(date);
493
+ this.inputValue = this.value;
494
+ if (this.input) {
495
+ this.input.value = this.value;
496
+ }
497
+ } else if (target.value) {
498
+ this.selectedDate = null;
499
+ this.value = target.value;
500
+ this.inputValue = target.value;
501
+ } else {
502
+ this.selectedDate = null;
503
+ this.value = '';
504
+ this.inputValue = '';
505
+ }
506
+
507
+ this.updateClearButton();
508
+ this.dispatchChangeEvent();
509
+ }
510
+
511
+ @on('focus', '.input')
512
+ handleFocus() {
513
+ this.dispatchFocusEvent();
514
+ }
515
+
516
+ @on('click', '.input')
517
+ handleInputClick() {
518
+ if (!this.showCalendar && !this.disabled && !this.readonly) {
519
+ this.open();
520
+ }
521
+ }
522
+
523
+ @on('blur', '.input')
524
+ handleBlur() {
525
+ this.dispatchBlurEvent();
526
+ }
527
+
528
+ @on('click', '.calendar-toggle')
529
+ handleCalendarToggle() {
530
+ if (this.showCalendar) {
531
+ this.close();
532
+ } else {
533
+ this.open();
534
+ }
535
+ }
536
+
537
+ @on('click', '.clear-button')
538
+ handleClear() {
539
+ this.clear();
540
+ }
541
+
542
+ @on('click', '.calendar')
543
+ handleCalendarClick(e: Event) {
544
+ e.stopPropagation();
545
+ const target = e.target as HTMLElement;
546
+
547
+ if (target.closest('[data-date]')) {
548
+ const dateString = target.closest('[data-date]')?.getAttribute('data-date');
549
+ if (dateString) {
550
+ const date = new Date(dateString);
551
+ this.selectDate(date);
552
+ }
553
+ } else if (target.closest('[data-nav]')) {
554
+ const nav = target.closest('[data-nav]')?.getAttribute('data-nav');
555
+ this.handleNavigation(nav!);
556
+ }
557
+ }
558
+
559
+ @on('keydown', '.input')
560
+ handleKeydown(e: KeyboardEvent) {
561
+ if (e.key === 'Enter' || e.key === ' ') {
562
+ e.preventDefault();
563
+ if (!this.showCalendar) {
564
+ this.open();
565
+ }
566
+ } else if (e.key === 'Escape' && this.showCalendar) {
567
+ this.close();
568
+ this.focus();
569
+ }
570
+ }
571
+
572
+ private handleNavigation(nav: string) {
573
+ switch (nav) {
574
+ case 'prev-month':
575
+ this.viewDate.setMonth(this.viewDate.getMonth() - 1);
576
+ this.updateCalendarGrid();
577
+ break;
578
+ case 'next-month':
579
+ this.viewDate.setMonth(this.viewDate.getMonth() + 1);
580
+ this.updateCalendarGrid();
581
+ break;
582
+ case 'today':
583
+ this.goToToday();
584
+ break;
585
+ }
586
+ }
587
+
588
+ @watch('value')
589
+ handleValueChange() {
590
+ const date = this.parseDate(this.value);
591
+ this.selectedDate = date;
592
+ if (date) {
593
+ this.viewDate = new Date(date);
594
+ }
595
+ this.updateInputValue();
596
+ if (this.showCalendar) {
597
+ this.updateCalendarGrid();
598
+ }
599
+ }
600
+
601
+ // Manual DOM manipulation required since Snice is imperative
602
+ @watch('show-calendar')
603
+ handleShowCalendarChange() {
604
+ console.log('showCalendar changed:', this.calendar);
605
+ if (this.calendar) {
606
+ if (this.showCalendar) {
607
+ this.calendar.removeAttribute('hidden');
608
+ this.dispatchOpenEvent();
609
+ // Focus selected date or first available date for accessibility
610
+ setTimeout(() => {
611
+ (this.selectedDayButton || this.firstDayButton)?.focus();
612
+ }, 100);
613
+ } else {
614
+ this.calendar.setAttribute('hidden', '');
615
+ this.dispatchCloseEvent();
616
+ }
617
+ }
618
+ }
619
+
620
+ @watch('disabled')
621
+ handleDisabledChange() {
622
+ if (this.input) {
623
+ this.input.disabled = this.disabled;
624
+ }
625
+ if (this.calendarToggle) {
626
+ this.calendarToggle.disabled = this.disabled;
627
+ }
628
+ this.updateClearButton();
629
+ }
630
+
631
+ @watch('readonly')
632
+ handleReadonlyChange() {
633
+ if (this.input) {
634
+ this.input.readOnly = this.readonly;
635
+ }
636
+ this.updateClearButton();
637
+ }
638
+
639
+ @watch('invalid')
640
+ handleInvalidChange() {
641
+ if (this.input) {
642
+ this.input.setAttribute('aria-invalid', String(this.invalid));
643
+ this.input.classList.toggle('input--invalid', this.invalid);
644
+ }
645
+ }
646
+
647
+ @watch('format')
648
+ handleFormatChange() {
649
+ this.updateInputValue();
650
+ }
651
+
652
+ @dispatch('@snice/datepicker-input', { bubbles: true, composed: true })
653
+ private dispatchInputEvent() {
654
+ return { value: this.value, datePicker: this };
655
+ }
656
+
657
+ @dispatch('@snice/datepicker-change', { bubbles: true, composed: true })
658
+ private dispatchChangeEvent() {
659
+ return {
660
+ value: this.value,
661
+ date: this.selectedDate,
662
+ formatted: this.selectedDate ? this.formatDate(this.selectedDate) : '',
663
+ iso: this.selectedDate ? this.selectedDate.toISOString().split('T')[0] : '',
664
+ datePicker: this
665
+ };
666
+ }
667
+
668
+ @dispatch('@snice/datepicker-focus', { bubbles: true, composed: true })
669
+ private dispatchFocusEvent() {
670
+ return { datePicker: this };
671
+ }
672
+
673
+ @dispatch('@snice/datepicker-blur', { bubbles: true, composed: true })
674
+ private dispatchBlurEvent() {
675
+ return { datePicker: this };
676
+ }
677
+
678
+ @dispatch('@snice/datepicker-open', { bubbles: true, composed: true })
679
+ private dispatchOpenEvent() {
680
+ return { datePicker: this };
681
+ }
682
+
683
+ @dispatch('@snice/datepicker-close', { bubbles: true, composed: true })
684
+ private dispatchCloseEvent() {
685
+ return { datePicker: this };
686
+ }
687
+
688
+ @dispatch('@snice/datepicker-clear', { bubbles: true, composed: true })
689
+ private dispatchClearEvent() {
690
+ return { datePicker: this };
691
+ }
692
+
693
+ @dispatch('@snice/datepicker-select', { bubbles: true, composed: true })
694
+ private dispatchSelectEvent(date: Date) {
695
+ return {
696
+ date,
697
+ formatted: this.formatDate(date),
698
+ iso: date.toISOString().split('T')[0],
699
+ datePicker: this
700
+ };
701
+ }
702
+
703
+ focus() {
704
+ this.input?.focus();
705
+ }
706
+
707
+ blur() {
708
+ this.input?.blur();
709
+ }
710
+
711
+ clear() {
712
+ this.selectedDate = null;
713
+ this.value = '';
714
+ this.updateInputValue();
715
+ this.dispatchClearEvent();
716
+ this.dispatchChangeEvent();
717
+ this.focus();
718
+ }
719
+
720
+ open() {
721
+ if (!this.disabled && !this.readonly) {
722
+ this.showCalendar = true;
723
+ if (this.selectedDate) {
724
+ this.viewDate = new Date(this.selectedDate);
725
+ }
726
+ this.updateCalendarGrid();
727
+
728
+ if (this.calendar) {
729
+ this.calendar.removeAttribute('hidden');
730
+ }
731
+ this.dispatchOpenEvent();
732
+ }
733
+ }
734
+
735
+ close() {
736
+ this.showCalendar = false;
737
+
738
+ if (this.calendar) {
739
+ this.calendar.setAttribute('hidden', '');
740
+ }
741
+ this.dispatchCloseEvent();
742
+ }
743
+
744
+ selectDate(date: Date) {
745
+ this.selectedDate = date;
746
+ this.value = this.formatDate(date);
747
+ this.viewDate = new Date(date);
748
+ this.updateInputValue();
749
+ this.updateCalendarGrid();
750
+ this.dispatchSelectEvent(date);
751
+ this.dispatchChangeEvent();
752
+ this.close();
753
+ this.focus();
754
+ }
755
+
756
+ goToMonth(year: number, month: number) {
757
+ this.viewDate = new Date(year, month, 1);
758
+ this.updateCalendarGrid();
759
+ }
760
+
761
+ goToToday() {
762
+ const today = new Date();
763
+ this.selectDate(today);
764
+ }
765
+
766
+ checkValidity() {
767
+ return this.input?.checkValidity() ?? true;
768
+ }
769
+
770
+ reportValidity() {
771
+ return this.input?.reportValidity() ?? true;
772
+ }
773
+
774
+ setCustomValidity(message: string) {
775
+ this.input?.setCustomValidity(message);
776
+ }
777
+ }