strata-css 1.0.4 → 1.1.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.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  **A modern CSS framework combining Bootstrap's component architecture with Tailwind's JIT processing.**
6
6
 
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
- [![Version](https://img.shields.io/badge/version-1.0.0-green.svg)]()
8
+ [![Version](https://img.shields.io/badge/version-1.0.4-green.svg)]()
9
9
  [![Node](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg)]()
10
10
  [![npm](https://img.shields.io/badge/npm-strata--css-red.svg)](https://www.npmjs.com/package/strata-css)
11
11
  [![css-framework](https://img.shields.io/badge/css--framework-%E2%9C%93-blue.svg)]()
@@ -41,14 +41,13 @@ Strata is an open source CSS framework that takes the best from Bootstrap and Ta
41
41
 
42
42
  ## Benchmarks
43
43
 
44
- Strata outperforms Tailwind on every metric in watch mode — the speed developers actually feel on every file save.
45
-
46
- | Metric | Strata | Tailwind |
44
+ | Metric | Strata | Tailwind CSS 3 |
47
45
  |---|---|---|
48
- | Cold build average | 1.89ms | 7.21ms |
49
- | Cold build median | 0.15ms | 4.55ms |
50
- | Warm rebuild average | 0.14ms | 2.70ms |
51
- | Warm rebuild p95 | 0.23ms | 6.12ms |
46
+ | Cold build average | 3.82ms | 7.21ms |
47
+ | Cold build median | 3.78ms | 4.55ms |
48
+ | Cold build p95 | 4.40ms | 6.12ms |
49
+
50
+ > Tailwind figures are official watch-mode reference numbers. Strata numbers are from a cold build with cache invalidated on every run — the most conservative possible measurement. Warm rebuilds (the common case in development) are significantly faster as unchanged output is returned from cache with zero reprocessing.
52
51
 
53
52
  Results generated via `npm run benchmark`. See [`benchmark/`](./benchmark/) for the reproducible script.
54
53
 
@@ -75,9 +74,11 @@ npm install strata-css
75
74
  ### Scaffold a new project
76
75
 
77
76
  ```bash
78
- npx strata init
77
+ npx strata-css init
79
78
  ```
80
79
 
80
+ > **Note:** Use `strata-css` (with the hyphen), not `strata` — there is an unrelated npm package called `strata` that will be picked up instead.
81
+
81
82
  This creates:
82
83
  ```
83
84
  strata.config.js ← configuration
@@ -376,6 +377,45 @@ element.setAttribute('data-st-collapsed', 'true')
376
377
 
377
378
  ---
378
379
 
380
+ ## Standalone Packages
381
+
382
+ All Strata plugins are available as independent packages. Use them without Strata, or use them with Strata — the API is identical either way.
383
+
384
+ | Package | Standalone global | With Strata | Install |
385
+ |---|---|---|---|
386
+ | `@strata-css/skeleton-loader` | `SkeletonLoader` | `Strata.skeleton` | `npm i @strata-css/skeleton-loader` |
387
+ | `@strata-css/modal` | `StrataModal` | `Strata.Modal` | `npm i @strata-css/modal` |
388
+ | `@strata-css/chart` | `StrataChart` | `Strata.Chart` | `npm i @strata-css/chart` |
389
+
390
+ ### How detection works
391
+
392
+ When `strata.components.js` is loaded it sets `data-strata` on `<html>`. Each plugin checks for this at runtime and registers under the Strata namespace if present, or its own standalone global if not. **No configuration required from you** — it is automatic.
393
+
394
+ ### Standalone usage (no Strata)
395
+
396
+ ```html
397
+ <!-- Skeleton -->
398
+ <link rel="stylesheet" href="node_modules/@strata-css/skeleton-loader/skeleton-loader.css">
399
+ <script src="node_modules/@strata-css/skeleton-loader/skeleton-loader.js"></script>
400
+ <script>SkeletonLoader.init('.card')</script>
401
+
402
+ <!-- Modal -->
403
+ <link rel="stylesheet" href="node_modules/@strata-css/modal/modal.css">
404
+ <script src="node_modules/@strata-css/modal/modal.js"></script>
405
+ <script>StrataModal.open('#myModal')</script>
406
+
407
+ <!-- Chart (requires Three.js) -->
408
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
409
+ <script src="node_modules/@strata-css/chart/chart.js"></script>
410
+ <script>StrataChart.create('#myChart', { type: 'bar', data: [...] })</script>
411
+ ```
412
+
413
+ ### Migrating from standalone to Strata
414
+
415
+ If you installed a standalone package first and later add Strata, **no code changes are required.** Strata's presence is detected automatically and the plugin re-registers under the Strata namespace. Your existing markup and JS calls continue to work.
416
+
417
+ ---
418
+
379
419
  ## Skeleton Loader
380
420
 
381
421
  Skeleton loading shows animated shimmer placeholders while content is fetching — preventing layout shift and giving users instant visual feedback that something is coming.
@@ -498,6 +538,69 @@ A card that shimmers while data loads, then transitions to real content:
498
538
 
499
539
  ---
500
540
 
541
+ ## Modal
542
+
543
+ Strata's modal is attribute-driven. Open and close modals via `data-st-*` attributes or the JS API — no class toggling.
544
+
545
+ ### Basic usage
546
+
547
+ ```html
548
+ <!-- Trigger -->
549
+ <button data-st-toggle="modal" data-st-target="#myModal">Open Modal</button>
550
+
551
+ <!-- Modal -->
552
+ <div class="modal" id="myModal" aria-hidden="true">
553
+ <div class="modal-dialog">
554
+ <div class="modal-content">
555
+ <div class="modal-header">
556
+ <h5 class="modal-title">Title</h5>
557
+ <button data-st-dismiss="modal">&times;</button>
558
+ </div>
559
+ <div class="modal-body">
560
+ <p>Modal content here.</p>
561
+ </div>
562
+ <div class="modal-footer">
563
+ <button class="btn-secondary" data-st-dismiss="modal">Close</button>
564
+ <button class="btn-primary">Save</button>
565
+ </div>
566
+ </div>
567
+ </div>
568
+ </div>
569
+ ```
570
+
571
+ ### JS API
572
+
573
+ ```js
574
+ Strata.Modal.open('#myModal') // open by selector or element
575
+ Strata.Modal.close() // close current modal
576
+ ```
577
+
578
+ ### Static backdrop
579
+
580
+ ```html
581
+ <div class="modal" data-st-backdrop="static" ...>
582
+ ```
583
+
584
+ Clicking outside the modal shakes it instead of closing it.
585
+
586
+ ### Size variants
587
+
588
+ ```html
589
+ <div class="modal modal-sm"> <!-- 300px -->
590
+ <div class="modal modal-lg"> <!-- 800px -->
591
+ <div class="modal modal-xl"> <!-- 1140px -->
592
+ <div class="modal modal-fullscreen">
593
+ ```
594
+
595
+ ### Events
596
+
597
+ ```js
598
+ document.addEventListener('st:modal:open', e => console.log('opened', e.detail.modal))
599
+ document.addEventListener('st:modal:close', e => console.log('closed', e.detail.modal))
600
+ ```
601
+
602
+ ---
603
+
501
604
  ## Custom CSS
502
605
 
503
606
  Strata uses CSS `@layer` internally. Any CSS you write outside a layer automatically wins over Strata styles.
package/bin/strata.js CHANGED
@@ -79,12 +79,21 @@ async function build(cssMinify = false, jsMinify = true) {
79
79
  }
80
80
 
81
81
  // Bundle + optionally minify JS components
82
+ // Load order: init.js (sets data-strata) → packages (detect Strata, register accordingly)
82
83
  const componentsDir = path.join(__dirname, '..', 'src', 'components', 'modules')
83
- const jsDest = path.join(path.dirname(outputFile), 'strata.components.js')
84
+ const pkgDir = path.join(__dirname, '..', 'packages')
85
+ const packageFiles = [
86
+ path.join(pkgDir, 'modal', 'modal.js'),
87
+ path.join(pkgDir, 'skeleton-loader','skeleton-loader.js'),
88
+ path.join(pkgDir, 'chart', 'chart.js'),
89
+ ]
90
+ const jsDest = path.join(path.dirname(outputFile), 'strata.components.js')
84
91
  if (fs.existsSync(componentsDir)) {
85
92
  const files = fs.readdirSync(componentsDir).filter(f => f.endsWith('.js')).sort()
86
93
  const banner = `/*! Strata Components — built ${new Date().toISOString().slice(0,10)} */\n`
87
- const raw = files.map(f => fs.readFileSync(path.join(componentsDir, f), 'utf8')).join('\n')
94
+ const parts = files.map(f => fs.readFileSync(path.join(componentsDir, f), 'utf8'))
95
+ packageFiles.forEach(p => { if (fs.existsSync(p)) parts.push(fs.readFileSync(p, 'utf8')) })
96
+ const raw = parts.join('\n')
88
97
  const output = jsMinify ? banner + minifyJS(raw) : banner + raw
89
98
  fs.mkdirSync(path.dirname(jsDest), { recursive: true })
90
99
  fs.writeFileSync(jsDest, output)
@@ -166,71 +175,147 @@ function detectFramework(cwd) {
166
175
  return 'generic'
167
176
  }
168
177
 
169
- // ─── Init ─────────────────────────────────────────────────────────────
170
- async function init() {
171
- const isESM = isESMProject(cwd)
172
- const framework = detectFramework(cwd)
173
- const output = detectOutputPath(cwd)
178
+ // ─── Init helpers ─────────────────────────────────────────────────────
179
+
180
+ const PACKAGES = [
181
+ { name: '@strata-css/modal', label: 'modal — attribute-driven modal dialogs' },
182
+ { name: '@strata-css/skeleton-loader',label: 'skeleton-loader — shimmer loading placeholders' },
183
+ { name: '@strata-css/chart', label: 'chart — Three.js data visualisations' },
184
+ ]
185
+
186
+ const FRAMEWORK_DEV = {
187
+ 'astro': 'astro dev',
188
+ 'laravel': 'vite',
189
+ 'next': 'next dev',
190
+ 'sveltekit': 'vite dev',
191
+ 'nuxt': 'nuxt dev',
192
+ 'react-vite': 'vite',
193
+ 'vue-vite': 'vite',
194
+ 'generic': 'npm start'
195
+ }
174
196
 
175
- const frameworkDev = {
176
- 'astro': 'astro dev',
177
- 'laravel': 'vite',
178
- 'next': 'next dev',
179
- 'sveltekit': 'vite dev',
180
- 'nuxt': 'nuxt dev',
181
- 'react-vite': 'vite',
182
- 'vue-vite': 'vite',
183
- 'generic': 'npm start'
184
- }
197
+ const FRAMEWORK_BUILD = {
198
+ 'astro': 'astro build',
199
+ 'laravel': 'vite build',
200
+ 'next': 'next build',
201
+ 'sveltekit': 'vite build',
202
+ 'nuxt': 'nuxt build',
203
+ 'react-vite': 'vite build',
204
+ 'vue-vite': 'vite build',
205
+ 'generic': 'npm run build'
206
+ }
185
207
 
186
- const frameworkBuild = {
187
- 'astro': 'astro build',
188
- 'laravel': 'vite build',
189
- 'next': 'next build',
190
- 'sveltekit': 'vite build',
191
- 'nuxt': 'nuxt build',
192
- 'react-vite': 'vite build',
193
- 'vue-vite': 'vite build',
194
- 'generic': 'npm run build'
195
- }
208
+ const FRAMEWORK_LAYOUT = {
209
+ 'astro': 'src/layouts/Layout.astro',
210
+ 'laravel': 'resources/views/layouts/app.blade.php',
211
+ 'next': 'src/app/layout.tsx',
212
+ 'sveltekit': 'src/routes/+layout.svelte',
213
+ 'nuxt': 'layouts/default.vue',
214
+ 'react-vite': 'index.html',
215
+ 'vue-vite': 'index.html',
216
+ 'generic': 'index.html'
217
+ }
196
218
 
197
- const rl = readline.createInterface({
198
- input: process.stdin,
199
- output: process.stdout
219
+ function askChoice(rl, question, count, defaultChoice = 1) {
220
+ return new Promise(resolve => {
221
+ rl.question(`${question} [${defaultChoice}]: `, answer => {
222
+ const n = parseInt(answer.trim(), 10)
223
+ if (!answer.trim()) return resolve(defaultChoice)
224
+ if (n >= 1 && n <= count) return resolve(n)
225
+ resolve(defaultChoice)
226
+ })
200
227
  })
228
+ }
201
229
 
202
- console.log('')
203
- console.log(' strata Setup initiated.')
204
- console.log('')
205
- console.log(` ◼ Framework detected : ${framework}`)
206
- console.log(` ◼ Project type : ${isESM ? 'ESM' : 'CommonJS'}`)
207
- console.log(` ◼ Output path : ${output}`)
208
- console.log('')
209
-
210
- const installConcurrently = await askYesNo(rl,
211
- ' deps Install concurrently for clean parallel dev?')
230
+ async function askCheckbox(rl, items) {
231
+ const selected = new Set()
232
+ const render = () => {
233
+ console.log('')
234
+ items.forEach((item, i) => {
235
+ const tick = selected.has(i) ? 'x' : ' '
236
+ console.log(` [${tick}] ${i + 1} ${item.label}`)
237
+ })
238
+ console.log('')
239
+ }
240
+ render()
241
+ while (true) {
242
+ const answer = await ask(rl, ' Toggle number(s) or press Enter to confirm: ')
243
+ if (!answer.trim()) break
244
+ answer.trim().split(/[\s,]+/).forEach(token => {
245
+ const n = parseInt(token, 10) - 1
246
+ if (n >= 0 && n < items.length) {
247
+ if (selected.has(n)) selected.delete(n)
248
+ else selected.add(n)
249
+ }
250
+ })
251
+ render()
252
+ }
253
+ return Array.from(selected).map(i => items[i])
254
+ }
212
255
 
213
- const updateScripts = await askYesNo(rl,
214
- ' scripts Auto-update package.json dev and build scripts?')
256
+ function packageUsageSnippet(pkgName) {
257
+ if (pkgName === '@strata-css/modal') return `
258
+ <!-- Modal usage (standalone) -->
259
+ <link rel="stylesheet" href="node_modules/@strata-css/modal/modal.css">
260
+ <script src="node_modules/@strata-css/modal/modal.js"></script>
261
+
262
+ <button data-st-toggle="modal" data-st-target="#myModal">Open</button>
263
+ <div class="modal" id="myModal" aria-hidden="true">
264
+ <div class="modal-dialog"><div class="modal-content">
265
+ <div class="modal-header"><h5 class="modal-title">Title</h5></div>
266
+ <div class="modal-body"><p>Content here.</p></div>
267
+ <div class="modal-footer">
268
+ <button class="btn-secondary" data-st-dismiss="modal">Close</button>
269
+ </div>
270
+ </div></div>
271
+ </div>`
272
+
273
+ if (pkgName === '@strata-css/skeleton-loader') return `
274
+ <!-- Skeleton loader usage (standalone) -->
275
+ <link rel="stylesheet" href="node_modules/@strata-css/skeleton-loader/skeleton-loader.css">
276
+ <script src="node_modules/@strata-css/skeleton-loader/skeleton-loader.js"></script>
277
+
278
+ <div class="card" data-st-skeleton="true">
279
+ <div class="card-body"><p>Content loading...</p></div>
280
+ </div>
281
+ <script>
282
+ SkeletonLoader.init()
283
+ fetchData().then(() => SkeletonLoader.reveal())
284
+ </script>`
285
+
286
+ if (pkgName === '@strata-css/chart') return `
287
+ <!-- Chart usage (standalone, requires Three.js) -->
288
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
289
+ <script src="node_modules/@strata-css/chart/chart.js"></script>
290
+
291
+ <canvas id="myChart"></canvas>
292
+ <script>
293
+ StrataChart.create('#myChart', {
294
+ type: 'bar',
295
+ data: [{ label: 'Jan', value: 42 }, { label: 'Feb', value: 67 }]
296
+ })
297
+ </script>`
215
298
 
216
- console.log('')
217
- console.log(' html Where is your main HTML or layout file?')
218
- console.log(' Examples:')
219
- console.log(' src/layouts/Layout.astro')
220
- console.log(' resources/views/layouts/app.blade.php')
221
- console.log(' src/App.jsx')
222
- console.log(' index.html')
223
- const htmlFile = await ask(rl,
224
- ' Path (leave blank to skip): ')
299
+ return ''
300
+ }
225
301
 
226
- rl.close()
227
- console.log('')
302
+ function buildContentGlob(framework) {
303
+ const base = './src/**/*.{html,jsx,tsx,vue,astro,svelte,js,ts}'
304
+ if (framework === 'laravel') {
305
+ return [base, './resources/views/**/*.blade.php']
306
+ }
307
+ return [base]
308
+ }
228
309
 
229
- // Create config files
310
+ function scaffoldStrataCore(cwd, isESM, output, framework) {
230
311
  const configFile = isESM ? 'strata.config.cjs' : 'strata.config.js'
231
312
  const postcssFile = isESM ? 'postcss.config.cjs' : 'postcss.config.js'
313
+ const globs = buildContentGlob(framework)
314
+ const globStr = globs.length === 1
315
+ ? `["${globs[0]}"]`
316
+ : `[\n "${globs.join('",\n "')}"\n ]`
232
317
 
233
- const configContent = `module.exports = {\n content: ["./src/**/*.{html,jsx,tsx,vue,astro,svelte,js,ts}"],\n input: "./strata.css",\n output: "${output}"\n}\n`
318
+ const configContent = `module.exports = {\n content: ${globStr},\n input: "./strata.css",\n output: "${output}"\n}\n`
234
319
  const postcssContent = `module.exports = {\n plugins: [\n require('strata-css'),\n require('autoprefixer')\n ]\n}\n`
235
320
  const strataCssContent = `@strata base;\n@strata components;\n@strata utilities;\n`
236
321
 
@@ -242,79 +327,189 @@ async function init() {
242
327
  console.log(` ✔ Created: strata.css`)
243
328
  console.log(` ✔ Created: ${postcssFile}`)
244
329
 
245
- // Install concurrently
246
- if (installConcurrently) {
247
- console.log(' ◼ Installing concurrently...')
248
- require('child_process').execSync(
249
- 'npm install --save-dev concurrently',
250
- { stdio: 'inherit', cwd }
251
- )
252
- console.log(' ✔ concurrently installed')
330
+ return { configFile, postcssFile }
331
+ }
332
+
333
+ function updatePackageScripts(cwd, framework, installConcurrently) {
334
+ const strataWatch = 'node node_modules/strata-css/bin/strata.js --watch'
335
+ const strataBuild = 'node node_modules/strata-css/bin/strata.js --build'
336
+ const devCmd = FRAMEWORK_DEV[framework] || 'npm start'
337
+ const buildCmd = FRAMEWORK_BUILD[framework] || 'npm run build'
338
+
339
+ const pkgPath = path.resolve(cwd, 'package.json')
340
+ let pkg
341
+ try { pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')) } catch { return }
342
+ if (!pkg.scripts) pkg.scripts = {}
343
+
344
+ const hasConcurrently = installConcurrently ||
345
+ pkg.dependencies?.['concurrently'] ||
346
+ pkg.devDependencies?.['concurrently']
347
+
348
+ pkg.scripts.dev = hasConcurrently
349
+ ? `concurrently "${strataWatch}" "${devCmd}"`
350
+ : `${strataWatch} & ${devCmd}`
351
+ pkg.scripts.build = `${strataBuild} && ${buildCmd}`
352
+ pkg.scripts['strata:watch'] = strataWatch
353
+ pkg.scripts['strata:build'] = strataBuild
354
+
355
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2))
356
+ console.log(' ✔ Updated: package.json scripts')
357
+ }
358
+
359
+ function injectIntoLayout(cwd, htmlFile, outputCssPath, injectScript) {
360
+ const htmlPath = path.resolve(cwd, htmlFile.trim())
361
+ if (!fs.existsSync(htmlPath)) {
362
+ console.log(` ✗ File not found: ${htmlFile.trim()} — skipped`)
363
+ return
364
+ }
365
+
366
+ let content = fs.readFileSync(htmlPath, 'utf8')
367
+ const cssHref = outputCssPath.replace('./', '/')
368
+ const cssLink = `\t\t<link rel="stylesheet" href="${cssHref}">`
369
+ const scriptTag = `\t\t<script src="${cssHref.replace('strata.output.css', 'strata.components.js')}"></script>`
370
+ const themeAttr = 'data-st-theme="light"'
371
+
372
+ if (!content.includes('strata.output.css')) {
373
+ content = content.replace('</head>', `${cssLink}\n\t</head>`)
253
374
  }
375
+ if (injectScript && !content.includes('strata.components.js')) {
376
+ content = content.replace('</body>', `${scriptTag}\n\t</body>`)
377
+ }
378
+ if (!content.includes('data-st-theme')) {
379
+ content = content.replace('<html', `<html ${themeAttr}`)
380
+ }
381
+
382
+ fs.writeFileSync(htmlPath, content)
383
+ console.log(` ✔ Updated: ${htmlFile.trim()}`)
384
+ }
385
+
386
+ // ─── Init ─────────────────────────────────────────────────────────────
387
+ async function init() {
388
+ const isESM = isESMProject(cwd)
389
+ const framework = detectFramework(cwd)
390
+ const output = detectOutputPath(cwd)
391
+ const exec = require('child_process').execSync
392
+
393
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
394
+
395
+ console.log('')
396
+ console.log(' strata Universal installer')
397
+ console.log('')
398
+ console.log(` ◼ Framework detected : ${framework}`)
399
+ console.log(` ◼ Project type : ${isESM ? 'ESM' : 'CommonJS'}`)
400
+ console.log(` ◼ Output path : ${output}`)
401
+ console.log('')
402
+ console.log(' install What would you like to install?')
403
+ console.log('')
404
+ console.log(' 1 Strata core only')
405
+ console.log(' 2 Strata + all packages (modal, skeleton-loader, chart)')
406
+ console.log(' 3 Strata + select packages')
407
+ console.log(' 4 Single package (no Strata core)')
408
+ console.log(' 5 Multiple packages (no Strata core)')
409
+ console.log('')
254
410
 
255
- // Update package.json scripts
256
- if (updateScripts) {
257
- const strataWatch = 'node node_modules/strata-css/bin/strata.js --watch'
258
- const strataBuild = 'node node_modules/strata-css/bin/strata.js --build'
259
- const devCmd = frameworkDev[framework] || 'npm start'
260
- const buildCmd = frameworkBuild[framework] || 'npm run build'
261
-
262
- const pkgPath = path.resolve(cwd, 'package.json')
263
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
264
- if (!pkg.scripts) pkg.scripts = {}
265
-
266
- const hasConcurrently = installConcurrently ||
267
- pkg.dependencies?.['concurrently'] ||
268
- pkg.devDependencies?.['concurrently']
269
-
270
- pkg.scripts.dev = hasConcurrently
271
- ? `concurrently "${strataWatch}" "${devCmd}"`
272
- : `${strataWatch} & ${devCmd}`
273
- pkg.scripts.build = `${strataBuild} && ${buildCmd}`
274
- pkg.scripts['strata:watch'] = strataWatch
275
- pkg.scripts['strata:build'] = strataBuild
276
-
277
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2))
278
- console.log(' Updated: package.json scripts')
411
+ const installChoice = await askChoice(rl, ' Choice', 5, 1)
412
+ console.log('')
413
+
414
+ const withCore = installChoice <= 3
415
+ const withPackages = installChoice >= 2
416
+
417
+ // ── Determine which packages to install ──────────────────────────────
418
+ let selectedPackages = []
419
+
420
+ if (installChoice === 2) {
421
+ selectedPackages = PACKAGES.slice()
422
+ } else if (installChoice === 3) {
423
+ console.log(' packages Which packages? (type numbers to toggle, Enter to confirm)')
424
+ selectedPackages = await askCheckbox(rl, PACKAGES)
425
+ } else if (installChoice === 4) {
426
+ console.log(' package Which package?')
427
+ console.log('')
428
+ PACKAGES.forEach((p, i) => console.log(` ${i + 1} ${p.label}`))
429
+ console.log('')
430
+ const n = await askChoice(rl, ' Choice', PACKAGES.length, 1)
431
+ selectedPackages = [PACKAGES[n - 1]]
432
+ console.log('')
433
+ } else if (installChoice === 5) {
434
+ console.log(' packages Which packages? (type numbers to toggle, Enter to confirm)')
435
+ selectedPackages = await askCheckbox(rl, PACKAGES)
279
436
  }
280
437
 
281
- // Inject CSS link and theme into HTML file
282
- if (htmlFile && htmlFile.trim()) {
283
- const htmlPath = path.resolve(cwd, htmlFile.trim())
284
- if (fs.existsSync(htmlPath)) {
285
- let content = fs.readFileSync(htmlPath, 'utf8')
286
- const cssLink = `\t\t<link rel="stylesheet" href="/strata.output.css">`
287
- const themeAttr = 'data-st-theme="light"'
438
+ // ── Strata core setup questions ──────────────────────────────────────
439
+ let installConcurrently = false
440
+ let updateScripts = false
441
+ let htmlFile = ''
442
+
443
+ if (withCore) {
444
+ installConcurrently = await askYesNo(rl,
445
+ ' deps Install concurrently for clean parallel dev?')
446
+
447
+ updateScripts = await askYesNo(rl,
448
+ ' scripts Auto-update package.json dev and build scripts?')
449
+
450
+ const defaultLayout = FRAMEWORK_LAYOUT[framework] || 'index.html'
451
+ console.log('')
452
+ console.log(` html Where is your main layout file?`)
453
+ console.log(` Default for ${framework}: ${defaultLayout}`)
454
+ htmlFile = await ask(rl, ` Path (leave blank for default): `)
455
+ if (!htmlFile.trim()) htmlFile = defaultLayout
456
+ console.log('')
457
+ }
288
458
 
289
- if (!content.includes('strata.output.css')) {
290
- content = content.replace('</head>', `${cssLink}\n\t</head>`)
291
- }
292
- if (!content.includes('data-st-theme')) {
293
- content = content.replace('<html', `<html ${themeAttr}`)
294
- }
459
+ rl.close()
460
+
461
+ // ── Execute: Strata core scaffold ─────────────────────────────────────
462
+ if (withCore) {
463
+ scaffoldStrataCore(cwd, isESM, output, framework)
295
464
 
296
- fs.writeFileSync(htmlPath, content)
297
- console.log(` Updated: ${htmlFile.trim()}`)
298
- } else {
299
- console.log(` File not found: ${htmlFile.trim()} — skipped`)
465
+ if (installConcurrently) {
466
+ console.log(' Installing concurrently...')
467
+ exec('npm install --save-dev concurrently', { stdio: 'inherit', cwd })
468
+ console.log(' concurrently installed')
300
469
  }
470
+
471
+ if (updateScripts) updatePackageScripts(cwd, framework, installConcurrently)
472
+ }
473
+
474
+ // ── Execute: install selected packages ───────────────────────────────
475
+ if (selectedPackages.length > 0) {
476
+ const pkgNames = selectedPackages.map(p => p.name).join(' ')
477
+ console.log(` ◼ Installing ${pkgNames}...`)
478
+ exec(`npm install ${pkgNames}`, { stdio: 'inherit', cwd })
479
+ console.log(` ✔ Packages installed`)
301
480
  }
302
481
 
303
- // Run initial build
304
- console.log(' ◼ Running initial build...')
305
- await strata.build(
306
- path.resolve(cwd, 'strata.css'),
307
- path.resolve(cwd, output.replace('./', '')),
308
- { cwd }
309
- )
310
- console.log(' ✔ Initial build complete')
482
+ // ── Execute: inject into layout ──────────────────────────────────────
483
+ if (withCore && htmlFile) {
484
+ injectIntoLayout(cwd, htmlFile, output, withPackages && selectedPackages.length > 0)
485
+ }
311
486
 
487
+ // ── Execute: initial CSS build ───────────────────────────────────────
488
+ if (withCore) {
489
+ console.log(' ◼ Running initial build...')
490
+ await build(false, false)
491
+ console.log(' ✔ Initial build complete')
492
+ }
493
+
494
+ // ── Done ─────────────────────────────────────────────────────────────
312
495
  console.log('')
313
496
  console.log(' strata Setup complete!')
314
497
  console.log('')
315
- console.log(' next Your project is ready.')
316
- console.log(' Run npm run dev to start.')
317
- console.log('')
498
+
499
+ if (withCore) {
500
+ console.log(' next Run npm run dev to start.')
501
+ console.log('')
502
+ }
503
+
504
+ if (!withCore && selectedPackages.length > 0) {
505
+ console.log(' usage Copy the snippets below into your project:')
506
+ selectedPackages.forEach(p => {
507
+ console.log('')
508
+ console.log(` ── ${p.name} ──`)
509
+ console.log(packageUsageSnippet(p.name))
510
+ })
511
+ console.log('')
512
+ }
318
513
  }
319
514
 
320
515
  // ─── Run ──────────────────────────────────────────────────────────────