create-nativecore 0.1.0 → 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 +10 -18
  2. package/bin/index.mjs +407 -489
  3. package/package.json +4 -3
  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,452 @@
1
+ /**
2
+ * NcSelect Component
3
+ *
4
+ * NativeCore Framework Core Component
5
+ *
6
+ * Options are provided as a JSON array via the `options` attribute or by
7
+ * populating child `<option>` elements before the component mounts.
8
+ *
9
+ * Attributes:
10
+ * - options: JSON string — array of { value, label, disabled? }
11
+ * - value: string — currently selected value
12
+ * - placeholder: string — shown when no value selected (default: 'Select...')
13
+ * - name: string — form field name
14
+ * - disabled: boolean — disabled state
15
+ * - size: 'sm' | 'md' | 'lg' (default: 'md')
16
+ * - variant: 'default' | 'filled' (default: 'default')
17
+ * - searchable: boolean — adds a live filter input inside the dropdown
18
+ *
19
+ * Events:
20
+ * - change: CustomEvent<{ value: string; label: string; name: string }>
21
+ *
22
+ * Usage:
23
+ * <nc-select
24
+ * name="country"
25
+ * placeholder="Pick a country"
26
+ * options='[{"value":"us","label":"United States"},{"value":"ca","label":"Canada"}]'>
27
+ * </nc-select>
28
+ */
29
+
30
+ import { Component, defineComponent } from '@core/component.js';
31
+
32
+ interface SelectOption {
33
+ value: string;
34
+ label: string;
35
+ disabled?: boolean;
36
+ }
37
+
38
+ export class NcSelect extends Component {
39
+ static useShadowDOM = true;
40
+
41
+ static attributeOptions = {
42
+ variant: ['default', 'filled'],
43
+ size: ['sm', 'md', 'lg']
44
+ };
45
+
46
+ static get observedAttributes() {
47
+ return ['options', 'value', 'placeholder', 'name', 'disabled', 'size', 'variant', 'searchable'];
48
+ }
49
+
50
+ private _open = false;
51
+ private _filterText = '';
52
+
53
+ constructor() {
54
+ super();
55
+ }
56
+
57
+ private _getOptions(): SelectOption[] {
58
+ try {
59
+ const raw = this.getAttribute('options');
60
+ if (raw) return JSON.parse(raw) as SelectOption[];
61
+ } catch {
62
+ // fall through
63
+ }
64
+ return [];
65
+ }
66
+
67
+ private _getSelectedLabel(): string {
68
+ const value = this.getAttribute('value') || '';
69
+ if (!value) return '';
70
+ const opt = this._getOptions().find(o => o.value === value);
71
+ return opt?.label ?? value;
72
+ }
73
+
74
+ template() {
75
+ const value = this.getAttribute('value') || '';
76
+ const placeholder = this.getAttribute('placeholder') || 'Select...';
77
+ const disabled = this.hasAttribute('disabled');
78
+ const searchable = this.hasAttribute('searchable');
79
+ const selectedLabel = this._getSelectedLabel() || placeholder;
80
+ const hasValue = !!value;
81
+
82
+ const options = this._getOptions();
83
+ const filtered = this._filterText
84
+ ? options.filter(o => o.label.toLowerCase().includes(this._filterText.toLowerCase()))
85
+ : options;
86
+
87
+ const optionItems = filtered.map(o => `
88
+ <div class="option${o.value === value ? ' option--selected' : ''}${o.disabled ? ' option--disabled' : ''}"
89
+ data-value="${o.value}"
90
+ role="option"
91
+ aria-selected="${o.value === value}"
92
+ aria-disabled="${o.disabled ? 'true' : 'false'}">
93
+ ${o.label}
94
+ ${o.value === value ? `
95
+ <svg class="option__check" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" fill="none" width="12" height="12">
96
+ <path d="M2 6l3 3 5-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
97
+ </svg>` : ''}
98
+ </div>
99
+ `).join('');
100
+
101
+ return `
102
+ <style>
103
+ :host {
104
+ display: inline-block;
105
+ position: relative;
106
+ font-family: var(--nc-font-family);
107
+ width: 100%;
108
+ }
109
+
110
+ .select-trigger {
111
+ display: flex;
112
+ align-items: center;
113
+ justify-content: space-between;
114
+ width: 100%;
115
+ box-sizing: border-box;
116
+ padding: var(--nc-spacing-sm) var(--nc-spacing-md);
117
+ background: var(--nc-bg);
118
+ border: var(--nc-input-border);
119
+ border-radius: var(--nc-input-radius);
120
+ cursor: ${disabled ? 'not-allowed' : 'pointer'};
121
+ color: ${hasValue ? 'var(--nc-text)' : 'var(--nc-text-muted)'};
122
+ font-size: var(--nc-font-size-base);
123
+ transition: border-color var(--nc-transition-fast), box-shadow var(--nc-transition-fast);
124
+ opacity: ${disabled ? '0.5' : '1'};
125
+ user-select: none;
126
+ gap: var(--nc-spacing-sm);
127
+ min-height: 40px;
128
+ }
129
+
130
+ /* Size variants */
131
+ :host([size="sm"]) .select-trigger {
132
+ padding: var(--nc-spacing-xs) var(--nc-spacing-sm);
133
+ font-size: var(--nc-font-size-sm);
134
+ min-height: 32px;
135
+ }
136
+
137
+ :host([size="lg"]) .select-trigger {
138
+ padding: var(--nc-spacing-md) var(--nc-spacing-lg);
139
+ font-size: var(--nc-font-size-lg);
140
+ min-height: 48px;
141
+ }
142
+
143
+ /* Filled variant */
144
+ :host([variant="filled"]) .select-trigger {
145
+ background: var(--nc-bg-tertiary);
146
+ border-color: transparent;
147
+ }
148
+
149
+ :host([variant="filled"]) .select-trigger:hover:not([disabled]) {
150
+ background: var(--nc-bg-secondary);
151
+ }
152
+
153
+ .select-trigger:hover {
154
+ border-color: var(--nc-input-focus-border);
155
+ }
156
+
157
+ .select-trigger.open {
158
+ border-color: var(--nc-input-focus-border);
159
+ box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.15);
160
+ }
161
+
162
+ .trigger-label {
163
+ flex: 1;
164
+ overflow: hidden;
165
+ white-space: nowrap;
166
+ text-overflow: ellipsis;
167
+ }
168
+
169
+ .chevron {
170
+ flex-shrink: 0;
171
+ transition: transform var(--nc-transition-fast);
172
+ color: var(--nc-text-muted);
173
+ }
174
+
175
+ .chevron.open {
176
+ transform: rotate(180deg);
177
+ }
178
+
179
+ /* Dropdown */
180
+ .dropdown {
181
+ display: none;
182
+ position: absolute;
183
+ top: calc(100% + 4px);
184
+ left: 0;
185
+ right: 0;
186
+ background: var(--nc-bg);
187
+ border: var(--nc-input-border);
188
+ border-radius: var(--nc-radius-md);
189
+ box-shadow: var(--nc-shadow-lg);
190
+ z-index: var(--nc-z-dropdown);
191
+ overflow: hidden;
192
+ max-height: 240px;
193
+ flex-direction: column;
194
+ }
195
+
196
+ .dropdown.open {
197
+ display: flex;
198
+ }
199
+
200
+ .search-wrap {
201
+ padding: var(--nc-spacing-xs) var(--nc-spacing-sm);
202
+ border-bottom: 1px solid var(--nc-border);
203
+ }
204
+
205
+ .search-input {
206
+ width: 100%;
207
+ box-sizing: border-box;
208
+ border: var(--nc-input-border);
209
+ border-radius: var(--nc-radius-sm);
210
+ padding: var(--nc-spacing-xs) var(--nc-spacing-sm);
211
+ font-size: var(--nc-font-size-sm);
212
+ font-family: var(--nc-font-family);
213
+ color: var(--nc-text);
214
+ background: var(--nc-bg-secondary);
215
+ outline: none;
216
+ }
217
+
218
+ .search-input:focus {
219
+ border-color: var(--nc-input-focus-border);
220
+ }
221
+
222
+ .options-list {
223
+ overflow-y: auto;
224
+ flex: 1;
225
+ }
226
+
227
+ .option {
228
+ display: flex;
229
+ align-items: center;
230
+ justify-content: space-between;
231
+ padding: var(--nc-spacing-sm) var(--nc-spacing-md);
232
+ cursor: pointer;
233
+ font-size: var(--nc-font-size-base);
234
+ color: var(--nc-text);
235
+ transition: background var(--nc-transition-fast);
236
+ gap: var(--nc-spacing-sm);
237
+ }
238
+
239
+ .option:hover:not(.option--disabled) {
240
+ background: var(--nc-bg-secondary);
241
+ }
242
+
243
+ .option--selected {
244
+ color: var(--nc-primary);
245
+ font-weight: var(--nc-font-weight-medium);
246
+ }
247
+
248
+ .option--disabled {
249
+ opacity: 0.4;
250
+ cursor: not-allowed;
251
+ }
252
+
253
+ .option__check {
254
+ flex-shrink: 0;
255
+ }
256
+
257
+ .empty {
258
+ padding: var(--nc-spacing-md);
259
+ text-align: center;
260
+ color: var(--nc-text-muted);
261
+ font-size: var(--nc-font-size-sm);
262
+ }
263
+ </style>
264
+
265
+ <input type="hidden"
266
+ name="${this.getAttribute('name') || ''}"
267
+ value="${value}"
268
+ />
269
+
270
+ <div class="select-trigger${this._open ? ' open' : ''}" role="combobox" aria-expanded="${this._open}" aria-haspopup="listbox">
271
+ <span class="trigger-label">${selectedLabel}</span>
272
+ <svg class="chevron${this._open ? ' open' : ''}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" width="16" height="16">
273
+ <path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
274
+ </svg>
275
+ </div>
276
+
277
+ <div class="dropdown${this._open ? ' open' : ''}" role="listbox">
278
+ ${searchable ? `
279
+ <div class="search-wrap">
280
+ <input class="search-input" type="text" placeholder="Search..." value="${this._filterText}" autocomplete="off" />
281
+ </div>` : ''}
282
+ <div class="options-list">
283
+ ${optionItems || `<div class="empty">No options</div>`}
284
+ </div>
285
+ </div>
286
+ `;
287
+ }
288
+
289
+ onMount() {
290
+ if (!this.hasAttribute('tabindex')) {
291
+ this.setAttribute('tabindex', '0');
292
+ }
293
+
294
+ const sr = this.shadowRoot!;
295
+
296
+ // Single click listener — never re-added
297
+ sr.addEventListener('click', (e) => {
298
+ if (this.hasAttribute('disabled')) return;
299
+ const target = e.target as HTMLElement;
300
+
301
+ const option = target.closest('.option') as HTMLElement | null;
302
+ if (option) {
303
+ if (option.classList.contains('option--disabled')) return;
304
+ this._select(option.dataset.value ?? '');
305
+ return;
306
+ }
307
+
308
+ if (target.closest('.select-trigger')) {
309
+ this._setOpen(!this._open);
310
+ }
311
+ });
312
+
313
+ // Search filter
314
+ sr.addEventListener('input', (e) => {
315
+ const input = e.target as HTMLInputElement;
316
+ if (input.classList.contains('search-input')) {
317
+ this._filterText = input.value;
318
+ this._rerenderDropdown();
319
+ }
320
+ });
321
+
322
+ // Outside click — registered once, cleaned up in onUnmount
323
+ document.addEventListener('click', this._onOutsideClick);
324
+
325
+ // Keyboard
326
+ this.addEventListener('keydown', (e: KeyboardEvent) => {
327
+ if (e.key === 'Escape') { this._setOpen(false); return; }
328
+ if (e.key === 'Enter' || e.key === ' ') {
329
+ e.preventDefault();
330
+ this._setOpen(!this._open);
331
+ }
332
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
333
+ e.preventDefault();
334
+ this._navigateOptions(e.key === 'ArrowDown' ? 1 : -1);
335
+ }
336
+ });
337
+ }
338
+
339
+ private _onOutsideClick = (e: MouseEvent) => {
340
+ if (!this.contains(e.target as Node) && !this.shadowRoot!.contains(e.target as Node)) {
341
+ this._setOpen(false);
342
+ }
343
+ };
344
+
345
+ private _setOpen(open: boolean) {
346
+ this._open = open;
347
+ if (!open) this._filterText = '';
348
+
349
+ const sr = this.shadowRoot!;
350
+ const trigger = sr.querySelector('.select-trigger');
351
+ const chevron = sr.querySelector('.chevron');
352
+ const dropdown = sr.querySelector('.dropdown');
353
+
354
+ if (trigger) {
355
+ trigger.classList.toggle('open', open);
356
+ trigger.setAttribute('aria-expanded', String(open));
357
+ }
358
+ if (chevron) chevron.classList.toggle('open', open);
359
+ if (dropdown) {
360
+ dropdown.classList.toggle('open', open);
361
+ if (!open) {
362
+ // Clear search when closing
363
+ const searchInput = dropdown.querySelector<HTMLInputElement>('.search-input');
364
+ if (searchInput) searchInput.value = '';
365
+ this._rerenderDropdown();
366
+ }
367
+ }
368
+
369
+ if (open) {
370
+ const search = sr.querySelector<HTMLInputElement>('.search-input');
371
+ if (search) search.focus();
372
+ }
373
+ }
374
+
375
+ private _rerenderDropdown() {
376
+ const sr = this.shadowRoot!;
377
+ const list = sr.querySelector('.options-list');
378
+ if (!list) return;
379
+
380
+ const value = this.getAttribute('value') || '';
381
+ const options = this._getOptions();
382
+ const filtered = this._filterText
383
+ ? options.filter(o => o.label.toLowerCase().includes(this._filterText.toLowerCase()))
384
+ : options;
385
+
386
+ list.innerHTML = filtered.length ? filtered.map(o => `
387
+ <div class="option${o.value === value ? ' option--selected' : ''}${o.disabled ? ' option--disabled' : ''}"
388
+ data-value="${o.value}"
389
+ role="option"
390
+ aria-selected="${o.value === value}"
391
+ aria-disabled="${o.disabled ? 'true' : 'false'}">
392
+ ${o.label}
393
+ ${o.value === value ? `
394
+ <svg class="option__check" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" fill="none" width="12" height="12">
395
+ <path d="M2 6l3 3 5-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
396
+ </svg>` : ''}
397
+ </div>
398
+ `).join('') : '<div class="empty">No results</div>';
399
+ }
400
+
401
+ private _select(value: string) {
402
+ const opts = this._getOptions();
403
+ const opt = opts.find(o => o.value === value);
404
+ if (!opt) return;
405
+
406
+ this._open = false;
407
+ this._filterText = '';
408
+
409
+ // Update trigger label and hidden input directly
410
+ const sr = this.shadowRoot!;
411
+ const label = sr.querySelector('.trigger-label');
412
+ if (label) label.textContent = opt.label;
413
+ const hidden = sr.querySelector<HTMLInputElement>('input[type="hidden"]');
414
+ if (hidden) hidden.value = value;
415
+
416
+ // Close the dropdown
417
+ this._setOpen(false);
418
+
419
+ // Re-render option list to show the new checkmark
420
+ this.setAttribute('value', value);
421
+
422
+ this.dispatchEvent(new CustomEvent('change', {
423
+ bubbles: true,
424
+ composed: true,
425
+ detail: {
426
+ value,
427
+ label: opt.label,
428
+ name: this.getAttribute('name') || ''
429
+ }
430
+ }));
431
+ }
432
+
433
+ private _navigateOptions(direction: number) {
434
+ const opts = this._getOptions().filter(o => !o.disabled);
435
+ const current = this.getAttribute('value') || '';
436
+ const idx = opts.findIndex(o => o.value === current);
437
+ const next = opts[Math.max(0, Math.min(opts.length - 1, idx + direction))];
438
+ if (next) this._select(next.value);
439
+ }
440
+
441
+ onUnmount() {
442
+ document.removeEventListener('click', this._onOutsideClick);
443
+ }
444
+
445
+ attributeChangedCallback(name: string, oldValue: string, newValue: string) {
446
+ if (oldValue !== newValue && this._mounted) {
447
+ this.render();
448
+ }
449
+ }
450
+ }
451
+
452
+ defineComponent('nc-select', NcSelect);
@@ -0,0 +1,107 @@
1
+ /**
2
+ * NcSkeleton Component
3
+ *
4
+ * Attributes:
5
+ * - variant: 'text'|'rect'|'circle'|'card' (default: 'text')
6
+ * - width: string — CSS width (default: '100%')
7
+ * - height: string — CSS height
8
+ * - lines: number — for variant="text", how many text lines to show (default: 1)
9
+ * - animate: 'pulse'|'wave'|'none' (default: 'wave')
10
+ *
11
+ * Usage:
12
+ * <nc-skeleton></nc-skeleton>
13
+ * <nc-skeleton variant="circle" width="40px" height="40px"></nc-skeleton>
14
+ * <nc-skeleton variant="text" lines="3"></nc-skeleton>
15
+ * <nc-skeleton variant="card"></nc-skeleton>
16
+ */
17
+
18
+ import { Component, defineComponent } from '@core/component.js';
19
+
20
+ export class NcSkeleton extends Component {
21
+ static useShadowDOM = true;
22
+
23
+ static get observedAttributes() {
24
+ return ['variant', 'width', 'height', 'lines', 'animate'];
25
+ }
26
+
27
+ template() {
28
+ const variant = this.getAttribute('variant') || 'text';
29
+ const width = this.getAttribute('width') || '100%';
30
+ const height = this.getAttribute('height') || '';
31
+ const lines = Math.max(1, Number(this.getAttribute('lines') || 1));
32
+ const animate = this.getAttribute('animate') ?? 'wave';
33
+
34
+ const baseHeight = height || (variant === 'text' ? '0.875em' : variant === 'rect' ? '120px' : variant === 'circle' ? width : '160px');
35
+
36
+ let content = '';
37
+
38
+ if (variant === 'text') {
39
+ content = Array.from({ length: lines }, (_, i) => {
40
+ // Last line is shorter to look natural
41
+ const w = i === lines - 1 && lines > 1 ? '75%' : '100%';
42
+ return `<span class="bone bone--text" style="width:${w}"></span>`;
43
+ }).join('');
44
+ } else if (variant === 'circle') {
45
+ content = `<span class="bone bone--circle" style="width:${width};height:${width}"></span>`;
46
+ } else if (variant === 'card') {
47
+ content = `
48
+ <span class="bone bone--rect" style="height:120px;margin-bottom:12px"></span>
49
+ <span class="bone bone--text" style="width:60%;margin-bottom:8px"></span>
50
+ <span class="bone bone--text" style="width:90%;margin-bottom:8px"></span>
51
+ <span class="bone bone--text" style="width:75%"></span>`;
52
+ } else {
53
+ content = `<span class="bone bone--rect" style="height:${baseHeight}"></span>`;
54
+ }
55
+
56
+ return `
57
+ <style>
58
+ :host { display: block; width: ${variant === 'circle' ? 'auto' : width}; }
59
+
60
+ .skeleton {
61
+ display: flex;
62
+ flex-direction: column;
63
+ gap: 6px;
64
+ }
65
+
66
+ .bone {
67
+ display: block;
68
+ background: var(--nc-bg-tertiary);
69
+ border-radius: 4px;
70
+ position: relative;
71
+ overflow: hidden;
72
+ }
73
+
74
+ .bone--text { height: ${baseHeight}; border-radius: 3px; }
75
+ .bone--circle { border-radius: 50%; flex-shrink: 0; }
76
+ .bone--rect { border-radius: var(--nc-radius-md, 8px); width: 100%; }
77
+
78
+ ${animate === 'wave' ? `
79
+ @keyframes nc-skeleton-wave {
80
+ 0% { transform: translateX(-100%); }
81
+ 100% { transform: translateX(100%); }
82
+ }
83
+ .bone::after {
84
+ content: '';
85
+ position: absolute;
86
+ inset: 0;
87
+ background: linear-gradient(90deg, transparent 0%, rgba(255,255,255,.08) 50%, transparent 100%);
88
+ animation: nc-skeleton-wave 1.6s ease-in-out infinite;
89
+ }` : ''}
90
+
91
+ ${animate === 'pulse' ? `
92
+ @keyframes nc-skeleton-pulse {
93
+ 0%, 100% { opacity: 1; }
94
+ 50% { opacity: 0.4; }
95
+ }
96
+ .bone { animation: nc-skeleton-pulse 1.8s ease-in-out infinite; }` : ''}
97
+ </style>
98
+ <div class="skeleton" aria-hidden="true">${content}</div>
99
+ `;
100
+ }
101
+
102
+ attributeChangedCallback(name: string, oldValue: string, newValue: string) {
103
+ if (oldValue !== newValue && this._mounted) this.render();
104
+ }
105
+ }
106
+
107
+ defineComponent('nc-skeleton', NcSkeleton);