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,285 @@
1
+ /**
2
+ * NcSlider Component
3
+ *
4
+ * NativeCore Framework Core Component
5
+ *
6
+ * Attributes:
7
+ * - name: string — form field name
8
+ * - value: number — current value (default: min)
9
+ * - min: number — minimum value (default: 0)
10
+ * - max: number — maximum value (default: 100)
11
+ * - step: number — step increment (default: 1)
12
+ * - disabled: boolean — disabled state
13
+ * - show-value: boolean — show current value bubble above thumb
14
+ * - size: 'sm' | 'md' | 'lg' (default: 'md')
15
+ * - variant: 'primary' | 'success' | 'danger' (default: 'primary')
16
+ *
17
+ * Events:
18
+ * - change: CustomEvent<{ value: number; name: string }>
19
+ * - input: CustomEvent<{ value: number; name: string }>
20
+ *
21
+ * Usage:
22
+ * <nc-slider name="volume" min="0" max="100" value="50" show-value></nc-slider>
23
+ * <nc-slider name="opacity" min="0" max="1" step="0.01" value="0.5"></nc-slider>
24
+ */
25
+
26
+ import { Component, defineComponent } from '@core/component.js';
27
+
28
+ export class NcSlider extends Component {
29
+ static useShadowDOM = true;
30
+
31
+ static attributeOptions = {
32
+ variant: ['primary', 'success', 'danger'],
33
+ size: ['sm', 'md', 'lg']
34
+ };
35
+
36
+ static get observedAttributes() {
37
+ return ['name', 'value', 'min', 'max', 'step', 'disabled', 'show-value', 'size', 'variant'];
38
+ }
39
+
40
+ constructor() {
41
+ super();
42
+ }
43
+
44
+ private _getNum(attr: string, fallback: number): number {
45
+ const v = this.getAttribute(attr);
46
+ return v !== null && v !== '' ? Number(v) : fallback;
47
+ }
48
+
49
+ template() {
50
+ const min = this._getNum('min', 0);
51
+ const max = this._getNum('max', 100);
52
+ const step = this._getNum('step', 1);
53
+ const value = this._getNum('value', min);
54
+ const disabled = this.hasAttribute('disabled');
55
+ const showValue = this.hasAttribute('show-value');
56
+
57
+ const pct = max === min ? 0 : ((value - min) / (max - min)) * 100;
58
+
59
+ // Set fill % as a host CSS variable so it can be updated without re-render
60
+ this.style.setProperty('--_fill-pct', `${pct}%`);
61
+
62
+ return `
63
+ <style>
64
+ :host {
65
+ display: block;
66
+ font-family: var(--nc-font-family);
67
+ width: 100%;
68
+ }
69
+
70
+ .slider-wrap {
71
+ position: relative;
72
+ display: flex;
73
+ align-items: center;
74
+ width: 100%;
75
+ padding-top: ${showValue ? '1.75rem' : '0'};
76
+ }
77
+
78
+ input[type="range"] {
79
+ -webkit-appearance: none;
80
+ appearance: none;
81
+ width: 100%;
82
+ background: transparent;
83
+ cursor: ${disabled ? 'not-allowed' : 'pointer'};
84
+ outline: none;
85
+ margin: 0;
86
+ opacity: ${disabled ? '0.5' : '1'};
87
+ }
88
+
89
+ /* Track — WebKit: uses the host CSS variable for live fill */
90
+ input[type="range"]::-webkit-slider-runnable-track {
91
+ height: var(--track-h);
92
+ border-radius: var(--nc-radius-full);
93
+ background: linear-gradient(
94
+ to right,
95
+ var(--track-fill) 0%,
96
+ var(--track-fill) var(--_fill-pct, ${pct}%),
97
+ var(--nc-gray-200) var(--_fill-pct, ${pct}%),
98
+ var(--nc-gray-200) 100%
99
+ );
100
+ }
101
+
102
+ /* Track — Firefox */
103
+ input[type="range"]::-moz-range-track {
104
+ height: var(--track-h);
105
+ border-radius: var(--nc-radius-full);
106
+ background: var(--nc-gray-200);
107
+ }
108
+
109
+ input[type="range"]::-moz-range-progress {
110
+ height: var(--track-h);
111
+ border-radius: var(--nc-radius-full);
112
+ background: var(--track-fill);
113
+ }
114
+
115
+ /* Thumb — WebKit */
116
+ input[type="range"]::-webkit-slider-thumb {
117
+ -webkit-appearance: none;
118
+ appearance: none;
119
+ width: var(--thumb-size);
120
+ height: var(--thumb-size);
121
+ border-radius: var(--nc-radius-full);
122
+ background: var(--nc-white);
123
+ border: 2px solid var(--track-fill);
124
+ box-shadow: var(--nc-shadow-sm);
125
+ margin-top: calc((var(--track-h) - var(--thumb-size)) / 2);
126
+ transition: box-shadow var(--nc-transition-fast), transform var(--nc-transition-fast);
127
+ cursor: ${disabled ? 'not-allowed' : 'pointer'};
128
+ }
129
+
130
+ input[type="range"]::-webkit-slider-thumb:hover {
131
+ box-shadow: 0 0 0 6px color-mix(in srgb, var(--track-fill) 15%, transparent);
132
+ transform: scale(1.1);
133
+ }
134
+
135
+ input[type="range"]::-webkit-slider-thumb:active {
136
+ transform: scale(1.15);
137
+ }
138
+
139
+ /* Thumb — Firefox */
140
+ input[type="range"]::-moz-range-thumb {
141
+ width: var(--thumb-size);
142
+ height: var(--thumb-size);
143
+ border-radius: var(--nc-radius-full);
144
+ background: var(--nc-white);
145
+ border: 2px solid var(--track-fill);
146
+ box-shadow: var(--nc-shadow-sm);
147
+ cursor: ${disabled ? 'not-allowed' : 'pointer'};
148
+ }
149
+
150
+ input[type="range"]:focus-visible::-webkit-slider-thumb {
151
+ box-shadow: 0 0 0 3px var(--nc-bg), 0 0 0 5px var(--track-fill);
152
+ }
153
+
154
+ /* Size tokens */
155
+ :host([size="sm"]),
156
+ :host(:not([size])) {
157
+ --track-h: 3px;
158
+ --thumb-size: 14px;
159
+ }
160
+
161
+ :host([size="md"]) {
162
+ --track-h: 4px;
163
+ --thumb-size: 18px;
164
+ }
165
+
166
+ :host([size="lg"]) {
167
+ --track-h: 6px;
168
+ --thumb-size: 22px;
169
+ }
170
+
171
+ :host {
172
+ --track-h: 4px;
173
+ --thumb-size: 18px;
174
+ }
175
+
176
+ :host,
177
+ :host([variant="primary"]) { --track-fill: var(--nc-primary); }
178
+ :host([variant="success"]) { --track-fill: var(--nc-success); }
179
+ :host([variant="danger"]) { --track-fill: var(--nc-danger); }
180
+
181
+ /* Value bubble — position driven by --_fill-pct */
182
+ .value-bubble {
183
+ position: absolute;
184
+ top: 0;
185
+ left: var(--_fill-pct, ${pct}%);
186
+ transform: translateX(-50%);
187
+ background: var(--track-fill);
188
+ color: var(--nc-white);
189
+ font-size: var(--nc-font-size-xs);
190
+ font-weight: var(--nc-font-weight-semibold);
191
+ padding: 1px 6px;
192
+ border-radius: var(--nc-radius-sm);
193
+ white-space: nowrap;
194
+ pointer-events: none;
195
+ line-height: 1.5;
196
+ }
197
+
198
+ .value-bubble::after {
199
+ content: '';
200
+ position: absolute;
201
+ top: 100%;
202
+ left: 50%;
203
+ transform: translateX(-50%);
204
+ border: 4px solid transparent;
205
+ border-top-color: var(--track-fill);
206
+ }
207
+ </style>
208
+
209
+ <div class="slider-wrap">
210
+ ${showValue ? `<span class="value-bubble">${value}</span>` : ''}
211
+ <input
212
+ type="range"
213
+ min="${min}"
214
+ max="${max}"
215
+ step="${step}"
216
+ value="${value}"
217
+ ${disabled ? 'disabled' : ''}
218
+ name="${this.getAttribute('name') || ''}"
219
+ aria-valuemin="${min}"
220
+ aria-valuemax="${max}"
221
+ aria-valuenow="${value}"
222
+ />
223
+ </div>
224
+ `;
225
+ }
226
+
227
+ onMount() {
228
+ this._bindEvents();
229
+ }
230
+
231
+ private _updateLive(val: number) {
232
+ const min = this._getNum('min', 0);
233
+ const max = this._getNum('max', 100);
234
+ const pct = max === min ? 0 : ((val - min) / (max - min)) * 100;
235
+
236
+ // Update CSS var — drives both the track gradient and bubble position
237
+ this.style.setProperty('--_fill-pct', `${pct}%`);
238
+
239
+ // Update bubble text without touching the shadow DOM structure
240
+ const bubble = this.shadowRoot!.querySelector<HTMLElement>('.value-bubble');
241
+ if (bubble) bubble.textContent = String(val);
242
+ }
243
+
244
+ private _bindEvents() {
245
+ const input = this.shadowRoot!.querySelector<HTMLInputElement>('input[type="range"]');
246
+ if (!input) return;
247
+
248
+ input.addEventListener('input', () => {
249
+ const val = Number(input.value);
250
+ this._updateLive(val);
251
+ this.dispatchEvent(new CustomEvent('input', {
252
+ bubbles: true,
253
+ composed: true,
254
+ detail: { value: val, name: this.getAttribute('name') || '' }
255
+ }));
256
+ });
257
+
258
+ input.addEventListener('change', () => {
259
+ const val = Number(input.value);
260
+ // Safe to commit after drag is done — no longer mid-interaction
261
+ this._isDragging = false;
262
+ this.setAttribute('value', String(val));
263
+ this.dispatchEvent(new CustomEvent('change', {
264
+ bubbles: true,
265
+ composed: true,
266
+ detail: { value: val, name: this.getAttribute('name') || '' }
267
+ }));
268
+ });
269
+
270
+ input.addEventListener('mousedown', () => { this._isDragging = true; });
271
+ input.addEventListener('touchstart', () => { this._isDragging = true; });
272
+ }
273
+
274
+ private _isDragging = false;
275
+
276
+ attributeChangedCallback(name: string, oldValue: string, newValue: string) {
277
+ if (oldValue === newValue) return;
278
+ // Never re-render while dragging or on value-only changes (live updates handle those)
279
+ if (name === 'value' && this._isDragging) return;
280
+ this.render();
281
+ this._bindEvents();
282
+ }
283
+ }
284
+
285
+ defineComponent('nc-slider', NcSlider);
@@ -0,0 +1,230 @@
1
+ /**
2
+ * NcSnackbar Component
3
+ *
4
+ * A singleton toast notification manager. Attach once to the app shell;
5
+ * dispatch 'nc-toast' events from anywhere to show notifications.
6
+ *
7
+ * Attributes:
8
+ * - position: 'top-right'|'top-center'|'top-left'|'bottom-right'|'bottom-center'|'bottom-left'
9
+ * (default: 'bottom-right')
10
+ * - max: number — max visible toasts (default: 5)
11
+ *
12
+ * Programmatic API (after import):
13
+ * NcSnackbar.show({ message, variant?, duration?, dismissible? })
14
+ *
15
+ * Event-based API (no import needed):
16
+ * document.dispatchEvent(new CustomEvent('nc-toast', {
17
+ * detail: { message: 'Saved!', variant: 'success', duration: 3000 }
18
+ * }));
19
+ *
20
+ * Toast options:
21
+ * - message: string
22
+ * - variant: 'default'|'success'|'warning'|'danger'|'info' (default: 'default')
23
+ * - duration: number — ms before auto-dismiss (default: 4000, 0 = sticky)
24
+ * - dismissible: boolean — show close button (default: true)
25
+ * - icon: boolean — show variant icon (default: true)
26
+ */
27
+
28
+ import { Component, defineComponent } from '@core/component.js';
29
+ import { dom } from '@utils/dom.js';
30
+
31
+ interface ToastOptions {
32
+ message: string;
33
+ variant?: string;
34
+ duration?: number;
35
+ dismissible?: boolean;
36
+ icon?: boolean;
37
+ }
38
+
39
+ interface Toast extends Required<ToastOptions> {
40
+ id: number;
41
+ }
42
+
43
+ const ICONS: Record<string, string> = {
44
+ success: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>`,
45
+ danger: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg>`,
46
+ warning: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>`,
47
+ info: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/></svg>`,
48
+ default: '',
49
+ };
50
+
51
+ let _seq = 0;
52
+
53
+ export class NcSnackbar extends Component {
54
+ static useShadowDOM = true;
55
+
56
+ private _toasts: Toast[] = [];
57
+ private _timers = new Map<number, ReturnType<typeof setTimeout>>();
58
+
59
+ static get observedAttributes() { return ['position', 'max']; }
60
+
61
+ // Static helper so other modules can call NcSnackbar.show({...})
62
+ static show(opts: ToastOptions) {
63
+ const el = dom.query<NcSnackbar>('nc-snackbar');
64
+ el?._add(opts);
65
+ }
66
+
67
+ template() {
68
+ const position = this.getAttribute('position') || 'bottom-right';
69
+ const [vPos, hPos] = position.split('-');
70
+
71
+ return `
72
+ <style>
73
+ :host {
74
+ position: fixed;
75
+ ${vPos === 'top' ? 'top: var(--nc-spacing-lg, 1.5rem);' : 'bottom: var(--nc-spacing-lg, 1.5rem);'}
76
+ ${hPos === 'right' ? 'right: var(--nc-spacing-lg, 1.5rem);' : hPos === 'left' ? 'left: var(--nc-spacing-lg, 1.5rem);' : 'left: 50%; transform: translateX(-50%);'}
77
+ z-index: 10000;
78
+ display: flex;
79
+ flex-direction: ${vPos === 'top' ? 'column' : 'column-reverse'};
80
+ gap: var(--nc-spacing-sm);
81
+ pointer-events: none;
82
+ max-width: min(380px, calc(100vw - 2rem));
83
+ width: max-content;
84
+ }
85
+
86
+ .toast {
87
+ display: flex;
88
+ align-items: flex-start;
89
+ gap: var(--nc-spacing-sm);
90
+ padding: var(--nc-spacing-sm) var(--nc-spacing-md);
91
+ border-radius: var(--nc-radius-md, 8px);
92
+ box-shadow: var(--nc-shadow-lg);
93
+ font-family: var(--nc-font-family);
94
+ font-size: var(--nc-font-size-sm);
95
+ pointer-events: all;
96
+ animation: nc-toast-in 0.22s ease forwards;
97
+ min-width: 240px;
98
+ max-width: 380px;
99
+ border: 1px solid transparent;
100
+ }
101
+
102
+ .toast.out { animation: nc-toast-out 0.18s ease forwards; }
103
+
104
+ @keyframes nc-toast-in {
105
+ from { opacity: 0; transform: ${vPos === 'top' ? 'translateY(-8px)' : 'translateY(8px)'}; }
106
+ to { opacity: 1; transform: translateY(0); }
107
+ }
108
+ @keyframes nc-toast-out {
109
+ to { opacity: 0; transform: scale(0.95); }
110
+ }
111
+
112
+ .toast--default { background: var(--nc-bg); color: var(--nc-text); border-color: var(--nc-border); }
113
+ .toast--success { background: #f0fdf4; color: #166534; border-color: #86efac; }
114
+ .toast--danger { background: #fef2f2; color: #991b1b; border-color: #fca5a5; }
115
+ .toast--warning { background: #fffbeb; color: #92400e; border-color: #fcd34d; }
116
+ .toast--info { background: #eff6ff; color: #1e40af; border-color: #93c5fd; }
117
+
118
+ .toast__icon { flex-shrink: 0; margin-top: 1px; }
119
+ .toast__msg { flex: 1; line-height: 1.5; }
120
+
121
+ .toast__close {
122
+ background: none;
123
+ border: none;
124
+ cursor: pointer;
125
+ padding: 0;
126
+ color: inherit;
127
+ opacity: 0.5;
128
+ flex-shrink: 0;
129
+ line-height: 1;
130
+ transition: opacity var(--nc-transition-fast);
131
+ }
132
+ .toast__close:hover { opacity: 1; }
133
+ </style>
134
+ ${this._toasts.map(t => this._renderToast(t)).join('')}
135
+ `;
136
+ }
137
+
138
+ private _renderToast(t: Toast): string {
139
+ const icon = ICONS[t.variant] || '';
140
+ return `
141
+ <div class="toast toast--${t.variant}" data-id="${t.id}" role="alert" aria-live="polite">
142
+ ${icon && t.icon ? `<span class="toast__icon">${icon}</span>` : ''}
143
+ <span class="toast__msg">${t.message}</span>
144
+ ${t.dismissible ? `
145
+ <button class="toast__close" data-dismiss="${t.id}" type="button" aria-label="Dismiss">
146
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" fill="none" width="12" height="12">
147
+ <path d="M2 2l8 8M10 2l-8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
148
+ </svg>
149
+ </button>` : ''}
150
+ </div>`;
151
+ }
152
+
153
+ onMount() {
154
+ this._bindEvents();
155
+
156
+ // Global event listener
157
+ document.addEventListener('nc-toast', (e: Event) => {
158
+ this._add((e as CustomEvent<ToastOptions>).detail);
159
+ });
160
+ }
161
+
162
+ private _bindEvents() {
163
+ this.shadowRoot!.addEventListener('click', (e) => {
164
+ const btn = (e.target as HTMLElement).closest<HTMLElement>('[data-dismiss]');
165
+ if (btn) {
166
+ const id = Number(btn.dataset.dismiss);
167
+ this._dismiss(id);
168
+ }
169
+ });
170
+ }
171
+
172
+ _add(opts: ToastOptions) {
173
+ const max = Number(this.getAttribute('max') || 5);
174
+ const toast: Toast = {
175
+ id: ++_seq,
176
+ message: opts.message,
177
+ variant: opts.variant || 'default',
178
+ duration: opts.duration ?? 4000,
179
+ dismissible: opts.dismissible ?? true,
180
+ icon: opts.icon ?? true,
181
+ };
182
+
183
+ this._toasts.push(toast);
184
+
185
+ // Cap to max
186
+ while (this._toasts.length > max) {
187
+ const removed = this._toasts.shift()!;
188
+ this._clearTimer(removed.id);
189
+ }
190
+
191
+ this.render();
192
+ this._bindEvents();
193
+
194
+ if (toast.duration > 0) {
195
+ const timer = setTimeout(() => this._dismiss(toast.id), toast.duration);
196
+ this._timers.set(toast.id, timer);
197
+ }
198
+ }
199
+
200
+ private _dismiss(id: number) {
201
+ this._clearTimer(id);
202
+ const el = this.shadowRoot!.querySelector<HTMLElement>(`[data-id="${id}"]`);
203
+ if (el) {
204
+ el.classList.add('out');
205
+ el.addEventListener('animationend', () => {
206
+ this._toasts = this._toasts.filter(t => t.id !== id);
207
+ this.render();
208
+ this._bindEvents();
209
+ }, { once: true });
210
+ } else {
211
+ this._toasts = this._toasts.filter(t => t.id !== id);
212
+ }
213
+ }
214
+
215
+ private _clearTimer(id: number) {
216
+ const t = this._timers.get(id);
217
+ if (t) { clearTimeout(t); this._timers.delete(id); }
218
+ }
219
+
220
+ onUnmount() {
221
+ this._timers.forEach(t => clearTimeout(t));
222
+ this._timers.clear();
223
+ }
224
+
225
+ attributeChangedCallback(name: string, oldValue: string, newValue: string) {
226
+ if (oldValue !== newValue && this._mounted) this.render();
227
+ }
228
+ }
229
+
230
+ defineComponent('nc-snackbar', NcSnackbar);