create-nativecore 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/README.md +6 -14
  2. package/bin/index.mjs +403 -431
  3. package/package.json +3 -2
  4. package/template/.env.example +28 -0
  5. package/template/.htmlhintrc +14 -0
  6. package/template/api/data/dashboard.json +11 -0
  7. package/template/api/data/users.json +18 -0
  8. package/template/api/mockApi.js +161 -0
  9. package/template/assets/icon.svg +13 -0
  10. package/template/assets/logo.svg +25 -0
  11. package/template/eslint.config.js +94 -0
  12. package/template/index.html +137 -0
  13. package/template/manifest.json +19 -0
  14. package/template/public/.well-known/security.txt +9 -0
  15. package/template/public/_headers +24 -0
  16. package/template/public/_redirects +14 -0
  17. package/template/public/assets/icon.svg +13 -0
  18. package/template/public/assets/logo.svg +25 -0
  19. package/template/public/manifest.json +19 -0
  20. package/template/public/robots.txt +13 -0
  21. package/template/public/sitemap.xml +27 -0
  22. package/template/scripts/build-for-bots.mjs +121 -0
  23. package/template/scripts/convert-to-ts.mjs +106 -0
  24. package/template/scripts/fix-encoding.mjs +38 -0
  25. package/template/scripts/fix-svg-paths.mjs +32 -0
  26. package/template/scripts/generate-cf-router.mjs +52 -0
  27. package/template/scripts/inject-dev-tools.mjs +41 -0
  28. package/template/scripts/inject-version.mjs +65 -0
  29. package/template/scripts/make-component.mjs +445 -0
  30. package/template/scripts/make-component.mjs.backup +432 -0
  31. package/template/scripts/make-controller.mjs +119 -0
  32. package/template/scripts/make-core-component.mjs +303 -0
  33. package/template/scripts/make-view.mjs +346 -0
  34. package/template/scripts/minify.mjs +71 -0
  35. package/template/scripts/prepare-static-assets.mjs +141 -0
  36. package/template/scripts/prompt-bot-build.mjs +223 -0
  37. package/template/scripts/remove-component.mjs +170 -0
  38. package/template/scripts/remove-core-component.mjs +156 -0
  39. package/template/scripts/remove-dev.mjs +13 -0
  40. package/template/scripts/remove-view.mjs +200 -0
  41. package/template/scripts/strip-dev-blocks.mjs +30 -0
  42. package/template/scripts/watch-compile.mjs +69 -0
  43. package/template/server.js +1066 -0
  44. package/template/src/app.ts +115 -0
  45. package/template/src/components/appRegistry.ts +8 -0
  46. package/template/src/components/core/app-footer.ts +27 -0
  47. package/template/src/components/core/app-header.ts +175 -0
  48. package/template/src/components/core/app-sidebar.ts +238 -0
  49. package/template/src/components/core/loading-spinner.ts +25 -0
  50. package/template/src/components/core/nc-a.ts +313 -0
  51. package/template/src/components/core/nc-accordion.ts +186 -0
  52. package/template/src/components/core/nc-alert.ts +153 -0
  53. package/template/src/components/core/nc-animation.ts +1150 -0
  54. package/template/src/components/core/nc-autocomplete.ts +271 -0
  55. package/template/src/components/core/nc-avatar-group.ts +113 -0
  56. package/template/src/components/core/nc-avatar.ts +148 -0
  57. package/template/src/components/core/nc-badge.ts +86 -0
  58. package/template/src/components/core/nc-bottom-nav.ts +214 -0
  59. package/template/src/components/core/nc-breadcrumb.ts +96 -0
  60. package/template/src/components/core/nc-button.ts +307 -0
  61. package/template/src/components/core/nc-card.ts +160 -0
  62. package/template/src/components/core/nc-checkbox.ts +282 -0
  63. package/template/src/components/core/nc-chip.ts +115 -0
  64. package/template/src/components/core/nc-code.ts +314 -0
  65. package/template/src/components/core/nc-collapsible.ts +154 -0
  66. package/template/src/components/core/nc-color-picker.ts +268 -0
  67. package/template/src/components/core/nc-copy-button.ts +119 -0
  68. package/template/src/components/core/nc-date-picker.ts +443 -0
  69. package/template/src/components/core/nc-div.ts +280 -0
  70. package/template/src/components/core/nc-divider.ts +81 -0
  71. package/template/src/components/core/nc-drawer.ts +230 -0
  72. package/template/src/components/core/nc-dropdown.ts +178 -0
  73. package/template/src/components/core/nc-empty-state.ts +134 -0
  74. package/template/src/components/core/nc-file-upload.ts +354 -0
  75. package/template/src/components/core/nc-form.ts +312 -0
  76. package/template/src/components/core/nc-image.ts +184 -0
  77. package/template/src/components/core/nc-input.ts +383 -0
  78. package/template/src/components/core/nc-kbd.ts +48 -0
  79. package/template/src/components/core/nc-menu-item.ts +193 -0
  80. package/template/src/components/core/nc-menu.ts +376 -0
  81. package/template/src/components/core/nc-modal.ts +238 -0
  82. package/template/src/components/core/nc-nav-item.ts +151 -0
  83. package/template/src/components/core/nc-number-input.ts +350 -0
  84. package/template/src/components/core/nc-otp-input.ts +235 -0
  85. package/template/src/components/core/nc-pagination.ts +178 -0
  86. package/template/src/components/core/nc-popover.ts +260 -0
  87. package/template/src/components/core/nc-progress-circular.ts +119 -0
  88. package/template/src/components/core/nc-progress.ts +134 -0
  89. package/template/src/components/core/nc-radio.ts +235 -0
  90. package/template/src/components/core/nc-rating.ts +266 -0
  91. package/template/src/components/core/nc-rich-text.ts +283 -0
  92. package/template/src/components/core/nc-scroll-top.ts +116 -0
  93. package/template/src/components/core/nc-select.ts +452 -0
  94. package/template/src/components/core/nc-skeleton.ts +107 -0
  95. package/template/src/components/core/nc-slider.ts +285 -0
  96. package/template/src/components/core/nc-snackbar.ts +230 -0
  97. package/template/src/components/core/nc-splash.ts +343 -0
  98. package/template/src/components/core/nc-stepper.ts +247 -0
  99. package/template/src/components/core/nc-switch.ts +281 -0
  100. package/template/src/components/core/nc-tab-item.ts +138 -0
  101. package/template/src/components/core/nc-table.ts +279 -0
  102. package/template/src/components/core/nc-tabs.ts +554 -0
  103. package/template/src/components/core/nc-tag-input.ts +279 -0
  104. package/template/src/components/core/nc-textarea.ts +216 -0
  105. package/template/src/components/core/nc-time-picker.ts +438 -0
  106. package/template/src/components/core/nc-timeline.ts +186 -0
  107. package/template/src/components/core/nc-tooltip.ts +143 -0
  108. package/template/src/components/frameworkRegistry.ts +68 -0
  109. package/template/src/components/preloadRegistry.ts +28 -0
  110. package/template/src/components/registry.ts +8 -0
  111. package/template/src/components/ui/dashboard-signal-lab.ts +284 -0
  112. package/template/src/constants/apiEndpoints.ts +27 -0
  113. package/template/src/constants/errorMessages.ts +23 -0
  114. package/template/src/constants/index.ts +8 -0
  115. package/template/src/constants/routePaths.ts +15 -0
  116. package/template/src/constants/storageKeys.ts +18 -0
  117. package/template/src/controllers/dashboard.controller.ts +200 -0
  118. package/template/src/controllers/home.controller.ts +21 -0
  119. package/template/src/controllers/index.ts +11 -0
  120. package/template/src/controllers/login.controller.ts +131 -0
  121. package/template/src/core/component.ts +354 -0
  122. package/template/src/core/errorHandler.ts +85 -0
  123. package/template/src/core/gpu-animation.ts +604 -0
  124. package/template/src/core/http.ts +173 -0
  125. package/template/src/core/lazyComponents.ts +90 -0
  126. package/template/src/core/router.ts +653 -0
  127. package/template/src/core/signals.ts +146 -0
  128. package/template/src/core/state.ts +248 -0
  129. package/template/src/dev/component-editor.ts +1363 -0
  130. package/template/src/dev/component-overlay.ts +278 -0
  131. package/template/src/dev/context-menu.ts +223 -0
  132. package/template/src/dev/denc-tools.ts +250 -0
  133. package/template/src/dev/hmr.ts +189 -0
  134. package/template/src/dev/nfbs.code-workspace +27 -0
  135. package/template/src/dev/outline-panel.ts +1247 -0
  136. package/template/src/middleware/auth.middleware.ts +23 -0
  137. package/template/src/routes/routes.ts +38 -0
  138. package/template/src/services/api.service.ts +394 -0
  139. package/template/src/services/auth.service.ts +176 -0
  140. package/template/src/services/index.ts +8 -0
  141. package/template/src/services/logger.service.ts +74 -0
  142. package/template/src/services/storage.service.ts +88 -0
  143. package/template/src/stores/appStore.ts +57 -0
  144. package/template/src/stores/uiStore.ts +36 -0
  145. package/template/src/styles/core-variables.css +219 -0
  146. package/template/src/styles/core.css +710 -0
  147. package/template/src/styles/main.css +3164 -0
  148. package/template/src/styles/variables.css +152 -0
  149. package/template/src/types/global.d.ts +47 -0
  150. package/template/src/utils/cacheBuster.ts +20 -0
  151. package/template/src/utils/dom.ts +149 -0
  152. package/template/src/utils/events.ts +203 -0
  153. package/template/src/utils/form.ts +176 -0
  154. package/template/src/utils/formatters.ts +169 -0
  155. package/template/src/utils/helpers.ts +195 -0
  156. package/template/src/utils/markdown.ts +307 -0
  157. package/template/src/utils/sidebar.ts +96 -0
  158. package/template/src/utils/smoothScroll.ts +85 -0
  159. package/template/src/utils/templates.ts +23 -0
  160. package/template/src/utils/validation.ts +73 -0
  161. package/template/src/views/protected/dashboard.html +293 -0
  162. package/template/src/views/public/home.html +150 -0
  163. package/template/src/views/public/login.html +102 -0
  164. package/template/tests/unit/component.test.ts +87 -0
  165. package/template/tests/unit/computed.test.ts +79 -0
  166. package/template/tests/unit/form.test.ts +68 -0
  167. package/template/tests/unit/formatters.test.ts +49 -0
  168. package/template/tests/unit/lazy-components.test.ts +59 -0
  169. package/template/tests/unit/markdown.test.ts +62 -0
  170. package/template/tests/unit/router.test.ts +112 -0
  171. package/template/tests/unit/signals.test.ts +54 -0
  172. package/template/tests/unit/validation.test.ts +50 -0
  173. package/template/tsconfig.build.json +21 -0
  174. package/template/tsconfig.json +51 -0
  175. package/template/vitest.config.ts +36 -0
@@ -0,0 +1,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);