create-bascik 1.0.0-rc.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.
@@ -0,0 +1,844 @@
1
+ /**
2
+ * @module scaffold
3
+ *
4
+ * Core file-creation logic for create-bascik.
5
+ * Separated from the CLI entry so it can be unit-tested without I/O.
6
+ */
7
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
8
+ import { join } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ const SKILL_MD_PATH = fileURLToPath(new URL("../assets/SKILL.md", import.meta.url));
11
+ // ─── Root files ───────────────────────────────────────────────────────────────
12
+ export const PACKAGE_JSON = (name) => JSON.stringify({
13
+ name,
14
+ version: "0.1.0",
15
+ type: "module",
16
+ engines: {
17
+ node: ">=24.0.0",
18
+ },
19
+ scripts: {
20
+ dev: "bascik",
21
+ build: "bascik --build",
22
+ lint: "bascik-language-server --check",
23
+ test: "vitest run",
24
+ "test:watch": "vitest",
25
+ "test:coverage": "vitest run --coverage",
26
+ e2e: "playwright test --config e2e/playwright.config.ts",
27
+ },
28
+ dependencies: {
29
+ "@bascik/bascik": "^1.0.0-rc.1",
30
+ },
31
+ devDependencies: {
32
+ "@bascik/language-server": "^0.1.0-rc.1",
33
+ "@playwright/test": "^1.62.0",
34
+ "@types/node": "^24.0.0",
35
+ "@vitest/coverage-v8": "^4.1.10",
36
+ vitest: "^4.1.10",
37
+ },
38
+ }, null, 2) + "\n";
39
+ export const VSCODE_EXTENSIONS_JSON = JSON.stringify({
40
+ recommendations: [
41
+ "bascik.bascik-vscode",
42
+ ],
43
+ }, null, 2) + "\n";
44
+ export const VSCODE_LAUNCH_JSON = JSON.stringify({
45
+ version: "0.2.0",
46
+ configurations: [
47
+ {
48
+ name: "Debug Dev Server",
49
+ type: "node",
50
+ request: "launch",
51
+ runtimeExecutable: "npx",
52
+ runtimeArgs: ["bascik"],
53
+ console: "integratedTerminal",
54
+ restart: true,
55
+ skipFiles: ["<node_internals>/**"],
56
+ },
57
+ {
58
+ name: "Debug Unit Tests",
59
+ type: "node",
60
+ request: "launch",
61
+ runtimeExecutable: "npx",
62
+ runtimeArgs: ["vitest", "run"],
63
+ console: "integratedTerminal",
64
+ skipFiles: ["<node_internals>/**"],
65
+ },
66
+ {
67
+ name: "Launch Chrome",
68
+ type: "chrome",
69
+ request: "launch",
70
+ url: "http://localhost:8080",
71
+ webRoot: "${workspaceFolder}",
72
+ },
73
+ ],
74
+ }, null, 2) + "\n";
75
+ export const VITE_CONFIG = `import { defineConfig } from 'vite';
76
+
77
+ export default defineConfig({
78
+ test: {
79
+ include: ['src/**/*.test.ts'],
80
+ coverage: {
81
+ provider: 'v8',
82
+ reporter: ['text', 'json', 'html'],
83
+ },
84
+ },
85
+ });
86
+ `;
87
+ export const PLAYWRIGHT_CONFIG = `import { defineConfig } from '@playwright/test';
88
+
89
+ export default defineConfig({
90
+ testDir: './',
91
+ use: {
92
+ baseURL: 'http://localhost:8080',
93
+ headless: true,
94
+ },
95
+ webServer: {
96
+ command: 'npx bascik --build && npx bascik --server --port 8080',
97
+ url: 'http://localhost:8080',
98
+ reuseExistingServer: !process.env.CI,
99
+ },
100
+ });
101
+ `;
102
+ export const E2E_APP_SPEC = `import { test, expect } from '@playwright/test';
103
+
104
+ test.describe('Starter App E2E Tests', () => {
105
+ test('navigates pages and checks titles', async ({ page }) => {
106
+ await page.goto('/');
107
+ await expect(page).toHaveTitle(/Home/);
108
+
109
+ await page.getByRole('link', { name: 'About' }).first().click();
110
+ await expect(page).toHaveURL('/about');
111
+ await expect(page).toHaveTitle(/About/);
112
+ });
113
+
114
+ test('counter component increments and decrements count', async ({ page }) => {
115
+ await page.goto('/');
116
+
117
+ const val = page.getByTestId('counter-val').first();
118
+ const decBtn = page.getByTestId('counter-dec').first();
119
+ const incBtn = page.getByTestId('counter-inc').first();
120
+
121
+ await expect(val).toHaveText('0');
122
+
123
+ await incBtn.click();
124
+ await expect(val).toHaveText('1');
125
+
126
+ await decBtn.click();
127
+ await expect(val).toHaveText('0');
128
+ });
129
+
130
+ test('mobile navigation toggle expands and collapses menu', async ({ page }) => {
131
+ await page.setViewportSize({ width: 375, height: 667 });
132
+ await page.goto('/');
133
+
134
+ const toggle = page.getByTestId('nav-toggle').first();
135
+ const menu = page.getByTestId('nav-menu').first();
136
+
137
+ await expect(toggle).toHaveAttribute('aria-expanded', 'false');
138
+
139
+ await toggle.click();
140
+ await expect(toggle).toHaveAttribute('aria-expanded', 'true');
141
+ await expect(menu).toHaveClass(/is-open/);
142
+
143
+ await toggle.click();
144
+ await expect(toggle).toHaveAttribute('aria-expanded', 'false');
145
+ });
146
+ });
147
+ `;
148
+ export const GITIGNORE = `node_modules/
149
+ dist/
150
+ coverage/
151
+ *.pem
152
+ `;
153
+ // ─── Global CSS ───────────────────────────────────────────────────────────────
154
+ export const STYLES_CSS = `/* ── Design system ── */
155
+ *, *::before, *::after { box-sizing: border-box; }
156
+
157
+ :root {
158
+ --bg: #18191b;
159
+ --surface: #1e2022;
160
+ --elevated: #242628;
161
+ --border: rgba(255,255,255,0.07);
162
+ --border-hover: rgba(211,255,141,0.35);
163
+ --accent: #d3ff8d;
164
+ --accent-dim: rgba(211,255,141,0.12);
165
+ --text: #f0f1f2;
166
+ --text-muted: #8d929e;
167
+ --mono: 'SF Mono','Fira Code',Menlo,monospace;
168
+ --r: 10px;
169
+ --r-sm: 6px;
170
+ }
171
+
172
+ html { scroll-behavior: smooth; }
173
+
174
+ body {
175
+ background: var(--bg);
176
+ color: var(--text);
177
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
178
+ font-size: 16px;
179
+ line-height: 1.7;
180
+ margin: 0;
181
+ min-height: 100vh;
182
+ display: flex;
183
+ flex-direction: column;
184
+ }
185
+
186
+ main { flex: 1; }
187
+
188
+ /* ── Layout ── */
189
+ .container { max-width: 1140px; margin: 0 auto; padding: 0 28px; }
190
+ .section { padding: 72px 0; }
191
+ .section + .section { border-top: 1px solid var(--border); }
192
+
193
+ /* ── Typography ── */
194
+ h1, h2, h3, h4 { margin-top: 0; line-height: 1.25; font-weight: 700; }
195
+ h1 { font-size: clamp(2rem, 5vw, 3.25rem); margin-bottom: 16px; }
196
+ h2 { font-size: clamp(1.4rem, 3vw, 2rem); margin-bottom: 12px; }
197
+ h3 { font-size: 1.05rem; margin-bottom: 6px; }
198
+ p { margin-top: 0; color: var(--text-muted); }
199
+
200
+ a { color: var(--accent); text-decoration: none; transition: opacity .15s; }
201
+ a:hover { opacity: 0.8; }
202
+ p a, li a { text-decoration: underline; text-underline-offset: 3px; }
203
+
204
+ code {
205
+ font-family: var(--mono);
206
+ font-size: 0.84em;
207
+ background: var(--elevated);
208
+ border: 1px solid var(--border);
209
+ border-radius: var(--r-sm);
210
+ padding: 2px 7px;
211
+ color: var(--accent);
212
+ }
213
+
214
+ /* ── Buttons ── */
215
+ .btn {
216
+ display: inline-flex; align-items: center; gap: 8px;
217
+ padding: 10px 22px; border: none; border-radius: var(--r-sm);
218
+ font-size: 0.9rem; font-weight: 600; cursor: pointer;
219
+ text-decoration: none; transition: filter .15s, opacity .15s; line-height: 1;
220
+ }
221
+ .btn-primary { background: var(--accent); color: #18191b; }
222
+ .btn-ghost { background: transparent; color: var(--text); border: 1px solid var(--border); }
223
+ @media (hover: hover) {
224
+ .btn-primary:hover { filter: brightness(0.88); opacity: 1; }
225
+ .btn-ghost:hover { border-color: var(--accent); color: var(--accent); opacity: 1; }
226
+ }
227
+
228
+ /* ── Cards ── */
229
+ .card {
230
+ background: var(--elevated); border: 1px solid var(--border);
231
+ border-radius: var(--r); padding: 24px;
232
+ transition: border-color .2s, box-shadow .2s;
233
+ }
234
+ .card:hover { border-color: var(--border-hover); box-shadow: 0 0 0 1px var(--accent-dim); }
235
+ .card h3 { margin-top: 0; }
236
+ .card p { margin-bottom: 0; }
237
+
238
+ /* ── Grid ── */
239
+ .grid-2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; }
240
+ .grid-3 { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 20px; }
241
+
242
+ /* ── Callout ── */
243
+ .callout {
244
+ background: var(--accent-dim); border: 1px solid rgba(211,255,141,0.22);
245
+ border-radius: var(--r); padding: 16px 20px; margin: 20px 0;
246
+ }
247
+ .callout p { color: var(--text); margin: 0; }
248
+
249
+ /* ── Form ── */
250
+ .form-group { margin-bottom: 20px; }
251
+ .form-group label {
252
+ display: block; font-size: 0.88rem; font-weight: 600;
253
+ color: var(--text); margin-bottom: 6px;
254
+ }
255
+ .form-group input,
256
+ .form-group textarea {
257
+ width: 100%; background: var(--elevated); border: 1px solid var(--border);
258
+ border-radius: var(--r-sm); color: var(--text); font-family: inherit;
259
+ font-size: 0.9rem; padding: 10px 14px; transition: border-color .15s; outline: none;
260
+ }
261
+ .form-group input:focus,
262
+ .form-group textarea:focus { border-color: var(--accent); }
263
+ .form-group textarea { min-height: 120px; resize: vertical; }
264
+
265
+ /* ── Hero ── */
266
+ .hero { padding: 96px 0 72px; text-align: center; }
267
+ .hero h1 span { color: var(--accent); }
268
+ .hero > .container > p { font-size: 1.1rem; max-width: 520px; margin: 0 auto 36px; }
269
+ .hero-actions { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
270
+
271
+ /* ── Utilities ── */
272
+ .text-accent { color: var(--accent); }
273
+ .text-muted { color: var(--text-muted); }
274
+ .section-label {
275
+ font-size: 0.72rem; font-weight: 700; text-transform: uppercase;
276
+ letter-spacing: .1em; color: var(--accent); margin-bottom: 8px;
277
+ }
278
+ .page-intro { font-size: 1.05rem; color: var(--text-muted); max-width: 600px; margin-bottom: 40px; }
279
+
280
+ /* ── Responsive ── */
281
+ @media (max-width: 640px) {
282
+ .section { padding: 48px 0; }
283
+ .hero { padding: 64px 0 48px; }
284
+ }
285
+ `;
286
+ // ─── Components ───────────────────────────────────────────────────────────────
287
+ export const FAVICON_SVG = `<svg viewBox="0 0 32 32" width="32" height="32" xmlns="http://www.w3.org/2000/svg">
288
+ <polygon points="9,2 30,2 23,30 2,30" fill="#d3ff8d" />
289
+ <rect x="15" y="9" width="2" height="14" rx="1" fill="#0e0f10">
290
+ <animate attributeName="opacity" values="0.9;0.9;0;0" keyTimes="0;0.49;0.5;1" dur="1.1s" repeatCount="indefinite" />
291
+ </rect>
292
+ </svg>
293
+ `;
294
+ export const FAVICON_ICO_B64 = `AAABAAMAEBAAAAEAIADIAAAANgAAACAgAAABACAACAEAAP4AAAAwMAAAAQAgAGcBAAAGAgAiVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAeklEQVQ4jWNgoDa4/L93/uX/vf8J4577OAzouU+cAb3zMTSf/9+vQKTm/xf/9yRgGHDxf08CsqINV8vBGJsBIMsI+r+g3Q+MyfZ/AU4DiPR/AQ4DiPL/ZTwGEOX/yzgNICH+C7C7gPj434AlGon2/2UcmGj/0y/9UwoAAEX+8tkvEHwAAAAASUVORK5CYIKJUE5HDQoaCgAAAA1JSERSAAAAIAAAACAIBgAAAHN6evQAAAAJcEhZcwAACxMAAAsTAQCanBgAAAC6SURBVFiF7ZbRDYMwDEQzwcEIDFC6QJXf0jEYKBEjMBJCGYBRXNEFepZshaqx5O+8+HQ+h9DqS22yDEWy2HY6Alu7pNkeIK80QJG8WgOcn1IApMMa4JSVenxr+svF9X88bxKnsZ7+6PpPV/M/1ADG/ocSwNz/UAKY+x8qAIf9D90E7Pc/FAAu+x8KAJf9DxrAKf/BT8An/0ECuOU/SAC3/I/TKPF1r6N/4bvdf4OL/oXqpn/6kfv/r+sNLlrC+A4T1ksAAAAASUVORK5CYIKJUE5HDQoaCgAAAA1JSERSAAAAMAAAADAIBgAAAFcC+YcAAAAJcEhZcwAACxMAAAsTAQCanBgAAAEZSURBVGiB7ZjBDcJADARTAVBCKKKGSwfpgo+PlHDfQDOWSQGpBIwuD/7EVpYIr2Qpr+hGXln2Nk0otL0mzWXSrJii2QGAZhxALqbHsw4t8PEqSskEUH+ABGAdWhNA+F/D/+sV/teY/9vNf3mS0piWerxof/OfxqSH42mpfOv2N//7y/kDUL935//eFSBvv/94Aghi//EEYMT+4wdAmP3fsQMFsv97AQhq//cCYNT+7wNAuPvXqQMFdv96AAjy/vUAYOT9awcgAANi40iAAAn3/00EAB2U7f56m/2eQf30E14yR/0AAAABJRU5ErkJggg==`;
295
+ export const FAVICON_32_PNG_B64 = `iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAuklEQVRYhe2W0Q2DMAxEM8HBCAxQukCV39IxGCgRIzASQhmAUVzRBXqWbIWqseTvvPh0PofQ6kttsgxFsth2OgJbu6TZHiCvNECRvFoDnJ9SAKTDGuCUlXp8a/rLxfV/PG8Sp7Ge/uj6T1fzP9QAxv6HEsDc/1ACmPsfKgCH/Q/dBOz3PxQALvsfCgCX/Q8awCn/wU/AJ/9BArjlP0gAt/yP0yjxda+jf+G73X+Di/6F6qZ/+pH7/6/rDS5awvgOE9ZLAAAAAElFTkSuQmCC`;
296
+ export const APPLE_TOUCH_ICON_PNG_B64 = `iVBORw0KGgoAAAANSUhEUgAAALQAAAC0CAYAAAA9zQYyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGKUlEQVR4nO2bzWqdVRSGN72AtqO0WicWJ1KtoTi0dGjTK8gFGGtnH1ghJpXOw1l0kHoTUvlIU6rQa9Dg2R9W8OcK2guwky0nkIGU0kTfsvZZ63nhnWWwz7ufnTwcSCmEEEIIIYQQQgghhBBCCCGEEEII6SG12Xptsyd0Fm2DrZIxtdlYmzVq0TYYSra0dvfUvM2edTA+bdoNpnZvtWTL4kMDU7zHNG+z54tfViVbFn+WvMen9iY2GEvG4M9hH9RQsgV/jtsJf/a/BGqSDfBnYGrBNsCfO7gE2mQb4M8AZWE2wJ87uARqkg3wZ2CK9pjGkjF8/xy2Q8kWvn+O24nvn/0vgZpkA/wZmKI9prFkDP4ctkPJFvw5bif82f8SqEk2wJ+BKdpjGkvG4M9hO5RswZ/jdsKf/S+BmmQD/BmYoj2msWQM/hy2Q8kW/DluJ/zZ/xKoSTbAn4Ep2mMaS8bgz2E7lGzBn+N2wp/9L4GaZAP8GZiiPaaxZAz+HLZDyRb8OW4n/Nn/EqhJNsCfgSnaYxpLxuDPYTuUbMGf43bCn5evj3/farsPNw77+I9t9/PUToo/L1n3ft1sn3z6YTt95uy/evX65bb3dNP9fNW/+POy9Lufvmzn3l55Ceajnr+w0h4c3HY/Z/Ut/rwM/fnvnfb+6ruvhPmol65cbAcvdtzPW52KPy9Jd/c+ey3MR72/v+F+3upQ/HmJenN77dhAf3Fnzf281af487J0/da1YwO9+Fnv81af4s8dXAJAN81G+HMHoAK0SfbBnzuAFKBNuRH+7A0pQJtyI/zZG1KANtk++HMHkAK0SbbBnzsAFKBNuQ/+7A0oQJtyH/zZG1CANtk2+HMHgAK0SXbBnzuAE6BNuQ3+7A0nQJtyG/zZG04ANtku+HMHcAK0STbBnzsAE6BNuQv+7A0mQJtyF/zZG0yANtkm+HMHYAK0SfbAnzuAEqBNuQn+7A0lQJtyE/zZG0qANtke+HMHUAK0SbbAnzsAEgBNuQf+7A0kQJtyD/zZG0iANtkW+HMHQAK0SXbAnzuAEaBNuQX+7A0jQJtyC/zZG0aANtkO+HMHMAK0STbAnzsAEQBNuQP+7A0iQJtyB/zZG0SANtkG+HMHIAK0ZgP8uQMIAdqUG+DP3hACtCk3wJ+9IQRok22AP3cAIUCbZAP8uQMAAdqUG+DP3gACtCk3wJ+9AQRok22AP3cAIECbZAP8uQP4ANqUG+DP3vABtCk3wJ+94QNok22AP3cAH0CbZAP8uQPwANqUG+DP3uABtCk3wJ+9wQNok22AP3cAHkCbZAP8uQPoANqUG+DP3tABtCk3wJ+9oQNok22AP3cAHUCbZAP8uQPgANqUG+DP3sABtCk3wJ+9gQNok22AP3cAHECbZAP8uQPYANqUG+DP3rABtCk3wJ+9YQNok22AP3cAG0CbZAP8uQPQANqUG+DP3qABtCk3wJ+9QQNok22AP3cAGkCbZAP8uQPIANqUG+DPmXpze62dPnP2WL31zQ3389aTF3/O1N2HG8cG+ttHn7uft56w+HOyHrzYaZeuXHwtzB98/N7hz3qft56g+HPSPji43c5fWHklzG+9c659/8tX7uesJy/+nLV7Tzfb1euXX4L52o2P2v5vX7ufr/634s/Z+8Of2+3+/sZhf/zrjvt56v8o/tzBJVCTbIA/A1O0xzSWjFl88A7Gp02+Af4MWHEe1tTurZZsWXxo7+GpyTfAnwEr2sMaS8bgz2E7lGxp7e6peZs962B82rQb4M9AFeZRzdvs+eKXVcmWxZ8l7/GpvYnW/d2iwC0CBAgAIAAAAAAAAgAAAAAAQIECBAgAIAAAAAAAAgAAAAAAQIECBAgAIAAAAAAAAgAAAAAAQIECBAgAIAAAAAAAAgAAAAAAQIEC`;
297
+ /** Head component — charset, viewport, favicon, CSS link. */
298
+ export const SITE_META_HTML = `<meta charset="UTF-8" />
299
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
300
+ <meta name="description" content="A fast, modern website built with Bascik." />
301
+ <link rel="icon" href="/favicon.ico" sizes="any" />
302
+ <link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png" />
303
+ <link rel="icon" type="image/svg+xml" href="/assets/favicon.svg" />
304
+ <link rel="apple-touch-icon" href="/assets/apple-touch-icon.png" />
305
+ <link rel="stylesheet" href="/css/styles.css" />
306
+ `;
307
+ export const SITE_HEADER_CSS = `.header {
308
+ position: sticky;
309
+ top: 0;
310
+ z-index: 100;
311
+ background: rgba(24, 25, 27, 0.9);
312
+ backdrop-filter: blur(12px);
313
+ -webkit-backdrop-filter: blur(12px);
314
+ border-bottom: 1px solid var(--border);
315
+ }
316
+
317
+ .container { max-width: 1140px; margin: 0 auto; padding: 0 28px; }
318
+
319
+ .nav {
320
+ display: flex;
321
+ align-items: center;
322
+ height: 60px;
323
+ gap: 24px;
324
+ }
325
+
326
+ .logo {
327
+ font-weight: 700;
328
+ font-size: 1rem;
329
+ color: var(--text);
330
+ letter-spacing: -0.01em;
331
+ flex-shrink: 0;
332
+ }
333
+ .logo:hover { opacity: 0.8; }
334
+
335
+ .nav-menu {
336
+ list-style: none;
337
+ margin: 0 0 0 auto;
338
+ padding: 0;
339
+ display: flex;
340
+ gap: 4px;
341
+ }
342
+ .nav-menu a {
343
+ display: block;
344
+ padding: 6px 12px;
345
+ font-size: 0.9rem;
346
+ color: var(--text-muted);
347
+ border-radius: var(--r-sm);
348
+ transition: color .15s, background .15s;
349
+ }
350
+ .nav-menu a:hover { color: var(--text); background: var(--elevated); opacity: 1; }
351
+
352
+ .nav-toggle {
353
+ display: none;
354
+ flex-direction: column;
355
+ justify-content: center;
356
+ gap: 5px;
357
+ width: 36px;
358
+ height: 36px;
359
+ background: none;
360
+ border: none;
361
+ cursor: pointer;
362
+ padding: 6px;
363
+ margin-left: auto;
364
+ border-radius: var(--r-sm);
365
+ }
366
+ .nav-toggle span {
367
+ display: block;
368
+ height: 2px;
369
+ background: var(--text);
370
+ border-radius: 2px;
371
+ }
372
+
373
+ @media (max-width: 640px) {
374
+ .nav-toggle { display: flex; }
375
+
376
+ .nav-menu {
377
+ display: none;
378
+ position: absolute;
379
+ top: 60px;
380
+ left: 0;
381
+ right: 0;
382
+ flex-direction: column;
383
+ gap: 0;
384
+ background: var(--surface);
385
+ border-bottom: 1px solid var(--border);
386
+ padding: 8px 16px;
387
+ }
388
+ .nav-menu.is-open { display: flex; }
389
+ .nav-menu li { width: 100%; }
390
+ .nav-menu a { padding: 10px 4px; }
391
+ }
392
+ `;
393
+ /** Sticky top nav with logo (brand prop) and mobile hamburger. */
394
+ export const SITE_HEADER_HTML = `<style>
395
+ ${SITE_HEADER_CSS}</style>
396
+ <header class="header">
397
+ <div class="container">
398
+ <nav class="nav">
399
+ <a href="/" class="logo" data-bascik-prop-brand>My Site</a>
400
+ <button id="nav-toggle" class="nav-toggle" aria-label="Toggle menu" aria-expanded="false" data-testid="nav-toggle">
401
+ <span></span><span></span><span></span>
402
+ </button>
403
+ <ul id="nav-menu" class="nav-menu" data-testid="nav-menu">
404
+ <li><a href="/">Home</a></li>
405
+ <li><a href="/about">About</a></li>
406
+ <li><a href="/contact">Contact</a></li>
407
+ </ul>
408
+ </nav>
409
+ </div>
410
+ </header>
411
+ <script>
412
+ var toggle = document.getElementById('nav-toggle');
413
+ var menu = document.getElementById('nav-menu');
414
+ toggle.addEventListener('click', function () {
415
+ var open = toggle.getAttribute('aria-expanded') === 'true';
416
+ toggle.setAttribute('aria-expanded', String(!open));
417
+ menu.classList.toggle('is-open');
418
+ });
419
+ </script>
420
+ `;
421
+ export const SITE_FOOTER_CSS = `.footer {
422
+ border-top: 1px solid var(--border);
423
+ padding: 28px 0;
424
+ }
425
+
426
+ .container { max-width: 1140px; margin: 0 auto; padding: 0 28px; }
427
+
428
+ .footer-inner {
429
+ display: flex;
430
+ align-items: center;
431
+ justify-content: space-between;
432
+ gap: 16px;
433
+ flex-wrap: wrap;
434
+ }
435
+
436
+ .footer-copy { font-size: 0.85rem; color: var(--text-muted); }
437
+
438
+ .footer-nav {
439
+ display: flex;
440
+ gap: 20px;
441
+ }
442
+ .footer-nav a {
443
+ font-size: 0.85rem;
444
+ color: var(--text-muted);
445
+ }
446
+ .footer-nav a:hover { color: var(--text); opacity: 1; }
447
+
448
+ @media (max-width: 480px) {
449
+ .footer-inner { flex-direction: column; align-items: flex-start; }
450
+ }
451
+ `;
452
+ /** Footer with brand prop and build-time year. */
453
+ export const SITE_FOOTER_HTML = `<style>
454
+ ${SITE_FOOTER_CSS}</style>
455
+ <footer class="footer">
456
+ <div class="container">
457
+ <div class="footer-inner">
458
+ <span class="footer-copy">
459
+ &copy; <script data-bascik-build>console.log(new Date().getFullYear())</script>
460
+ <span data-bascik-prop-brand>My Site</span>
461
+ </span>
462
+ <nav class="footer-nav" aria-label="Footer navigation">
463
+ <a href="/">Home</a>
464
+ <a href="/about">About</a>
465
+ <a href="/contact">Contact</a>
466
+ </nav>
467
+ </div>
468
+ </div>
469
+ </footer>
470
+ `;
471
+ export const FEAT_CARD_CSS = `.fcard {
472
+ background: var(--elevated);
473
+ border: 1px solid var(--border);
474
+ border-radius: var(--r);
475
+ padding: 24px;
476
+ display: flex;
477
+ flex-direction: column;
478
+ gap: 12px;
479
+ transition: border-color .2s, box-shadow .2s, transform .2s;
480
+ }
481
+ .fcard:hover {
482
+ border-color: var(--border-hover);
483
+ box-shadow: 0 0 0 1px var(--accent-dim), 0 8px 32px rgba(0,0,0,0.3);
484
+ transform: translateY(-2px);
485
+ }
486
+
487
+ .fcard-header {
488
+ font-size: 0.7rem;
489
+ font-weight: 700;
490
+ text-transform: uppercase;
491
+ letter-spacing: .1em;
492
+ color: var(--accent);
493
+ }
494
+ .fcard-header:empty { display: none; }
495
+
496
+ .fcard-body { flex: 1; }
497
+ .fcard-body h3 { margin: 0 0 6px; font-size: 1rem; color: var(--text); }
498
+ .fcard-body p { margin: 0; font-size: 0.88rem; color: var(--text-muted); }
499
+
500
+ .fcard-footer {
501
+ padding-top: 12px;
502
+ border-top: 1px solid var(--border);
503
+ margin-top: auto;
504
+ }
505
+ .fcard-footer:empty { display: none; }
506
+ `;
507
+ /**
508
+ * Card component demonstrating named slots.
509
+ * Slots: header (label/icon area), default (title + body), footer (actions).
510
+ */
511
+ export const FEAT_CARD_HTML = `<style>
512
+ ${FEAT_CARD_CSS}</style>
513
+ <article class="fcard">
514
+ <div class="fcard-header" data-bascik-slot="header"></div>
515
+ <div class="fcard-body" data-bascik-slot></div>
516
+ <div class="fcard-footer" data-bascik-slot="footer"></div>
517
+ </article>
518
+ `;
519
+ export const MY_COUNTER_CSS = `.counter {
520
+ display: inline-flex;
521
+ align-items: center;
522
+ gap: 16px;
523
+ font-size: 1.25rem;
524
+ }
525
+
526
+ .count-val {
527
+ font-family: var(--mono);
528
+ font-size: 1.5rem;
529
+ min-width: 2ch;
530
+ text-align: center;
531
+ color: var(--accent);
532
+ }
533
+
534
+ .btn {
535
+ display: inline-flex; align-items: center; gap: 8px;
536
+ padding: 10px 22px; border: none; border-radius: var(--r-sm);
537
+ font-size: 0.9rem; font-weight: 600; cursor: pointer;
538
+ text-decoration: none; transition: filter .15s, opacity .15s; line-height: 1;
539
+ }
540
+ .btn-primary { background: var(--accent); color: #18191b; }
541
+ .btn-ghost { background: transparent; color: var(--text); border: 1px solid var(--border); }
542
+ @media (hover: hover) {
543
+ .btn-primary:hover { filter: brightness(0.88); opacity: 1; }
544
+ .btn-ghost:hover { border-color: var(--accent); color: var(--accent); opacity: 1; }
545
+ }
546
+ `;
547
+ /** Interactive counter — two instances on a page stay fully independent. */
548
+ export const MY_COUNTER_HTML = `<style>
549
+ ${MY_COUNTER_CSS}</style>
550
+ <div class="counter">
551
+ <button id="btn-dec" class="btn btn-ghost" data-testid="counter-dec">−</button>
552
+ <span id="count-val" class="count-val" data-testid="counter-val">0</span>
553
+ <button id="btn-inc" class="btn btn-primary" data-testid="counter-inc">+</button>
554
+ </div>
555
+ <script>
556
+ (function () {
557
+ var count = 0;
558
+ var val = document.getElementById('count-val');
559
+ document.getElementById('btn-dec').addEventListener('click', function () {
560
+ val.textContent = --count;
561
+ });
562
+ document.getElementById('btn-inc').addEventListener('click', function () {
563
+ val.textContent = ++count;
564
+ });
565
+ }());
566
+ </script>
567
+ `;
568
+ // ─── Component Tests ──────────────────────────────────────────────────────────
569
+ export const SITE_META_TEST = `import { describe, it, expect } from 'vitest';
570
+ import { readFile } from 'node:fs/promises';
571
+ import { join } from 'node:path';
572
+
573
+ describe('site-meta component', () => {
574
+ const path = join(process.cwd(), 'src/components/site-meta/site-meta.html');
575
+
576
+ it('contains charset, viewport, favicon, and stylesheet link tags', async () => {
577
+ const html = await readFile(path, 'utf8');
578
+ expect(html).toContain('charset="UTF-8"');
579
+ expect(html).toContain('name="viewport"');
580
+ expect(html).toContain('name="description"');
581
+ expect(html).toContain('favicon.ico');
582
+ expect(html).toContain('favicon-32x32.png');
583
+ expect(html).toContain('favicon.svg');
584
+ expect(html).toContain('apple-touch-icon.png');
585
+ expect(html).toContain('styles.css');
586
+ });
587
+ });
588
+ `;
589
+ export const SITE_HEADER_TEST = `import { describe, it, expect } from 'vitest';
590
+ import { readFile } from 'node:fs/promises';
591
+ import { join } from 'node:path';
592
+
593
+ describe('site-header component', () => {
594
+ const path = join(process.cwd(), 'src/components/site-header/site-header.html');
595
+
596
+ it('renders brand prop placeholder and navigation links', async () => {
597
+ const html = await readFile(path, 'utf8');
598
+ expect(html).toContain('data-bascik-prop-brand');
599
+ expect(html).toContain('href="/"');
600
+ expect(html).toContain('href="/about"');
601
+ expect(html).toContain('href="/contact"');
602
+ expect(html).toContain('id="nav-toggle"');
603
+ });
604
+ });
605
+ `;
606
+ export const SITE_FOOTER_TEST = `import { describe, it, expect } from 'vitest';
607
+ import { readFile } from 'node:fs/promises';
608
+ import { join } from 'node:path';
609
+
610
+ describe('site-footer component', () => {
611
+ const path = join(process.cwd(), 'src/components/site-footer/site-footer.html');
612
+
613
+ it('renders brand prop placeholder and build-time year script', async () => {
614
+ const html = await readFile(path, 'utf8');
615
+ expect(html).toContain('data-bascik-prop-brand');
616
+ expect(html).toContain('data-bascik-build');
617
+ expect(html).toContain('getFullYear');
618
+ });
619
+ });
620
+ `;
621
+ export const FEAT_CARD_TEST = `import { describe, it, expect } from 'vitest';
622
+ import { readFile } from 'node:fs/promises';
623
+ import { join } from 'node:path';
624
+
625
+ describe('feat-card component', () => {
626
+ const path = join(process.cwd(), 'src/components/feat-card/feat-card.html');
627
+
628
+ it('defines header, default, and footer slots', async () => {
629
+ const html = await readFile(path, 'utf8');
630
+ expect(html).toContain('data-bascik-slot="header"');
631
+ expect(html).toContain('data-bascik-slot');
632
+ expect(html).toContain('data-bascik-slot="footer"');
633
+ });
634
+ });
635
+ `;
636
+ export const MY_COUNTER_TEST = `import { describe, it, expect } from 'vitest';
637
+ import { readFile } from 'node:fs/promises';
638
+ import { join } from 'node:path';
639
+
640
+ describe('my-counter component', () => {
641
+ const path = join(process.cwd(), 'src/components/my-counter/my-counter.html');
642
+
643
+ it('renders increment and decrement controls with scoped script', async () => {
644
+ const html = await readFile(path, 'utf8');
645
+ expect(html).toContain('id="btn-dec"');
646
+ expect(html).toContain('id="btn-inc"');
647
+ expect(html).toContain('id="count-val"');
648
+ expect(html).toContain('addEventListener');
649
+ });
650
+ });
651
+ `;
652
+ // ─── Pages ───────────────────────────────────────────────────────────────────
653
+ const pageShell = (title, brand, body) => `<!DOCTYPE html>
654
+ <html lang="en">
655
+ <head>
656
+ <title>${title}</title>
657
+ <site-meta />
658
+ </head>
659
+ <body>
660
+ <site-header data-bascik-prop-brand="${brand}" />
661
+ <main>
662
+ ${body}
663
+ </main>
664
+ <site-footer data-bascik-prop-brand="${brand}" />
665
+ </body>
666
+ </html>
667
+ `;
668
+ export const indexPage = (brand) => pageShell(`Home — ${brand}`, brand, ` <section class="hero">
669
+ <div class="container">
670
+ <p class="section-label">Welcome</p>
671
+ <h1>Build fast with <span>${brand}</span></h1>
672
+ <p>A Bascik site. HTML components, scoped CSS, and zero runtime overhead.</p>
673
+ <div class="hero-actions">
674
+ <a href="/about" class="btn btn-primary">About</a>
675
+ <a href="/contact" class="btn btn-ghost">Contact</a>
676
+ </div>
677
+ </div>
678
+ </section>
679
+
680
+ <section class="section">
681
+ <div class="container">
682
+ <p class="section-label">Features</p>
683
+ <h2>Why Bascik</h2>
684
+ <p>Each card below is a <code>&lt;feat-card&gt;</code> component with three named slots: <code>header</code>, default, and <code>footer</code>.</p>
685
+ <div class="grid-3" style="margin-top:28px;">
686
+ <feat-card>
687
+ <div data-bascik-slot="header">Zero runtime</div>
688
+ <h3>No framework overhead</h3>
689
+ <p>Components compile at build time. Nothing extra ships to the browser.</p>
690
+ <div data-bascik-slot="footer"><a href="/about" class="btn btn-ghost">Learn more →</a></div>
691
+ </feat-card>
692
+ <feat-card>
693
+ <div data-bascik-slot="header">Scoped CSS</div>
694
+ <h3>Styles that never leak</h3>
695
+ <p>Each component's class names are automatically namespaced at build time.</p>
696
+ <div data-bascik-slot="footer"><a href="/about" class="btn btn-ghost">Learn more →</a></div>
697
+ </feat-card>
698
+ <feat-card>
699
+ <div data-bascik-slot="header">Named slots</div>
700
+ <h3>Flexible composition</h3>
701
+ <p>Pass header, body, and footer content into components from the usage site.</p>
702
+ <div data-bascik-slot="footer"><a href="/about" class="btn btn-ghost">Learn more →</a></div>
703
+ </feat-card>
704
+ </div>
705
+ </div>
706
+ </section>
707
+
708
+ <section class="section">
709
+ <div class="container">
710
+ <p class="section-label">Interactive</p>
711
+ <h2>Counter component</h2>
712
+ <p>Each instance holds its own state. Place the same component twice — they stay independent.</p>
713
+ <div style="display:flex;gap:48px;flex-wrap:wrap;margin-top:24px;">
714
+ <my-counter />
715
+ <my-counter />
716
+ </div>
717
+ </div>
718
+ </section>`);
719
+ export const aboutPage = (brand) => pageShell(`About — ${brand}`, brand, ` <div class="container">
720
+ <section class="section">
721
+ <p class="section-label">About</p>
722
+ <h1>About us</h1>
723
+ <p class="page-intro">This is the about page. Tell visitors who you are and what you do.</p>
724
+
725
+ <div class="grid-2" style="margin-top:32px;">
726
+ <div class="card">
727
+ <h2>Our mission</h2>
728
+ <p>Describe your mission or purpose here. Keep it concise and genuine.</p>
729
+ </div>
730
+ <div class="card">
731
+ <h2>Our approach</h2>
732
+ <p>Explain how you work or what makes you different from others.</p>
733
+ </div>
734
+ </div>
735
+
736
+ <div class="callout" style="margin-top:40px;">
737
+ <p>Want to work together? <a href="/contact">Get in touch →</a></p>
738
+ </div>
739
+ </section>
740
+ </div>`);
741
+ export const contactPage = (brand) => pageShell(`Contact — ${brand}`, brand, ` <div class="container">
742
+ <section class="section">
743
+ <p class="section-label">Contact</p>
744
+ <h1>Get in touch</h1>
745
+ <p class="page-intro">Have a question or want to work together? Fill out the form below.</p>
746
+
747
+ <form style="max-width:520px;" onsubmit="return false;">
748
+ <div class="form-group">
749
+ <label for="name">Name</label>
750
+ <input type="text" id="name" name="name" placeholder="Your name" autocomplete="name" />
751
+ </div>
752
+ <div class="form-group">
753
+ <label for="email">Email</label>
754
+ <input type="email" id="email" name="email" placeholder="you@example.com" autocomplete="email" />
755
+ </div>
756
+ <div class="form-group">
757
+ <label for="message">Message</label>
758
+ <textarea id="message" name="message" placeholder="How can we help?"></textarea>
759
+ </div>
760
+ <button type="submit" class="btn btn-primary">Send message</button>
761
+ </form>
762
+ </section>
763
+ </div>`);
764
+ export const notFoundPage = (brand) => pageShell(`404 — ${brand}`, brand, ` <div class="container">
765
+ <section class="section" style="text-align:center;">
766
+ <p class="section-label">Error</p>
767
+ <h1 style="font-size:clamp(4rem,12vw,8rem);color:var(--accent);margin-bottom:8px;">404</h1>
768
+ <h2 style="margin-bottom:12px;">Page not found</h2>
769
+ <p style="margin-bottom:32px;">The page you're looking for doesn't exist.</p>
770
+ <a href="/" class="btn btn-primary">Go home</a>
771
+ </section>
772
+ </div>`);
773
+ // ─── Validation ───────────────────────────────────────────────────────────────
774
+ /** Returns an error message if the name is invalid, or null if it is fine. */
775
+ export function validateProjectName(name) {
776
+ if (!name)
777
+ return "Project name cannot be empty.";
778
+ if (!/^[a-z0-9@._/-][a-z0-9@._/\- ]*$/i.test(name)) {
779
+ return `"${name}" is not a valid directory name.`;
780
+ }
781
+ return null;
782
+ }
783
+ // ─── Scaffold ─────────────────────────────────────────────────────────────────
784
+ /**
785
+ * Scaffold a new Bascik project into `targetDir/<projectName>`.
786
+ *
787
+ * @param projectName The name used for the directory and package.json name.
788
+ * @param targetDir Parent directory in which to create the project (defaults to cwd).
789
+ */
790
+ export async function scaffold(projectName, targetDir = process.cwd()) {
791
+ const root = join(targetDir, projectName);
792
+ // Create all directories up front
793
+ await Promise.all([
794
+ mkdir(join(root, ".vscode"), { recursive: true }),
795
+ mkdir(join(root, ".github", "skills", "bascik"), { recursive: true }),
796
+ mkdir(join(root, ".claude", "skills", "bascik"), { recursive: true }),
797
+ mkdir(join(root, "e2e"), { recursive: true }),
798
+ mkdir(join(root, "src", "pages", "assets"), { recursive: true }),
799
+ mkdir(join(root, "src", "pages", "css"), { recursive: true }),
800
+ mkdir(join(root, "src", "components", "site-meta"), { recursive: true }),
801
+ mkdir(join(root, "src", "components", "site-header"), { recursive: true }),
802
+ mkdir(join(root, "src", "components", "site-footer"), { recursive: true }),
803
+ mkdir(join(root, "src", "components", "feat-card"), { recursive: true }),
804
+ mkdir(join(root, "src", "components", "my-counter"), { recursive: true }),
805
+ ]);
806
+ const skillMd = await readFile(SKILL_MD_PATH, "utf8");
807
+ await Promise.all([
808
+ // Root
809
+ writeFile(join(root, "package.json"), PACKAGE_JSON(projectName), "utf8"),
810
+ writeFile(join(root, "vite.config.js"), VITE_CONFIG, "utf8"),
811
+ writeFile(join(root, ".gitignore"), GITIGNORE, "utf8"),
812
+ writeFile(join(root, ".vscode", "launch.json"), VSCODE_LAUNCH_JSON, "utf8"),
813
+ writeFile(join(root, ".vscode", "extensions.json"), VSCODE_EXTENSIONS_JSON, "utf8"),
814
+ writeFile(join(root, ".github", "skills", "bascik", "SKILL.md"), skillMd, "utf8"),
815
+ writeFile(join(root, ".claude", "skills", "bascik", "SKILL.md"), skillMd, "utf8"),
816
+ // E2E
817
+ writeFile(join(root, "e2e", "playwright.config.ts"), PLAYWRIGHT_CONFIG, "utf8"),
818
+ writeFile(join(root, "e2e", "app.spec.ts"), E2E_APP_SPEC, "utf8"),
819
+ // Assets
820
+ writeFile(join(root, "src", "pages", "favicon.ico"), Buffer.from(FAVICON_ICO_B64, "base64")),
821
+ writeFile(join(root, "src", "pages", "assets", "favicon-32x32.png"), Buffer.from(FAVICON_32_PNG_B64, "base64")),
822
+ writeFile(join(root, "src", "pages", "assets", "apple-touch-icon.png"), Buffer.from(APPLE_TOUCH_ICON_PNG_B64, "base64")),
823
+ writeFile(join(root, "src", "pages", "assets", "favicon.svg"), FAVICON_SVG, "utf8"),
824
+ // Global CSS
825
+ writeFile(join(root, "src", "pages", "css", "styles.css"), STYLES_CSS, "utf8"),
826
+ // Pages
827
+ writeFile(join(root, "src", "pages", "index.html"), indexPage(projectName), "utf8"),
828
+ writeFile(join(root, "src", "pages", "about.html"), aboutPage(projectName), "utf8"),
829
+ writeFile(join(root, "src", "pages", "contact.html"), contactPage(projectName), "utf8"),
830
+ writeFile(join(root, "src", "pages", "404.html"), notFoundPage(projectName), "utf8"),
831
+ // Components
832
+ writeFile(join(root, "src", "components", "site-meta", "site-meta.html"), SITE_META_HTML, "utf8"),
833
+ writeFile(join(root, "src", "components", "site-meta", "site-meta.test.ts"), SITE_META_TEST, "utf8"),
834
+ writeFile(join(root, "src", "components", "site-header", "site-header.html"), SITE_HEADER_HTML, "utf8"),
835
+ writeFile(join(root, "src", "components", "site-header", "site-header.test.ts"), SITE_HEADER_TEST, "utf8"),
836
+ writeFile(join(root, "src", "components", "site-footer", "site-footer.html"), SITE_FOOTER_HTML, "utf8"),
837
+ writeFile(join(root, "src", "components", "site-footer", "site-footer.test.ts"), SITE_FOOTER_TEST, "utf8"),
838
+ writeFile(join(root, "src", "components", "feat-card", "feat-card.html"), FEAT_CARD_HTML, "utf8"),
839
+ writeFile(join(root, "src", "components", "feat-card", "feat-card.test.ts"), FEAT_CARD_TEST, "utf8"),
840
+ writeFile(join(root, "src", "components", "my-counter", "my-counter.html"), MY_COUNTER_HTML, "utf8"),
841
+ writeFile(join(root, "src", "components", "my-counter", "my-counter.test.ts"), MY_COUNTER_TEST, "utf8"),
842
+ ]);
843
+ }
844
+ //# sourceMappingURL=scaffold.js.map