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,312 @@
1
+ /**
2
+ * NcForm + NcField Components
3
+ *
4
+ * nc-form:
5
+ * A form wrapper that collects values from all nc-* form controls within it,
6
+ * runs validation, and emits structured events.
7
+ *
8
+ * Attributes:
9
+ * - novalidate: boolean — skip HTML5 native validation
10
+ *
11
+ * Methods:
12
+ * - form.getValues(): Record<string, string> — collect all named form control values
13
+ * - form.validate(): boolean — trigger validation; returns true if all valid
14
+ * - form.reset() — reset all fields to initial value/clear errors
15
+ *
16
+ * Events:
17
+ * - submit: CustomEvent<{ values: Record<string, string> }> — on valid submit
18
+ * - invalid: CustomEvent<{ fields: string[] }> — on invalid submit (list of failing names)
19
+ *
20
+ * nc-field:
21
+ * A labelled wrapper for a single form control. Handles the label, hint, and
22
+ * error message display in a consistent layout. Pairs naturally with nc-input,
23
+ * nc-select, nc-textarea, nc-autocomplete, nc-date-picker, etc.
24
+ *
25
+ * Attributes:
26
+ * - label: string — field label
27
+ * - for: string — id of the slotted input (for focus on label click)
28
+ * - required: boolean — shows required marker
29
+ * - hint: string — sub-label hint text
30
+ * - error: string — error message (shown in red; overrides hint)
31
+ *
32
+ * Usage:
33
+ * <nc-form id="signup-form">
34
+ * <nc-field label="Email" required hint="We'll never share your email.">
35
+ * <nc-input name="email" type="email" placeholder="you@example.com"></nc-input>
36
+ * </nc-field>
37
+ * <nc-field label="Password" required>
38
+ * <nc-input name="password" type="password" show-password-toggle></nc-input>
39
+ * </nc-field>
40
+ * <nc-button type="submit">Sign up</nc-button>
41
+ * </nc-form>
42
+ *
43
+ * document.getElementById('signup-form').addEventListener('submit', e => {
44
+ * console.log(e.detail.values);
45
+ * });
46
+ */
47
+
48
+ import { Component, defineComponent } from '@core/component.js';
49
+
50
+ // ── NcField ──────────────────────────────────────────────────────────────────
51
+
52
+ export class NcField extends Component {
53
+ static useShadowDOM = true;
54
+
55
+ static get observedAttributes() {
56
+ return ['label', 'for', 'required', 'hint', 'error'];
57
+ }
58
+
59
+ template() {
60
+ const label = this.getAttribute('label') || '';
61
+ const forAttr = this.getAttribute('for') || '';
62
+ const required = this.hasAttribute('required');
63
+ const hint = this.getAttribute('hint') || '';
64
+ const error = this.getAttribute('error') || '';
65
+
66
+ return `
67
+ <style>
68
+ :host { display: block; font-family: var(--nc-font-family); }
69
+
70
+ .field { display: flex; flex-direction: column; gap: 4px; }
71
+
72
+ label {
73
+ font-size: var(--nc-font-size-sm);
74
+ font-weight: var(--nc-font-weight-medium);
75
+ color: var(--nc-text);
76
+ cursor: ${forAttr ? 'pointer' : 'default'};
77
+ }
78
+
79
+ .required {
80
+ color: var(--nc-danger, #ef4444);
81
+ margin-left: 2px;
82
+ }
83
+
84
+ .subtext {
85
+ font-size: var(--nc-font-size-xs);
86
+ line-height: 1.4;
87
+ }
88
+ .subtext--hint { color: var(--nc-text-muted); }
89
+ .subtext--error { color: var(--nc-danger, #ef4444); }
90
+ </style>
91
+ <div class="field">
92
+ ${label ? `
93
+ <label ${forAttr ? `for="${forAttr}"` : ''}>
94
+ ${label}${required ? `<span class="required" aria-hidden="true">*</span>` : ''}
95
+ </label>` : ''}
96
+ <slot></slot>
97
+ ${error
98
+ ? `<span class="subtext subtext--error" role="alert">${error}</span>`
99
+ : hint ? `<span class="subtext subtext--hint">${hint}</span>` : ''}
100
+ </div>
101
+ `;
102
+ }
103
+
104
+ setError(msg: string) {
105
+ if (msg) this.setAttribute('error', msg);
106
+ else this.removeAttribute('error');
107
+ }
108
+
109
+ clearError() { this.removeAttribute('error'); }
110
+
111
+ attributeChangedCallback(name: string, oldValue: string, newValue: string) {
112
+ if (oldValue !== newValue && this._mounted) this.render();
113
+ }
114
+ }
115
+
116
+ defineComponent('nc-field', NcField);
117
+
118
+ // ── NcForm ───────────────────────────────────────────────────────────────────
119
+
120
+ const FORM_CONTROLS = [
121
+ 'nc-input', 'nc-textarea', 'nc-select', 'nc-checkbox', 'nc-radio',
122
+ 'nc-switch', 'nc-slider', 'nc-rating', 'nc-number-input',
123
+ 'nc-autocomplete', 'nc-date-picker', 'nc-time-picker', 'nc-color-picker',
124
+ 'input', 'textarea', 'select',
125
+ ];
126
+
127
+ export class NcForm extends Component {
128
+ static useShadowDOM = true;
129
+
130
+ private readonly _submitHandler = this._onSubmit.bind(this);
131
+
132
+ private readonly _keydownHandler = (e: Event) => {
133
+ const ke = e as KeyboardEvent;
134
+ const target = ke.target as HTMLElement;
135
+ if (ke.key === 'Enter' && target.tagName !== 'TEXTAREA') {
136
+ this._handleSubmit();
137
+ }
138
+ };
139
+
140
+ private readonly _clickHandler = (e: Event) => {
141
+ const btn = (e.target as HTMLElement).closest<HTMLElement>('[type="submit"], nc-button[type="submit"]');
142
+ if (btn && this.contains(btn)) {
143
+ e.preventDefault();
144
+ this._handleSubmit();
145
+ }
146
+ };
147
+
148
+ template() {
149
+ return `
150
+ <style>
151
+ :host {
152
+ display: block;
153
+ width: 100%;
154
+ }
155
+ </style>
156
+ <slot></slot>
157
+ `;
158
+ }
159
+
160
+ connectedCallback() {
161
+ super.connectedCallback?.();
162
+ this.addEventListener('submit', this._submitHandler as EventListener);
163
+ this.addEventListener('keydown', this._keydownHandler);
164
+ this.addEventListener('click', this._clickHandler);
165
+ }
166
+
167
+ disconnectedCallback() {
168
+ this.removeEventListener('submit', this._submitHandler as EventListener);
169
+ this.removeEventListener('keydown', this._keydownHandler);
170
+ this.removeEventListener('click', this._clickHandler);
171
+ super.disconnectedCallback?.();
172
+ }
173
+
174
+ private _onSubmit(e: Event) {
175
+ const customEvent = e as CustomEvent<{ values?: Record<string, string> }>;
176
+ if (customEvent.detail?.values) {
177
+ return;
178
+ }
179
+
180
+ e.preventDefault();
181
+ this._handleSubmit();
182
+ }
183
+
184
+ private _handleSubmit() {
185
+ if (!this.hasAttribute('novalidate')) {
186
+ const valid = this.validate();
187
+ if (!valid) return;
188
+ }
189
+ const values = this.getValues();
190
+ this.dispatchEvent(new CustomEvent('submit', {
191
+ bubbles: true, composed: true,
192
+ detail: { values }
193
+ }));
194
+ }
195
+
196
+ getValues(): Record<string, string> {
197
+ const result: Record<string, string> = {};
198
+ FORM_CONTROLS.forEach(tag => {
199
+ this.querySelectorAll<HTMLElement>(tag).forEach(el => {
200
+ const name = el.getAttribute('name');
201
+ if (!name) return;
202
+ // nc-checkbox/nc-switch: use checked state
203
+ if (tag === 'nc-checkbox' || tag === 'nc-switch') {
204
+ result[name] = el.hasAttribute('checked') ? 'true' : 'false';
205
+ } else {
206
+ const controlWithValue = el as HTMLElement & { value?: string };
207
+ result[name] = controlWithValue.value ?? el.getAttribute('value') ?? (el as HTMLInputElement).value ?? '';
208
+ }
209
+ });
210
+ });
211
+ return result;
212
+ }
213
+
214
+ validate(): boolean {
215
+ let valid = true;
216
+ const invalidFields: string[] = [];
217
+
218
+ // Check nc-field wrappers first
219
+ this.querySelectorAll<NcField>('nc-field').forEach(field => {
220
+ // Find the first named control inside this field
221
+ const ctrl = FORM_CONTROLS.map(tag => field.querySelector<HTMLElement>(tag)).find(Boolean) as (HTMLElement & {
222
+ value?: string;
223
+ checkValidity?: () => boolean;
224
+ getValidationMessage?: () => string;
225
+ clearValidationError?: () => void;
226
+ validate?: () => boolean;
227
+ }) | undefined;
228
+ if (!ctrl) return;
229
+
230
+ const name = ctrl.getAttribute('name') || '';
231
+ const value = ctrl.value ?? ctrl.getAttribute('value') ?? (ctrl as HTMLInputElement).value ?? '';
232
+ const isRequired = field.hasAttribute('required') || ctrl.hasAttribute('required');
233
+
234
+ if (isRequired && !String(value).trim()) {
235
+ valid = false;
236
+ field.setError('This field is required.');
237
+ invalidFields.push(name);
238
+ ctrl.clearValidationError?.();
239
+ return;
240
+ }
241
+
242
+ if (typeof ctrl.checkValidity === 'function' && !ctrl.checkValidity()) {
243
+ valid = false;
244
+ field.setError(
245
+ typeof ctrl.getValidationMessage === 'function'
246
+ ? ctrl.getValidationMessage()
247
+ : 'Enter a valid value.'
248
+ );
249
+ invalidFields.push(name);
250
+ ctrl.clearValidationError?.();
251
+ return;
252
+ }
253
+
254
+ field.clearError();
255
+ ctrl.clearValidationError?.();
256
+ });
257
+
258
+ // Validate standalone controls not wrapped in nc-field.
259
+ FORM_CONTROLS.forEach(tag => {
260
+ this.querySelectorAll<HTMLElement>(tag).forEach(ctrl => {
261
+ if (ctrl.closest('nc-field')) return;
262
+
263
+ const element = ctrl as HTMLElement & {
264
+ validate?: () => boolean;
265
+ checkValidity?: () => boolean;
266
+ clearValidationError?: () => void;
267
+ };
268
+
269
+ if (typeof element.validate === 'function') {
270
+ const isValid = element.validate();
271
+ if (!isValid) {
272
+ valid = false;
273
+ const name = ctrl.getAttribute('name');
274
+ if (name) invalidFields.push(name);
275
+ }
276
+ return;
277
+ }
278
+
279
+ if (typeof element.checkValidity === 'function' && !element.checkValidity()) {
280
+ valid = false;
281
+ const name = ctrl.getAttribute('name');
282
+ if (name) invalidFields.push(name);
283
+ return;
284
+ }
285
+
286
+ element.clearValidationError?.();
287
+ });
288
+ });
289
+
290
+ if (!valid) {
291
+ this.dispatchEvent(new CustomEvent('invalid', {
292
+ bubbles: true, composed: true,
293
+ detail: { fields: Array.from(new Set(invalidFields)) }
294
+ }));
295
+ }
296
+ return valid;
297
+ }
298
+
299
+ reset() {
300
+ FORM_CONTROLS.forEach(tag => {
301
+ this.querySelectorAll<HTMLElement>(tag).forEach(el => {
302
+ el.setAttribute('value', '');
303
+ if (tag === 'nc-checkbox' || tag === 'nc-switch') el.removeAttribute('checked');
304
+ });
305
+ });
306
+ this.querySelectorAll<NcField>('nc-field').forEach(f => f.clearError());
307
+ }
308
+
309
+ attributeChangedCallback(_name: string, _oldValue: string, _newValue: string) {}
310
+ }
311
+
312
+ defineComponent('nc-form', NcForm);
@@ -0,0 +1,184 @@
1
+ /**
2
+ * NcImage Component — responsive image with lazy loading and skeleton placeholder
3
+ *
4
+ * Attributes:
5
+ * src — image URL
6
+ * alt — alt text (required for accessibility)
7
+ * width — intrinsic width (CSS value or px integer)
8
+ * height — intrinsic height
9
+ * fit — CSS object-fit: 'cover'(default)|'contain'|'fill'|'none'|'scale-down'
10
+ * position — CSS object-position (default: 'center')
11
+ * radius — border-radius CSS value or preset: 'none'|'sm'|'md'|'lg'|'full'
12
+ * loading — 'lazy'(default)|'eager'
13
+ * fallback — fallback image URL on error
14
+ * placeholder — 'skeleton'(default)|'blur'|'none'
15
+ * aspect — aspect ratio shorthand: '16/9'|'4/3'|'1/1'|'3/2' etc.
16
+ * caption — optional caption text below image
17
+ *
18
+ * Events:
19
+ * load — image loaded
20
+ * error — image failed
21
+ *
22
+ * Usage:
23
+ * <nc-image src="/photo.jpg" alt="Mountain view" aspect="16/9" radius="md"></nc-image>
24
+ */
25
+ import { Component, defineComponent } from '@core/component.js';
26
+
27
+ const RADIUS: Record<string, string> = {
28
+ none: '0',
29
+ sm: 'var(--nc-radius-sm)',
30
+ md: 'var(--nc-radius-md)',
31
+ lg: 'var(--nc-radius-lg)',
32
+ full: '9999px',
33
+ };
34
+
35
+ export class NcImage extends Component {
36
+ static useShadowDOM = true;
37
+
38
+ private _loaded = false;
39
+ private _errored = false;
40
+
41
+ static get observedAttributes() { return ['src', 'alt', 'width', 'height', 'fit', 'radius', 'aspect', 'fallback']; }
42
+
43
+ template() {
44
+ const src = this.getAttribute('src') ?? '';
45
+ const alt = this.getAttribute('alt') ?? '';
46
+ const width = this.getAttribute('width') ?? '';
47
+ const height = this.getAttribute('height') ?? '';
48
+ const fit = this.getAttribute('fit') ?? 'cover';
49
+ const pos = this.getAttribute('position') ?? 'center';
50
+ const radius = this.getAttribute('radius') ?? 'none';
51
+ const loading = this.getAttribute('loading') ?? 'lazy';
52
+ const placeholder = this.getAttribute('placeholder') ?? 'skeleton';
53
+ const aspect = this.getAttribute('aspect') ?? '';
54
+ const caption = this.getAttribute('caption') ?? '';
55
+
56
+ const radVal = RADIUS[radius] ?? radius;
57
+ const aspectStyle = aspect ? `aspect-ratio: ${aspect.replace('/', '/')};` : '';
58
+ const wStyle = width ? `width:${/^\d+$/.test(width) ? width + 'px' : width};` : '';
59
+ const hStyle = height ? `height:${/^\d+$/.test(height) ? height + 'px' : height};` : '';
60
+ const showSkeleton = !this._loaded && !this._errored && placeholder === 'skeleton';
61
+
62
+ return `
63
+ <style>
64
+ :host { display: inline-block; }
65
+ figure {
66
+ margin: 0;
67
+ padding: 0;
68
+ display: block;
69
+ ${wStyle} ${hStyle}
70
+ border-radius: ${radVal};
71
+ overflow: hidden;
72
+ position: relative;
73
+ ${aspectStyle}
74
+ }
75
+ .skeleton {
76
+ position: absolute;
77
+ inset: 0;
78
+ background: linear-gradient(
79
+ 90deg,
80
+ var(--nc-bg-secondary) 25%,
81
+ var(--nc-bg-tertiary, #e2e8f0) 50%,
82
+ var(--nc-bg-secondary) 75%
83
+ );
84
+ background-size: 200% 100%;
85
+ animation: nc-img-shimmer 1.4s infinite linear;
86
+ border-radius: inherit;
87
+ display: ${showSkeleton ? 'block' : 'none'};
88
+ }
89
+ @keyframes nc-img-shimmer {
90
+ 0% { background-position: -200% 0; }
91
+ 100% { background-position: 200% 0; }
92
+ }
93
+ img {
94
+ display: block;
95
+ width: 100%;
96
+ height: 100%;
97
+ object-fit: ${fit};
98
+ object-position: ${pos};
99
+ border-radius: inherit;
100
+ opacity: ${this._loaded ? 1 : 0};
101
+ transition: opacity var(--nc-transition-base);
102
+ }
103
+ .error-plate {
104
+ display: ${this._errored ? 'flex' : 'none'};
105
+ align-items: center;
106
+ justify-content: center;
107
+ position: absolute;
108
+ inset: 0;
109
+ background: var(--nc-bg-secondary);
110
+ color: var(--nc-text-muted);
111
+ font-size: var(--nc-font-size-xs);
112
+ font-family: var(--nc-font-family);
113
+ flex-direction: column;
114
+ gap: 4px;
115
+ }
116
+ figcaption {
117
+ font-family: var(--nc-font-family);
118
+ font-size: var(--nc-font-size-xs);
119
+ color: var(--nc-text-muted);
120
+ text-align: center;
121
+ padding-top: 4px;
122
+ line-height: 1.4;
123
+ }
124
+ </style>
125
+ <figure>
126
+ <div class="skeleton"></div>
127
+ <img
128
+ id="img"
129
+ src="${src}"
130
+ alt="${alt}"
131
+ loading="${loading}"
132
+ decoding="async"
133
+ ${width ? `width="${width}"` : ''}
134
+ ${height ? `height="${height}"` : ''}
135
+ />
136
+ <div class="error-plate" aria-hidden="true">
137
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
138
+ <rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/>
139
+ </svg>
140
+ <span>Image not found</span>
141
+ </div>
142
+ </figure>
143
+ ${caption ? `<figcaption>${caption}</figcaption>` : ''}
144
+ `;
145
+ }
146
+
147
+ onMount() {
148
+ const img = this.$<HTMLImageElement>('#img');
149
+ if (!img) return;
150
+
151
+ if (img.complete && img.naturalWidth > 0) {
152
+ this._loaded = true;
153
+ this.render();
154
+ return;
155
+ }
156
+
157
+ img.addEventListener('load', () => {
158
+ this._loaded = true;
159
+ this._errored = false;
160
+ this.render();
161
+ this.dispatchEvent(new CustomEvent('load', { bubbles: true, composed: true }));
162
+ }, { once: true });
163
+
164
+ img.addEventListener('error', () => {
165
+ const fallback = this.getAttribute('fallback');
166
+ if (fallback && img.src !== fallback) {
167
+ img.src = fallback;
168
+ return;
169
+ }
170
+ this._errored = true;
171
+ this._loaded = false;
172
+ this.render();
173
+ this.dispatchEvent(new CustomEvent('error', { bubbles: true, composed: true }));
174
+ }, { once: true });
175
+ }
176
+
177
+ attributeChangedCallback(n: string, o: string, v: string) {
178
+ if (o === v || !this._mounted) return;
179
+ if (n === 'src') { this._loaded = false; this._errored = false; }
180
+ this.render();
181
+ }
182
+ }
183
+
184
+ defineComponent('nc-image', NcImage);