create-nativecore 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/README.md +6 -14
  2. package/bin/index.mjs +402 -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 +642 -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,314 @@
1
+ /**
2
+ * NcCode Component — syntax-highlighted code block with copy button
3
+ *
4
+ * Performs simple tokenization for common languages so it works
5
+ * without a runtime dependency (zero JS syntax libraries).
6
+ *
7
+ * Attributes:
8
+ * language — 'typescript'|'javascript'|'html'|'css'|'json'|'bash'|'text' (default: 'text')
9
+ * label — optional filename / caption shown in header bar
10
+ * no-copy — boolean — hide the copy button
11
+ * no-lines — boolean — hide line numbers
12
+ * max-height — CSS value e.g. '400px' — sets overflow-y: auto
13
+ * highlight — comma-separated 1-based line numbers to highlight (e.g. "2,4,6")
14
+ * wrap — boolean — enable word wrap
15
+ *
16
+ * Slots:
17
+ * (default) — code content (plain text; HTML will be escaped automatically)
18
+ * OR use the `code` attribute for programmatic use.
19
+ *
20
+ * code property:
21
+ * el.code = '...' — set code programmatically (overrides slot content)
22
+ *
23
+ * Usage:
24
+ * <nc-code language="typescript" label="app.ts">
25
+ * const x = useState(0);
26
+ * </nc-code>
27
+ */
28
+ import { Component, defineComponent } from '@core/component.js';
29
+
30
+ // ── Simple tokenizer ─────────────────────────────────────────────────────────
31
+
32
+ const KEYWORDS_JS = new Set(['const','let','var','function','return','import','export','default','class','extends','new','this','super','if','else','for','while','do','switch','case','break','continue','try','catch','finally','throw','typeof','instanceof','in','of','async','await','yield','static','get','set','null','undefined','true','false','void','delete']);
33
+ const KEYWORDS_TS = new Set([...KEYWORDS_JS,'type','interface','enum','namespace','declare','abstract','as','from','implements','keyof','readonly','infer','never','unknown','any','string','number','boolean','object','symbol','bigint']);
34
+ function escHtml(s: string): string {
35
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
36
+ }
37
+
38
+ function tokenize(code: string, lang: string): string {
39
+ if (lang === 'json') return tokenizeJson(code);
40
+ if (lang === 'html') return tokenizeHtml(code);
41
+ if (lang === 'css') return tokenizeCss(code);
42
+ if (lang === 'bash' || lang === 'text') return escHtml(code);
43
+ // JS / TS
44
+ const kw = lang === 'typescript' ? KEYWORDS_TS : KEYWORDS_JS;
45
+ let result = '';
46
+ let i = 0;
47
+ const src = code;
48
+ while (i < src.length) {
49
+ // Single-line comment
50
+ if (src[i] === '/' && src[i+1] === '/') {
51
+ const end = src.indexOf('\n', i);
52
+ const chunk = end === -1 ? src.slice(i) : src.slice(i, end);
53
+ result += `<span class="t-comment">${escHtml(chunk)}</span>`;
54
+ i += chunk.length; continue;
55
+ }
56
+ // Multi-line comment
57
+ if (src[i] === '/' && src[i+1] === '*') {
58
+ const end = src.indexOf('*/', i+2);
59
+ const chunk = end === -1 ? src.slice(i) : src.slice(i, end+2);
60
+ result += `<span class="t-comment">${escHtml(chunk)}</span>`;
61
+ i += chunk.length; continue;
62
+ }
63
+ // Template literal
64
+ if (src[i] === '`') {
65
+ let j = i+1;
66
+ while (j < src.length && src[j] !== '`') {
67
+ if (src[j] === '\\') j++;
68
+ j++;
69
+ }
70
+ const chunk = src.slice(i, j+1);
71
+ result += `<span class="t-string">${escHtml(chunk)}</span>`;
72
+ i = j+1; continue;
73
+ }
74
+ // String
75
+ if (src[i] === '"' || src[i] === "'") {
76
+ const q = src[i]; let j = i+1;
77
+ while (j < src.length && src[j] !== q) {
78
+ if (src[j] === '\\') j++;
79
+ j++;
80
+ }
81
+ const chunk = src.slice(i, j+1);
82
+ result += `<span class="t-string">${escHtml(chunk)}</span>`;
83
+ i = j+1; continue;
84
+ }
85
+ // Number
86
+ if (/[0-9]/.test(src[i]) && (i === 0 || !/\w/.test(src[i-1]))) {
87
+ let j = i;
88
+ while (j < src.length && /[0-9_.xXeEbBoO]/.test(src[j])) j++;
89
+ result += `<span class="t-number">${escHtml(src.slice(i, j))}</span>`;
90
+ i = j; continue;
91
+ }
92
+ // Identifier / keyword
93
+ if (/[a-zA-Z_$]/.test(src[i])) {
94
+ let j = i;
95
+ while (j < src.length && /\w/.test(src[j])) j++;
96
+ const word = src.slice(i, j);
97
+ if (kw.has(word)) {
98
+ result += `<span class="t-keyword">${escHtml(word)}</span>`;
99
+ } else if (/^[A-Z]/.test(word)) {
100
+ result += `<span class="t-class">${escHtml(word)}</span>`;
101
+ } else {
102
+ result += escHtml(word);
103
+ }
104
+ i = j; continue;
105
+ }
106
+ // Operator chars
107
+ if (/[=><!+\-*/%&|^~?:]/.test(src[i])) {
108
+ result += `<span class="t-op">${escHtml(src[i])}</span>`;
109
+ i++; continue;
110
+ }
111
+ result += escHtml(src[i]); i++;
112
+ }
113
+ return result;
114
+ }
115
+
116
+ function tokenizeJson(code: string): string {
117
+ return escHtml(code)
118
+ .replace(/("(?:[^"\\]|\\.)*")\s*:/g, '<span class="t-attr">$1</span>:')
119
+ .replace(/:\s*("(?:[^"\\]|\\.)*")/g, ': <span class="t-string">$1</span>')
120
+ .replace(/:\s*(-?[0-9]+(?:\.[0-9]+)?)/g, ': <span class="t-number">$1</span>')
121
+ .replace(/:\s*(true|false|null)/g, ': <span class="t-keyword">$1</span>');
122
+ }
123
+
124
+ function tokenizeHtml(code: string): string {
125
+ return escHtml(code)
126
+ .replace(/&lt;!--[\s\S]*?--&gt;/g, m => `<span class="t-comment">${m}</span>`)
127
+ .replace(/(&lt;\/?)([\w:-]+)/g, (_, lt, tag) => `${lt}<span class="t-keyword">${tag}</span>`)
128
+ .replace(/([\w:-]+)=(&quot;[^&]*&quot;)/g, `<span class="t-attr">$1</span>=<span class="t-string">$2</span>`);
129
+ }
130
+
131
+ function tokenizeCss(code: string): string {
132
+ return escHtml(code)
133
+ .replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="t-comment">$1</span>')
134
+ .replace(/(#[0-9a-fA-F]{3,8})/g, '<span class="t-number">$1</span>')
135
+ .replace(/([.#]?[\w:-]+)\s*{/g, '<span class="t-class">$1</span> {')
136
+ .replace(/([\w-]+)\s*:/g, '<span class="t-attr">$1</span>:');
137
+ }
138
+
139
+ // ── Component ─────────────────────────────────────────────────────────────────
140
+
141
+ export class NcCode extends Component {
142
+ static useShadowDOM = true;
143
+
144
+ private _code: string | null = null;
145
+
146
+ get code(): string { return this._code ?? ''; }
147
+ set code(v: string) { this._code = v; if (this._mounted) this.render(); }
148
+
149
+ template() {
150
+ const lang = this.getAttribute('language') ?? 'text';
151
+ const label = this.getAttribute('label') ?? '';
152
+ const noCopy = this.hasAttribute('no-copy');
153
+ const noLines = this.hasAttribute('no-lines');
154
+ const maxH = this.getAttribute('max-height') ?? '';
155
+ const wrap = this.hasAttribute('wrap');
156
+ const hlLines = new Set((this.getAttribute('highlight') ?? '').split(',').map(s => parseInt(s.trim(), 10)).filter(Boolean));
157
+
158
+ // Get code from property, attribute, or slot text
159
+ let raw = this._code ?? '';
160
+ if (!raw) {
161
+ const slot = this.shadowRoot?.querySelector('slot');
162
+ if (slot) {
163
+ raw = slot.assignedNodes({ flatten: true })
164
+ .map(n => n.textContent ?? '').join('');
165
+ }
166
+ }
167
+ raw = raw.replace(/^\n/, '').replace(/\n$/, '');
168
+
169
+ const tokens = tokenize(raw, lang);
170
+ const tokenLines = tokens.split('\n');
171
+
172
+ const linesHtml = tokenLines.map((line, i) => {
173
+ const lineNum = i + 1;
174
+ const hl = hlLines.has(lineNum) ? ' class="hl"' : '';
175
+ const lnHtml = noLines ? '' : `<span class="ln" aria-hidden="true">${lineNum}</span>`;
176
+ return `<span${hl}>${lnHtml}<span class="lc">${line || '&ZeroWidthSpace;'}</span></span>`;
177
+ }).join('\n');
178
+
179
+ const langLabels: Record<string, string> = {
180
+ typescript: 'TypeScript', javascript: 'JavaScript', html: 'HTML',
181
+ css: 'CSS', json: 'JSON', bash: 'Bash', text: 'Plain text',
182
+ };
183
+
184
+ return `
185
+ <style>
186
+ :host {
187
+ display: block;
188
+ border-radius: var(--nc-radius-lg);
189
+ overflow: hidden;
190
+ background: var(--nc-code-bg, #1a1b26);
191
+ font-family: var(--nc-font-family-mono, 'SFMono-Regular', Consolas, monospace);
192
+ font-size: var(--nc-font-size-sm);
193
+ line-height: 1.6;
194
+ }
195
+ .header {
196
+ display: flex;
197
+ align-items: center;
198
+ justify-content: space-between;
199
+ padding: 8px 14px;
200
+ background: rgba(255,255,255,.04);
201
+ border-bottom: 1px solid rgba(255,255,255,.06);
202
+ font-size: 12px;
203
+ color: rgba(255,255,255,.5);
204
+ user-select: none;
205
+ }
206
+ .lang-badge {
207
+ font-size: 10px;
208
+ background: rgba(255,255,255,.08);
209
+ border-radius: 4px;
210
+ padding: 1px 7px;
211
+ color: rgba(255,255,255,.6);
212
+ text-transform: uppercase;
213
+ letter-spacing: .04em;
214
+ }
215
+ .copy-btn {
216
+ display: flex;
217
+ align-items: center;
218
+ gap: 5px;
219
+ background: none;
220
+ border: 1px solid rgba(255,255,255,.15);
221
+ border-radius: 5px;
222
+ color: rgba(255,255,255,.6);
223
+ cursor: pointer;
224
+ font-family: inherit;
225
+ font-size: 11px;
226
+ padding: 3px 9px;
227
+ transition: background .15s, color .15s;
228
+ outline: none;
229
+ }
230
+ .copy-btn:hover { background: rgba(255,255,255,.08); color: #fff; }
231
+ .copy-btn.done { color: #4ade80; border-color: #4ade80; }
232
+ pre {
233
+ margin: 0;
234
+ padding: 14px 0;
235
+ overflow-x: auto;
236
+ ${maxH ? `max-height: ${maxH}; overflow-y: auto;` : ''}
237
+ ${wrap ? 'white-space: pre-wrap; word-break: break-word;' : ''}
238
+ scrollbar-width: thin;
239
+ scrollbar-color: rgba(255,255,255,.12) transparent;
240
+ }
241
+ code {
242
+ display: block;
243
+ color: var(--nc-code-text, #c0caf5);
244
+ }
245
+ code > span {
246
+ display: block;
247
+ padding: 0 14px;
248
+ min-height: 1.6em;
249
+ }
250
+ code > span.hl {
251
+ background: rgba(255,255,200,.07);
252
+ border-left: 3px solid #fbbf24;
253
+ padding-left: 11px;
254
+ }
255
+ .ln {
256
+ display: inline-block;
257
+ min-width: 30px;
258
+ color: rgba(255,255,255,.2);
259
+ user-select: none;
260
+ font-size: .9em;
261
+ margin-right: 12px;
262
+ text-align: right;
263
+ }
264
+ .t-keyword { color: #9d7cd8; }
265
+ .t-string { color: #9ece6a; }
266
+ .t-number { color: #ff9e64; }
267
+ .t-comment { color: #565f89; font-style: italic; }
268
+ .t-class { color: #7dcfff; }
269
+ .t-attr { color: #f7768e; }
270
+ .t-op { color: #89ddff; }
271
+ .hidden-slot { display: none; }
272
+ </style>
273
+ <div class="header">
274
+ <span>${label || '&nbsp;'}</span>
275
+ <div style="display:flex;gap:8px;align-items:center">
276
+ <span class="lang-badge">${langLabels[lang] ?? lang}</span>
277
+ ${!noCopy ? `<button class="copy-btn" id="copy-btn" type="button">
278
+ <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
279
+ <rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
280
+ </svg>
281
+ Copy
282
+ </button>` : ''}
283
+ </div>
284
+ </div>
285
+ <pre><code id="code">${linesHtml}</code></pre>
286
+ <div class="hidden-slot"><slot></slot></div>
287
+ `;
288
+ }
289
+
290
+ onMount() {
291
+ this.shadowRoot!.querySelector('slot')?.addEventListener('slotchange', () => {
292
+ if (!this._code) this.render();
293
+ });
294
+
295
+ const copyBtn = this.$<HTMLButtonElement>('#copy-btn');
296
+ if (copyBtn) {
297
+ copyBtn.addEventListener('click', () => this._copy(copyBtn));
298
+ }
299
+ }
300
+
301
+ private _copy(btn: HTMLButtonElement) {
302
+ const raw = this._code ?? this.$<HTMLElement>('#code')?.textContent?.replace(/\u200B/g, '') ?? '';
303
+ navigator.clipboard.writeText(raw).then(() => {
304
+ btn.textContent = 'Copied!';
305
+ btn.classList.add('done');
306
+ setTimeout(() => {
307
+ btn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg> Copy`;
308
+ btn.classList.remove('done');
309
+ }, 2000);
310
+ });
311
+ }
312
+ }
313
+
314
+ defineComponent('nc-code', NcCode);
@@ -0,0 +1,154 @@
1
+ /**
2
+ * NcCollapsible Component — single expand/collapse panel
3
+ *
4
+ * Attributes:
5
+ * open — boolean — expanded state
6
+ * disabled — boolean
7
+ * duration — transition duration ms (default: 250)
8
+ * icon — 'chevron'(default)|'plus'|'arrow'|'none'
9
+ *
10
+ * Slots:
11
+ * trigger — the clickable header content
12
+ * (default) — collapsible body content
13
+ *
14
+ * Events:
15
+ * open — CustomEvent — after opened
16
+ * close — CustomEvent — after closed
17
+ * toggle — CustomEvent<{ open: boolean }>
18
+ *
19
+ * Usage:
20
+ * <nc-collapsible>
21
+ * <span slot="trigger">Section title</span>
22
+ * <p>Hidden content revealed on click.</p>
23
+ * </nc-collapsible>
24
+ */
25
+ import { Component, defineComponent } from '@core/component.js';
26
+
27
+ export class NcCollapsible extends Component {
28
+ static useShadowDOM = true;
29
+
30
+ static get observedAttributes() { return ['open', 'disabled']; }
31
+
32
+ template() {
33
+ const open = this.hasAttribute('open');
34
+ const disabled = this.hasAttribute('disabled');
35
+ const dur = parseInt(this.getAttribute('duration') ?? '250', 10);
36
+ const icon = this.getAttribute('icon') ?? 'chevron';
37
+
38
+ const iconSvg: Record<string, string> = {
39
+ chevron: `<svg class="icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16"
40
+ viewBox="0 0 24 24" fill="none" stroke="currentColor"
41
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
42
+ <polyline points="6 9 12 15 18 9"/>
43
+ </svg>`,
44
+ plus: `<svg class="icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16"
45
+ viewBox="0 0 24 24" fill="none" stroke="currentColor"
46
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
47
+ <line x1="12" y1="5" x2="12" y2="19"/>
48
+ ${open ? '' : '<line x1="5" y1="12" x2="19" y2="12"/>'}
49
+ </svg>`,
50
+ arrow: `<svg class="icon" xmlns="http://www.w3.org/2000/svg" width="14" height="14"
51
+ viewBox="0 0 24 24" fill="none" stroke="currentColor"
52
+ stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
53
+ <polyline points="9 18 15 12 9 6"/>
54
+ </svg>`,
55
+ none: '',
56
+ };
57
+
58
+ return `
59
+ <style>
60
+ :host {
61
+ display: block;
62
+ border: 1px solid var(--nc-border);
63
+ border-radius: var(--nc-radius-md);
64
+ overflow: hidden;
65
+ font-family: var(--nc-font-family);
66
+ }
67
+ .trigger {
68
+ width: 100%;
69
+ display: flex;
70
+ align-items: center;
71
+ justify-content: space-between;
72
+ gap: var(--nc-spacing-sm);
73
+ padding: var(--nc-spacing-md) var(--nc-spacing-lg);
74
+ background: var(--nc-bg);
75
+ border: none;
76
+ cursor: ${disabled ? 'not-allowed' : 'pointer'};
77
+ text-align: left;
78
+ font-family: inherit;
79
+ font-size: var(--nc-font-size-base);
80
+ font-weight: var(--nc-font-weight-medium);
81
+ color: ${disabled ? 'var(--nc-text-muted)' : 'var(--nc-text)'};
82
+ opacity: ${disabled ? 0.5 : 1};
83
+ transition: background var(--nc-transition-fast);
84
+ user-select: none;
85
+ outline: none;
86
+ }
87
+ .trigger:hover:not(:disabled) { background: var(--nc-bg-secondary); }
88
+ .trigger:focus-visible { outline: 2px solid var(--nc-primary); outline-offset: -2px; }
89
+ .icon {
90
+ flex-shrink: 0;
91
+ color: var(--nc-text-muted);
92
+ transform: rotate(${open ? '180' : '0'}deg);
93
+ transition: transform ${dur}ms var(--nc-ease-out);
94
+ }
95
+ :host([icon="arrow"]) .icon {
96
+ transform: rotate(${open ? '90' : '0'}deg);
97
+ }
98
+ .body {
99
+ display: grid;
100
+ grid-template-rows: ${open ? '1fr' : '0fr'};
101
+ transition: grid-template-rows ${dur}ms var(--nc-ease-out);
102
+ }
103
+ .body-inner {
104
+ overflow: hidden;
105
+ }
106
+ .body-content {
107
+ padding: var(--nc-spacing-md) var(--nc-spacing-lg) var(--nc-spacing-lg);
108
+ color: var(--nc-text-secondary);
109
+ font-size: var(--nc-font-size-sm);
110
+ line-height: var(--nc-line-height-relaxed, 1.7);
111
+ border-top: 1px solid var(--nc-border);
112
+ }
113
+ </style>
114
+ <button
115
+ class="trigger"
116
+ type="button"
117
+ aria-expanded="${open}"
118
+ ${disabled ? 'disabled' : ''}
119
+ >
120
+ <slot name="trigger"></slot>
121
+ ${iconSvg[icon] ?? iconSvg.chevron}
122
+ </button>
123
+ <div class="body" role="region">
124
+ <div class="body-inner">
125
+ <div class="body-content">
126
+ <slot></slot>
127
+ </div>
128
+ </div>
129
+ </div>
130
+ `;
131
+ }
132
+
133
+ onMount() {
134
+ this.shadowRoot!.addEventListener('click', (e) => {
135
+ if ((e.target as HTMLElement).closest('.trigger') && !this.hasAttribute('disabled')) {
136
+ this._toggle();
137
+ }
138
+ });
139
+ }
140
+
141
+ private _toggle() {
142
+ const nowOpen = !this.hasAttribute('open');
143
+ if (nowOpen) this.setAttribute('open', '');
144
+ else this.removeAttribute('open');
145
+ this.dispatchEvent(new CustomEvent('toggle', { detail: { open: nowOpen }, bubbles: true, composed: true }));
146
+ this.dispatchEvent(new CustomEvent(nowOpen ? 'open' : 'close', { bubbles: true, composed: true }));
147
+ }
148
+
149
+ attributeChangedCallback(name: string, oldVal: string, newVal: string) {
150
+ if (oldVal !== newVal && this._mounted) this.render();
151
+ }
152
+ }
153
+
154
+ defineComponent('nc-collapsible', NcCollapsible);