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.
- package/README.md +6 -14
- package/bin/index.mjs +402 -431
- package/package.json +3 -2
- package/template/.env.example +28 -0
- package/template/.htmlhintrc +14 -0
- package/template/api/data/dashboard.json +11 -0
- package/template/api/data/users.json +18 -0
- package/template/api/mockApi.js +161 -0
- package/template/assets/icon.svg +13 -0
- package/template/assets/logo.svg +25 -0
- package/template/eslint.config.js +94 -0
- package/template/index.html +137 -0
- package/template/manifest.json +19 -0
- package/template/public/.well-known/security.txt +9 -0
- package/template/public/_headers +24 -0
- package/template/public/_redirects +14 -0
- package/template/public/assets/icon.svg +13 -0
- package/template/public/assets/logo.svg +25 -0
- package/template/public/manifest.json +19 -0
- package/template/public/robots.txt +13 -0
- package/template/public/sitemap.xml +27 -0
- package/template/scripts/build-for-bots.mjs +121 -0
- package/template/scripts/convert-to-ts.mjs +106 -0
- package/template/scripts/fix-encoding.mjs +38 -0
- package/template/scripts/fix-svg-paths.mjs +32 -0
- package/template/scripts/generate-cf-router.mjs +52 -0
- package/template/scripts/inject-dev-tools.mjs +41 -0
- package/template/scripts/inject-version.mjs +65 -0
- package/template/scripts/make-component.mjs +445 -0
- package/template/scripts/make-component.mjs.backup +432 -0
- package/template/scripts/make-controller.mjs +119 -0
- package/template/scripts/make-core-component.mjs +303 -0
- package/template/scripts/make-view.mjs +346 -0
- package/template/scripts/minify.mjs +71 -0
- package/template/scripts/prepare-static-assets.mjs +141 -0
- package/template/scripts/prompt-bot-build.mjs +223 -0
- package/template/scripts/remove-component.mjs +170 -0
- package/template/scripts/remove-core-component.mjs +156 -0
- package/template/scripts/remove-dev.mjs +13 -0
- package/template/scripts/remove-view.mjs +200 -0
- package/template/scripts/strip-dev-blocks.mjs +30 -0
- package/template/scripts/watch-compile.mjs +69 -0
- package/template/server.js +1066 -0
- package/template/src/app.ts +115 -0
- package/template/src/components/appRegistry.ts +8 -0
- package/template/src/components/core/app-footer.ts +27 -0
- package/template/src/components/core/app-header.ts +175 -0
- package/template/src/components/core/app-sidebar.ts +238 -0
- package/template/src/components/core/loading-spinner.ts +25 -0
- package/template/src/components/core/nc-a.ts +313 -0
- package/template/src/components/core/nc-accordion.ts +186 -0
- package/template/src/components/core/nc-alert.ts +153 -0
- package/template/src/components/core/nc-animation.ts +1150 -0
- package/template/src/components/core/nc-autocomplete.ts +271 -0
- package/template/src/components/core/nc-avatar-group.ts +113 -0
- package/template/src/components/core/nc-avatar.ts +148 -0
- package/template/src/components/core/nc-badge.ts +86 -0
- package/template/src/components/core/nc-bottom-nav.ts +214 -0
- package/template/src/components/core/nc-breadcrumb.ts +96 -0
- package/template/src/components/core/nc-button.ts +307 -0
- package/template/src/components/core/nc-card.ts +160 -0
- package/template/src/components/core/nc-checkbox.ts +282 -0
- package/template/src/components/core/nc-chip.ts +115 -0
- package/template/src/components/core/nc-code.ts +314 -0
- package/template/src/components/core/nc-collapsible.ts +154 -0
- package/template/src/components/core/nc-color-picker.ts +268 -0
- package/template/src/components/core/nc-copy-button.ts +119 -0
- package/template/src/components/core/nc-date-picker.ts +443 -0
- package/template/src/components/core/nc-div.ts +280 -0
- package/template/src/components/core/nc-divider.ts +81 -0
- package/template/src/components/core/nc-drawer.ts +230 -0
- package/template/src/components/core/nc-dropdown.ts +178 -0
- package/template/src/components/core/nc-empty-state.ts +134 -0
- package/template/src/components/core/nc-file-upload.ts +354 -0
- package/template/src/components/core/nc-form.ts +312 -0
- package/template/src/components/core/nc-image.ts +184 -0
- package/template/src/components/core/nc-input.ts +383 -0
- package/template/src/components/core/nc-kbd.ts +48 -0
- package/template/src/components/core/nc-menu-item.ts +193 -0
- package/template/src/components/core/nc-menu.ts +376 -0
- package/template/src/components/core/nc-modal.ts +238 -0
- package/template/src/components/core/nc-nav-item.ts +151 -0
- package/template/src/components/core/nc-number-input.ts +350 -0
- package/template/src/components/core/nc-otp-input.ts +235 -0
- package/template/src/components/core/nc-pagination.ts +178 -0
- package/template/src/components/core/nc-popover.ts +260 -0
- package/template/src/components/core/nc-progress-circular.ts +119 -0
- package/template/src/components/core/nc-progress.ts +134 -0
- package/template/src/components/core/nc-radio.ts +235 -0
- package/template/src/components/core/nc-rating.ts +266 -0
- package/template/src/components/core/nc-rich-text.ts +283 -0
- package/template/src/components/core/nc-scroll-top.ts +116 -0
- package/template/src/components/core/nc-select.ts +452 -0
- package/template/src/components/core/nc-skeleton.ts +107 -0
- package/template/src/components/core/nc-slider.ts +285 -0
- package/template/src/components/core/nc-snackbar.ts +230 -0
- package/template/src/components/core/nc-splash.ts +343 -0
- package/template/src/components/core/nc-stepper.ts +247 -0
- package/template/src/components/core/nc-switch.ts +281 -0
- package/template/src/components/core/nc-tab-item.ts +138 -0
- package/template/src/components/core/nc-table.ts +279 -0
- package/template/src/components/core/nc-tabs.ts +554 -0
- package/template/src/components/core/nc-tag-input.ts +279 -0
- package/template/src/components/core/nc-textarea.ts +216 -0
- package/template/src/components/core/nc-time-picker.ts +438 -0
- package/template/src/components/core/nc-timeline.ts +186 -0
- package/template/src/components/core/nc-tooltip.ts +143 -0
- package/template/src/components/frameworkRegistry.ts +68 -0
- package/template/src/components/preloadRegistry.ts +28 -0
- package/template/src/components/registry.ts +8 -0
- package/template/src/components/ui/dashboard-signal-lab.ts +284 -0
- package/template/src/constants/apiEndpoints.ts +27 -0
- package/template/src/constants/errorMessages.ts +23 -0
- package/template/src/constants/index.ts +8 -0
- package/template/src/constants/routePaths.ts +15 -0
- package/template/src/constants/storageKeys.ts +18 -0
- package/template/src/controllers/dashboard.controller.ts +200 -0
- package/template/src/controllers/home.controller.ts +21 -0
- package/template/src/controllers/index.ts +11 -0
- package/template/src/controllers/login.controller.ts +131 -0
- package/template/src/core/component.ts +354 -0
- package/template/src/core/errorHandler.ts +85 -0
- package/template/src/core/gpu-animation.ts +604 -0
- package/template/src/core/http.ts +173 -0
- package/template/src/core/lazyComponents.ts +90 -0
- package/template/src/core/router.ts +642 -0
- package/template/src/core/signals.ts +146 -0
- package/template/src/core/state.ts +248 -0
- package/template/src/dev/component-editor.ts +1363 -0
- package/template/src/dev/component-overlay.ts +278 -0
- package/template/src/dev/context-menu.ts +223 -0
- package/template/src/dev/denc-tools.ts +250 -0
- package/template/src/dev/hmr.ts +189 -0
- package/template/src/dev/nfbs.code-workspace +27 -0
- package/template/src/dev/outline-panel.ts +1247 -0
- package/template/src/middleware/auth.middleware.ts +23 -0
- package/template/src/routes/routes.ts +38 -0
- package/template/src/services/api.service.ts +394 -0
- package/template/src/services/auth.service.ts +176 -0
- package/template/src/services/index.ts +8 -0
- package/template/src/services/logger.service.ts +74 -0
- package/template/src/services/storage.service.ts +88 -0
- package/template/src/stores/appStore.ts +57 -0
- package/template/src/stores/uiStore.ts +36 -0
- package/template/src/styles/core-variables.css +219 -0
- package/template/src/styles/core.css +710 -0
- package/template/src/styles/main.css +3164 -0
- package/template/src/styles/variables.css +152 -0
- package/template/src/types/global.d.ts +47 -0
- package/template/src/utils/cacheBuster.ts +20 -0
- package/template/src/utils/dom.ts +149 -0
- package/template/src/utils/events.ts +203 -0
- package/template/src/utils/form.ts +176 -0
- package/template/src/utils/formatters.ts +169 -0
- package/template/src/utils/helpers.ts +195 -0
- package/template/src/utils/markdown.ts +307 -0
- package/template/src/utils/sidebar.ts +96 -0
- package/template/src/utils/smoothScroll.ts +85 -0
- package/template/src/utils/templates.ts +23 -0
- package/template/src/utils/validation.ts +73 -0
- package/template/src/views/protected/dashboard.html +293 -0
- package/template/src/views/public/home.html +150 -0
- package/template/src/views/public/login.html +102 -0
- package/template/tests/unit/component.test.ts +87 -0
- package/template/tests/unit/computed.test.ts +79 -0
- package/template/tests/unit/form.test.ts +68 -0
- package/template/tests/unit/formatters.test.ts +49 -0
- package/template/tests/unit/lazy-components.test.ts +59 -0
- package/template/tests/unit/markdown.test.ts +62 -0
- package/template/tests/unit/router.test.ts +112 -0
- package/template/tests/unit/signals.test.ts +54 -0
- package/template/tests/unit/validation.test.ts +50 -0
- package/template/tsconfig.build.json +21 -0
- package/template/tsconfig.json +51 -0
- 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);
|