strata-css 1.0.3 → 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
@@ -4,6 +4,7 @@
4
4
 
5
5
  const fs = require('fs')
6
6
  const path = require('path')
7
+ const readline = require('readline')
7
8
  const chokidar = require('chokidar')
8
9
  const strata = require('../src/index')
9
10
  const { getWatchFiles } = require('../src/scanner/scanner')
@@ -11,6 +12,20 @@ const { getWatchFiles } = require('../src/scanner/scanner')
11
12
  const args = process.argv.slice(2)
12
13
  const cwd = process.cwd()
13
14
 
15
+ function ask(rl, question) {
16
+ return new Promise(resolve => rl.question(question, resolve))
17
+ }
18
+
19
+ function askYesNo(rl, question, defaultYes = true) {
20
+ const hint = defaultYes ? '[Y/n]' : '[y/N]'
21
+ return new Promise(resolve => {
22
+ rl.question(`${question} ${hint} `, answer => {
23
+ if (!answer.trim()) return resolve(defaultYes)
24
+ resolve(answer.trim().toLowerCase().startsWith('y'))
25
+ })
26
+ })
27
+ }
28
+
14
29
  function loadConfig(cwd) {
15
30
  const configPath = path.resolve(cwd, 'strata.config.js')
16
31
  const configPathCjs = path.resolve(cwd, 'strata.config.cjs')
@@ -64,12 +79,21 @@ async function build(cssMinify = false, jsMinify = true) {
64
79
  }
65
80
 
66
81
  // Bundle + optionally minify JS components
82
+ // Load order: init.js (sets data-strata) → packages (detect Strata, register accordingly)
67
83
  const componentsDir = path.join(__dirname, '..', 'src', 'components', 'modules')
68
- 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')
69
91
  if (fs.existsSync(componentsDir)) {
70
92
  const files = fs.readdirSync(componentsDir).filter(f => f.endsWith('.js')).sort()
71
93
  const banner = `/*! Strata Components — built ${new Date().toISOString().slice(0,10)} */\n`
72
- 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')
73
97
  const output = jsMinify ? banner + minifyJS(raw) : banner + raw
74
98
  fs.mkdirSync(path.dirname(jsDest), { recursive: true })
75
99
  fs.writeFileSync(jsDest, output)
@@ -151,118 +175,345 @@ function detectFramework(cwd) {
151
175
  return 'generic'
152
176
  }
153
177
 
154
- // ─── Script injection ─────────────────────────────────────────────────
155
- function injectScripts(cwd, framework) {
156
- const pkgPath = path.resolve(cwd, 'package.json')
157
- const strataWatch = 'node node_modules/strata-css/bin/strata.js --watch'
158
- const strataBuild = 'node node_modules/strata-css/bin/strata.js --build'
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
+ }
159
196
 
160
- try {
161
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
162
- if (!pkg.scripts) pkg.scripts = {}
163
-
164
- const frameworkDev = {
165
- 'astro': 'astro dev',
166
- 'laravel': 'vite',
167
- 'next': 'next dev',
168
- 'sveltekit': 'vite dev',
169
- 'nuxt': 'nuxt dev',
170
- 'react-vite': 'vite',
171
- 'vue-vite': 'vite',
172
- 'generic': pkg.scripts.dev || 'npm start'
173
- }
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
+ }
174
207
 
175
- const frameworkBuild = {
176
- 'astro': 'astro build',
177
- 'laravel': 'vite build',
178
- 'next': 'next build',
179
- 'sveltekit': 'vite build',
180
- 'nuxt': 'nuxt build',
181
- 'react-vite': 'vite build',
182
- 'vue-vite': 'vite build',
183
- 'generic': pkg.scripts.build || 'npm run build'
184
- }
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
+ }
218
+
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
+ })
227
+ })
228
+ }
185
229
 
186
- const devCmd = frameworkDev[framework]
187
- const buildCmd = frameworkBuild[framework]
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
+ }
188
255
 
189
- const hasConcurrently = pkg.dependencies?.['concurrently'] ||
190
- pkg.devDependencies?.['concurrently']
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>`
298
+
299
+ return ''
300
+ }
191
301
 
192
- if (hasConcurrently) {
193
- pkg.scripts.dev = `concurrently "${devCmd}" "${strataWatch}"`
194
- } else {
195
- pkg.scripts.dev = `${strataWatch} & ${devCmd}`
196
- }
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
+ }
197
309
 
198
- pkg.scripts.build = `${strataBuild} && ${buildCmd}`
199
- pkg.scripts['strata:watch'] = strataWatch
200
- pkg.scripts['strata:build'] = strataBuild
310
+ function scaffoldStrataCore(cwd, isESM, output, framework) {
311
+ const configFile = isESM ? 'strata.config.cjs' : 'strata.config.js'
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 ]`
201
317
 
202
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2))
203
- return true
204
- } catch (e) {
205
- return false
318
+ const configContent = `module.exports = {\n content: ${globStr},\n input: "./strata.css",\n output: "${output}"\n}\n`
319
+ const postcssContent = `module.exports = {\n plugins: [\n require('strata-css'),\n require('autoprefixer')\n ]\n}\n`
320
+ const strataCssContent = `@strata base;\n@strata components;\n@strata utilities;\n`
321
+
322
+ fs.writeFileSync(path.resolve(cwd, configFile), configContent)
323
+ fs.writeFileSync(path.resolve(cwd, postcssFile), postcssContent)
324
+ fs.writeFileSync(path.resolve(cwd, 'strata.css'), strataCssContent)
325
+
326
+ console.log(` ✔ Created: ${configFile}`)
327
+ console.log(` ✔ Created: strata.css`)
328
+ console.log(` ✔ Created: ${postcssFile}`)
329
+
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>`)
206
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()}`)
207
384
  }
208
385
 
209
386
  // ─── Init ─────────────────────────────────────────────────────────────
210
- function init() {
387
+ async function init() {
211
388
  const isESM = isESMProject(cwd)
212
- const output = detectOutputPath(cwd)
213
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('')
410
+
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)
436
+ }
214
437
 
215
- console.log('[Strata] Initializing project...')
216
- console.log(`[Strata] Detected: ${isESM ? 'ESM' : 'CommonJS'} project`)
217
- console.log(`[Strata] Framework: ${framework}`)
218
-
219
- const configFile = isESM ? 'strata.config.cjs' : 'strata.config.js'
220
- const postcssFile = isESM ? 'postcss.config.cjs' : 'postcss.config.js'
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
+ }
221
458
 
222
- const configContent = `module.exports = {\n content: ["./src/**/*.{html,jsx,tsx,vue,astro,svelte,js,ts}"],\n input: "./strata.css",\n output: "${output}"\n}\n`
223
- const postcssContent = `module.exports = {\n plugins: [\n require('strata-css'),\n require('autoprefixer')\n ]\n}\n`
224
- const strataCssContent = `@strata base;\n@strata components;\n@strata utilities;\n`
459
+ rl.close()
225
460
 
226
- fs.writeFileSync(path.resolve(cwd, configFile), configContent)
227
- fs.writeFileSync(path.resolve(cwd, postcssFile), postcssContent)
228
- fs.writeFileSync(path.resolve(cwd, 'strata.css'), strataCssContent)
461
+ // ── Execute: Strata core scaffold ─────────────────────────────────────
462
+ if (withCore) {
463
+ scaffoldStrataCore(cwd, isESM, output, framework)
229
464
 
230
- console.log(`[Strata] Created: ${configFile}`)
231
- console.log(`[Strata] Created: strata.css`)
232
- console.log(`[Strata] Created: ${postcssFile}`)
233
-
234
- const injected = injectScripts(cwd, framework)
235
-
236
- if (injected) {
237
- let hasConcurrently = false
238
- try {
239
- const pkg = JSON.parse(fs.readFileSync(path.resolve(cwd, 'package.json'), 'utf8'))
240
- hasConcurrently = !!(pkg.dependencies?.['concurrently'] || pkg.devDependencies?.['concurrently'])
241
- } catch {}
242
-
243
- console.log(`[Strata] Updated package.json scripts:`)
244
- console.log(`[Strata] dev → runs Strata watcher + framework dev server`)
245
- console.log(`[Strata] build → runs Strata build then framework build`)
246
- console.log(`[Strata] strata:watch → node node_modules/strata-css/bin/strata.js --watch`)
247
- console.log(`[Strata] strata:build → node node_modules/strata-css/bin/strata.js --build`)
248
-
249
- if (!hasConcurrently) {
250
- console.log(`[Strata]`)
251
- console.log(`[Strata] Tip: install concurrently for cleaner parallel dev:`)
252
- console.log(`[Strata] npm install --save-dev concurrently`)
465
+ if (installConcurrently) {
466
+ console.log(' ◼ Installing concurrently...')
467
+ exec('npm install --save-dev concurrently', { stdio: 'inherit', cwd })
468
+ console.log(' ✔ concurrently installed')
253
469
  }
254
- } else {
255
- console.log(`[Strata] Warning: could not update package.json scripts — update manually.`)
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`)
480
+ }
481
+
482
+ // ── Execute: inject into layout ──────────────────────────────────────
483
+ if (withCore && htmlFile) {
484
+ injectIntoLayout(cwd, htmlFile, output, withPackages && selectedPackages.length > 0)
485
+ }
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')
256
492
  }
257
493
 
258
- console.log(`[Strata] Done! Next steps:`)
259
- console.log(`[Strata] 1. Add <link rel="stylesheet" href="/strata.output.css"> to your HTML`)
260
- console.log(`[Strata] 2. Add data-st-theme="light" to your <html> tag`)
261
- console.log(`[Strata] 3. Run: npm run dev`)
494
+ // ── Done ─────────────────────────────────────────────────────────────
495
+ console.log('')
496
+ console.log(' strata Setup complete!')
497
+ 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
+ }
262
513
  }
263
514
 
264
515
  // ─── Run ──────────────────────────────────────────────────────────────
265
- if (args[0] === 'init') init()
516
+ if (args[0] === 'init') init().catch(console.error)
266
517
  else if (args.includes('--watch')) watch()
267
518
  else if (args.includes('--minify')) build(true, true)
268
519
  else if (args.includes('--build')) build(false, true)