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,151 @@
1
+ /**
2
+ * NcNavItem Component — sidebar / navigation link with icon, label, badge, active state
3
+ *
4
+ * Attributes:
5
+ * href — link URL; if omitted renders as a <button>
6
+ * label — visible text (can also use default slot)
7
+ * icon — preset icon name OR raw SVG string (see below)
8
+ * active — boolean — mark as active
9
+ * disabled — boolean
10
+ * badge — badge count (number string); shown as a pill on the right
11
+ * badge-variant — 'primary'(default)|'success'|'danger'|'warning'
12
+ * indent — indentation level for nested nav (default: 0)
13
+ * target — anchor target (default: '_self')
14
+ * exact — boolean — only activate on exact URL match (router integration)
15
+ *
16
+ * Slots:
17
+ * icon — custom icon (overrides icon attribute)
18
+ * (default) — label text (overrides label attribute)
19
+ * badge — custom badge content
20
+ *
21
+ * Events:
22
+ * nav-click — CustomEvent<{ href: string | null }> — bubbles from both <a> and <button>
23
+ *
24
+ * Usage:
25
+ * <nc-nav-item href="/dashboard" label="Dashboard" icon="home" active></nc-nav-item>
26
+ * <nc-nav-item href="/users" label="Users" icon="users" badge="14"></nc-nav-item>
27
+ */
28
+ import { Component, defineComponent } from '@core/component.js';
29
+
30
+ const NAV_ICONS: Record<string, string> = {
31
+ home: `<path d="M3 9.5L12 3l9 6.5V20a1 1 0 0 1-1 1H14v-5h-4v5H4a1 1 0 0 1-1-1V9.5z"/>`,
32
+ dashboard: `<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/>`,
33
+ users: `<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>`,
34
+ settings: `<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>`,
35
+ chart: `<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>`,
36
+ inbox: `<polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>`,
37
+ file: `<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>`,
38
+ folder: `<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>`,
39
+ bell: `<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/>`,
40
+ lock: `<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>`,
41
+ logout: `<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/>`,
42
+ help: `<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/>`,
43
+ };
44
+
45
+ const svgWrap = (paths: string) =>
46
+ `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${paths}</svg>`;
47
+
48
+ export class NcNavItem extends Component {
49
+ static useShadowDOM = true;
50
+
51
+ static get observedAttributes() { return ['active', 'disabled', 'badge', 'href']; }
52
+
53
+ template() {
54
+ const href = this.getAttribute('href');
55
+ const label = this.getAttribute('label') ?? '';
56
+ const iconName = this.getAttribute('icon') ?? '';
57
+ const active = this.hasAttribute('active');
58
+ const disabled = this.hasAttribute('disabled');
59
+ const badge = this.getAttribute('badge') ?? '';
60
+ const badgeVariant = this.getAttribute('badge-variant') ?? 'primary';
61
+ const indent = parseInt(this.getAttribute('indent') ?? '0', 10);
62
+ const target = this.getAttribute('target') ?? '_self';
63
+
64
+ const iconHtml = NAV_ICONS[iconName]
65
+ ? svgWrap(NAV_ICONS[iconName])
66
+ : iconName.startsWith('<') ? iconName : '';
67
+
68
+ const badgeColors: Record<string, [string,string]> = {
69
+ primary: ['var(--nc-primary)', 'var(--nc-white)'],
70
+ success: ['var(--nc-success)', 'var(--nc-white)'],
71
+ danger: ['var(--nc-danger)', 'var(--nc-white)'],
72
+ warning: ['var(--nc-warning)', 'var(--nc-text)'],
73
+ };
74
+ const [bgColor, textColor] = badgeColors[badgeVariant] ?? badgeColors.primary;
75
+
76
+ const paddingLeft = `calc(var(--nc-spacing-md) + ${indent * 16}px)`;
77
+ const tag = href ? 'a' : 'button';
78
+ const tagAttrs = href
79
+ ? `href="${href}" target="${target}"`
80
+ : `type="button"`;
81
+
82
+ return `
83
+ <style>
84
+ :host { display: block; }
85
+ ${tag} {
86
+ display: flex;
87
+ align-items: center;
88
+ gap: var(--nc-spacing-sm);
89
+ width: 100%;
90
+ padding: 9px ${paddingLeft === 'calc(var(--nc-spacing-md) + 0px)' ? 'var(--nc-spacing-md)' : `var(--nc-spacing-md) var(--nc-spacing-md) var(--nc-spacing-md) ${paddingLeft}`};
91
+ border-radius: var(--nc-radius-md);
92
+ text-decoration: none;
93
+ font-family: var(--nc-font-family);
94
+ font-size: var(--nc-font-size-sm);
95
+ font-weight: ${active ? 'var(--nc-font-weight-semibold)' : 'var(--nc-font-weight-normal)'};
96
+ color: ${active ? 'var(--nc-primary)' : disabled ? 'var(--nc-text-muted)' : 'var(--nc-text-secondary)'};
97
+ background: ${active ? 'rgba(var(--nc-primary-rgb, 99,102,241), 0.1)' : 'transparent'};
98
+ border: none;
99
+ cursor: ${disabled ? 'not-allowed' : 'pointer'};
100
+ opacity: ${disabled ? 0.5 : 1};
101
+ text-align: left;
102
+ outline: none;
103
+ transition: background var(--nc-transition-fast), color var(--nc-transition-fast);
104
+ user-select: none;
105
+ position: relative;
106
+ }
107
+ ${active ? `${tag}::before { content: ''; position: absolute; left: 0; top: 20%; height: 60%; width: 3px; background: var(--nc-primary); border-radius: 0 2px 2px 0; }` : ''}
108
+ ${tag}:hover:not([disabled]) {
109
+ background: ${active ? 'rgba(var(--nc-primary-rgb, 99,102,241), 0.14)' : 'var(--nc-bg-secondary)'};
110
+ color: ${active ? 'var(--nc-primary)' : 'var(--nc-text)'};
111
+ }
112
+ ${tag}:focus-visible { outline: 2px solid var(--nc-primary); outline-offset: -2px; }
113
+ .icon { flex-shrink: 0; display: flex; opacity: ${active ? 1 : 0.65}; }
114
+ .label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
115
+ .badge {
116
+ flex-shrink: 0;
117
+ background: ${bgColor};
118
+ color: ${textColor};
119
+ font-size: 10px;
120
+ font-weight: var(--nc-font-weight-semibold);
121
+ line-height: 1;
122
+ padding: 2px 6px;
123
+ border-radius: 99px;
124
+ min-width: 18px;
125
+ text-align: center;
126
+ }
127
+ </style>
128
+ <${tag} ${tagAttrs} ${disabled ? (href ? 'aria-disabled="true"' : 'disabled') : ''} aria-current="${active ? 'page' : 'false'}">
129
+ ${iconHtml ? `<span class="icon">${iconHtml}<slot name="icon"></slot></span>` : '<slot name="icon"></slot>'}
130
+ <span class="label">${label}<slot></slot></span>
131
+ ${badge ? `<span class="badge">${badge}<slot name="badge"></slot></span>` : '<slot name="badge"></slot>'}
132
+ </${tag}>
133
+ `;
134
+ }
135
+
136
+ onMount() {
137
+ this.shadowRoot!.addEventListener('click', (e) => {
138
+ if (this.hasAttribute('disabled')) { e.preventDefault(); return; }
139
+ this.dispatchEvent(new CustomEvent('nav-click', {
140
+ detail: { href: this.getAttribute('href') },
141
+ bubbles: true, composed: true,
142
+ }));
143
+ });
144
+ }
145
+
146
+ attributeChangedCallback(n: string, o: string, v: string) {
147
+ if (o !== v && this._mounted) this.render();
148
+ }
149
+ }
150
+
151
+ defineComponent('nc-nav-item', NcNavItem);
@@ -0,0 +1,350 @@
1
+ /**
2
+ * NcNumberInput Component
3
+ *
4
+ * NativeCore Framework Core Component
5
+ *
6
+ * Attributes:
7
+ * - name: string — form field name
8
+ * - value: number — current value (default: min or 0)
9
+ * - min: number — minimum value (default: no limit)
10
+ * - max: number — maximum value (default: no limit)
11
+ * - step: number — increment/decrement amount (default: 1)
12
+ * - placeholder: string — placeholder text
13
+ * - disabled: boolean — disabled state
14
+ * - readonly: boolean — read-only state
15
+ * - size: 'sm' | 'md' | 'lg' (default: 'md')
16
+ * - variant: 'default' | 'filled' (default: 'default')
17
+ *
18
+ * Events:
19
+ * - change: CustomEvent<{ value: number; name: string }>
20
+ * - input: CustomEvent<{ value: number; name: string }>
21
+ *
22
+ * Usage:
23
+ * <nc-number-input name="qty" value="1" min="1" max="99"></nc-number-input>
24
+ * <nc-number-input name="price" value="9.99" step="0.01" min="0"></nc-number-input>
25
+ */
26
+
27
+ import { Component, defineComponent } from '@core/component.js';
28
+
29
+ export class NcNumberInput extends Component {
30
+ static useShadowDOM = true;
31
+
32
+ static attributeOptions = {
33
+ variant: ['default', 'filled'],
34
+ size: ['sm', 'md', 'lg']
35
+ };
36
+
37
+ static get observedAttributes() {
38
+ return ['name', 'value', 'min', 'max', 'step', 'placeholder', 'disabled', 'readonly', 'size', 'variant'];
39
+ }
40
+
41
+ // Track hold-to-repeat timer
42
+ private _holdTimer: ReturnType<typeof setTimeout> | null = null;
43
+ private _holdInterval: ReturnType<typeof setInterval> | null = null;
44
+
45
+ constructor() {
46
+ super();
47
+ }
48
+
49
+ private _getNum(attr: string, fallback: number): number {
50
+ const v = this.getAttribute(attr);
51
+ return v !== null && v !== '' ? Number(v) : fallback;
52
+ }
53
+
54
+ private _getCurrentValue(): number {
55
+ const v = this.getAttribute('value');
56
+ return v !== null && v !== '' ? Number(v) : 0;
57
+ }
58
+
59
+ private _clamp(val: number): number {
60
+ const min = this.getAttribute('min');
61
+ const max = this.getAttribute('max');
62
+ if (min !== null && val < Number(min)) return Number(min);
63
+ if (max !== null && val > Number(max)) return Number(max);
64
+ return val;
65
+ }
66
+
67
+ private _atMin(): boolean {
68
+ const min = this.getAttribute('min');
69
+ return min !== null && this._getCurrentValue() <= Number(min);
70
+ }
71
+
72
+ private _atMax(): boolean {
73
+ const max = this.getAttribute('max');
74
+ return max !== null && this._getCurrentValue() >= Number(max);
75
+ }
76
+
77
+ template() {
78
+ const value = this._getCurrentValue();
79
+ const placeholder = this.getAttribute('placeholder') || '';
80
+ const disabled = this.hasAttribute('disabled');
81
+ const readonly = this.hasAttribute('readonly');
82
+ const min = this.getAttribute('min');
83
+ const max = this.getAttribute('max');
84
+ const step = this._getNum('step', 1);
85
+ const atMin = min !== null && value <= Number(min);
86
+ const atMax = max !== null && value >= Number(max);
87
+
88
+ return `
89
+ <style>
90
+ :host {
91
+ display: inline-flex;
92
+ font-family: var(--nc-font-family);
93
+ }
94
+
95
+ .wrap {
96
+ display: inline-flex;
97
+ align-items: stretch;
98
+ border: var(--nc-input-border);
99
+ border-radius: var(--nc-input-radius);
100
+ overflow: hidden;
101
+ transition: border-color var(--nc-transition-fast), box-shadow var(--nc-transition-fast);
102
+ background: var(--nc-bg);
103
+ opacity: ${disabled ? '0.5' : '1'};
104
+ width: 100%;
105
+ }
106
+
107
+ :host([variant="filled"]) .wrap {
108
+ background: var(--nc-bg-tertiary);
109
+ border-color: transparent;
110
+ }
111
+
112
+ .wrap:focus-within {
113
+ border-color: var(--nc-input-focus-border);
114
+ box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.15);
115
+ }
116
+
117
+ .btn {
118
+ display: flex;
119
+ align-items: center;
120
+ justify-content: center;
121
+ flex-shrink: 0;
122
+ background: var(--nc-bg-secondary);
123
+ border: none;
124
+ cursor: ${disabled || readonly ? 'not-allowed' : 'pointer'};
125
+ color: var(--nc-text-muted);
126
+ transition: background var(--nc-transition-fast), color var(--nc-transition-fast);
127
+ user-select: none;
128
+ -webkit-user-select: none;
129
+ padding: 0;
130
+ }
131
+
132
+ :host([size="sm"]) .btn { width: 28px; }
133
+ :host([size="lg"]) .btn { width: 40px; }
134
+ :host,
135
+ :host([size="md"]) { }
136
+ .btn { width: 34px; }
137
+
138
+ .btn:hover:not(:disabled):not([aria-disabled="true"]) {
139
+ background: var(--nc-bg-tertiary);
140
+ color: var(--nc-text);
141
+ }
142
+
143
+ .btn:active:not(:disabled):not([aria-disabled="true"]) {
144
+ background: var(--nc-border);
145
+ }
146
+
147
+ .btn[aria-disabled="true"] {
148
+ opacity: 0.35;
149
+ cursor: not-allowed;
150
+ }
151
+
152
+ input[type="number"] {
153
+ flex: 1;
154
+ min-width: 0;
155
+ border: none;
156
+ outline: none;
157
+ background: transparent;
158
+ color: var(--nc-text);
159
+ font-size: var(--nc-font-size-base);
160
+ font-family: var(--nc-font-family);
161
+ text-align: center;
162
+ padding: 0;
163
+ cursor: ${disabled ? 'not-allowed' : 'auto'};
164
+ -moz-appearance: textfield;
165
+ }
166
+
167
+ input[type="number"]::-webkit-outer-spin-button,
168
+ input[type="number"]::-webkit-inner-spin-button {
169
+ -webkit-appearance: none;
170
+ margin: 0;
171
+ }
172
+
173
+ input[type="number"]::placeholder {
174
+ color: var(--nc-text-muted);
175
+ }
176
+
177
+ :host([size="sm"]) input { font-size: var(--nc-font-size-sm); padding: var(--nc-spacing-xs) 0; }
178
+ :host([size="md"]) input,
179
+ :host input { padding: var(--nc-spacing-sm) 0; }
180
+ :host([size="lg"]) input { font-size: var(--nc-font-size-lg); padding: var(--nc-spacing-md) 0; }
181
+ </style>
182
+
183
+ <div class="wrap">
184
+ <button
185
+ class="btn btn-dec"
186
+ type="button"
187
+ aria-label="Decrease"
188
+ aria-disabled="${atMin || disabled || readonly}"
189
+ tabindex="${disabled ? '-1' : '0'}"
190
+ >
191
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" width="14" height="14">
192
+ <path d="M3 8h10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
193
+ </svg>
194
+ </button>
195
+
196
+ <input
197
+ type="number"
198
+ value="${value}"
199
+ ${min !== null ? `min="${min}"` : ''}
200
+ ${max !== null ? `max="${max}"` : ''}
201
+ step="${step}"
202
+ ${disabled ? 'disabled' : ''}
203
+ ${readonly ? 'readonly' : ''}
204
+ placeholder="${placeholder}"
205
+ name="${this.getAttribute('name') || ''}"
206
+ aria-label="${this.getAttribute('name') || 'number'}"
207
+ />
208
+
209
+ <button
210
+ class="btn btn-inc"
211
+ type="button"
212
+ aria-label="Increase"
213
+ aria-disabled="${atMax || disabled || readonly}"
214
+ tabindex="${disabled ? '-1' : '0'}"
215
+ >
216
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" width="14" height="14">
217
+ <path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
218
+ </svg>
219
+ </button>
220
+ </div>
221
+ `;
222
+ }
223
+
224
+ onMount() {
225
+ this._bindEvents();
226
+ }
227
+
228
+ private _bindEvents() {
229
+ const sr = this.shadowRoot!;
230
+ const input = sr.querySelector<HTMLInputElement>('input[type="number"]')!;
231
+ const decBtn = sr.querySelector<HTMLButtonElement>('.btn-dec')!;
232
+ const incBtn = sr.querySelector<HTMLButtonElement>('.btn-inc')!;
233
+
234
+ // Direct input
235
+ input.addEventListener('input', () => {
236
+ const val = this._clamp(Number(input.value));
237
+ this._updateButtons(val);
238
+ this.dispatchEvent(new CustomEvent('input', {
239
+ bubbles: true, composed: true,
240
+ detail: { value: val, name: this.getAttribute('name') || '' }
241
+ }));
242
+ });
243
+
244
+ input.addEventListener('change', () => {
245
+ const val = this._clamp(Number(input.value));
246
+ input.value = String(val);
247
+ this.setAttribute('value', String(val));
248
+ this._updateButtons(val);
249
+ this.dispatchEvent(new CustomEvent('change', {
250
+ bubbles: true, composed: true,
251
+ detail: { value: val, name: this.getAttribute('name') || '' }
252
+ }));
253
+ });
254
+
255
+ // Scroll on focused input
256
+ input.addEventListener('wheel', (e) => {
257
+ if (document.activeElement !== this && !this.shadowRoot!.activeElement) return;
258
+ e.preventDefault();
259
+ const delta = (e as WheelEvent).deltaY;
260
+ if (delta < 0) {
261
+ this._step(1);
262
+ } else {
263
+ this._step(-1);
264
+ }
265
+ }, { passive: false });
266
+
267
+ // Buttons — click + hold to repeat
268
+ const setupHold = (btn: HTMLButtonElement, dir: 1 | -1) => {
269
+ btn.addEventListener('mousedown', (e) => {
270
+ if (e.button !== 0) return;
271
+ if (btn.getAttribute('aria-disabled') === 'true') return;
272
+ this._step(dir);
273
+ this._holdTimer = setTimeout(() => {
274
+ this._holdInterval = setInterval(() => this._step(dir), 80);
275
+ }, 400);
276
+ });
277
+ };
278
+
279
+ setupHold(decBtn, -1);
280
+ setupHold(incBtn, 1);
281
+
282
+ const stopHold = () => {
283
+ if (this._holdTimer) { clearTimeout(this._holdTimer); this._holdTimer = null; }
284
+ if (this._holdInterval) { clearInterval(this._holdInterval); this._holdInterval = null; }
285
+ };
286
+
287
+ document.addEventListener('mouseup', stopHold);
288
+
289
+ // Keyboard on buttons
290
+ decBtn.addEventListener('click', () => { if (decBtn.getAttribute('aria-disabled') !== 'true') this._step(-1); });
291
+ incBtn.addEventListener('click', () => { if (incBtn.getAttribute('aria-disabled') !== 'true') this._step(1); });
292
+
293
+ // Arrow keys on input
294
+ input.addEventListener('keydown', (e: KeyboardEvent) => {
295
+ if (e.key === 'ArrowUp') { e.preventDefault(); this._step(1); }
296
+ if (e.key === 'ArrowDown') { e.preventDefault(); this._step(-1); }
297
+ });
298
+ }
299
+
300
+ private _step(dir: 1 | -1) {
301
+ if (this.hasAttribute('disabled') || this.hasAttribute('readonly')) return;
302
+ const step = this._getNum('step', 1);
303
+ // Use toFixed to avoid floating point drift (e.g. 0.1 + 0.2)
304
+ const decimals = step.toString().split('.')[1]?.length ?? 0;
305
+ const next = this._clamp(parseFloat((this._getCurrentValue() + dir * step).toFixed(decimals)));
306
+ this._setValue(next);
307
+ }
308
+
309
+ private _setValue(value: number) {
310
+ const input = this.shadowRoot!.querySelector<HTMLInputElement>('input[type="number"]');
311
+ if (input) input.value = String(value);
312
+ this.setAttribute('value', String(value));
313
+ this._updateButtons(value);
314
+ this.dispatchEvent(new CustomEvent('change', {
315
+ bubbles: true, composed: true,
316
+ detail: { value, name: this.getAttribute('name') || '' }
317
+ }));
318
+ }
319
+
320
+ private _updateButtons(value: number) {
321
+ const sr = this.shadowRoot!;
322
+ const min = this.getAttribute('min');
323
+ const max = this.getAttribute('max');
324
+ const decBtn = sr.querySelector<HTMLButtonElement>('.btn-dec');
325
+ const incBtn = sr.querySelector<HTMLButtonElement>('.btn-inc');
326
+ if (decBtn) decBtn.setAttribute('aria-disabled', String(min !== null && value <= Number(min)));
327
+ if (incBtn) incBtn.setAttribute('aria-disabled', String(max !== null && value >= Number(max)));
328
+ }
329
+
330
+ onUnmount() {
331
+ if (this._holdTimer) clearTimeout(this._holdTimer);
332
+ if (this._holdInterval) clearInterval(this._holdInterval);
333
+ }
334
+
335
+ attributeChangedCallback(name: string, oldValue: string, newValue: string) {
336
+ if (oldValue === newValue) return;
337
+ if (name === 'value' && this._mounted) {
338
+ const input = this.shadowRoot!.querySelector<HTMLInputElement>('input[type="number"]');
339
+ if (input) input.value = newValue || '0';
340
+ this._updateButtons(Number(newValue || 0));
341
+ return;
342
+ }
343
+ if (this._mounted) {
344
+ this.render();
345
+ this._bindEvents();
346
+ }
347
+ }
348
+ }
349
+
350
+ defineComponent('nc-number-input', NcNumberInput);