create-nativecore 0.1.1 → 0.2.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 (175) hide show
  1. package/README.md +6 -14
  2. package/bin/index.mjs +402 -431
  3. package/package.json +3 -2
  4. package/template/.env.example +28 -0
  5. package/template/.htmlhintrc +14 -0
  6. package/template/api/data/dashboard.json +11 -0
  7. package/template/api/data/users.json +18 -0
  8. package/template/api/mockApi.js +161 -0
  9. package/template/assets/icon.svg +13 -0
  10. package/template/assets/logo.svg +25 -0
  11. package/template/eslint.config.js +94 -0
  12. package/template/index.html +137 -0
  13. package/template/manifest.json +19 -0
  14. package/template/public/.well-known/security.txt +9 -0
  15. package/template/public/_headers +24 -0
  16. package/template/public/_redirects +14 -0
  17. package/template/public/assets/icon.svg +13 -0
  18. package/template/public/assets/logo.svg +25 -0
  19. package/template/public/manifest.json +19 -0
  20. package/template/public/robots.txt +13 -0
  21. package/template/public/sitemap.xml +27 -0
  22. package/template/scripts/build-for-bots.mjs +121 -0
  23. package/template/scripts/convert-to-ts.mjs +106 -0
  24. package/template/scripts/fix-encoding.mjs +38 -0
  25. package/template/scripts/fix-svg-paths.mjs +32 -0
  26. package/template/scripts/generate-cf-router.mjs +52 -0
  27. package/template/scripts/inject-dev-tools.mjs +41 -0
  28. package/template/scripts/inject-version.mjs +65 -0
  29. package/template/scripts/make-component.mjs +445 -0
  30. package/template/scripts/make-component.mjs.backup +432 -0
  31. package/template/scripts/make-controller.mjs +119 -0
  32. package/template/scripts/make-core-component.mjs +303 -0
  33. package/template/scripts/make-view.mjs +346 -0
  34. package/template/scripts/minify.mjs +71 -0
  35. package/template/scripts/prepare-static-assets.mjs +141 -0
  36. package/template/scripts/prompt-bot-build.mjs +223 -0
  37. package/template/scripts/remove-component.mjs +170 -0
  38. package/template/scripts/remove-core-component.mjs +156 -0
  39. package/template/scripts/remove-dev.mjs +13 -0
  40. package/template/scripts/remove-view.mjs +200 -0
  41. package/template/scripts/strip-dev-blocks.mjs +30 -0
  42. package/template/scripts/watch-compile.mjs +69 -0
  43. package/template/server.js +1066 -0
  44. package/template/src/app.ts +115 -0
  45. package/template/src/components/appRegistry.ts +8 -0
  46. package/template/src/components/core/app-footer.ts +27 -0
  47. package/template/src/components/core/app-header.ts +175 -0
  48. package/template/src/components/core/app-sidebar.ts +238 -0
  49. package/template/src/components/core/loading-spinner.ts +25 -0
  50. package/template/src/components/core/nc-a.ts +313 -0
  51. package/template/src/components/core/nc-accordion.ts +186 -0
  52. package/template/src/components/core/nc-alert.ts +153 -0
  53. package/template/src/components/core/nc-animation.ts +1150 -0
  54. package/template/src/components/core/nc-autocomplete.ts +271 -0
  55. package/template/src/components/core/nc-avatar-group.ts +113 -0
  56. package/template/src/components/core/nc-avatar.ts +148 -0
  57. package/template/src/components/core/nc-badge.ts +86 -0
  58. package/template/src/components/core/nc-bottom-nav.ts +214 -0
  59. package/template/src/components/core/nc-breadcrumb.ts +96 -0
  60. package/template/src/components/core/nc-button.ts +307 -0
  61. package/template/src/components/core/nc-card.ts +160 -0
  62. package/template/src/components/core/nc-checkbox.ts +282 -0
  63. package/template/src/components/core/nc-chip.ts +115 -0
  64. package/template/src/components/core/nc-code.ts +314 -0
  65. package/template/src/components/core/nc-collapsible.ts +154 -0
  66. package/template/src/components/core/nc-color-picker.ts +268 -0
  67. package/template/src/components/core/nc-copy-button.ts +119 -0
  68. package/template/src/components/core/nc-date-picker.ts +443 -0
  69. package/template/src/components/core/nc-div.ts +280 -0
  70. package/template/src/components/core/nc-divider.ts +81 -0
  71. package/template/src/components/core/nc-drawer.ts +230 -0
  72. package/template/src/components/core/nc-dropdown.ts +178 -0
  73. package/template/src/components/core/nc-empty-state.ts +134 -0
  74. package/template/src/components/core/nc-file-upload.ts +354 -0
  75. package/template/src/components/core/nc-form.ts +312 -0
  76. package/template/src/components/core/nc-image.ts +184 -0
  77. package/template/src/components/core/nc-input.ts +383 -0
  78. package/template/src/components/core/nc-kbd.ts +48 -0
  79. package/template/src/components/core/nc-menu-item.ts +193 -0
  80. package/template/src/components/core/nc-menu.ts +376 -0
  81. package/template/src/components/core/nc-modal.ts +238 -0
  82. package/template/src/components/core/nc-nav-item.ts +151 -0
  83. package/template/src/components/core/nc-number-input.ts +350 -0
  84. package/template/src/components/core/nc-otp-input.ts +235 -0
  85. package/template/src/components/core/nc-pagination.ts +178 -0
  86. package/template/src/components/core/nc-popover.ts +260 -0
  87. package/template/src/components/core/nc-progress-circular.ts +119 -0
  88. package/template/src/components/core/nc-progress.ts +134 -0
  89. package/template/src/components/core/nc-radio.ts +235 -0
  90. package/template/src/components/core/nc-rating.ts +266 -0
  91. package/template/src/components/core/nc-rich-text.ts +283 -0
  92. package/template/src/components/core/nc-scroll-top.ts +116 -0
  93. package/template/src/components/core/nc-select.ts +452 -0
  94. package/template/src/components/core/nc-skeleton.ts +107 -0
  95. package/template/src/components/core/nc-slider.ts +285 -0
  96. package/template/src/components/core/nc-snackbar.ts +230 -0
  97. package/template/src/components/core/nc-splash.ts +343 -0
  98. package/template/src/components/core/nc-stepper.ts +247 -0
  99. package/template/src/components/core/nc-switch.ts +281 -0
  100. package/template/src/components/core/nc-tab-item.ts +138 -0
  101. package/template/src/components/core/nc-table.ts +279 -0
  102. package/template/src/components/core/nc-tabs.ts +554 -0
  103. package/template/src/components/core/nc-tag-input.ts +279 -0
  104. package/template/src/components/core/nc-textarea.ts +216 -0
  105. package/template/src/components/core/nc-time-picker.ts +438 -0
  106. package/template/src/components/core/nc-timeline.ts +186 -0
  107. package/template/src/components/core/nc-tooltip.ts +143 -0
  108. package/template/src/components/frameworkRegistry.ts +68 -0
  109. package/template/src/components/preloadRegistry.ts +28 -0
  110. package/template/src/components/registry.ts +8 -0
  111. package/template/src/components/ui/dashboard-signal-lab.ts +284 -0
  112. package/template/src/constants/apiEndpoints.ts +27 -0
  113. package/template/src/constants/errorMessages.ts +23 -0
  114. package/template/src/constants/index.ts +8 -0
  115. package/template/src/constants/routePaths.ts +15 -0
  116. package/template/src/constants/storageKeys.ts +18 -0
  117. package/template/src/controllers/dashboard.controller.ts +200 -0
  118. package/template/src/controllers/home.controller.ts +21 -0
  119. package/template/src/controllers/index.ts +11 -0
  120. package/template/src/controllers/login.controller.ts +131 -0
  121. package/template/src/core/component.ts +354 -0
  122. package/template/src/core/errorHandler.ts +85 -0
  123. package/template/src/core/gpu-animation.ts +604 -0
  124. package/template/src/core/http.ts +173 -0
  125. package/template/src/core/lazyComponents.ts +90 -0
  126. package/template/src/core/router.ts +642 -0
  127. package/template/src/core/signals.ts +146 -0
  128. package/template/src/core/state.ts +248 -0
  129. package/template/src/dev/component-editor.ts +1363 -0
  130. package/template/src/dev/component-overlay.ts +278 -0
  131. package/template/src/dev/context-menu.ts +223 -0
  132. package/template/src/dev/denc-tools.ts +250 -0
  133. package/template/src/dev/hmr.ts +189 -0
  134. package/template/src/dev/nfbs.code-workspace +27 -0
  135. package/template/src/dev/outline-panel.ts +1247 -0
  136. package/template/src/middleware/auth.middleware.ts +23 -0
  137. package/template/src/routes/routes.ts +38 -0
  138. package/template/src/services/api.service.ts +394 -0
  139. package/template/src/services/auth.service.ts +176 -0
  140. package/template/src/services/index.ts +8 -0
  141. package/template/src/services/logger.service.ts +74 -0
  142. package/template/src/services/storage.service.ts +88 -0
  143. package/template/src/stores/appStore.ts +57 -0
  144. package/template/src/stores/uiStore.ts +36 -0
  145. package/template/src/styles/core-variables.css +219 -0
  146. package/template/src/styles/core.css +710 -0
  147. package/template/src/styles/main.css +3164 -0
  148. package/template/src/styles/variables.css +152 -0
  149. package/template/src/types/global.d.ts +47 -0
  150. package/template/src/utils/cacheBuster.ts +20 -0
  151. package/template/src/utils/dom.ts +149 -0
  152. package/template/src/utils/events.ts +203 -0
  153. package/template/src/utils/form.ts +176 -0
  154. package/template/src/utils/formatters.ts +169 -0
  155. package/template/src/utils/helpers.ts +195 -0
  156. package/template/src/utils/markdown.ts +307 -0
  157. package/template/src/utils/sidebar.ts +96 -0
  158. package/template/src/utils/smoothScroll.ts +85 -0
  159. package/template/src/utils/templates.ts +23 -0
  160. package/template/src/utils/validation.ts +73 -0
  161. package/template/src/views/protected/dashboard.html +293 -0
  162. package/template/src/views/public/home.html +150 -0
  163. package/template/src/views/public/login.html +102 -0
  164. package/template/tests/unit/component.test.ts +87 -0
  165. package/template/tests/unit/computed.test.ts +79 -0
  166. package/template/tests/unit/form.test.ts +68 -0
  167. package/template/tests/unit/formatters.test.ts +49 -0
  168. package/template/tests/unit/lazy-components.test.ts +59 -0
  169. package/template/tests/unit/markdown.test.ts +62 -0
  170. package/template/tests/unit/router.test.ts +112 -0
  171. package/template/tests/unit/signals.test.ts +54 -0
  172. package/template/tests/unit/validation.test.ts +50 -0
  173. package/template/tsconfig.build.json +21 -0
  174. package/template/tsconfig.json +51 -0
  175. package/template/vitest.config.ts +36 -0
@@ -0,0 +1,443 @@
1
+ /**
2
+ * NcDatePicker Component
3
+ *
4
+ * Attributes:
5
+ * - name: string
6
+ * - value: string — ISO date string YYYY-MM-DD
7
+ * - min: string — minimum selectable date (YYYY-MM-DD)
8
+ * - max: string — maximum selectable date (YYYY-MM-DD)
9
+ * - placeholder: string (default: 'Select date')
10
+ * - disabled: boolean
11
+ * - readonly: boolean
12
+ * - size: 'sm'|'md'|'lg' (default: 'md')
13
+ * - variant: 'default'|'filled' (default: 'default')
14
+ * - first-day: 0|1 — 0=Sunday, 1=Monday (default: 0)
15
+ *
16
+ * Events:
17
+ * - change: CustomEvent<{ value: string; date: Date | null; name: string }>
18
+ *
19
+ * Usage:
20
+ * <nc-date-picker name="dob" value="2024-03-15"></nc-date-picker>
21
+ * <nc-date-picker name="from" min="2024-01-01" max="2024-12-31"></nc-date-picker>
22
+ */
23
+
24
+ import { Component, defineComponent } from '@core/component.js';
25
+
26
+ const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'];
27
+ const DAYS_SUN = ['Su','Mo','Tu','We','Th','Fr','Sa'];
28
+ const DAYS_MON = ['Mo','Tu','We','Th','Fr','Sa','Su'];
29
+
30
+ function pad(n: number) { return String(n).padStart(2, '0'); }
31
+ function toISO(d: Date) { return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`; }
32
+ function parseDate(s: string | null): Date | null {
33
+ if (!s) return null;
34
+ const d = new Date(s + 'T00:00:00');
35
+ return isNaN(d.getTime()) ? null : d;
36
+ }
37
+ function formatDisplay(d: Date | null): string {
38
+ if (!d) return '';
39
+ return `${MONTHS[d.getMonth()].slice(0,3)} ${d.getDate()}, ${d.getFullYear()}`;
40
+ }
41
+
42
+ export class NcDatePicker extends Component {
43
+ static useShadowDOM = true;
44
+
45
+ static get observedAttributes() {
46
+ return ['name', 'value', 'min', 'max', 'placeholder', 'disabled', 'readonly', 'size', 'variant', 'first-day'];
47
+ }
48
+
49
+ private _open = false;
50
+ private _selected: Date | null = null;
51
+ private _viewYear = new Date().getFullYear();
52
+ private _viewMonth = new Date().getMonth();
53
+
54
+ constructor() { super(); }
55
+
56
+ template() {
57
+ if (!this._mounted) {
58
+ this._selected = parseDate(this.getAttribute('value'));
59
+ if (this._selected) {
60
+ this._viewYear = this._selected.getFullYear();
61
+ this._viewMonth = this._selected.getMonth();
62
+ }
63
+ }
64
+
65
+ const disabled = this.hasAttribute('disabled');
66
+ const readonly = this.hasAttribute('readonly');
67
+ const placeholder = this.getAttribute('placeholder') || 'Select date';
68
+ const displayValue = formatDisplay(this._selected);
69
+ const firstDay = Number(this.getAttribute('first-day') ?? 0);
70
+ const dayLabels = firstDay === 1 ? DAYS_MON : DAYS_SUN;
71
+
72
+ return `
73
+ <style>
74
+ :host { display: block; position: relative; font-family: var(--nc-font-family); }
75
+
76
+ .input-wrap {
77
+ display: flex;
78
+ align-items: center;
79
+ border: var(--nc-input-border);
80
+ border-radius: var(--nc-input-radius);
81
+ background: var(--nc-bg);
82
+ overflow: hidden;
83
+ cursor: ${disabled || readonly ? 'not-allowed' : 'pointer'};
84
+ opacity: ${disabled ? '0.5' : '1'};
85
+ transition: border-color var(--nc-transition-fast), box-shadow var(--nc-transition-fast);
86
+ }
87
+
88
+ :host([variant="filled"]) .input-wrap { background: var(--nc-bg-tertiary); border-color: transparent; }
89
+ .input-wrap:focus-within { border-color: var(--nc-input-focus-border); box-shadow: 0 0 0 3px rgba(16,185,129,.15); }
90
+
91
+ .input-text {
92
+ flex: 1;
93
+ padding: var(--nc-spacing-sm) var(--nc-spacing-md);
94
+ font-size: var(--nc-font-size-base);
95
+ color: ${displayValue ? 'var(--nc-text)' : 'var(--nc-text-muted)'};
96
+ background: none;
97
+ border: none;
98
+ outline: none;
99
+ cursor: inherit;
100
+ white-space: nowrap;
101
+ min-width: 0;
102
+ }
103
+
104
+ :host([size="sm"]) .input-text { font-size: var(--nc-font-size-sm); padding: var(--nc-spacing-xs) var(--nc-spacing-sm); }
105
+ :host([size="lg"]) .input-text { font-size: var(--nc-font-size-lg); padding: var(--nc-spacing-md) var(--nc-spacing-lg); }
106
+
107
+ .input-icon {
108
+ padding: 0 var(--nc-spacing-sm);
109
+ color: var(--nc-text-muted);
110
+ display: flex;
111
+ align-items: center;
112
+ flex-shrink: 0;
113
+ }
114
+
115
+ .calendar {
116
+ position: absolute;
117
+ top: calc(100% + 6px);
118
+ left: 0;
119
+ z-index: 500;
120
+ background: var(--nc-bg);
121
+ border: 1px solid var(--nc-border);
122
+ border-radius: var(--nc-radius-md, 8px);
123
+ box-shadow: var(--nc-shadow-lg);
124
+ padding: var(--nc-spacing-md);
125
+ width: 280px;
126
+ display: ${this._open ? 'block' : 'none'};
127
+ }
128
+
129
+ .cal-header {
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: space-between;
133
+ margin-bottom: var(--nc-spacing-sm);
134
+ gap: 4px;
135
+ }
136
+
137
+ .cal-nav {
138
+ background: none;
139
+ border: none;
140
+ cursor: pointer;
141
+ padding: 4px 6px;
142
+ border-radius: var(--nc-radius-sm, 4px);
143
+ color: var(--nc-text-muted);
144
+ display: flex;
145
+ transition: background var(--nc-transition-fast), color var(--nc-transition-fast);
146
+ }
147
+ .cal-nav:hover { background: var(--nc-bg-secondary); color: var(--nc-text); }
148
+
149
+ .cal-title {
150
+ flex: 1;
151
+ text-align: center;
152
+ font-weight: var(--nc-font-weight-semibold);
153
+ font-size: var(--nc-font-size-sm);
154
+ color: var(--nc-text);
155
+ }
156
+
157
+ .cal-grid {
158
+ display: grid;
159
+ grid-template-columns: repeat(7, 1fr);
160
+ gap: 2px;
161
+ }
162
+
163
+ .cal-day-label {
164
+ text-align: center;
165
+ font-size: 0.65rem;
166
+ font-weight: var(--nc-font-weight-semibold);
167
+ color: var(--nc-text-muted);
168
+ padding: 2px 0;
169
+ text-transform: uppercase;
170
+ }
171
+
172
+ .cal-day {
173
+ text-align: center;
174
+ font-size: var(--nc-font-size-sm);
175
+ padding: 5px 2px;
176
+ border-radius: var(--nc-radius-sm, 4px);
177
+ cursor: pointer;
178
+ color: var(--nc-text);
179
+ transition: background var(--nc-transition-fast), color var(--nc-transition-fast);
180
+ border: none;
181
+ background: none;
182
+ width: 100%;
183
+ font-family: var(--nc-font-family);
184
+ }
185
+
186
+ .cal-day:hover:not(:disabled) { background: var(--nc-bg-secondary); }
187
+ .cal-day--other-month { color: var(--nc-text-muted); opacity: 0.4; }
188
+ .cal-day--today { font-weight: var(--nc-font-weight-semibold); border: 1px solid var(--nc-border-dark); }
189
+ .cal-day--selected { background: var(--nc-primary) !important; color: #fff !important; }
190
+ .cal-day:disabled { opacity: 0.25; cursor: not-allowed; }
191
+
192
+ .cal-footer {
193
+ margin-top: var(--nc-spacing-sm);
194
+ display: flex;
195
+ justify-content: space-between;
196
+ gap: var(--nc-spacing-xs);
197
+ }
198
+
199
+ .cal-btn {
200
+ flex: 1;
201
+ padding: 5px;
202
+ border-radius: var(--nc-radius-sm, 4px);
203
+ font-size: var(--nc-font-size-xs);
204
+ font-family: var(--nc-font-family);
205
+ cursor: pointer;
206
+ border: 1px solid var(--nc-border);
207
+ background: var(--nc-bg-secondary);
208
+ color: var(--nc-text);
209
+ transition: background var(--nc-transition-fast);
210
+ }
211
+ .cal-btn:hover { background: var(--nc-bg-tertiary); }
212
+ .cal-btn--primary { background: var(--nc-primary); color: #fff; border-color: var(--nc-primary); }
213
+ .cal-btn--primary:hover { opacity: 0.9; }
214
+ </style>
215
+
216
+ <div class="input-wrap" tabindex="${disabled ? '-1' : '0'}" role="button" aria-haspopup="dialog" aria-expanded="${this._open}">
217
+ <span class="input-text">${displayValue || placeholder}</span>
218
+ <span class="input-icon">
219
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" width="14" height="14">
220
+ <rect x="2" y="3" width="12" height="11" rx="2" stroke="currentColor" stroke-width="1.2"/>
221
+ <path d="M5 1v3M11 1v3M2 7h12" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
222
+ </svg>
223
+ </span>
224
+ </div>
225
+
226
+ <div class="calendar" role="dialog" aria-label="Date picker" aria-modal="false">
227
+ ${this._renderCalendar(dayLabels, firstDay)}
228
+ </div>
229
+
230
+ <input type="hidden" name="${this.getAttribute('name') || ''}" value="${this._selected ? toISO(this._selected) : ''}" />
231
+ `;
232
+ }
233
+
234
+ private _renderCalendar(dayLabels: string[], firstDay: number): string {
235
+ const year = this._viewYear;
236
+ const month = this._viewMonth;
237
+ const today = new Date();
238
+ const min = parseDate(this.getAttribute('min'));
239
+ const max = parseDate(this.getAttribute('max'));
240
+
241
+ // Grid: fill from firstDay
242
+ const firstOfMonth = new Date(year, month, 1).getDay(); // 0=Sun
243
+ // offset to align with firstDay
244
+ const startOffset = (firstOfMonth - firstDay + 7) % 7;
245
+
246
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
247
+ const daysInPrev = new Date(year, month, 0).getDate();
248
+ const cells: { date: Date; type: 'prev'|'curr'|'next' }[] = [];
249
+
250
+ for (let i = startOffset - 1; i >= 0; i--) {
251
+ cells.push({ date: new Date(year, month - 1, daysInPrev - i), type: 'prev' });
252
+ }
253
+ for (let d = 1; d <= daysInMonth; d++) {
254
+ cells.push({ date: new Date(year, month, d), type: 'curr' });
255
+ }
256
+ const remaining = 42 - cells.length;
257
+ for (let d = 1; d <= remaining; d++) {
258
+ cells.push({ date: new Date(year, month + 1, d), type: 'next' });
259
+ }
260
+
261
+ const dayLabelsHtml = dayLabels.map(l => `<span class="cal-day-label">${l}</span>`).join('');
262
+
263
+ const daysHtml = cells.map(({ date, type }) => {
264
+ const iso = toISO(date);
265
+ const isToday = iso === toISO(today);
266
+ const isSelected = this._selected && iso === toISO(this._selected);
267
+ const isDisabled = (min && date < min) || (max && date > max);
268
+ const classes = [
269
+ 'cal-day',
270
+ type !== 'curr' ? 'cal-day--other-month' : '',
271
+ isToday ? 'cal-day--today' : '',
272
+ isSelected ? 'cal-day--selected' : '',
273
+ ].filter(Boolean).join(' ');
274
+ return `<button class="${classes}" data-date="${iso}" ${isDisabled ? 'disabled' : ''} type="button" tabindex="-1" aria-label="${iso}" aria-pressed="${!!isSelected}">${date.getDate()}</button>`;
275
+ }).join('');
276
+
277
+ return `
278
+ <div class="cal-header">
279
+ <button class="cal-nav" data-nav="prev-year" type="button" aria-label="Previous year">
280
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" fill="none" width="10" height="10"><path d="M9 2L5 6l4 4M5 2L1 6l4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
281
+ </button>
282
+ <button class="cal-nav" data-nav="prev-month" type="button" aria-label="Previous month">
283
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" fill="none" width="10" height="10"><path d="M8 2L4 6l4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
284
+ </button>
285
+ <span class="cal-title">${MONTHS[month]} ${year}</span>
286
+ <button class="cal-nav" data-nav="next-month" type="button" aria-label="Next month">
287
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" fill="none" width="10" height="10"><path d="M4 2l4 4-4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
288
+ </button>
289
+ <button class="cal-nav" data-nav="next-year" type="button" aria-label="Next year">
290
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" fill="none" width="10" height="10"><path d="M3 2l4 4-4 4M7 2l4 4-4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
291
+ </button>
292
+ </div>
293
+ <div class="cal-grid">
294
+ ${dayLabelsHtml}
295
+ ${daysHtml}
296
+ </div>
297
+ <div class="cal-footer">
298
+ <button class="cal-btn" data-action="today" type="button">Today</button>
299
+ <button class="cal-btn" data-action="clear" type="button">Clear</button>
300
+ </div>
301
+ `;
302
+ }
303
+
304
+ onMount() {
305
+ this._bindEvents();
306
+ }
307
+
308
+ private _bindEvents() {
309
+ const wrap = this.$<HTMLElement>('.input-wrap')!;
310
+ const calendar = this.$<HTMLElement>('.calendar')!;
311
+
312
+ // Toggle open
313
+ wrap.addEventListener('click', () => {
314
+ if (this.hasAttribute('disabled') || this.hasAttribute('readonly')) return;
315
+ this._open = !this._open;
316
+ calendar.style.display = this._open ? 'block' : 'none';
317
+ wrap.setAttribute('aria-expanded', String(this._open));
318
+ });
319
+
320
+ wrap.addEventListener('keydown', (e: KeyboardEvent) => {
321
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); wrap.click(); }
322
+ if (e.key === 'Escape') { this._open = false; calendar.style.display = 'none'; }
323
+ });
324
+
325
+ // Navigation
326
+ calendar.addEventListener('click', (e) => {
327
+ const target = e.target as HTMLElement;
328
+ const btn = target.closest<HTMLElement>('[data-nav]');
329
+ const day = target.closest<HTMLElement>('[data-date]');
330
+ const action = target.closest<HTMLElement>('[data-action]');
331
+
332
+ if (btn) {
333
+ e.stopPropagation();
334
+ switch (btn.dataset.nav) {
335
+ case 'prev-month': this._viewMonth--; if (this._viewMonth < 0) { this._viewMonth = 11; this._viewYear--; } break;
336
+ case 'next-month': this._viewMonth++; if (this._viewMonth > 11) { this._viewMonth = 0; this._viewYear++; } break;
337
+ case 'prev-year': this._viewYear--; break;
338
+ case 'next-year': this._viewYear++; break;
339
+ }
340
+ this._refreshCalendar();
341
+ }
342
+
343
+ if (day && day.dataset.date) {
344
+ this._select(parseDate(day.dataset.date));
345
+ }
346
+
347
+ if (action) {
348
+ if (action.dataset.action === 'today') {
349
+ const today = new Date();
350
+ this._viewYear = today.getFullYear();
351
+ this._viewMonth = today.getMonth();
352
+ this._select(today);
353
+ } else if (action.dataset.action === 'clear') {
354
+ this._select(null);
355
+ }
356
+ }
357
+ });
358
+
359
+ // Close on outside click
360
+ this._outsideClick = (e: MouseEvent) => {
361
+ if (!this.contains(e.target as Node) && !this.shadowRoot!.contains(e.target as Node)) {
362
+ this._open = false;
363
+ if (calendar) calendar.style.display = 'none';
364
+ if (wrap) wrap.setAttribute('aria-expanded', 'false');
365
+ }
366
+ };
367
+ document.addEventListener('mousedown', this._outsideClick);
368
+ }
369
+
370
+ private _outsideClick: ((e: MouseEvent) => void) | null = null;
371
+
372
+ private _select(date: Date | null) {
373
+ this._selected = date;
374
+ if (date) {
375
+ this._viewYear = date.getFullYear();
376
+ this._viewMonth = date.getMonth();
377
+ }
378
+ this._open = false;
379
+
380
+ const wrap = this.$<HTMLElement>('.input-wrap');
381
+ const calendar = this.$<HTMLElement>('.calendar');
382
+ const inputText = this.$<HTMLElement>('.input-text');
383
+ const hidden = this.$<HTMLInputElement>('input[type="hidden"]');
384
+
385
+ if (calendar) calendar.style.display = 'none';
386
+ if (wrap) wrap.setAttribute('aria-expanded', 'false');
387
+
388
+ const isoValue = date ? toISO(date) : '';
389
+ const displayValue = formatDisplay(date);
390
+ const placeholder = this.getAttribute('placeholder') || 'Select date';
391
+
392
+ if (inputText) {
393
+ inputText.textContent = displayValue || placeholder;
394
+ inputText.style.color = displayValue ? 'var(--nc-text)' : 'var(--nc-text-muted)';
395
+ }
396
+ if (hidden) hidden.value = isoValue;
397
+
398
+ this.setAttribute('value', isoValue);
399
+
400
+ this.dispatchEvent(new CustomEvent('change', {
401
+ bubbles: true, composed: true,
402
+ detail: { value: isoValue, date, name: this.getAttribute('name') || '' }
403
+ }));
404
+ }
405
+
406
+ private _refreshCalendar() {
407
+ const calendar = this.$<HTMLElement>('.calendar');
408
+ if (!calendar) return;
409
+ const firstDay = Number(this.getAttribute('first-day') ?? 0);
410
+ const dayLabels = firstDay === 1 ? DAYS_MON : DAYS_SUN;
411
+ calendar.innerHTML = this._renderCalendar(dayLabels, firstDay);
412
+ // Re-bind the navigation click handler is already on .calendar itself, no re-bind needed
413
+ }
414
+
415
+ onUnmount() {
416
+ if (this._outsideClick) document.removeEventListener('mousedown', this._outsideClick);
417
+ }
418
+
419
+ attributeChangedCallback(name: string, oldValue: string, newValue: string) {
420
+ if (oldValue === newValue) return;
421
+ if (name === 'value' && this._mounted) {
422
+ this._selected = parseDate(newValue);
423
+ if (this._selected) {
424
+ this._viewYear = this._selected.getFullYear();
425
+ this._viewMonth = this._selected.getMonth();
426
+ }
427
+ const inputText = this.$<HTMLElement>('.input-text');
428
+ const hidden = this.$<HTMLInputElement>('input[type="hidden"]');
429
+ const placeholder = this.getAttribute('placeholder') || 'Select date';
430
+ const displayValue = formatDisplay(this._selected);
431
+ if (inputText) {
432
+ inputText.textContent = displayValue || placeholder;
433
+ inputText.style.color = displayValue ? 'var(--nc-text)' : 'var(--nc-text-muted)';
434
+ }
435
+ if (hidden) hidden.value = newValue;
436
+ this._refreshCalendar();
437
+ return;
438
+ }
439
+ if (this._mounted) { this.render(); this._bindEvents(); }
440
+ }
441
+ }
442
+
443
+ defineComponent('nc-date-picker', NcDatePicker);