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.
- package/README.md +10 -18
- package/bin/index.mjs +407 -489
- package/package.json +4 -3
- 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,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NcSwitch Component
|
|
3
|
+
*
|
|
4
|
+
* NativeCore Framework Core Component
|
|
5
|
+
*
|
|
6
|
+
* Attributes:
|
|
7
|
+
* - label: string — text label shown next to the switch
|
|
8
|
+
* - label-position: 'left' | 'right' (default: 'right')
|
|
9
|
+
* - name: string — form field name
|
|
10
|
+
* - value: string — value submitted with a form (default: 'on')
|
|
11
|
+
* - checked: boolean — on state
|
|
12
|
+
* - disabled: boolean — disabled state
|
|
13
|
+
* - size: 'sm' | 'md' | 'lg' (default: 'md')
|
|
14
|
+
* - variant: 'primary' | 'success' | 'danger' (default: 'primary')
|
|
15
|
+
*
|
|
16
|
+
* Events:
|
|
17
|
+
* - change: CustomEvent<{ checked: boolean; value: string; name: string }>
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* <nc-switch label="Enable notifications" name="notifs"></nc-switch>
|
|
21
|
+
* <nc-switch label="Active" checked variant="success"></nc-switch>
|
|
22
|
+
* <nc-switch label="Danger mode" variant="danger" size="lg"></nc-switch>
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { Component, defineComponent } from '@core/component.js';
|
|
26
|
+
|
|
27
|
+
export class NcSwitch extends Component {
|
|
28
|
+
static useShadowDOM = true;
|
|
29
|
+
|
|
30
|
+
static attributeOptions = {
|
|
31
|
+
variant: ['primary', 'success', 'danger'],
|
|
32
|
+
size: ['sm', 'md', 'lg'],
|
|
33
|
+
'label-position': ['left', 'right']
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
static get observedAttributes() {
|
|
37
|
+
return ['label', 'label-position', 'name', 'value', 'checked', 'disabled', 'size', 'variant'];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
constructor() {
|
|
41
|
+
super();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
template() {
|
|
45
|
+
const label = this.getAttribute('label') || '';
|
|
46
|
+
const labelPosition = this.getAttribute('label-position') || 'right';
|
|
47
|
+
const disabled = this.hasAttribute('disabled');
|
|
48
|
+
|
|
49
|
+
const labelEl = label
|
|
50
|
+
? `<span class="label">${label}</span>`
|
|
51
|
+
: `<slot></slot>`;
|
|
52
|
+
|
|
53
|
+
const track = `
|
|
54
|
+
<span class="track">
|
|
55
|
+
<span class="thumb"></span>
|
|
56
|
+
</span>
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
return `
|
|
60
|
+
<style>
|
|
61
|
+
:host {
|
|
62
|
+
display: inline-flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
gap: var(--nc-spacing-sm);
|
|
65
|
+
cursor: ${disabled ? 'not-allowed' : 'pointer'};
|
|
66
|
+
user-select: none;
|
|
67
|
+
font-family: var(--nc-font-family);
|
|
68
|
+
opacity: ${disabled ? '0.5' : '1'};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.switch-wrapper {
|
|
72
|
+
display: inline-flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
gap: var(--nc-spacing-sm);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
input[type="checkbox"] {
|
|
78
|
+
position: absolute;
|
|
79
|
+
opacity: 0;
|
|
80
|
+
width: 0;
|
|
81
|
+
height: 0;
|
|
82
|
+
pointer-events: none;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* Track */
|
|
86
|
+
.track {
|
|
87
|
+
display: inline-flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
flex-shrink: 0;
|
|
90
|
+
border-radius: var(--nc-radius-full);
|
|
91
|
+
background: var(--nc-gray-300);
|
|
92
|
+
transition: background var(--nc-transition-fast);
|
|
93
|
+
position: relative;
|
|
94
|
+
box-sizing: border-box;
|
|
95
|
+
padding: 2px;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* Size: sm */
|
|
99
|
+
:host([size="sm"]) .track {
|
|
100
|
+
width: 32px;
|
|
101
|
+
height: 18px;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
:host([size="sm"]) .thumb {
|
|
105
|
+
width: 14px;
|
|
106
|
+
height: 14px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Size: md (default) */
|
|
110
|
+
.track {
|
|
111
|
+
width: 44px;
|
|
112
|
+
height: 24px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.thumb {
|
|
116
|
+
width: 20px;
|
|
117
|
+
height: 20px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
:host([size="md"]) .track {
|
|
121
|
+
width: 44px;
|
|
122
|
+
height: 24px;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
:host([size="md"]) .thumb {
|
|
126
|
+
width: 20px;
|
|
127
|
+
height: 20px;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* Size: lg */
|
|
131
|
+
:host([size="lg"]) .track {
|
|
132
|
+
width: 56px;
|
|
133
|
+
height: 30px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
:host([size="lg"]) .thumb {
|
|
137
|
+
width: 26px;
|
|
138
|
+
height: 26px;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* Thumb */
|
|
142
|
+
.thumb {
|
|
143
|
+
border-radius: var(--nc-radius-full);
|
|
144
|
+
background: var(--nc-white);
|
|
145
|
+
box-shadow: var(--nc-shadow-sm);
|
|
146
|
+
transition: transform var(--nc-transition-fast);
|
|
147
|
+
transform: translateX(0);
|
|
148
|
+
flex-shrink: 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* Checked — move thumb right */
|
|
152
|
+
:host([checked]) .thumb {
|
|
153
|
+
transform: translateX(calc(100% - 0px));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
:host([size="sm"][checked]) .thumb {
|
|
157
|
+
transform: translateX(14px);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
:host([size="md"][checked]) .thumb,
|
|
161
|
+
:host([checked]:not([size])) .thumb {
|
|
162
|
+
transform: translateX(20px);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
:host([size="lg"][checked]) .thumb {
|
|
166
|
+
transform: translateX(26px);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* Checked track colors */
|
|
170
|
+
:host([checked]) .track {
|
|
171
|
+
background: var(--nc-primary);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
:host([variant="success"][checked]) .track {
|
|
175
|
+
background: var(--nc-success);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
:host([variant="danger"][checked]) .track {
|
|
179
|
+
background: var(--nc-danger);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* Hover glow */
|
|
183
|
+
:host(:not([disabled])) .track:hover {
|
|
184
|
+
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.15);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
:host([variant="success"]:not([disabled])) .track:hover {
|
|
188
|
+
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.15);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
:host([variant="danger"]:not([disabled])) .track:hover {
|
|
192
|
+
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.15);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* Focus ring */
|
|
196
|
+
:host(:focus-visible) .track {
|
|
197
|
+
outline: 2px solid var(--nc-primary);
|
|
198
|
+
outline-offset: 2px;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.label {
|
|
202
|
+
font-size: var(--nc-font-size-base);
|
|
203
|
+
color: var(--nc-text);
|
|
204
|
+
line-height: var(--nc-line-height-normal);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
:host([size="sm"]) .label {
|
|
208
|
+
font-size: var(--nc-font-size-sm);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
:host([size="lg"]) .label {
|
|
212
|
+
font-size: var(--nc-font-size-lg);
|
|
213
|
+
}
|
|
214
|
+
</style>
|
|
215
|
+
|
|
216
|
+
<input type="hidden"
|
|
217
|
+
name="${this.getAttribute('name') || ''}"
|
|
218
|
+
value="${this.hasAttribute('checked') ? (this.getAttribute('value') || 'on') : ''}"
|
|
219
|
+
/>
|
|
220
|
+
<span class="switch-wrapper">
|
|
221
|
+
${labelPosition === 'left' ? labelEl : ''}
|
|
222
|
+
${track}
|
|
223
|
+
${labelPosition !== 'left' ? labelEl : ''}
|
|
224
|
+
</span>
|
|
225
|
+
`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
onMount() {
|
|
229
|
+
if (!this.hasAttribute('tabindex')) {
|
|
230
|
+
this.setAttribute('tabindex', '0');
|
|
231
|
+
}
|
|
232
|
+
this.setAttribute('role', 'switch');
|
|
233
|
+
this.setAttribute('aria-checked', String(this.hasAttribute('checked')));
|
|
234
|
+
|
|
235
|
+
this.addEventListener('click', () => {
|
|
236
|
+
if (this.hasAttribute('disabled')) return;
|
|
237
|
+
this._toggle();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
this.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
241
|
+
if (e.key === ' ' || e.key === 'Enter') {
|
|
242
|
+
e.preventDefault();
|
|
243
|
+
if (!this.hasAttribute('disabled')) this._toggle();
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private _toggle() {
|
|
249
|
+
if (this.hasAttribute('checked')) {
|
|
250
|
+
this.removeAttribute('checked');
|
|
251
|
+
} else {
|
|
252
|
+
this.setAttribute('checked', '');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
this.setAttribute('aria-checked', String(this.hasAttribute('checked')));
|
|
256
|
+
|
|
257
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
258
|
+
bubbles: true,
|
|
259
|
+
composed: true,
|
|
260
|
+
detail: {
|
|
261
|
+
checked: this.hasAttribute('checked'),
|
|
262
|
+
value: this.getAttribute('value') || 'on',
|
|
263
|
+
name: this.getAttribute('name') || ''
|
|
264
|
+
}
|
|
265
|
+
}));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
|
|
269
|
+
if (oldValue === newValue) return;
|
|
270
|
+
|
|
271
|
+
if (name === 'checked') {
|
|
272
|
+
// Let :host([checked]) CSS handle the visual — no re-render needed
|
|
273
|
+
this.setAttribute('aria-checked', String(this.hasAttribute('checked')));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
this.render();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
defineComponent('nc-switch', NcSwitch);
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NativeCore Tab Item Component (nc-tab-item)
|
|
3
|
+
*
|
|
4
|
+
* A single content panel inside an nc-tabs container.
|
|
5
|
+
* Visibility is driven entirely by the `active` attribute set by the parent
|
|
6
|
+
* nc-tabs — no JS show/hide logic required here.
|
|
7
|
+
*
|
|
8
|
+
* Attributes:
|
|
9
|
+
* label — Text shown in the tab bar button (read by nc-tabs)
|
|
10
|
+
* active — Boolean. Present = panel visible. Managed by nc-tabs.
|
|
11
|
+
* disabled — Boolean. Prevents the tab from being selected.
|
|
12
|
+
*
|
|
13
|
+
* Slots:
|
|
14
|
+
* default — Any content for this panel.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* <nc-tabs>
|
|
18
|
+
* <nc-tab-item label="Overview">...</nc-tab-item>
|
|
19
|
+
* <nc-tab-item label="Settings">...</nc-tab-item>
|
|
20
|
+
* <nc-tab-item label="Logs" disabled>...</nc-tab-item>
|
|
21
|
+
* </nc-tabs>
|
|
22
|
+
*
|
|
23
|
+
* Events emitted:
|
|
24
|
+
* (none — nc-tabs owns all interaction events)
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { Component, defineComponent } from '@core/component.js';
|
|
28
|
+
import { html } from '@utils/templates.js';
|
|
29
|
+
|
|
30
|
+
export class NcTabItem extends Component {
|
|
31
|
+
static useShadowDOM = true;
|
|
32
|
+
|
|
33
|
+
static get observedAttributes() {
|
|
34
|
+
return ['label', 'active', 'disabled'];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
template(): string {
|
|
38
|
+
return html`
|
|
39
|
+
<style>
|
|
40
|
+
:host {
|
|
41
|
+
display: none;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
:host([active]) {
|
|
45
|
+
display: block;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* ── Animation variants driven by data-nc-transition set by nc-tabs ── */
|
|
49
|
+
:host([active]) .panel {
|
|
50
|
+
animation: nc-tab-fade 250ms ease both;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
:host([data-nc-transition="fade"][active]) .panel {
|
|
54
|
+
animation: nc-tab-fade 250ms ease both;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
:host([data-nc-transition="slide-up"][active]) .panel {
|
|
58
|
+
animation: nc-tab-slide-up 300ms ease both;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
:host([data-nc-transition="slide-right"][active]) .panel {
|
|
62
|
+
animation: nc-tab-slide-right 300ms ease both;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
:host([data-nc-transition="slide-left"][active]) .panel {
|
|
66
|
+
animation: nc-tab-slide-left 300ms ease both;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
:host([data-nc-transition="slide-down"][active]) .panel {
|
|
70
|
+
animation: nc-tab-slide-down 300ms ease both;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
:host([data-nc-transition="none"][active]) .panel {
|
|
74
|
+
animation: none;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* ── Keyframes ───────────────────────────────────────────────── */
|
|
78
|
+
@keyframes nc-tab-fade {
|
|
79
|
+
from { opacity: 0; }
|
|
80
|
+
to { opacity: 1; }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@keyframes nc-tab-slide-up {
|
|
84
|
+
from { opacity: 0; transform: translateY(20px); }
|
|
85
|
+
to { opacity: 1; transform: translateY(0); }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@keyframes nc-tab-slide-right {
|
|
89
|
+
from { opacity: 0; transform: translateX(-24px); }
|
|
90
|
+
to { opacity: 1; transform: translateX(0); }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@keyframes nc-tab-slide-left {
|
|
94
|
+
from { opacity: 0; transform: translateX(24px); }
|
|
95
|
+
to { opacity: 1; transform: translateX(0); }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@keyframes nc-tab-slide-down {
|
|
99
|
+
from { opacity: 0; transform: translateY(-20px); }
|
|
100
|
+
to { opacity: 1; transform: translateY(0); }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.panel {
|
|
104
|
+
box-sizing: border-box;
|
|
105
|
+
padding: var(--nc-spacing-lg);
|
|
106
|
+
background: var(--nc-bg-secondary);
|
|
107
|
+
border-radius: var(--nc-radius-lg);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@media (max-width: 640px) {
|
|
111
|
+
.panel {
|
|
112
|
+
padding: var(--nc-spacing-sm);
|
|
113
|
+
border-radius: var(--nc-radius-md);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
</style>
|
|
117
|
+
<div class="panel"><slot></slot></div>
|
|
118
|
+
`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Override to suppress re-renders — :host([active]) CSS handles show/hide,
|
|
123
|
+
* and label/disabled changes are handled by nc-tabs rebuilding its bar.
|
|
124
|
+
* A full re-render would needlessly flash slot content on every tab click.
|
|
125
|
+
*/
|
|
126
|
+
attributeChangedCallback(
|
|
127
|
+
_name: string,
|
|
128
|
+
_oldValue: string | null,
|
|
129
|
+
_newValue: string | null
|
|
130
|
+
): void {
|
|
131
|
+
// intentionally empty — CSS selectors handle all visual state
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
onMount(): void {}
|
|
135
|
+
onUnmount(): void {}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
defineComponent('nc-tab-item', NcTabItem);
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NcTable Component — lightweight sortable data table
|
|
3
|
+
*
|
|
4
|
+
* Renders a table from JSON data with optional sorting, striping, compact mode,
|
|
5
|
+
* sticky header, and simple empty state. Zero dependencies.
|
|
6
|
+
*
|
|
7
|
+
* Attributes:
|
|
8
|
+
* columns — JSON array of column defs:
|
|
9
|
+
* [{ key, label?, sortable?, align?, width?, format? }]
|
|
10
|
+
* format: 'text'(default)|'number'|'currency'|'date'|'badge'
|
|
11
|
+
* rows — JSON array of row objects
|
|
12
|
+
* sortable — boolean — enable sorting on all columns unless column.sortable=false
|
|
13
|
+
* striped — boolean — alternating row backgrounds
|
|
14
|
+
* compact — boolean — reduced cell padding
|
|
15
|
+
* sticky-header — boolean — sticky thead
|
|
16
|
+
* empty — empty state text (default: 'No data available')
|
|
17
|
+
* max-height — CSS value to constrain height and enable scrolling
|
|
18
|
+
*
|
|
19
|
+
* Events:
|
|
20
|
+
* sort — CustomEvent<{ key: string; direction: 'asc'|'desc' }>
|
|
21
|
+
* row-click — CustomEvent<{ row: Record<string, unknown>; index: number }>
|
|
22
|
+
*
|
|
23
|
+
* Usage:
|
|
24
|
+
* <nc-table
|
|
25
|
+
* sortable
|
|
26
|
+
* striped
|
|
27
|
+
* columns='[{"key":"name","label":"Name"},{"key":"role","label":"Role"}]'
|
|
28
|
+
* rows='[{"name":"Alice","role":"Admin"},{"name":"Bob","role":"Editor"}]'>
|
|
29
|
+
* </nc-table>
|
|
30
|
+
*/
|
|
31
|
+
import { Component, defineComponent } from '@core/component.js';
|
|
32
|
+
|
|
33
|
+
type TableAlign = 'left' | 'center' | 'right';
|
|
34
|
+
interface TableColumn {
|
|
35
|
+
key: string;
|
|
36
|
+
label?: string;
|
|
37
|
+
sortable?: boolean;
|
|
38
|
+
align?: TableAlign;
|
|
39
|
+
width?: string;
|
|
40
|
+
format?: 'text' | 'number' | 'currency' | 'date' | 'badge';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type TableRow = Record<string, unknown>;
|
|
44
|
+
|
|
45
|
+
function esc(s: unknown): string {
|
|
46
|
+
return String(s ?? '')
|
|
47
|
+
.replace(/&/g, '&')
|
|
48
|
+
.replace(/</g, '<')
|
|
49
|
+
.replace(/>/g, '>')
|
|
50
|
+
.replace(/"/g, '"');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class NcTable extends Component {
|
|
54
|
+
static useShadowDOM = true;
|
|
55
|
+
|
|
56
|
+
static get observedAttributes() {
|
|
57
|
+
return ['columns', 'rows', 'sortable', 'striped', 'compact', 'sticky-header', 'empty', 'max-height'];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private _sortKey = '';
|
|
61
|
+
private _sortDir: 'asc' | 'desc' = 'asc';
|
|
62
|
+
|
|
63
|
+
private _parseColumns(): TableColumn[] {
|
|
64
|
+
try {
|
|
65
|
+
const raw = this.getAttribute('columns') ?? '[]';
|
|
66
|
+
const cols = JSON.parse(raw) as TableColumn[];
|
|
67
|
+
return Array.isArray(cols) ? cols : [];
|
|
68
|
+
} catch {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private _parseRows(): TableRow[] {
|
|
74
|
+
try {
|
|
75
|
+
const raw = this.getAttribute('rows') ?? '[]';
|
|
76
|
+
const rows = JSON.parse(raw) as TableRow[];
|
|
77
|
+
return Array.isArray(rows) ? rows : [];
|
|
78
|
+
} catch {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private _sortedRows(rows: TableRow[], columns: TableColumn[]): TableRow[] {
|
|
84
|
+
if (!this._sortKey) return rows;
|
|
85
|
+
const col = columns.find(c => c.key === this._sortKey);
|
|
86
|
+
if (!col) return rows;
|
|
87
|
+
const dir = this._sortDir === 'asc' ? 1 : -1;
|
|
88
|
+
return [...rows].sort((a, b) => {
|
|
89
|
+
const va = a[this._sortKey];
|
|
90
|
+
const vb = b[this._sortKey];
|
|
91
|
+
if (va == null && vb == null) return 0;
|
|
92
|
+
if (va == null) return -1 * dir;
|
|
93
|
+
if (vb == null) return 1 * dir;
|
|
94
|
+
if (typeof va === 'number' && typeof vb === 'number') return (va - vb) * dir;
|
|
95
|
+
const sa = String(va).toLowerCase();
|
|
96
|
+
const sb = String(vb).toLowerCase();
|
|
97
|
+
return sa.localeCompare(sb) * dir;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private _fmt(value: unknown, col: TableColumn): string {
|
|
102
|
+
if (value == null) return '';
|
|
103
|
+
switch (col.format) {
|
|
104
|
+
case 'number':
|
|
105
|
+
return typeof value === 'number' ? String(value) : esc(value);
|
|
106
|
+
case 'currency':
|
|
107
|
+
return typeof value === 'number'
|
|
108
|
+
? new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value)
|
|
109
|
+
: esc(value);
|
|
110
|
+
case 'date': {
|
|
111
|
+
const d = new Date(String(value));
|
|
112
|
+
return isNaN(d.getTime()) ? esc(value) : d.toLocaleDateString();
|
|
113
|
+
}
|
|
114
|
+
case 'badge':
|
|
115
|
+
return `<span class="badge">${esc(value)}</span>`;
|
|
116
|
+
default:
|
|
117
|
+
return esc(value);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
template() {
|
|
122
|
+
const columns = this._parseColumns();
|
|
123
|
+
const rows = this._sortedRows(this._parseRows(), columns);
|
|
124
|
+
const striped = this.hasAttribute('striped');
|
|
125
|
+
const compact = this.hasAttribute('compact');
|
|
126
|
+
const stickyHeader = this.hasAttribute('sticky-header');
|
|
127
|
+
const emptyText = this.getAttribute('empty') ?? 'No data available';
|
|
128
|
+
const maxHeight = this.getAttribute('max-height') ?? '';
|
|
129
|
+
const sortableAll = this.hasAttribute('sortable');
|
|
130
|
+
|
|
131
|
+
const tableRows = rows.length === 0
|
|
132
|
+
? `<tr><td class="empty" colspan="${Math.max(columns.length, 1)}">${esc(emptyText)}</td></tr>`
|
|
133
|
+
: rows.map((row, rowIndex) => `
|
|
134
|
+
<tr data-row-index="${rowIndex}">
|
|
135
|
+
${columns.map(col => {
|
|
136
|
+
const align = col.align ?? 'left';
|
|
137
|
+
return `<td style="text-align:${align}">${this._fmt(row[col.key], col)}</td>`;
|
|
138
|
+
}).join('')}
|
|
139
|
+
</tr>
|
|
140
|
+
`).join('');
|
|
141
|
+
|
|
142
|
+
const headers = columns.map(col => {
|
|
143
|
+
const align = col.align ?? 'left';
|
|
144
|
+
const sortable = sortableAll && col.sortable !== false;
|
|
145
|
+
const active = this._sortKey === col.key;
|
|
146
|
+
const arrow = active ? (this._sortDir === 'asc' ? '▲' : '▼') : '';
|
|
147
|
+
return `
|
|
148
|
+
<th style="text-align:${align};${col.width ? `width:${col.width};` : ''}">
|
|
149
|
+
<button class="head-btn ${sortable ? 'is-sortable' : ''} ${active ? 'is-active' : ''}" type="button" ${sortable ? `data-sort-key="${col.key}"` : 'disabled'}>
|
|
150
|
+
<span>${esc(col.label ?? col.key)}</span>
|
|
151
|
+
<span class="sort-indicator">${arrow}</span>
|
|
152
|
+
</button>
|
|
153
|
+
</th>
|
|
154
|
+
`;
|
|
155
|
+
}).join('');
|
|
156
|
+
|
|
157
|
+
return `
|
|
158
|
+
<style>
|
|
159
|
+
:host { display: block; font-family: var(--nc-font-family); }
|
|
160
|
+
.wrap {
|
|
161
|
+
border: 1px solid var(--nc-border);
|
|
162
|
+
border-radius: var(--nc-radius-lg);
|
|
163
|
+
overflow: auto;
|
|
164
|
+
background: var(--nc-bg);
|
|
165
|
+
${maxHeight ? `max-height:${maxHeight};` : ''}
|
|
166
|
+
}
|
|
167
|
+
table {
|
|
168
|
+
width: 100%;
|
|
169
|
+
border-collapse: collapse;
|
|
170
|
+
min-width: 480px;
|
|
171
|
+
}
|
|
172
|
+
thead th {
|
|
173
|
+
position: ${stickyHeader ? 'sticky' : 'static'};
|
|
174
|
+
top: 0;
|
|
175
|
+
z-index: 1;
|
|
176
|
+
background: var(--nc-bg-secondary);
|
|
177
|
+
border-bottom: 1px solid var(--nc-border);
|
|
178
|
+
padding: 0;
|
|
179
|
+
font-size: var(--nc-font-size-xs);
|
|
180
|
+
text-transform: uppercase;
|
|
181
|
+
letter-spacing: .04em;
|
|
182
|
+
color: var(--nc-text-muted);
|
|
183
|
+
}
|
|
184
|
+
.head-btn {
|
|
185
|
+
width: 100%;
|
|
186
|
+
display: flex;
|
|
187
|
+
align-items: center;
|
|
188
|
+
justify-content: space-between;
|
|
189
|
+
gap: 8px;
|
|
190
|
+
padding: ${compact ? '10px 12px' : '14px 16px'};
|
|
191
|
+
background: none;
|
|
192
|
+
border: none;
|
|
193
|
+
cursor: default;
|
|
194
|
+
font: inherit;
|
|
195
|
+
color: inherit;
|
|
196
|
+
text-align: inherit;
|
|
197
|
+
}
|
|
198
|
+
.head-btn.is-sortable { cursor: pointer; }
|
|
199
|
+
.head-btn.is-sortable:hover { background: rgba(0,0,0,.03); }
|
|
200
|
+
.head-btn.is-active { color: var(--nc-text); }
|
|
201
|
+
tbody tr {
|
|
202
|
+
transition: background var(--nc-transition-fast);
|
|
203
|
+
cursor: pointer;
|
|
204
|
+
}
|
|
205
|
+
tbody tr:hover { background: rgba(0,0,0,.02); }
|
|
206
|
+
${striped ? 'tbody tr:nth-child(even) { background: var(--nc-bg-secondary); }' : ''}
|
|
207
|
+
td {
|
|
208
|
+
padding: ${compact ? '10px 12px' : '14px 16px'};
|
|
209
|
+
border-bottom: 1px solid var(--nc-border);
|
|
210
|
+
font-size: var(--nc-font-size-sm);
|
|
211
|
+
color: var(--nc-text-secondary);
|
|
212
|
+
vertical-align: top;
|
|
213
|
+
}
|
|
214
|
+
tbody tr:last-child td { border-bottom: none; }
|
|
215
|
+
.empty {
|
|
216
|
+
text-align: center;
|
|
217
|
+
color: var(--nc-text-muted);
|
|
218
|
+
padding: 28px 16px;
|
|
219
|
+
cursor: default;
|
|
220
|
+
}
|
|
221
|
+
.badge {
|
|
222
|
+
display: inline-flex;
|
|
223
|
+
align-items: center;
|
|
224
|
+
padding: 2px 8px;
|
|
225
|
+
border-radius: 99px;
|
|
226
|
+
background: rgba(var(--nc-primary-rgb, 99,102,241), .12);
|
|
227
|
+
color: var(--nc-primary);
|
|
228
|
+
font-size: var(--nc-font-size-xs);
|
|
229
|
+
font-weight: var(--nc-font-weight-medium);
|
|
230
|
+
}
|
|
231
|
+
.sort-indicator {
|
|
232
|
+
min-width: 1em;
|
|
233
|
+
font-size: 10px;
|
|
234
|
+
text-align: center;
|
|
235
|
+
color: var(--nc-text-muted);
|
|
236
|
+
}
|
|
237
|
+
</style>
|
|
238
|
+
<div class="wrap">
|
|
239
|
+
<table role="table">
|
|
240
|
+
<thead><tr>${headers}</tr></thead>
|
|
241
|
+
<tbody>${tableRows}</tbody>
|
|
242
|
+
</table>
|
|
243
|
+
</div>
|
|
244
|
+
`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
onMount() {
|
|
248
|
+
this.shadowRoot!.addEventListener('click', (e) => {
|
|
249
|
+
const sortBtn = (e.target as HTMLElement).closest<HTMLElement>('[data-sort-key]');
|
|
250
|
+
if (sortBtn) {
|
|
251
|
+
const key = sortBtn.dataset.sortKey ?? '';
|
|
252
|
+
if (this._sortKey === key) this._sortDir = this._sortDir === 'asc' ? 'desc' : 'asc';
|
|
253
|
+
else { this._sortKey = key; this._sortDir = 'asc'; }
|
|
254
|
+
this.render();
|
|
255
|
+
this.dispatchEvent(new CustomEvent('sort', {
|
|
256
|
+
detail: { key: this._sortKey, direction: this._sortDir }, bubbles: true, composed: true,
|
|
257
|
+
}));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const row = (e.target as HTMLElement).closest<HTMLTableRowElement>('tbody tr[data-row-index]');
|
|
262
|
+
if (row) {
|
|
263
|
+
const index = parseInt(row.dataset.rowIndex ?? '-1', 10);
|
|
264
|
+
const rows = this._sortedRows(this._parseRows(), this._parseColumns());
|
|
265
|
+
if (index >= 0 && rows[index]) {
|
|
266
|
+
this.dispatchEvent(new CustomEvent('row-click', {
|
|
267
|
+
detail: { row: rows[index], index }, bubbles: true, composed: true,
|
|
268
|
+
}));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
|
|
275
|
+
if (oldValue !== newValue && this._mounted) this.render();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
defineComponent('nc-table', NcTable);
|