create-nativecore 0.1.1 → 0.2.1

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 +403 -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 +653 -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,383 @@
1
+ /**
2
+ * NcInput Component
3
+ *
4
+ * Attributes:
5
+ * - name: string
6
+ * - value: string
7
+ * - type: 'text'|'email'|'password'|'search'|'url'|'tel'|'number' (default: 'text')
8
+ * - placeholder: string
9
+ * - disabled: boolean
10
+ * - readonly: boolean
11
+ * - required: boolean
12
+ * - maxlength: number
13
+ * - minlength: number
14
+ * - pattern: string — validation regex pattern
15
+ * - autocomplete: string
16
+ * - size: 'sm'|'md'|'lg' (default: 'md')
17
+ * - variant: 'default'|'filled' (default: 'default')
18
+ * - icon-left: string — SVG/HTML string for leading icon
19
+ * - icon-right: string — SVG/HTML string for trailing icon
20
+ * - clearable: boolean — show clear (×) button when value is non-empty
21
+ * - show-password-toggle: boolean — toggles password visibility (type="password" only)
22
+ * - error: string — error message; also sets error styling
23
+ * - hint: string — hint text below the input
24
+ *
25
+ * Events:
26
+ * - input: CustomEvent<{ value: string; name: string }>
27
+ * - change: CustomEvent<{ value: string; name: string }>
28
+ * - clear: CustomEvent<{ name: string }> (when cleared via button)
29
+ *
30
+ * Methods:
31
+ * - checkValidity(): boolean
32
+ * - validate(): boolean
33
+ * - reportValidity(): boolean
34
+ * - getValidationMessage(): string
35
+ * - clearValidationError(): void
36
+ *
37
+ * Usage:
38
+ * <nc-input name="email" type="email" placeholder="you@example.com"></nc-input>
39
+ * <nc-input name="pwd" type="password" show-password-toggle></nc-input>
40
+ * <nc-input name="q" type="search" clearable placeholder="Search..."></nc-input>
41
+ */
42
+
43
+ import { Component, defineComponent } from '@core/component.js';
44
+
45
+ const EYE_OPEN = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" width="16" height="16"><path d="M1 10s3-6 9-6 9 6 9 6-3 6-9 6-9-6-9-6z"/><circle cx="10" cy="10" r="2.5"/></svg>`;
46
+ const EYE_CLOSED = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" width="16" height="16"><path d="M2 2l16 16M7.5 7.5A3 3 0 0012.5 12.5M4.2 4.2C2.6 5.5 1 8 1 10s3 6 9 6c2 0 3.8-.5 5.3-1.3M8 4.3A8.7 8.7 0 0110 4c6 0 9 6 9 6s-.9 1.8-2.5 3.2"/></svg>`;
47
+ const CLEAR_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" width="12" height="12"><path d="M3 3l10 10M13 3L3 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`;
48
+ const SEARCH_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" width="14" height="14"><circle cx="6.5" cy="6.5" r="4"/><path d="M11 11l3 3" stroke-linecap="round"/></svg>`;
49
+
50
+ export class NcInput extends Component {
51
+ static useShadowDOM = true;
52
+
53
+ static get observedAttributes() {
54
+ return [
55
+ 'name', 'value', 'type', 'placeholder', 'disabled', 'readonly', 'required',
56
+ 'maxlength', 'minlength', 'pattern', 'autocomplete',
57
+ 'size', 'variant', 'icon-left', 'icon-right',
58
+ 'clearable', 'show-password-toggle', 'error', 'hint',
59
+ ];
60
+ }
61
+
62
+ private _value = '';
63
+ private _showPassword = false;
64
+ private _validationError = '';
65
+ private readonly _handleInputEvent = (event: Event) => {
66
+ const input = event.target as HTMLInputElement | null;
67
+ if (!input || input.tagName !== 'INPUT') return;
68
+
69
+ this._value = input.value;
70
+ if (this._validationError) {
71
+ this._validationError = '';
72
+ this.render();
73
+ } else {
74
+ this._syncClearBtn();
75
+ }
76
+
77
+ this.dispatchEvent(new CustomEvent('input', {
78
+ bubbles: true, composed: true,
79
+ detail: { value: input.value, name: this.getAttribute('name') || '' }
80
+ }));
81
+ };
82
+
83
+ private readonly _handleChangeEvent = (event: Event) => {
84
+ const input = event.target as HTMLInputElement | null;
85
+ if (!input || input.tagName !== 'INPUT') return;
86
+
87
+ this._value = input.value;
88
+ this._validationError = '';
89
+ this.render();
90
+
91
+ this.dispatchEvent(new CustomEvent('change', {
92
+ bubbles: true, composed: true,
93
+ detail: { value: input.value, name: this.getAttribute('name') || '' }
94
+ }));
95
+ };
96
+
97
+ private readonly _handleClickEvent = (event: Event) => {
98
+ const btn = (event.target as HTMLElement).closest<HTMLElement>('[data-action]');
99
+ if (!btn) return;
100
+
101
+ if (btn.dataset.action === 'toggle-password') {
102
+ this._showPassword = !this._showPassword;
103
+ this.render();
104
+ this.$<HTMLInputElement>('input')?.focus();
105
+ }
106
+
107
+ if (btn.dataset.action === 'clear') {
108
+ const input = this.$<HTMLInputElement>('input');
109
+ this._value = '';
110
+ this._validationError = '';
111
+ if (input) input.value = '';
112
+ this.render();
113
+ this.$<HTMLInputElement>('input')?.focus();
114
+ this.dispatchEvent(new CustomEvent('clear', {
115
+ bubbles: true, composed: true,
116
+ detail: { name: this.getAttribute('name') || '' }
117
+ }));
118
+ this.dispatchEvent(new CustomEvent('input', {
119
+ bubbles: true, composed: true,
120
+ detail: { value: '', name: this.getAttribute('name') || '' }
121
+ }));
122
+ }
123
+ };
124
+
125
+ constructor() { super(); }
126
+
127
+ get value(): string {
128
+ return this.$<HTMLInputElement>('input')?.value ?? this._value;
129
+ }
130
+
131
+ set value(nextValue: string) {
132
+ this._value = nextValue ?? '';
133
+ this._validationError = '';
134
+ if (this._mounted) {
135
+ this.render();
136
+ }
137
+ }
138
+
139
+ template() {
140
+ if (!this._mounted) {
141
+ this._value = this.getAttribute('value') || '';
142
+ }
143
+
144
+ const type = this.getAttribute('type') || 'text';
145
+ const name = this.getAttribute('name') || '';
146
+ const placeholder = this.getAttribute('placeholder') || '';
147
+ const disabled = this.hasAttribute('disabled');
148
+ const readonly = this.hasAttribute('readonly');
149
+ const required = this.hasAttribute('required');
150
+ const maxlength = this.getAttribute('maxlength');
151
+ const minlength = this.getAttribute('minlength');
152
+ const pattern = this.getAttribute('pattern');
153
+ const autocomplete = this.getAttribute('autocomplete') || 'off';
154
+ const iconLeft = this.getAttribute('icon-left') || (type === 'search' ? SEARCH_ICON : '');
155
+ const iconRight = this.getAttribute('icon-right') || '';
156
+ const clearable = this.hasAttribute('clearable');
157
+ const showToggle = this.hasAttribute('show-password-toggle') && type === 'password';
158
+ const error = this.getAttribute('error') || this._validationError;
159
+ const hint = this.getAttribute('hint') || '';
160
+
161
+ const hasLeft = !!iconLeft;
162
+ const hasRight = !!(iconRight || (clearable && this._value) || showToggle);
163
+ const inputType = type === 'password' && this._showPassword ? 'text' : type;
164
+
165
+ return `
166
+ <style>
167
+ :host { display: block; width: 100%; font-family: var(--nc-font-family); }
168
+
169
+ .wrap { position: relative; display: flex; align-items: center; }
170
+
171
+ input {
172
+ width: 100%;
173
+ box-sizing: border-box;
174
+ padding: var(--nc-spacing-sm) var(--nc-spacing-md);
175
+ padding-left: ${hasLeft ? '2.4rem' : 'var(--nc-spacing-md)'};
176
+ padding-right: ${hasRight ? '2.4rem' : 'var(--nc-spacing-md)'};
177
+ background: var(--nc-bg);
178
+ border: var(--nc-input-border);
179
+ border-radius: var(--nc-input-radius);
180
+ color: var(--nc-text);
181
+ font-size: var(--nc-font-size-base);
182
+ font-family: var(--nc-font-family);
183
+ outline: none;
184
+ transition: border-color var(--nc-transition-fast), box-shadow var(--nc-transition-fast);
185
+ opacity: ${disabled ? '0.5' : '1'};
186
+ cursor: ${disabled ? 'not-allowed' : 'text'};
187
+ }
188
+
189
+ :host([size="sm"]) input { font-size: var(--nc-font-size-sm); padding-top: var(--nc-spacing-xs); padding-bottom: var(--nc-spacing-xs); }
190
+ :host([size="lg"]) input { font-size: var(--nc-font-size-lg); padding-top: var(--nc-spacing-md); padding-bottom: var(--nc-spacing-md); }
191
+
192
+ :host([variant="filled"]) input { background: var(--nc-bg-tertiary); border-color: transparent; }
193
+ :host([variant="filled"]) input:focus { background: var(--nc-bg); }
194
+
195
+ input:focus { border-color: var(--nc-input-focus-border); box-shadow: 0 0 0 3px rgba(16,185,129,.15); }
196
+ :host([error]) input, input.has-error { border-color: var(--nc-danger, #ef4444) !important; box-shadow: 0 0 0 3px rgba(239,68,68,.12) !important; }
197
+ input::placeholder { color: var(--nc-text-muted); }
198
+ input[type="search"]::-webkit-search-cancel-button { display: none; }
199
+
200
+ .icon {
201
+ position: absolute;
202
+ display: flex;
203
+ align-items: center;
204
+ justify-content: center;
205
+ color: var(--nc-text-muted);
206
+ pointer-events: none;
207
+ width: 2.2rem;
208
+ }
209
+ .icon--left { left: 0; }
210
+ .icon--right { right: 0; pointer-events: auto; }
211
+
212
+ .action-btn {
213
+ background: none;
214
+ border: none;
215
+ cursor: pointer;
216
+ padding: 4px;
217
+ color: var(--nc-text-muted);
218
+ display: flex;
219
+ align-items: center;
220
+ transition: color var(--nc-transition-fast);
221
+ border-radius: var(--nc-radius-sm, 4px);
222
+ }
223
+ .action-btn:hover { color: var(--nc-text); }
224
+
225
+ .subtext {
226
+ font-size: var(--nc-font-size-xs);
227
+ margin-top: 4px;
228
+ display: block;
229
+ }
230
+ .subtext--hint { color: var(--nc-text-muted); }
231
+ .subtext--error { color: var(--nc-danger, #ef4444); }
232
+ </style>
233
+
234
+ <div class="wrap">
235
+ ${hasLeft ? `<span class="icon icon--left">${iconLeft}</span>` : ''}
236
+
237
+ <input
238
+ type="${inputType}"
239
+ name="${name}"
240
+ value="${this._value}"
241
+ placeholder="${placeholder}"
242
+ autocomplete="${autocomplete}"
243
+ ${disabled ? 'disabled' : ''}
244
+ ${readonly ? 'readonly' : ''}
245
+ ${required ? 'required' : ''}
246
+ ${maxlength ? `maxlength="${maxlength}"` : ''}
247
+ ${minlength ? `minlength="${minlength}"` : ''}
248
+ ${pattern ? `pattern="${pattern}"` : ''}
249
+ ${error ? 'class="has-error"' : ''}
250
+ aria-invalid="${!!error}"
251
+ aria-describedby="${error ? 'subtext' : hint ? 'subtext' : ''}"
252
+ />
253
+
254
+ ${hasRight ? `
255
+ <span class="icon icon--right">
256
+ ${showToggle ? `
257
+ <button class="action-btn" type="button" data-action="toggle-password" aria-label="${this._showPassword ? 'Hide password' : 'Show password'}">
258
+ ${this._showPassword ? EYE_CLOSED : EYE_OPEN}
259
+ </button>` : ''}
260
+ ${clearable && this._value && !showToggle ? `
261
+ <button class="action-btn" type="button" data-action="clear" aria-label="Clear">
262
+ ${CLEAR_ICON}
263
+ </button>` : ''}
264
+ ${iconRight && !clearable && !showToggle ? iconRight : ''}
265
+ </span>` : ''}
266
+ </div>
267
+
268
+ ${error ? `<span class="subtext subtext--error" id="subtext" role="alert">${error}</span>` : ''}
269
+ ${hint && !error ? `<span class="subtext subtext--hint" id="subtext">${hint}</span>` : ''}
270
+ `;
271
+ }
272
+
273
+ onMount() {
274
+ this.shadowRoot?.addEventListener('input', this._handleInputEvent);
275
+ this.shadowRoot?.addEventListener('change', this._handleChangeEvent);
276
+ this.shadowRoot?.addEventListener('click', this._handleClickEvent);
277
+ }
278
+
279
+ onUnmount() {
280
+ this.shadowRoot?.removeEventListener('input', this._handleInputEvent);
281
+ this.shadowRoot?.removeEventListener('change', this._handleChangeEvent);
282
+ this.shadowRoot?.removeEventListener('click', this._handleClickEvent);
283
+ }
284
+
285
+ private _syncClearBtn() {
286
+ const clearBtn = this.$<HTMLElement>('[data-action="clear"]');
287
+ if (!clearBtn) return;
288
+ clearBtn.style.display = this._value ? 'flex' : 'none';
289
+ }
290
+
291
+ private _getInput(): HTMLInputElement | null {
292
+ return this.$<HTMLInputElement>('input');
293
+ }
294
+
295
+ private _buildValidationMessage(input: HTMLInputElement): string {
296
+ const { validity } = input;
297
+
298
+ if (validity.valueMissing) {
299
+ return 'This field is required.';
300
+ }
301
+
302
+ if (validity.typeMismatch) {
303
+ if (input.type === 'email') return 'Enter a valid email address.';
304
+ if (input.type === 'url') return 'Enter a valid URL.';
305
+ return 'Enter a valid value.';
306
+ }
307
+
308
+ if (validity.patternMismatch) {
309
+ return 'Enter a value in the expected format.';
310
+ }
311
+
312
+ if (validity.tooShort) {
313
+ const minLength = input.getAttribute('minlength');
314
+ return minLength
315
+ ? `Enter at least ${minLength} characters.`
316
+ : 'The value is too short.';
317
+ }
318
+
319
+ if (validity.tooLong) {
320
+ const maxLength = input.getAttribute('maxlength');
321
+ return maxLength
322
+ ? `Enter no more than ${maxLength} characters.`
323
+ : 'The value is too long.';
324
+ }
325
+
326
+ if (validity.badInput) {
327
+ return 'Enter a valid value.';
328
+ }
329
+
330
+ return '';
331
+ }
332
+
333
+ getValidationMessage(): string {
334
+ const explicitError = this.getAttribute('error');
335
+ if (explicitError) return explicitError;
336
+
337
+ const input = this._getInput();
338
+ if (!input) return this._validationError;
339
+ return this._buildValidationMessage(input);
340
+ }
341
+
342
+ checkValidity(): boolean {
343
+ const input = this._getInput();
344
+ if (!input) return true;
345
+ return input.checkValidity();
346
+ }
347
+
348
+ validate(): boolean {
349
+ const isValid = this.checkValidity();
350
+ this._validationError = isValid ? '' : this.getValidationMessage();
351
+ if (this._mounted) {
352
+ this.render();
353
+ }
354
+ return isValid;
355
+ }
356
+
357
+ reportValidity(): boolean {
358
+ return this.validate();
359
+ }
360
+
361
+ clearValidationError(): void {
362
+ if (!this._validationError) return;
363
+ this._validationError = '';
364
+ if (this._mounted) {
365
+ this.render();
366
+ }
367
+ }
368
+
369
+ attributeChangedCallback(name: string, oldValue: string, newValue: string) {
370
+ if (oldValue === newValue) return;
371
+ if (name === 'value' && this._mounted) {
372
+ this._value = newValue || '';
373
+ this._validationError = '';
374
+ const input = this.$<HTMLInputElement>('input');
375
+ if (input) input.value = this._value;
376
+ this._syncClearBtn();
377
+ return;
378
+ }
379
+ if (this._mounted) { this.render(); }
380
+ }
381
+ }
382
+
383
+ defineComponent('nc-input', NcInput);
@@ -0,0 +1,48 @@
1
+ /**
2
+ * NcKbd Component — keyboard key display
3
+ *
4
+ * Usage:
5
+ * <nc-kbd>Ctrl</nc-kbd>
6
+ * <nc-kbd>⌘</nc-kbd> + <nc-kbd>K</nc-kbd>
7
+ *
8
+ * Attributes:
9
+ * size — 'sm'|'md'(default)|'lg'
10
+ */
11
+ import { Component, defineComponent } from '@core/component.js';
12
+
13
+ export class NcKbd extends Component {
14
+ static useShadowDOM = true;
15
+
16
+ template() {
17
+ const size = this.getAttribute('size') || 'md';
18
+ const pad = size === 'sm' ? '1px 5px' : size === 'lg' ? '4px 12px' : '2px 8px';
19
+ const fs = size === 'sm' ? '11px' : size === 'lg' ? '15px' : '12px';
20
+
21
+ return `
22
+ <style>
23
+ :host { display: inline-block; }
24
+ kbd {
25
+ display: inline-flex;
26
+ align-items: center;
27
+ justify-content: center;
28
+ font-family: var(--nc-font-family-mono, 'SFMono-Regular', Consolas, monospace);
29
+ font-size: ${fs};
30
+ font-weight: var(--nc-font-weight-medium);
31
+ line-height: 1;
32
+ color: var(--nc-text);
33
+ background: var(--nc-bg-secondary);
34
+ border: 1px solid var(--nc-border);
35
+ border-bottom-width: 3px;
36
+ border-radius: var(--nc-radius-sm);
37
+ padding: ${pad};
38
+ white-space: nowrap;
39
+ user-select: none;
40
+ box-shadow: inset 0 -1px 0 rgba(0,0,0,.08);
41
+ }
42
+ </style>
43
+ <kbd><slot></slot></kbd>
44
+ `;
45
+ }
46
+ }
47
+
48
+ defineComponent('nc-kbd', NcKbd);
@@ -0,0 +1,193 @@
1
+ /**
2
+ * NativeCore Menu Item Component (nc-menu-item)
3
+ *
4
+ * A single selectable row inside an nc-menu. Renders an optional icon,
5
+ * a label via the default slot, and an optional end slot for badges/shortcuts.
6
+ *
7
+ * Attributes:
8
+ * icon — URL to a leading icon image (optional)
9
+ * alt — Alt text for the icon (accessibility)
10
+ * disabled — Boolean. Prevents selection and dims the row.
11
+ * active — Boolean. Marks the row as currently selected. Set by nc-menu
12
+ * or manually for controlled menus.
13
+ * danger — Boolean. Applies destructive/danger coloring.
14
+ *
15
+ * Slots:
16
+ * default — Item label / content.
17
+ * end — Trailing content (badge, keyboard shortcut, chevron, etc.)
18
+ *
19
+ * Events emitted:
20
+ * nc-select — {} — fires when the item is clicked or activated via keyboard.
21
+ * Bubbles + composed so nc-menu can delegate.
22
+ *
23
+ * Usage:
24
+ * <nc-menu-item icon="/icons/edit.svg" alt="Edit">Edit</nc-menu-item>
25
+ * <nc-menu-item danger>Delete</nc-menu-item>
26
+ * <nc-menu-item disabled>Unavailable</nc-menu-item>
27
+ * <nc-menu-item active>Selected</nc-menu-item>
28
+ * <nc-menu-item>
29
+ * Save
30
+ * <span slot="end">Ctrl+S</span>
31
+ * </nc-menu-item>
32
+ */
33
+
34
+ import { Component, defineComponent } from '@core/component.js';
35
+ import { html } from '@utils/templates.js';
36
+
37
+ export class NcMenuItem extends Component {
38
+ static useShadowDOM = true;
39
+
40
+ static get observedAttributes() {
41
+ return ['icon', 'alt', 'disabled', 'active', 'danger'];
42
+ }
43
+
44
+ private _onClick: ((e: Event) => void) | null = null;
45
+ private _onKeydown: ((e: Event) => void) | null = null;
46
+
47
+ template(): string {
48
+ const icon = this.getAttribute('icon');
49
+ const alt = this.getAttribute('alt') || '';
50
+ const disabled = this.hasAttribute('disabled');
51
+
52
+ const iconHTML = icon
53
+ ? `<img class="item__icon" src="${icon}" alt="${alt}" />`
54
+ : '';
55
+
56
+ return html`
57
+ <style>
58
+ :host {
59
+ display: block;
60
+ }
61
+
62
+ .item {
63
+ display: flex;
64
+ align-items: center;
65
+ gap: var(--nc-spacing-sm);
66
+ padding: var(--nc-spacing-sm) var(--nc-spacing-md);
67
+ border-radius: var(--nc-radius-md);
68
+ cursor: pointer;
69
+ font-family: var(--nc-font-family);
70
+ font-size: var(--nc-font-size-sm);
71
+ font-weight: var(--nc-font-weight-medium);
72
+ color: var(--nc-text);
73
+ background: transparent;
74
+ border: none;
75
+ width: 100%;
76
+ text-align: left;
77
+ box-sizing: border-box;
78
+ transition:
79
+ background var(--nc-transition-fast),
80
+ color var(--nc-transition-fast);
81
+ user-select: none;
82
+ outline: none;
83
+ }
84
+
85
+ .item:hover {
86
+ background: var(--nc-bg-tertiary);
87
+ color: var(--nc-text);
88
+ }
89
+
90
+ .item:focus-visible {
91
+ outline: 2px solid var(--nc-primary);
92
+ outline-offset: -2px;
93
+ }
94
+
95
+ /* Active / selected */
96
+ :host([active]) .item {
97
+ background: rgba(16, 185, 129, 0.1);
98
+ color: var(--nc-primary);
99
+ font-weight: var(--nc-font-weight-semibold);
100
+ }
101
+
102
+ :host([active]) .item:hover {
103
+ background: rgba(16, 185, 129, 0.15);
104
+ }
105
+
106
+ /* Danger */
107
+ :host([danger]) .item {
108
+ color: var(--nc-danger);
109
+ }
110
+
111
+ :host([danger]) .item:hover {
112
+ background: rgba(239, 68, 68, 0.08);
113
+ color: var(--nc-danger);
114
+ }
115
+
116
+ /* Disabled */
117
+ :host([disabled]) .item {
118
+ opacity: 0.4;
119
+ cursor: not-allowed;
120
+ pointer-events: none;
121
+ }
122
+
123
+ .item__icon {
124
+ width: 16px;
125
+ height: 16px;
126
+ object-fit: contain;
127
+ flex-shrink: 0;
128
+ opacity: 0.75;
129
+ }
130
+
131
+ :host([active]) .item__icon,
132
+ :host([danger]) .item__icon {
133
+ opacity: 1;
134
+ }
135
+
136
+ .item__label {
137
+ flex: 1;
138
+ min-width: 0;
139
+ overflow: hidden;
140
+ text-overflow: ellipsis;
141
+ white-space: nowrap;
142
+ }
143
+
144
+ .item__end {
145
+ flex-shrink: 0;
146
+ font-size: var(--nc-font-size-xs, 0.7rem);
147
+ color: var(--nc-text-muted, var(--nc-text-secondary));
148
+ opacity: 0.65;
149
+ }
150
+ </style>
151
+ <div class="item" role="menuitem" tabindex="${disabled ? -1 : 0}" aria-disabled="${disabled}">
152
+ ${iconHTML}
153
+ <span class="item__label"><slot></slot></span>
154
+ <span class="item__end"><slot name="end"></slot></span>
155
+ </div>
156
+ `;
157
+ }
158
+
159
+ onMount(): void {
160
+ this._onClick = (e: Event) => {
161
+ if (this.hasAttribute('disabled')) { e.stopPropagation(); return; }
162
+ this.emitEvent('nc-select', {}, { bubbles: true, composed: true });
163
+ };
164
+
165
+ this._onKeydown = (e: Event) => {
166
+ const ke = e as KeyboardEvent;
167
+ if (ke.key === 'Enter' || ke.key === ' ') {
168
+ e.preventDefault();
169
+ this._onClick!(e);
170
+ }
171
+ };
172
+
173
+ this.shadowRoot!.addEventListener('click', this._onClick);
174
+ this.shadowRoot!.addEventListener('keydown', this._onKeydown);
175
+ }
176
+
177
+ onUnmount(): void {
178
+ if (this._onClick) this.shadowRoot?.removeEventListener('click', this._onClick);
179
+ if (this._onKeydown) this.shadowRoot?.removeEventListener('keydown', this._onKeydown);
180
+ this._onClick = null;
181
+ this._onKeydown = null;
182
+ }
183
+
184
+ attributeChangedCallback(
185
+ _name: string,
186
+ oldValue: string | null,
187
+ newValue: string | null
188
+ ): void {
189
+ if (this._mounted && oldValue !== newValue) this.render();
190
+ }
191
+ }
192
+
193
+ defineComponent('nc-menu-item', NcMenuItem);