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,438 @@
1
+ /**
2
+ * NcTimePicker Component
3
+ *
4
+ * Attributes:
5
+ * - name: string
6
+ * - value: string — HH:MM or HH:MM:SS (24-hour)
7
+ * - format: '12'|'24' (default: '24')
8
+ * - show-seconds: boolean — include seconds column (default: false)
9
+ * - min: string — minimum time HH:MM
10
+ * - max: string — maximum time HH:MM
11
+ * - step: number — minute step interval (default: 1)
12
+ * - placeholder: string (default: '--:--')
13
+ * - disabled: boolean
14
+ * - readonly: boolean
15
+ * - size: 'sm'|'md'|'lg' (default: 'md')
16
+ *
17
+ * Events:
18
+ * - change: CustomEvent<{ value: string; name: string }>
19
+ *
20
+ * Usage:
21
+ * <nc-time-picker name="start" value="09:30"></nc-time-picker>
22
+ * <nc-time-picker name="alarm" format="12" show-seconds></nc-time-picker>
23
+ */
24
+
25
+ import { Component, defineComponent } from '@core/component.js';
26
+
27
+ function pad2(n: number) { return String(n).padStart(2, '0'); }
28
+
29
+ function parseTime(v: string | null): { h: number; m: number; s: number } | null {
30
+ if (!v) return null;
31
+ const parts = v.split(':').map(Number);
32
+ if (parts.length < 2 || parts.some(isNaN)) return null;
33
+ return { h: parts[0] ?? 0, m: parts[1] ?? 0, s: parts[2] ?? 0 };
34
+ }
35
+
36
+ function formatValue(h: number, m: number, s: number, showSec: boolean): string {
37
+ return showSec ? `${pad2(h)}:${pad2(m)}:${pad2(s)}` : `${pad2(h)}:${pad2(m)}`;
38
+ }
39
+
40
+ function to12(h: number): { display: number; ampm: 'AM' | 'PM' } {
41
+ return { display: h === 0 ? 12 : h > 12 ? h - 12 : h, ampm: h < 12 ? 'AM' : 'PM' };
42
+ }
43
+
44
+ export class NcTimePicker extends Component {
45
+ static useShadowDOM = true;
46
+
47
+ static get observedAttributes() {
48
+ return ['name', 'value', 'format', 'show-seconds', 'min', 'max', 'step', 'placeholder', 'disabled', 'readonly', 'size'];
49
+ }
50
+
51
+ private _open = false;
52
+ private _h = 0;
53
+ private _m = 0;
54
+ private _s = 0;
55
+ private _ampm: 'AM' | 'PM' = 'AM';
56
+ private _initialized = false;
57
+ private _outsideClick: ((e: MouseEvent) => void) | null = null;
58
+
59
+ constructor() { super(); }
60
+
61
+ private _initFromAttr() {
62
+ const t = parseTime(this.getAttribute('value'));
63
+ if (t) {
64
+ this._h = t.h;
65
+ this._m = t.m;
66
+ this._s = t.s;
67
+ const { ampm } = to12(t.h);
68
+ this._ampm = ampm;
69
+ }
70
+ this._initialized = true;
71
+ }
72
+
73
+ private _is12() { return this.getAttribute('format') === '12'; }
74
+ private _showSec() { return this.hasAttribute('show-seconds'); }
75
+
76
+ private _displayValue(): string {
77
+ if (!this._initialized && !this.getAttribute('value')) return '';
78
+ if (this._is12()) {
79
+ const { display } = to12(this._h);
80
+ return `${pad2(display)}:${pad2(this._m)}${this._showSec() ? `:${pad2(this._s)}` : ''} ${this._ampm}`;
81
+ }
82
+ return formatValue(this._h, this._m, this._s, this._showSec());
83
+ }
84
+
85
+ template() {
86
+ if (!this._initialized) this._initFromAttr();
87
+
88
+ const disabled = this.hasAttribute('disabled');
89
+ const readonly = this.hasAttribute('readonly');
90
+ const placeholder = this.getAttribute('placeholder') || '--:--';
91
+ const step = Number(this.getAttribute('step') || 1);
92
+ const is12 = this._is12();
93
+ const showSec = this._showSec();
94
+ const displayVal = this._displayValue();
95
+
96
+ // Build minute options honoring step
97
+ const minuteOptions = Array.from({ length: Math.ceil(60 / step) }, (_, i) => i * step).filter(m => m < 60);
98
+ const hourRange = is12 ? 12 : 24;
99
+ const hourOptions = Array.from({ length: hourRange }, (_, i) => is12 ? i + 1 : i);
100
+
101
+ return `
102
+ <style>
103
+ :host { display: block; position: relative; font-family: var(--nc-font-family); }
104
+
105
+ .input-wrap {
106
+ display: flex;
107
+ align-items: center;
108
+ border: var(--nc-input-border);
109
+ border-radius: var(--nc-input-radius);
110
+ background: var(--nc-bg);
111
+ cursor: ${disabled || readonly ? 'not-allowed' : 'pointer'};
112
+ opacity: ${disabled ? '0.5' : '1'};
113
+ transition: border-color var(--nc-transition-fast), box-shadow var(--nc-transition-fast);
114
+ user-select: none;
115
+ }
116
+ .input-wrap:focus-within { border-color: var(--nc-input-focus-border); box-shadow: 0 0 0 3px rgba(16,185,129,.15); }
117
+
118
+ .display {
119
+ flex: 1;
120
+ padding: var(--nc-spacing-sm) var(--nc-spacing-md);
121
+ font-size: var(--nc-font-size-base);
122
+ color: ${displayVal ? 'var(--nc-text)' : 'var(--nc-text-muted)'};
123
+ font-variant-numeric: tabular-nums;
124
+ }
125
+
126
+ :host([size="sm"]) .display { font-size: var(--nc-font-size-sm); padding: var(--nc-spacing-xs) var(--nc-spacing-sm); }
127
+ :host([size="lg"]) .display { font-size: var(--nc-font-size-lg); padding: var(--nc-spacing-md); }
128
+
129
+ .input-icon {
130
+ padding: 0 var(--nc-spacing-sm);
131
+ color: var(--nc-text-muted);
132
+ display: flex;
133
+ flex-shrink: 0;
134
+ }
135
+
136
+ .panel {
137
+ position: absolute;
138
+ top: calc(100% + 6px);
139
+ left: 0;
140
+ z-index: 500;
141
+ background: var(--nc-bg);
142
+ border: 1px solid var(--nc-border);
143
+ border-radius: var(--nc-radius-md, 8px);
144
+ box-shadow: var(--nc-shadow-lg);
145
+ display: ${this._open ? 'flex' : 'none'};
146
+ flex-direction: column;
147
+ overflow: hidden;
148
+ min-width: 240px;
149
+ }
150
+
151
+ .columns {
152
+ display: flex;
153
+ gap: 0;
154
+ max-height: 220px;
155
+ }
156
+
157
+ .col {
158
+ flex: 1;
159
+ overflow-y: auto;
160
+ scroll-snap-type: y mandatory;
161
+ border-right: 1px solid var(--nc-border);
162
+ scrollbar-width: thin;
163
+ }
164
+ .col:last-child { border-right: none; }
165
+
166
+ .col-header {
167
+ position: sticky;
168
+ top: 0;
169
+ background: var(--nc-bg-secondary);
170
+ font-size: var(--nc-font-size-xs);
171
+ font-weight: var(--nc-font-weight-semibold);
172
+ color: var(--nc-text-muted);
173
+ text-transform: uppercase;
174
+ letter-spacing: 0.05em;
175
+ padding: 5px var(--nc-spacing-sm);
176
+ text-align: center;
177
+ z-index: 1;
178
+ }
179
+
180
+ .col-item {
181
+ padding: 7px var(--nc-spacing-sm);
182
+ text-align: center;
183
+ font-size: var(--nc-font-size-sm);
184
+ font-variant-numeric: tabular-nums;
185
+ cursor: pointer;
186
+ color: var(--nc-text);
187
+ scroll-snap-align: start;
188
+ transition: background var(--nc-transition-fast);
189
+ }
190
+ .col-item:hover { background: var(--nc-bg-secondary); }
191
+ .col-item.selected { background: var(--nc-primary); color: #fff; font-weight: var(--nc-font-weight-semibold); }
192
+ .col-item:disabled, .col-item[disabled] { opacity: 0.3; pointer-events: none; }
193
+
194
+ .panel-footer {
195
+ display: flex;
196
+ justify-content: space-between;
197
+ gap: var(--nc-spacing-xs);
198
+ padding: var(--nc-spacing-sm);
199
+ border-top: 1px solid var(--nc-border);
200
+ }
201
+
202
+ .panel-btn {
203
+ flex: 1;
204
+ padding: 5px;
205
+ border-radius: var(--nc-radius-sm, 4px);
206
+ font-size: var(--nc-font-size-xs);
207
+ font-family: var(--nc-font-family);
208
+ cursor: pointer;
209
+ border: 1px solid var(--nc-border);
210
+ background: var(--nc-bg-secondary);
211
+ color: var(--nc-text);
212
+ transition: background var(--nc-transition-fast);
213
+ }
214
+ .panel-btn:hover { background: var(--nc-bg-tertiary); }
215
+ .panel-btn--primary { background: var(--nc-primary); color: #fff; border-color: var(--nc-primary); }
216
+ .panel-btn--primary:hover { opacity: 0.9; }
217
+ </style>
218
+
219
+ <div class="input-wrap" tabindex="${disabled ? '-1' : '0'}" role="button" aria-haspopup="dialog" aria-expanded="${this._open}">
220
+ <span class="display">${displayVal || placeholder}</span>
221
+ <span class="input-icon">
222
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" width="14" height="14">
223
+ <circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="1.2"/>
224
+ <path d="M8 5v3.5l2 1.5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
225
+ </svg>
226
+ </span>
227
+ </div>
228
+
229
+ <div class="panel" role="dialog">
230
+ <div class="columns">
231
+ <!-- Hours -->
232
+ <div class="col" data-col="hour">
233
+ <div class="col-header">HH</div>
234
+ ${hourOptions.map(h => {
235
+ const isSelected = is12 ? to12(this._h).display === h : this._h === h;
236
+ return `<div class="col-item${isSelected ? ' selected' : ''}" data-col="hour" data-value="${h}">${pad2(h)}</div>`;
237
+ }).join('')}
238
+ </div>
239
+ <!-- Minutes -->
240
+ <div class="col" data-col="minute">
241
+ <div class="col-header">MM</div>
242
+ ${minuteOptions.map(m => {
243
+ const isSelected = this._m === m;
244
+ return `<div class="col-item${isSelected ? ' selected' : ''}" data-col="minute" data-value="${m}">${pad2(m)}</div>`;
245
+ }).join('')}
246
+ </div>
247
+ ${showSec ? `
248
+ <!-- Seconds -->
249
+ <div class="col" data-col="second">
250
+ <div class="col-header">SS</div>
251
+ ${Array.from({ length: 60 }, (_, i) => i).map(s => {
252
+ const isSelected = this._s === s;
253
+ return `<div class="col-item${isSelected ? ' selected' : ''}" data-col="second" data-value="${s}">${pad2(s)}</div>`;
254
+ }).join('')}
255
+ </div>` : ''}
256
+ ${is12 ? `
257
+ <!-- AM/PM -->
258
+ <div class="col" data-col="ampm">
259
+ <div class="col-header">AM/PM</div>
260
+ <div class="col-item${this._ampm === 'AM' ? ' selected' : ''}" data-col="ampm" data-value="AM">AM</div>
261
+ <div class="col-item${this._ampm === 'PM' ? ' selected' : ''}" data-col="ampm" data-value="PM">PM</div>
262
+ </div>` : ''}
263
+ </div>
264
+ <div class="panel-footer">
265
+ <button class="panel-btn" data-action="clear" type="button">Clear</button>
266
+ <button class="panel-btn" data-action="now" type="button">Now</button>
267
+ <button class="panel-btn panel-btn--primary" data-action="apply" type="button">Apply</button>
268
+ </div>
269
+ </div>
270
+
271
+ <input type="hidden" name="${this.getAttribute('name') || ''}" value="${this._initialized ? formatValue(this._h, this._m, this._s, showSec) : ''}" />
272
+ `;
273
+ }
274
+
275
+ onMount() {
276
+ this._bindEvents();
277
+ this._scrollToSelected();
278
+ }
279
+
280
+ private _bindEvents() {
281
+ const wrap = this.$<HTMLElement>('.input-wrap')!;
282
+ const panel = this.$<HTMLElement>('.panel')!;
283
+
284
+ wrap.addEventListener('click', () => {
285
+ if (this.hasAttribute('disabled') || this.hasAttribute('readonly')) return;
286
+ this._open = !this._open;
287
+ panel.style.display = this._open ? 'flex' : 'none';
288
+ wrap.setAttribute('aria-expanded', String(this._open));
289
+ if (this._open) this._scrollToSelected();
290
+ });
291
+
292
+ wrap.addEventListener('keydown', (e: KeyboardEvent) => {
293
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); wrap.click(); }
294
+ if (e.key === 'Escape') { this._open = false; panel.style.display = 'none'; }
295
+ });
296
+
297
+ // Column item clicks
298
+ panel.addEventListener('click', (e) => {
299
+ const item = (e.target as HTMLElement).closest<HTMLElement>('[data-col][data-value]');
300
+ const action = (e.target as HTMLElement).closest<HTMLElement>('[data-action]');
301
+
302
+ if (item) {
303
+ const col = item.dataset.col!;
304
+ const val = item.dataset.value!;
305
+ this._handleColSelect(col, val);
306
+ // Update active highlight in column
307
+ const colEl = panel.querySelector<HTMLElement>(`[data-col="${col}"].col`);
308
+ colEl?.querySelectorAll<HTMLElement>('.col-item').forEach(el => {
309
+ el.classList.toggle('selected', el.dataset.value === val);
310
+ });
311
+ }
312
+
313
+ if (action) {
314
+ switch (action.dataset.action) {
315
+ case 'clear': this._clear(); break;
316
+ case 'now': this._setNow(); break;
317
+ case 'apply': this._apply(); break;
318
+ }
319
+ }
320
+ });
321
+
322
+ // Outside click
323
+ this._outsideClick = (e: MouseEvent) => {
324
+ if (!this.contains(e.target as Node) && !this.shadowRoot!.contains(e.target as Node)) {
325
+ this._open = false;
326
+ if (panel) panel.style.display = 'none';
327
+ if (wrap) wrap.setAttribute('aria-expanded', 'false');
328
+ }
329
+ };
330
+ document.addEventListener('mousedown', this._outsideClick);
331
+ }
332
+
333
+ private _handleColSelect(col: string, val: string) {
334
+ switch (col) {
335
+ case 'hour': {
336
+ const n = Number(val);
337
+ if (this._is12()) {
338
+ this._h = this._ampm === 'AM'
339
+ ? (n === 12 ? 0 : n)
340
+ : (n === 12 ? 12 : n + 12);
341
+ } else {
342
+ this._h = n;
343
+ }
344
+ break;
345
+ }
346
+ case 'minute': this._m = Number(val); break;
347
+ case 'second': this._s = Number(val); break;
348
+ case 'ampm': {
349
+ this._ampm = val as 'AM' | 'PM';
350
+ if (val === 'AM' && this._h >= 12) this._h -= 12;
351
+ if (val === 'PM' && this._h < 12) this._h += 12;
352
+ break;
353
+ }
354
+ }
355
+ this._updateDisplay();
356
+ }
357
+
358
+ private _updateDisplay() {
359
+ const wrap = this.$<HTMLElement>('.display');
360
+ const hidden = this.$<HTMLInputElement>('input[type="hidden"]');
361
+ const val = this._displayValue();
362
+ if (wrap) {
363
+ wrap.textContent = val || (this.getAttribute('placeholder') || '--:--');
364
+ wrap.style.color = val ? 'var(--nc-text)' : 'var(--nc-text-muted)';
365
+ }
366
+ if (hidden) hidden.value = formatValue(this._h, this._m, this._s, this._showSec());
367
+ }
368
+
369
+ private _apply() {
370
+ this._initialized = true;
371
+ const value = formatValue(this._h, this._m, this._s, this._showSec());
372
+ this.setAttribute('value', value);
373
+ this._open = false;
374
+ const panel = this.$<HTMLElement>('.panel');
375
+ const wrap = this.$<HTMLElement>('.input-wrap');
376
+ if (panel) panel.style.display = 'none';
377
+ if (wrap) wrap.setAttribute('aria-expanded', 'false');
378
+ this._updateDisplay();
379
+ this.dispatchEvent(new CustomEvent('change', {
380
+ bubbles: true, composed: true,
381
+ detail: { value, name: this.getAttribute('name') || '' }
382
+ }));
383
+ }
384
+
385
+ private _clear() {
386
+ this._h = 0; this._m = 0; this._s = 0; this._ampm = 'AM';
387
+ this._initialized = false;
388
+ this.removeAttribute('value');
389
+ this._open = false;
390
+ const panel = this.$<HTMLElement>('.panel');
391
+ if (panel) panel.style.display = 'none';
392
+ this._updateDisplay();
393
+ const wrap = this.$<HTMLElement>('.display');
394
+ if (wrap) wrap.style.color = 'var(--nc-text-muted)';
395
+ this.dispatchEvent(new CustomEvent('change', {
396
+ bubbles: true, composed: true,
397
+ detail: { value: '', name: this.getAttribute('name') || '' }
398
+ }));
399
+ }
400
+
401
+ private _setNow() {
402
+ const now = new Date();
403
+ this._h = now.getHours();
404
+ this._m = now.getMinutes();
405
+ this._s = now.getSeconds();
406
+ this._ampm = this._h < 12 ? 'AM' : 'PM';
407
+ this._initialized = true;
408
+ this.render();
409
+ this._bindEvents();
410
+ this._scrollToSelected();
411
+ }
412
+
413
+ private _scrollToSelected() {
414
+ requestAnimationFrame(() => {
415
+ this.shadowRoot!.querySelectorAll<HTMLElement>('.col').forEach(col => {
416
+ const selected = col.querySelector<HTMLElement>('.selected');
417
+ if (selected) selected.scrollIntoView({ block: 'nearest' });
418
+ });
419
+ });
420
+ }
421
+
422
+ onUnmount() {
423
+ if (this._outsideClick) document.removeEventListener('mousedown', this._outsideClick);
424
+ }
425
+
426
+ attributeChangedCallback(name: string, oldValue: string, newValue: string) {
427
+ if (oldValue === newValue) return;
428
+ if (name === 'value' && this._mounted) {
429
+ const t = parseTime(newValue);
430
+ if (t) { this._h = t.h; this._m = t.m; this._s = t.s; this._ampm = to12(t.h).ampm; this._initialized = true; }
431
+ this._updateDisplay();
432
+ return;
433
+ }
434
+ if (this._mounted) { this.render(); this._bindEvents(); }
435
+ }
436
+ }
437
+
438
+ defineComponent('nc-time-picker', NcTimePicker);
@@ -0,0 +1,186 @@
1
+ /**
2
+ * NcTimeline Component — vertical event timeline
3
+ *
4
+ * Attributes:
5
+ * align — 'left'(default)|'right'|'alternate' — which side the content appears
6
+ * size — 'sm'|'md'(default)|'lg' — dot and line scale
7
+ * dense — boolean — reduce spacing
8
+ *
9
+ * Slots: nc-timeline-item elements
10
+ *
11
+ * ---
12
+ *
13
+ * NcTimelineItem Component — single timeline event
14
+ *
15
+ * Attributes:
16
+ * color — dot color preset: 'primary'(default)|'success'|'warning'|'danger'|'neutral'
17
+ * OR any valid CSS color string
18
+ * icon — small icon inside the dot (same icon names as nc-nav-item)
19
+ * title — event heading
20
+ * time — timestamp / relative time string (shown muted)
21
+ * status — 'completed'|'active'|'pending'|'error' (sets color automatically)
22
+ * no-line — boolean — hide the connector line (usually set on last item)
23
+ *
24
+ * Slots:
25
+ * icon — custom dot content
26
+ * title — override title
27
+ * time — time content
28
+ * (default)— event body / description
29
+ *
30
+ * Usage:
31
+ * <nc-timeline>
32
+ * <nc-timeline-item title="Order placed" time="2h ago" status="completed">
33
+ * Your order #4521 was received.
34
+ * </nc-timeline-item>
35
+ * <nc-timeline-item title="Processing" status="active">
36
+ * We're preparing your items.
37
+ * </nc-timeline-item>
38
+ * <nc-timeline-item title="Delivery" status="pending" no-line>
39
+ * Estimated 2-3 business days.
40
+ * </nc-timeline-item>
41
+ * </nc-timeline>
42
+ */
43
+ import { Component, defineComponent } from '@core/component.js';
44
+
45
+ const STATUS_COLORS: Record<string, string> = {
46
+ completed: 'var(--nc-success)',
47
+ active: 'var(--nc-primary)',
48
+ pending: 'var(--nc-text-muted)',
49
+ error: 'var(--nc-danger)',
50
+ };
51
+
52
+ const SMALL_ICONS: Record<string, string> = {
53
+ check: `<polyline points="20 6 9 17 4 12"/>`,
54
+ x: `<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>`,
55
+ star: `<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>`,
56
+ info: `<circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/>`,
57
+ alert: `<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>`,
58
+ };
59
+ const iconSvg = (p: string, sz = 10) =>
60
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${sz}" height="${sz}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">${p}</svg>`;
61
+
62
+ // ── NcTimelineItem ────────────────────────────────────────────────────────────
63
+
64
+ export class NcTimelineItem extends Component {
65
+ static useShadowDOM = true;
66
+
67
+ static get observedAttributes() { return ['color', 'status', 'active', 'no-line']; }
68
+
69
+ template() {
70
+ const title = this.getAttribute('title') ?? '';
71
+ const time = this.getAttribute('time') ?? '';
72
+ const status = this.getAttribute('status') ?? '';
73
+ const iconKey = this.getAttribute('icon') ?? '';
74
+ const noLine = this.hasAttribute('no-line');
75
+ const color = this.getAttribute('color')
76
+ ?? STATUS_COLORS[status]
77
+ ?? 'var(--nc-primary)';
78
+
79
+ const dotContent = SMALL_ICONS[iconKey]
80
+ ? iconSvg(SMALL_ICONS[iconKey], 10)
81
+ : (status === 'completed' ? iconSvg(SMALL_ICONS.check, 10) : '');
82
+
83
+ const isActive = status === 'active';
84
+ const dotSz = isActive ? 14 : 12;
85
+
86
+ return `
87
+ <style>
88
+ :host { display: flex; font-family: var(--nc-font-family); }
89
+ .col-dot {
90
+ display: flex;
91
+ flex-direction: column;
92
+ align-items: center;
93
+ flex-shrink: 0;
94
+ width: 28px;
95
+ margin-right: var(--nc-spacing-md);
96
+ }
97
+ .dot {
98
+ width: ${dotSz}px;
99
+ height: ${dotSz}px;
100
+ border-radius: 50%;
101
+ background: ${color};
102
+ display: flex;
103
+ align-items: center;
104
+ justify-content: center;
105
+ flex-shrink: 0;
106
+ color: #fff;
107
+ ${isActive ? `box-shadow: 0 0 0 4px rgba(0,0,0,.1), 0 0 0 3px ${color}33;` : ''}
108
+ margin-top: 4px;
109
+ }
110
+ .line {
111
+ flex: 1;
112
+ width: 2px;
113
+ background: var(--nc-border);
114
+ margin-top: 4px;
115
+ min-height: 24px;
116
+ display: ${noLine ? 'none' : 'block'};
117
+ }
118
+ .content {
119
+ flex: 1;
120
+ padding-bottom: ${noLine ? '0' : 'var(--nc-spacing-lg)'};
121
+ min-width: 0;
122
+ }
123
+ .header {
124
+ display: flex;
125
+ align-items: baseline;
126
+ justify-content: space-between;
127
+ gap: var(--nc-spacing-sm);
128
+ margin-bottom: 4px;
129
+ }
130
+ .title {
131
+ font-size: var(--nc-font-size-sm);
132
+ font-weight: var(--nc-font-weight-semibold);
133
+ color: var(--nc-text);
134
+ margin: 0;
135
+ }
136
+ .time {
137
+ font-size: var(--nc-font-size-xs);
138
+ color: var(--nc-text-muted);
139
+ white-space: nowrap;
140
+ flex-shrink: 0;
141
+ }
142
+ .body {
143
+ font-size: var(--nc-font-size-sm);
144
+ color: var(--nc-text-secondary);
145
+ line-height: var(--nc-line-height-relaxed, 1.65);
146
+ }
147
+ </style>
148
+ <div class="col-dot">
149
+ <div class="dot">
150
+ <slot name="icon">${dotContent}</slot>
151
+ </div>
152
+ <div class="line"></div>
153
+ </div>
154
+ <div class="content">
155
+ <div class="header">
156
+ <p class="title"><slot name="title">${title}</slot></p>
157
+ ${time ? `<span class="time"><slot name="time">${time}</slot></span>` : '<slot name="time"></slot>'}
158
+ </div>
159
+ <div class="body"><slot></slot></div>
160
+ </div>
161
+ `;
162
+ }
163
+
164
+ attributeChangedCallback(n: string, o: string, v: string) {
165
+ if (o !== v && this._mounted) this.render();
166
+ }
167
+ }
168
+
169
+ defineComponent('nc-timeline-item', NcTimelineItem);
170
+
171
+ // ── NcTimeline ────────────────────────────────────────────────────────────────
172
+
173
+ export class NcTimeline extends Component {
174
+ static useShadowDOM = true;
175
+
176
+ template() {
177
+ return `
178
+ <style>
179
+ :host { display: block; padding: var(--nc-spacing-sm) 0; }
180
+ </style>
181
+ <slot></slot>
182
+ `;
183
+ }
184
+ }
185
+
186
+ defineComponent('nc-timeline', NcTimeline);