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,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NcOtpInput Component — One-time password / verification code input
|
|
3
|
+
*
|
|
4
|
+
* Attributes:
|
|
5
|
+
* length — number of boxes (default: 6)
|
|
6
|
+
* type — 'numeric'(default)|'alphanumeric'|'alpha'
|
|
7
|
+
* separator — insert a visual dash/space separator after this position (e.g. "3" for 3+3)
|
|
8
|
+
* disabled — boolean
|
|
9
|
+
* masked — boolean — mask input like a password
|
|
10
|
+
* autofocus — boolean — focus first box on mount
|
|
11
|
+
* label — accessible label
|
|
12
|
+
* error — error message
|
|
13
|
+
* hint — helper text
|
|
14
|
+
*
|
|
15
|
+
* Value (read/write via property):
|
|
16
|
+
* el.value — get/set current OTP string
|
|
17
|
+
*
|
|
18
|
+
* Events:
|
|
19
|
+
* change — CustomEvent<{ value: string; complete: boolean }>
|
|
20
|
+
* complete — CustomEvent<{ value: string }> — fired when all boxes are filled
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* <nc-otp-input length="6" type="numeric"></nc-otp-input>
|
|
24
|
+
*/
|
|
25
|
+
import { Component, defineComponent } from '@core/component.js';
|
|
26
|
+
|
|
27
|
+
export class NcOtpInput extends Component {
|
|
28
|
+
static useShadowDOM = true;
|
|
29
|
+
|
|
30
|
+
private _values: string[] = [];
|
|
31
|
+
|
|
32
|
+
static get observedAttributes() { return ['length', 'disabled', 'masked', 'error']; }
|
|
33
|
+
|
|
34
|
+
get value(): string { return this._values.join(''); }
|
|
35
|
+
set value(v: string) {
|
|
36
|
+
const len = this._length();
|
|
37
|
+
this._values = v.slice(0, len).split('');
|
|
38
|
+
while (this._values.length < len) this._values.push('');
|
|
39
|
+
this.render();
|
|
40
|
+
this._bindEvents();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private _length() { return parseInt(this.getAttribute('length') ?? '6', 10); }
|
|
44
|
+
|
|
45
|
+
template() {
|
|
46
|
+
const len = this._length();
|
|
47
|
+
const masked = this.hasAttribute('masked');
|
|
48
|
+
const disabled = this.hasAttribute('disabled');
|
|
49
|
+
const label = this.getAttribute('label') ?? '';
|
|
50
|
+
const error = this.getAttribute('error') ?? '';
|
|
51
|
+
const hint = this.getAttribute('hint') ?? '';
|
|
52
|
+
const separator = parseInt(this.getAttribute('separator') ?? '0', 10);
|
|
53
|
+
|
|
54
|
+
while (this._values.length < len) this._values.push('');
|
|
55
|
+
|
|
56
|
+
const boxesHtml = Array.from({ length: len }, (_, i) => {
|
|
57
|
+
const val = this._values[i] ?? '';
|
|
58
|
+
const showSep = separator > 0 && i === separator - 1 && i < len - 1;
|
|
59
|
+
return `
|
|
60
|
+
<input
|
|
61
|
+
class="box"
|
|
62
|
+
type="${masked ? 'password' : 'text'}"
|
|
63
|
+
inputmode="${masked ? 'text' : 'numeric'}"
|
|
64
|
+
maxlength="1"
|
|
65
|
+
data-idx="${i}"
|
|
66
|
+
value="${val}"
|
|
67
|
+
${disabled ? 'disabled' : ''}
|
|
68
|
+
autocomplete="one-time-code"
|
|
69
|
+
aria-label="${label ? label + ' ' : ''}digit ${i + 1}"
|
|
70
|
+
/>
|
|
71
|
+
${showSep ? '<span class="sep">–</span>' : ''}
|
|
72
|
+
`;
|
|
73
|
+
}).join('');
|
|
74
|
+
|
|
75
|
+
return `
|
|
76
|
+
<style>
|
|
77
|
+
:host { display: block; font-family: var(--nc-font-family); }
|
|
78
|
+
.wrap { display: flex; align-items: center; gap: var(--nc-spacing-xs); }
|
|
79
|
+
.box {
|
|
80
|
+
width: 44px;
|
|
81
|
+
height: 52px;
|
|
82
|
+
text-align: center;
|
|
83
|
+
font-size: var(--nc-font-size-xl);
|
|
84
|
+
font-weight: var(--nc-font-weight-semibold);
|
|
85
|
+
color: var(--nc-text);
|
|
86
|
+
background: var(--nc-bg);
|
|
87
|
+
border: 2px solid ${error ? 'var(--nc-danger)' : 'var(--nc-border)'};
|
|
88
|
+
border-radius: var(--nc-radius-md);
|
|
89
|
+
outline: none;
|
|
90
|
+
transition: border-color var(--nc-transition-fast), box-shadow var(--nc-transition-fast);
|
|
91
|
+
caret-color: transparent;
|
|
92
|
+
padding: 0;
|
|
93
|
+
}
|
|
94
|
+
.box:focus {
|
|
95
|
+
border-color: var(--nc-primary);
|
|
96
|
+
box-shadow: 0 0 0 3px rgba(var(--nc-primary-rgb, 99,102,241),.2);
|
|
97
|
+
}
|
|
98
|
+
.box:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
99
|
+
.box.filled { border-color: var(--nc-primary); background: var(--nc-bg-secondary); }
|
|
100
|
+
.sep {
|
|
101
|
+
color: var(--nc-text-muted);
|
|
102
|
+
font-size: var(--nc-font-size-lg);
|
|
103
|
+
font-weight: var(--nc-font-weight-medium);
|
|
104
|
+
user-select: none;
|
|
105
|
+
padding: 0 2px;
|
|
106
|
+
}
|
|
107
|
+
.hint { font-size: var(--nc-font-size-xs); color: var(--nc-text-muted); margin-top: 6px; }
|
|
108
|
+
.error { font-size: var(--nc-font-size-xs); color: var(--nc-danger); margin-top: 6px; }
|
|
109
|
+
</style>
|
|
110
|
+
<div class="wrap" role="group" aria-label="${label || 'OTP input'}">
|
|
111
|
+
${boxesHtml}
|
|
112
|
+
</div>
|
|
113
|
+
${error ? `<p class="error">${error}</p>` : hint ? `<p class="hint">${hint}</p>` : ''}
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
onMount() {
|
|
118
|
+
if (this.hasAttribute('autofocus')) {
|
|
119
|
+
requestAnimationFrame(() => this._boxAt(0)?.focus());
|
|
120
|
+
}
|
|
121
|
+
this._bindEvents();
|
|
122
|
+
this._applyFilledClass();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private _bindEvents() {
|
|
126
|
+
const boxes = this._boxes();
|
|
127
|
+
boxes.forEach((box, idx) => {
|
|
128
|
+
// Remove stale listeners by replacing (simple approach via re-render)
|
|
129
|
+
box.addEventListener('focus', () => box.select());
|
|
130
|
+
|
|
131
|
+
box.addEventListener('input', () => {
|
|
132
|
+
const type = this.getAttribute('type') ?? 'numeric';
|
|
133
|
+
const input = box as HTMLInputElement;
|
|
134
|
+
let val = input.value;
|
|
135
|
+
// Filter by type
|
|
136
|
+
if (type === 'numeric') val = val.replace(/\D/g, '');
|
|
137
|
+
if (type === 'alpha') val = val.replace(/[^a-zA-Z]/g, '');
|
|
138
|
+
if (type === 'alphanumeric') val = val.replace(/[^a-zA-Z0-9]/g, '');
|
|
139
|
+
val = val.slice(-1).toUpperCase();
|
|
140
|
+
input.value = val;
|
|
141
|
+
this._values[idx] = val;
|
|
142
|
+
this._applyFilledClass();
|
|
143
|
+
this._emitChange();
|
|
144
|
+
if (val && idx < boxes.length - 1) this._boxAt(idx + 1)?.focus();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
box.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
148
|
+
const input = box as HTMLInputElement;
|
|
149
|
+
if (e.key === 'Backspace') {
|
|
150
|
+
if (input.value) {
|
|
151
|
+
input.value = '';
|
|
152
|
+
this._values[idx] = '';
|
|
153
|
+
this._applyFilledClass();
|
|
154
|
+
this._emitChange();
|
|
155
|
+
} else if (idx > 0) {
|
|
156
|
+
this._boxAt(idx - 1)?.focus();
|
|
157
|
+
}
|
|
158
|
+
e.preventDefault();
|
|
159
|
+
} else if (e.key === 'ArrowLeft' && idx > 0) this._boxAt(idx - 1)?.focus();
|
|
160
|
+
else if (e.key === 'ArrowRight' && idx < boxes.length - 1) this._boxAt(idx + 1)?.focus();
|
|
161
|
+
else if (e.key === 'Delete') {
|
|
162
|
+
input.value = '';
|
|
163
|
+
this._values[idx] = '';
|
|
164
|
+
this._applyFilledClass();
|
|
165
|
+
this._emitChange();
|
|
166
|
+
e.preventDefault();
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Handle multi-character paste without blocking the native paste event.
|
|
171
|
+
box.addEventListener('paste', (e: ClipboardEvent) => {
|
|
172
|
+
const text = e.clipboardData?.getData('text') ?? '';
|
|
173
|
+
if (!text) return;
|
|
174
|
+
|
|
175
|
+
let filtered = text;
|
|
176
|
+
const type = this.getAttribute('type') ?? 'numeric';
|
|
177
|
+
if (type === 'numeric') filtered = text.replace(/\D/g, '');
|
|
178
|
+
if (type === 'alpha') filtered = text.replace(/[^a-zA-Z]/g, '');
|
|
179
|
+
if (type === 'alphanumeric') filtered = text.replace(/[^a-zA-Z0-9]/g, '');
|
|
180
|
+
|
|
181
|
+
requestAnimationFrame(() => {
|
|
182
|
+
const chars = filtered.toUpperCase().slice(0, this._length() - idx).split('');
|
|
183
|
+
chars.forEach((ch, offset) => {
|
|
184
|
+
const targetIndex = idx + offset;
|
|
185
|
+
this._values[targetIndex] = ch;
|
|
186
|
+
const targetBox = this._boxAt(targetIndex);
|
|
187
|
+
if (targetBox) targetBox.value = ch;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
this._applyFilledClass();
|
|
191
|
+
this._emitChange();
|
|
192
|
+
|
|
193
|
+
const nextFocus = Math.min(idx + chars.length, boxes.length - 1);
|
|
194
|
+
this._boxAt(nextFocus)?.focus();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private _applyFilledClass() {
|
|
201
|
+
this._boxes().forEach((box, i) => {
|
|
202
|
+
box.classList.toggle('filled', !!(this._values[i]));
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private _emitChange() {
|
|
207
|
+
const value = this.value;
|
|
208
|
+
const complete = value.length === this._length() && !value.includes('');
|
|
209
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
210
|
+
detail: { value, complete }, bubbles: true, composed: true,
|
|
211
|
+
}));
|
|
212
|
+
if (complete) {
|
|
213
|
+
this.dispatchEvent(new CustomEvent('complete', {
|
|
214
|
+
detail: { value }, bubbles: true, composed: true,
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private _boxes(): HTMLInputElement[] {
|
|
220
|
+
return Array.from(this.shadowRoot!.querySelectorAll<HTMLInputElement>('.box'));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private _boxAt(i: number): HTMLInputElement | null {
|
|
224
|
+
return this.shadowRoot!.querySelector<HTMLInputElement>(`.box[data-idx="${i}"]`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
attributeChangedCallback(name: string, oldVal: string, newVal: string) {
|
|
228
|
+
if (oldVal === newVal || !this._mounted) return;
|
|
229
|
+
this.render();
|
|
230
|
+
this._bindEvents();
|
|
231
|
+
this._applyFilledClass();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
defineComponent('nc-otp-input', NcOtpInput);
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NcPagination Component
|
|
3
|
+
*
|
|
4
|
+
* Attributes:
|
|
5
|
+
* - page: number — current page (1-based, default: 1)
|
|
6
|
+
* - total: number — total pages (required)
|
|
7
|
+
* - siblings: number — pages shown on each side of current (default: 1)
|
|
8
|
+
* - show-first-last: boolean — show First/Last buttons
|
|
9
|
+
* - disabled: boolean
|
|
10
|
+
* - size: 'sm'|'md'|'lg' (default: 'md')
|
|
11
|
+
* - variant: 'default'|'outline' (default: 'default')
|
|
12
|
+
*
|
|
13
|
+
* Events:
|
|
14
|
+
* - change: CustomEvent<{ page: number }>
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* <nc-pagination page="3" total="20"></nc-pagination>
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { Component, defineComponent } from '@core/component.js';
|
|
21
|
+
|
|
22
|
+
export class NcPagination extends Component {
|
|
23
|
+
static useShadowDOM = true;
|
|
24
|
+
|
|
25
|
+
static get observedAttributes() {
|
|
26
|
+
return ['page', 'total', 'siblings', 'show-first-last', 'disabled', 'size', 'variant'];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private _buildPages(current: number, total: number, siblings: number): (number | '...')[] {
|
|
30
|
+
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
|
|
31
|
+
|
|
32
|
+
const left = Math.max(2, current - siblings);
|
|
33
|
+
const right = Math.min(total - 1, current + siblings);
|
|
34
|
+
const pages: (number | '...')[] = [1];
|
|
35
|
+
|
|
36
|
+
if (left > 2) pages.push('...');
|
|
37
|
+
for (let i = left; i <= right; i++) pages.push(i);
|
|
38
|
+
if (right < total - 1) pages.push('...');
|
|
39
|
+
pages.push(total);
|
|
40
|
+
|
|
41
|
+
return pages;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
template() {
|
|
45
|
+
const current = Number(this.getAttribute('page') || 1);
|
|
46
|
+
const total = Number(this.getAttribute('total') || 1);
|
|
47
|
+
const siblings = Number(this.getAttribute('siblings') ?? 1);
|
|
48
|
+
const showFirstLast = this.hasAttribute('show-first-last');
|
|
49
|
+
const disabled = this.hasAttribute('disabled');
|
|
50
|
+
|
|
51
|
+
const pages = this._buildPages(current, total, siblings);
|
|
52
|
+
const atFirst = current <= 1;
|
|
53
|
+
const atLast = current >= total;
|
|
54
|
+
|
|
55
|
+
const navBtn = (dir: string, label: string, dis: boolean) => `
|
|
56
|
+
<button class="btn btn--nav" data-dir="${dir}" ${dis || disabled ? 'disabled' : ''} aria-label="${label}">
|
|
57
|
+
${dir === 'prev' || dir === 'first'
|
|
58
|
+
? `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" width="14" height="14"><path d="M${dir === 'first' ? '12 3L7 8l5 5M7 3L2 8l5 5' : '10 3L5 8l5 5'}" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`
|
|
59
|
+
: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" width="14" height="14"><path d="M${dir === 'last' ? '4 3l5 5-5 5M9 3l5 5-5 5' : '6 3l5 5-5 5'}" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`
|
|
60
|
+
}
|
|
61
|
+
</button>`;
|
|
62
|
+
|
|
63
|
+
return `
|
|
64
|
+
<style>
|
|
65
|
+
:host { display: block; font-family: var(--nc-font-family); }
|
|
66
|
+
|
|
67
|
+
.pagination {
|
|
68
|
+
display: inline-flex;
|
|
69
|
+
align-items: center;
|
|
70
|
+
gap: 4px;
|
|
71
|
+
flex-wrap: wrap;
|
|
72
|
+
opacity: ${disabled ? '0.5' : '1'};
|
|
73
|
+
pointer-events: ${disabled ? 'none' : 'auto'};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.btn {
|
|
77
|
+
display: inline-flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
justify-content: center;
|
|
80
|
+
border: 1px solid var(--nc-border);
|
|
81
|
+
background: var(--nc-bg);
|
|
82
|
+
color: var(--nc-text);
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
border-radius: var(--nc-radius-sm, 6px);
|
|
85
|
+
font-family: var(--nc-font-family);
|
|
86
|
+
font-size: var(--nc-font-size-sm);
|
|
87
|
+
transition: background var(--nc-transition-fast), color var(--nc-transition-fast), border-color var(--nc-transition-fast);
|
|
88
|
+
min-width: 36px;
|
|
89
|
+
height: 36px;
|
|
90
|
+
padding: 0 6px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
:host([size="sm"]) .btn { min-width: 28px; height: 28px; font-size: var(--nc-font-size-xs); }
|
|
94
|
+
:host([size="lg"]) .btn { min-width: 44px; height: 44px; font-size: var(--nc-font-size-base); }
|
|
95
|
+
|
|
96
|
+
.btn:hover:not(:disabled):not(.btn--active) { background: var(--nc-bg-secondary); border-color: var(--nc-border-dark); }
|
|
97
|
+
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
98
|
+
|
|
99
|
+
.btn--active {
|
|
100
|
+
background: var(--nc-primary);
|
|
101
|
+
color: #fff;
|
|
102
|
+
border-color: var(--nc-primary);
|
|
103
|
+
font-weight: var(--nc-font-weight-semibold);
|
|
104
|
+
pointer-events: none;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
:host([variant="outline"]) .btn--active {
|
|
108
|
+
background: transparent;
|
|
109
|
+
color: var(--nc-primary);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.ellipsis {
|
|
113
|
+
display: inline-flex;
|
|
114
|
+
align-items: center;
|
|
115
|
+
justify-content: center;
|
|
116
|
+
min-width: 36px;
|
|
117
|
+
height: 36px;
|
|
118
|
+
font-size: var(--nc-font-size-sm);
|
|
119
|
+
color: var(--nc-text-muted);
|
|
120
|
+
}
|
|
121
|
+
</style>
|
|
122
|
+
<nav aria-label="Pagination" class="pagination">
|
|
123
|
+
${showFirstLast ? navBtn('first', 'First page', atFirst) : ''}
|
|
124
|
+
${navBtn('prev', 'Previous page', atFirst)}
|
|
125
|
+
${pages.map(p =>
|
|
126
|
+
p === '...'
|
|
127
|
+
? `<span class="ellipsis" aria-hidden="true">...</span>`
|
|
128
|
+
: `<button class="btn${p === current ? ' btn--active' : ''}" data-page="${p}" aria-label="Page ${p}" aria-current="${p === current ? 'page' : 'false'}">${p}</button>`
|
|
129
|
+
).join('')}
|
|
130
|
+
${navBtn('next', 'Next page', atLast)}
|
|
131
|
+
${showFirstLast ? navBtn('last', 'Last page', atLast) : ''}
|
|
132
|
+
</nav>
|
|
133
|
+
`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
onMount() {
|
|
137
|
+
this._bindEvents();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private _bindEvents() {
|
|
141
|
+
this.$<HTMLElement>('.pagination')!.addEventListener('click', (e) => {
|
|
142
|
+
const btn = (e.target as HTMLElement).closest<HTMLButtonElement>('button');
|
|
143
|
+
if (!btn || btn.disabled) return;
|
|
144
|
+
|
|
145
|
+
const current = Number(this.getAttribute('page') || 1);
|
|
146
|
+
const total = Number(this.getAttribute('total') || 1);
|
|
147
|
+
let next = current;
|
|
148
|
+
|
|
149
|
+
if (btn.dataset.page) {
|
|
150
|
+
next = Number(btn.dataset.page);
|
|
151
|
+
} else {
|
|
152
|
+
switch (btn.dataset.dir) {
|
|
153
|
+
case 'first': next = 1; break;
|
|
154
|
+
case 'prev': next = Math.max(1, current - 1); break;
|
|
155
|
+
case 'next': next = Math.min(total, current + 1); break;
|
|
156
|
+
case 'last': next = total; break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (next !== current) {
|
|
161
|
+
this.setAttribute('page', String(next));
|
|
162
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
163
|
+
bubbles: true, composed: true,
|
|
164
|
+
detail: { page: next }
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
|
|
171
|
+
if (oldValue !== newValue && this._mounted) {
|
|
172
|
+
this.render();
|
|
173
|
+
this._bindEvents();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
defineComponent('nc-pagination', NcPagination);
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NcPopover Component — floating panel anchored to a trigger element
|
|
3
|
+
*
|
|
4
|
+
* More flexible than a dropdown: supports arbitrary slot content,
|
|
5
|
+
* arrow pointer, multiple placement options, and click/hover triggers.
|
|
6
|
+
*
|
|
7
|
+
* Attributes:
|
|
8
|
+
* placement — 'top'|'bottom'(default)|'left'|'right'
|
|
9
|
+
* + '-start' or '-end' suffix: 'bottom-start'|'top-end' etc.
|
|
10
|
+
* trigger — 'click'(default)|'hover'|'focus'|'manual'
|
|
11
|
+
* open — boolean — controlled open state
|
|
12
|
+
* offset — gap between anchor and popover in px (default: 8)
|
|
13
|
+
* arrow — boolean — show arrow pointer (default: true)
|
|
14
|
+
* width — popover width CSS value (default: 'auto')
|
|
15
|
+
* max-width — CSS value (default: '320px')
|
|
16
|
+
* close-on-outside — boolean(default true) — close on outside click
|
|
17
|
+
* disabled — boolean
|
|
18
|
+
* hover-delay — ms before hover-trigger opens (default: 200)
|
|
19
|
+
*
|
|
20
|
+
* Slots:
|
|
21
|
+
* trigger — the anchor element
|
|
22
|
+
* (default) — popover content
|
|
23
|
+
*
|
|
24
|
+
* Events:
|
|
25
|
+
* open — popover opened
|
|
26
|
+
* close — popover closed
|
|
27
|
+
*
|
|
28
|
+
* Methods:
|
|
29
|
+
* el.show() / el.hide() / el.toggle()
|
|
30
|
+
*
|
|
31
|
+
* Usage:
|
|
32
|
+
* <nc-popover placement="bottom-start">
|
|
33
|
+
* <nc-button slot="trigger">Info</nc-button>
|
|
34
|
+
* <div style="padding:12px">
|
|
35
|
+
* <p>Popover content here.</p>
|
|
36
|
+
* </div>
|
|
37
|
+
* </nc-popover>
|
|
38
|
+
*/
|
|
39
|
+
import { Component, defineComponent } from '@core/component.js';
|
|
40
|
+
|
|
41
|
+
type Placement = 'top'|'top-start'|'top-end'|'bottom'|'bottom-start'|'bottom-end'|'left'|'left-start'|'left-end'|'right'|'right-start'|'right-end';
|
|
42
|
+
|
|
43
|
+
export class NcPopover extends Component {
|
|
44
|
+
static useShadowDOM = true;
|
|
45
|
+
|
|
46
|
+
private _open = false;
|
|
47
|
+
private _hoverTimer: ReturnType<typeof setTimeout> | null = null;
|
|
48
|
+
private _outside: ((e: MouseEvent) => void) | null = null;
|
|
49
|
+
|
|
50
|
+
static get observedAttributes() { return ['open', 'placement', 'disabled']; }
|
|
51
|
+
|
|
52
|
+
template() {
|
|
53
|
+
const open = this._open;
|
|
54
|
+
const arrow = this.getAttribute('arrow') !== 'false';
|
|
55
|
+
const width = this.getAttribute('width') ?? 'auto';
|
|
56
|
+
const maxWidth = this.getAttribute('max-width') ?? '320px';
|
|
57
|
+
|
|
58
|
+
return `
|
|
59
|
+
<style>
|
|
60
|
+
:host { display: inline-block; position: relative; }
|
|
61
|
+
.trigger-wrap { display: contents; }
|
|
62
|
+
.popover {
|
|
63
|
+
position: absolute;
|
|
64
|
+
z-index: 1000;
|
|
65
|
+
background: var(--nc-bg-elevated, var(--nc-bg));
|
|
66
|
+
border: 1px solid var(--nc-border);
|
|
67
|
+
border-radius: var(--nc-radius-lg);
|
|
68
|
+
box-shadow: var(--nc-shadow-lg);
|
|
69
|
+
width: ${width};
|
|
70
|
+
max-width: ${maxWidth};
|
|
71
|
+
font-family: var(--nc-font-family);
|
|
72
|
+
font-size: var(--nc-font-size-sm);
|
|
73
|
+
color: var(--nc-text);
|
|
74
|
+
opacity: ${open ? 1 : 0};
|
|
75
|
+
pointer-events: ${open ? 'auto' : 'none'};
|
|
76
|
+
transform: ${open ? 'scale(1) translateY(0)' : 'scale(0.97) translateY(-4px)'};
|
|
77
|
+
transform-origin: top center;
|
|
78
|
+
transition:
|
|
79
|
+
opacity var(--nc-transition-fast),
|
|
80
|
+
transform var(--nc-transition-fast);
|
|
81
|
+
white-space: normal;
|
|
82
|
+
}
|
|
83
|
+
/* Placement styles applied via JS in _position() */
|
|
84
|
+
.arrow {
|
|
85
|
+
display: ${arrow ? 'block' : 'none'};
|
|
86
|
+
position: absolute;
|
|
87
|
+
width: 8px;
|
|
88
|
+
height: 8px;
|
|
89
|
+
background: var(--nc-bg-elevated, var(--nc-bg));
|
|
90
|
+
border: 1px solid var(--nc-border);
|
|
91
|
+
transform: rotate(45deg);
|
|
92
|
+
pointer-events: none;
|
|
93
|
+
}
|
|
94
|
+
</style>
|
|
95
|
+
<div class="trigger-wrap">
|
|
96
|
+
<slot name="trigger"></slot>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="popover" id="popover" role="dialog" aria-modal="false">
|
|
99
|
+
${arrow ? '<div class="arrow" id="arrow"></div>' : ''}
|
|
100
|
+
<slot></slot>
|
|
101
|
+
</div>
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
onMount() {
|
|
106
|
+
this._bindTrigger();
|
|
107
|
+
if (this.hasAttribute('open')) { this._open = true; this.render(); }
|
|
108
|
+
this._position();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
onUnmount() {
|
|
112
|
+
this._cleanup();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── Public API ──────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
show() { if (!this._open) { this._open = true; this.render(); this._position(); this._setupOutside(); this.dispatchEvent(new CustomEvent('open', { bubbles: true, composed: true })); } }
|
|
118
|
+
hide() { if (this._open) { this._open = false; this.render(); this._cleanupOutside(); this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true })); } }
|
|
119
|
+
toggle() {
|
|
120
|
+
if (this._open) this.hide();
|
|
121
|
+
else this.show();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Trigger binding ─────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
private _bindTrigger() {
|
|
127
|
+
const triggerSlot = this.shadowRoot!.querySelector<HTMLSlotElement>('slot[name="trigger"]');
|
|
128
|
+
if (!triggerSlot) return;
|
|
129
|
+
|
|
130
|
+
const getTrigger = (): HTMLElement | null => {
|
|
131
|
+
const els = triggerSlot.assignedElements({ flatten: true });
|
|
132
|
+
return (els[0] as HTMLElement) ?? null;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const mode = this.getAttribute('trigger') ?? 'click';
|
|
136
|
+
const hoverDelay = parseInt(this.getAttribute('hover-delay') ?? '200', 10);
|
|
137
|
+
|
|
138
|
+
if (mode === 'click') {
|
|
139
|
+
triggerSlot.addEventListener('slotchange', () => {
|
|
140
|
+
const el = getTrigger();
|
|
141
|
+
if (el) el.addEventListener('click', () => {
|
|
142
|
+
if (this.hasAttribute('disabled')) return;
|
|
143
|
+
this.toggle();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
// Also handle if already slotted
|
|
147
|
+
requestAnimationFrame(() => {
|
|
148
|
+
const el = getTrigger();
|
|
149
|
+
if (el) el.addEventListener('click', () => {
|
|
150
|
+
if (this.hasAttribute('disabled')) return;
|
|
151
|
+
this.toggle();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (mode === 'hover') {
|
|
157
|
+
triggerSlot.addEventListener('slotchange', () => {
|
|
158
|
+
const el = getTrigger();
|
|
159
|
+
if (!el) return;
|
|
160
|
+
el.addEventListener('mouseenter', () => {
|
|
161
|
+
if (this.hasAttribute('disabled')) return;
|
|
162
|
+
this._hoverTimer = setTimeout(() => this.show(), hoverDelay);
|
|
163
|
+
});
|
|
164
|
+
el.addEventListener('mouseleave', () => {
|
|
165
|
+
if (this._hoverTimer) { clearTimeout(this._hoverTimer); this._hoverTimer = null; }
|
|
166
|
+
this.hide();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (mode === 'focus') {
|
|
172
|
+
triggerSlot.addEventListener('slotchange', () => {
|
|
173
|
+
const el = getTrigger();
|
|
174
|
+
if (!el) return;
|
|
175
|
+
el.addEventListener('focusin', () => { if (!this.hasAttribute('disabled')) this.show(); });
|
|
176
|
+
el.addEventListener('focusout', () => this.hide());
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── Position calculation ────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
private _position() {
|
|
184
|
+
const popover = this.shadowRoot!.querySelector<HTMLElement>('#popover');
|
|
185
|
+
const arrow = this.shadowRoot!.querySelector<HTMLElement>('#arrow');
|
|
186
|
+
if (!popover) return;
|
|
187
|
+
|
|
188
|
+
const placement = (this.getAttribute('placement') ?? 'bottom') as Placement;
|
|
189
|
+
const offset = parseInt(this.getAttribute('offset') ?? '8', 10);
|
|
190
|
+
const [side, align = 'center'] = placement.split('-') as [string, string?];
|
|
191
|
+
|
|
192
|
+
// Reset
|
|
193
|
+
['top','bottom','left','right'].forEach(s => { popover.style[s as any] = ''; });
|
|
194
|
+
|
|
195
|
+
const arrowSz = 8;
|
|
196
|
+
const fullOff = offset + (this.getAttribute('arrow') !== 'false' ? arrowSz / 2 : 0);
|
|
197
|
+
|
|
198
|
+
if (side === 'bottom') {
|
|
199
|
+
popover.style.top = `calc(100% + ${fullOff}px)`;
|
|
200
|
+
popover.style.transformOrigin = 'top center';
|
|
201
|
+
if (align === 'start') popover.style.left = '0';
|
|
202
|
+
else if (align === 'end') popover.style.right = '0';
|
|
203
|
+
else { popover.style.left = '50%'; popover.style.transform = this._open ? 'translateX(-50%)' : 'translateX(-50%) scale(0.97) translateY(-4px)'; }
|
|
204
|
+
if (arrow) { arrow!.style.top = `-${arrowSz/2}px`; arrow!.style.left = '16px'; arrow!.style.borderRight = 'none'; arrow!.style.borderBottom = 'none'; }
|
|
205
|
+
} else if (side === 'top') {
|
|
206
|
+
popover.style.bottom = `calc(100% + ${fullOff}px)`;
|
|
207
|
+
popover.style.transformOrigin = 'bottom center';
|
|
208
|
+
if (align === 'start') popover.style.left = '0';
|
|
209
|
+
else if (align === 'end') popover.style.right = '0';
|
|
210
|
+
else { popover.style.left = '50%'; popover.style.transform = this._open ? 'translateX(-50%)' : 'translateX(-50%) scale(0.97) translateY(4px)'; }
|
|
211
|
+
if (arrow) { arrow!.style.bottom = `-${arrowSz/2}px`; arrow!.style.left = '16px'; arrow!.style.borderLeft = 'none'; arrow!.style.borderTop = 'none'; }
|
|
212
|
+
} else if (side === 'right') {
|
|
213
|
+
popover.style.left = `calc(100% + ${fullOff}px)`;
|
|
214
|
+
popover.style.top = '0';
|
|
215
|
+
popover.style.transformOrigin = 'left center';
|
|
216
|
+
if (arrow) { arrow!.style.left = `-${arrowSz/2}px`; arrow!.style.top = '12px'; arrow!.style.borderRight = 'none'; arrow!.style.borderTop = 'none'; }
|
|
217
|
+
} else { // left
|
|
218
|
+
popover.style.right = `calc(100% + ${fullOff}px)`;
|
|
219
|
+
popover.style.top = '0';
|
|
220
|
+
popover.style.transformOrigin = 'right center';
|
|
221
|
+
if (arrow) { arrow!.style.right = `-${arrowSz/2}px`; arrow!.style.top = '12px'; arrow!.style.borderLeft = 'none'; arrow!.style.borderBottom = 'none'; }
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private _setupOutside() {
|
|
226
|
+
if (this.getAttribute('close-on-outside') === 'false') return;
|
|
227
|
+
const handler = (e: MouseEvent) => {
|
|
228
|
+
if (!this.contains(e.target as Node)) this.hide();
|
|
229
|
+
};
|
|
230
|
+
document.addEventListener('mousedown', handler as EventListener);
|
|
231
|
+
this._outside = handler;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private _cleanupOutside() {
|
|
235
|
+
if (this._outside) {
|
|
236
|
+
document.removeEventListener('mousedown', this._outside as EventListener);
|
|
237
|
+
this._outside = null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private _cleanup() {
|
|
242
|
+
this._cleanupOutside();
|
|
243
|
+
if (this._hoverTimer) clearTimeout(this._hoverTimer);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
attributeChangedCallback(n: string, o: string, v: string) {
|
|
247
|
+
if (o === v || !this._mounted) return;
|
|
248
|
+
if (n === 'open') {
|
|
249
|
+
this._open = this.hasAttribute('open');
|
|
250
|
+
this.render();
|
|
251
|
+
if (this._open) { this._position(); this._setupOutside(); }
|
|
252
|
+
else this._cleanupOutside();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
this.render();
|
|
256
|
+
this._position();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
defineComponent('nc-popover', NcPopover);
|