swatchkit 0.6.2 → 0.8.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
@@ -201,10 +201,17 @@ SwatchKit automatically bundles your JS files, wraps them in a safety scope (IIF
201
201
  Understanding the build pipeline helps you know which files to edit and which are generated.
202
202
 
203
203
  ### 1. `swatchkit init` (Scaffolding)
204
- Copies "blueprints" into your project to get you started.
204
+ Copies "blueprints" into your project to get you started. Init tracks a manifest of every file it manages (token JSONs, CSS blueprints, layout templates) so it can report what's new, changed, or up to date.
205
+
206
+ * **Fresh project:** Creates directories and copies all blueprint files.
207
+ * **Already initialized:** Prints a status report comparing your files to the latest blueprints. Suggests `--force` if anything has changed.
208
+ * **`--force`:** Overwrites all init-managed files with the latest blueprints. Your custom swatch HTML files and any CSS files without blueprint counterparts are never touched.
209
+ * **`--dry-run`:** Shows what would happen without writing anything.
210
+
211
+ Files created:
205
212
  * **`tokens/*.json`**: These are your **Source of Truth**. You edit these files.
206
- * **`css/`**: Copies static CSS files (`main.css`, `reset.css`, etc.). **You own these files**.
207
- * **`swatchkit/`**: Sets up the documentation structure and layout.
213
+ * **`css/`**: Copies static CSS files (`main.css`, `reset.css`, compositions, etc.). **You own these files**.
214
+ * **`swatchkit/`**: Sets up the documentation structure, layout templates, and token display patterns.
208
215
 
209
216
  ### 2. `swatchkit` (Build Process)
210
217
  Compiles your documentation site into `dist/swatchkit/`.
@@ -228,7 +235,8 @@ SwatchKit includes sensible defaults in `css/variables.css` and `css/global-styl
228
235
  | `css/main.css` | ✅ **YES** | Your entry point. Safe. |
229
236
  | `css/global-styles.css` | ✅ **YES** | You own this. Manually update if you rename tokens. |
230
237
  | `css/tokens.css` | 🚫 **NO** | Overwritten by **every** `swatchkit build` and `swatchkit init`. |
231
- | `swatchkit/_layout.html`| ✅ **YES** | Safe. **Warning:** `swatchkit init --force` WILL overwrite this. |
238
+ | `swatchkit/_layout.html`| ✅ **YES** | Safe during normal use. `init --force` overwrites all init-managed files, including this one. |
239
+ | `swatchkit/_preview.html`| ✅ **YES** | Same as `_layout.html` — safe unless you run `init --force`. |
232
240
  | `swatchkit/tokens/*.html`| 🚫 **NO** | Overwritten by `swatchkit build` (visual previews). |
233
241
 
234
242
  ## CLI Reference
@@ -240,17 +248,20 @@ swatchkit [command] [options]
240
248
  ### Commands
241
249
 
242
250
  - `swatchkit` (Default): Builds the library.
243
- - `swatchkit init`: Scaffolds the layout and token files.
251
+ - `swatchkit init`: Scaffolds the layout and token files. If the project is already initialized, prints a status report showing which files differ from their blueprints (auto dry-run).
252
+ - `swatchkit init --force`: Overwrites all init-managed files with the latest blueprints. Custom swatch files and CSS files without blueprint counterparts are never touched.
253
+ - `swatchkit init --dry-run`: Shows what would be created or changed without writing anything.
244
254
 
245
255
  ### Flags
246
256
 
247
- | Flag | Short | Description |
248
- | :--------- | :---- | :---------------------------------------------- |
249
- | `--watch` | `-w` | Watch files and rebuild on change. |
250
- | `--config` | `-c` | Path to config file. |
251
- | `--input` | `-i` | Pattern directory (Default: `swatchkit/`). |
252
- | `--outDir` | `-o` | Output directory (Default: `dist/swatchkit`). |
253
- | `--force` | `-f` | Overwrite layout file during init. |
257
+ | Flag | Short | Description |
258
+ | :---------- | :---- | :-------------------------------------------------------------- |
259
+ | `--watch` | `-w` | Watch files and rebuild on change. |
260
+ | `--config` | `-c` | Path to config file. |
261
+ | `--input` | `-i` | Pattern directory (Default: `swatchkit/`). |
262
+ | `--outDir` | `-o` | Output directory (Default: `dist/swatchkit`). |
263
+ | `--force` | `-f` | Overwrite all init-managed files with latest blueprints. |
264
+ | `--dry-run` | | Show what init would create or change, without writing anything.|
254
265
 
255
266
  ## Configuration
256
267
 
@@ -270,11 +281,83 @@ module.exports = {
270
281
  // Override tokens directory (JSON token definitions)
271
282
  tokensDir: "./src/tokens",
272
283
 
284
+ // Skip copying CSS into SwatchKit's output directory.
285
+ // When false, SwatchKit references CSS at cssPath instead of copying it.
286
+ // See "Common Workflows" below for when to use this.
287
+ cssCopy: false,
288
+
289
+ // Relative path from SwatchKit's HTML to your CSS files.
290
+ // Only relevant when cssCopy is false.
291
+ // Defaults to "../<basename of cssDir>/" (e.g., "../css/" if cssDir is "./src/css").
292
+ // Set explicitly if your deployed CSS lives at a different path.
293
+ cssPath: "../css/",
294
+
273
295
  // Exclude files (supports glob patterns)
274
296
  exclude: ["*.test.js", "temp*"],
275
297
  };
276
298
  ```
277
299
 
300
+ ## Common Workflows
301
+
302
+ ### Deploy alongside your project
303
+
304
+ If your build tool (Vite, Astro, etc.) already outputs CSS to `dist/`, you don't need SwatchKit to copy it again. Set `cssCopy: false` and SwatchKit's HTML will reference your existing CSS.
305
+
306
+ ```javascript
307
+ // swatchkit.config.js
308
+ module.exports = {
309
+ cssDir: "./src/css",
310
+ cssCopy: false,
311
+ };
312
+ ```
313
+
314
+ ```
315
+ dist/
316
+ ├── css/ # Your build tool puts CSS here
317
+ │ ├── main.css
318
+ │ └── tokens.css
319
+ ├── index.html # Your project
320
+ └── swatchkit/
321
+ └── index.html # References ../css/main.css
322
+ ```
323
+
324
+ SwatchKit derives the default `cssPath` from your `cssDir` name. If `cssDir` is `"./src/css"`, it defaults to `"../css/"`. If `cssDir` is `"./styles"`, it defaults to `"../styles/"`.
325
+
326
+ If your deployed CSS ends up somewhere else (e.g., Vite hashes it into `dist/assets/`), set `cssPath` explicitly:
327
+
328
+ ```javascript
329
+ module.exports = {
330
+ cssDir: "./src/css",
331
+ cssCopy: false,
332
+ cssPath: "../assets/",
333
+ };
334
+ ```
335
+
336
+ ### Local dev only (self-contained)
337
+
338
+ If SwatchKit is just a development tool and you don't want it in `dist/`, set `outDir` to a separate directory. Keep `cssCopy` enabled (the default) so the output is fully self-contained.
339
+
340
+ ```javascript
341
+ // swatchkit.config.js
342
+ module.exports = {
343
+ cssDir: "./src/css",
344
+ outDir: "swatchkit-dist",
345
+ };
346
+ ```
347
+
348
+ ```
349
+ my-project/
350
+ ├── dist/ # Your production build (no SwatchKit)
351
+ │ └── ...
352
+ └── swatchkit-dist/ # Self-contained, serve locally during dev
353
+ ├── css/
354
+ │ ├── main.css
355
+ │ └── tokens.css
356
+ └── index.html
357
+ ```
358
+
359
+ You can serve `swatchkit-dist/` locally during development without affecting your production build.
360
+
278
361
  ## Using with a Framework
279
362
 
280
363
  SwatchKit outputs to `dist/swatchkit/` by default. If your framework (Vite, Astro, etc.) cleans the `dist/` directory during its build, run SwatchKit **after** your framework build:
package/build.js CHANGED
@@ -19,6 +19,7 @@ function parseArgs(args) {
19
19
  input: null,
20
20
  outDir: null,
21
21
  force: false,
22
+ dryRun: false,
22
23
  };
23
24
 
24
25
  for (let i = 2; i < args.length; i++) {
@@ -29,6 +30,8 @@ function parseArgs(args) {
29
30
  options.watch = true;
30
31
  } else if (arg === "-f" || arg === "--force") {
31
32
  options.force = true;
33
+ } else if (arg === "--dry-run") {
34
+ options.dryRun = true;
32
35
  } else if (arg === "-c" || arg === "--config") {
33
36
  // Handle case where flag is last arg
34
37
  if (i + 1 < args.length) {
@@ -132,12 +135,24 @@ function resolveSettings(cliOptions, fileConfig) {
132
135
  // Exclude patterns
133
136
  const exclude = fileConfig.exclude || [];
134
137
 
138
+ // CSS copy behavior
139
+ // When true (default), copies cssDir into outDir/css/ for a self-contained build.
140
+ // When false, skips the copy — expects CSS to already exist at cssPath relative to output.
141
+ const cssCopy = fileConfig.cssCopy !== undefined ? fileConfig.cssCopy : true;
142
+
143
+ // Relative path from SwatchKit HTML output to the user's CSS directory.
144
+ // Only used when cssCopy is false. Derived from cssDir basename by default
145
+ // (e.g., cssDir: "./src/css" -> "../css/", cssDir: "./styles" -> "../styles/").
146
+ const cssPath = fileConfig.cssPath || (cssCopy ? "css/" : `../${path.basename(cssDir)}/`);
147
+
135
148
  return {
136
149
  swatchkitDir,
137
150
  outDir,
138
151
  cssDir,
139
152
  tokensDir,
140
153
  exclude,
154
+ cssCopy,
155
+ cssPath,
141
156
  fileConfig, // Expose config to init
142
157
  // Internal layout templates (relative to this script)
143
158
  internalLayout: path.join(__dirname, "src/layout.html"),
@@ -158,184 +173,227 @@ function resolveSettings(cliOptions, fileConfig) {
158
173
  };
159
174
  }
160
175
 
161
- // --- 4. Init Command Logic ---
162
- function runInit(settings, options) {
163
- console.log("[SwatchKit] Scaffolding project structure...");
164
-
165
- // Ensure swatchkit directory exists
166
- if (!fs.existsSync(settings.swatchkitDir)) {
167
- console.log(`+ Directory: ${settings.swatchkitDir}`);
168
- fs.mkdirSync(settings.swatchkitDir, { recursive: true });
176
+ // --- 4. Init Manifest & Dry Run ---
177
+
178
+ // Builds a list of all files that init manages.
179
+ // Each entry maps a blueprint source to a project destination.
180
+ // Used by both the actual init and the dry-run status report.
181
+ function buildInitManifest(settings) {
182
+ const manifest = [];
183
+ const blueprintsDir = path.join(__dirname, "src/blueprints");
184
+ const templatesDir = path.join(__dirname, "src/templates");
185
+
186
+ // Token blueprint JSON files
187
+ const tokenFiles = [
188
+ "colors.json",
189
+ "text-weights.json",
190
+ "text-leading.json",
191
+ "viewports.json",
192
+ "text-sizes.json",
193
+ "spacing.json",
194
+ "fonts.json",
195
+ ];
196
+ for (const file of tokenFiles) {
197
+ manifest.push({
198
+ src: path.join(blueprintsDir, file),
199
+ dest: path.join(settings.tokensDir, file),
200
+ });
169
201
  }
170
202
 
171
- // Create tokens/ directory at project root (JSON token definitions)
172
- const tokensDir = settings.tokensDir;
173
- if (!fs.existsSync(tokensDir)) {
174
- console.log(`+ Directory: ${tokensDir}`);
175
- fs.mkdirSync(tokensDir, { recursive: true });
203
+ // Template files (swatchkit/tokens/)
204
+ const templateFiles = ["prose.html", "script.js"];
205
+ for (const file of templateFiles) {
206
+ manifest.push({
207
+ src: path.join(templatesDir, file),
208
+ dest: path.join(settings.swatchkitDir, "tokens", file),
209
+ transform: (content) => content.trim(),
210
+ });
176
211
  }
177
212
 
178
- // Create swatchkit/tokens/ directory (HTML/JS visual previews)
179
- const tokensUiDir = path.join(settings.swatchkitDir, "tokens");
180
- if (!fs.existsSync(tokensUiDir)) {
181
- console.log(`+ Directory: ${tokensUiDir}`);
182
- fs.mkdirSync(tokensUiDir, { recursive: true });
183
- }
213
+ // CSS entry point
214
+ manifest.push({
215
+ src: path.join(blueprintsDir, "main.css"),
216
+ dest: settings.mainCssFile,
217
+ });
184
218
 
185
- // Create css/ directory at project root
186
- if (!fs.existsSync(settings.cssDir)) {
187
- console.log(`+ Directory: ${settings.cssDir}`);
188
- fs.mkdirSync(settings.cssDir, { recursive: true });
189
- }
219
+ // SwatchKit UI styles
220
+ manifest.push({
221
+ src: path.join(blueprintsDir, "swatchkit-ui.css"),
222
+ dest: path.join(settings.cssDir, "swatchkit-ui.css"),
223
+ });
190
224
 
191
- // Create css/global directory
192
- const globalCssDir = path.join(settings.cssDir, "global");
193
- if (!fs.existsSync(globalCssDir)) {
194
- console.log(`+ Directory: ${globalCssDir}`);
195
- fs.mkdirSync(globalCssDir, { recursive: true });
225
+ // CSS folder blueprints (global, compositions, utilities)
226
+ const cssFolders = ["global", "compositions", "utilities"];
227
+ for (const folder of cssFolders) {
228
+ const srcDir = path.join(blueprintsDir, folder);
229
+ if (fs.existsSync(srcDir)) {
230
+ const files = fs
231
+ .readdirSync(srcDir)
232
+ .filter((f) => !fs.statSync(path.join(srcDir, f)).isDirectory());
233
+ for (const file of files) {
234
+ manifest.push({
235
+ src: path.join(srcDir, file),
236
+ dest: path.join(settings.cssDir, folder, file),
237
+ });
238
+ }
239
+ }
196
240
  }
197
241
 
198
- // Copy JSON token blueprints to tokens/ (project root)
199
- const copyDefault = (srcFilename, destFilename) => {
200
- const destPath = path.join(tokensDir, destFilename);
201
- if (!fs.existsSync(destPath)) {
202
- const srcPath = path.join(__dirname, "src/blueprints", srcFilename);
203
- const content = fs.readFileSync(srcPath, "utf-8");
204
- fs.writeFileSync(destPath, content);
205
- console.log(`+ Token Blueprint: ${destFilename}`);
206
- }
207
- };
242
+ // Layout files
243
+ manifest.push({
244
+ src: settings.internalLayout,
245
+ dest: settings.projectLayout,
246
+ });
247
+ manifest.push({
248
+ src: settings.internalPreviewLayout,
249
+ dest: settings.projectPreviewLayout,
250
+ });
251
+
252
+ return manifest;
253
+ }
254
+
255
+ // Directories that init ensures exist.
256
+ function getInitDirs(settings) {
257
+ return [
258
+ settings.swatchkitDir,
259
+ settings.tokensDir,
260
+ path.join(settings.swatchkitDir, "tokens"),
261
+ settings.cssDir,
262
+ path.join(settings.cssDir, "global"),
263
+ ];
264
+ }
265
+
266
+ // Compare init-managed files against their blueprint sources and print a
267
+ // status report showing what would be created, changed, or is up to date.
268
+ function reportInitStatus(settings) {
269
+ const cwd = process.cwd();
270
+ const manifest = buildInitManifest(settings);
271
+ const dirs = getInitDirs(settings);
208
272
 
209
- copyDefault("colors.json", "colors.json");
210
- copyDefault("text-weights.json", "text-weights.json");
211
- copyDefault("text-leading.json", "text-leading.json");
212
- copyDefault("viewports.json", "viewports.json");
213
- copyDefault("text-sizes.json", "text-sizes.json");
214
- copyDefault("spacing.json", "spacing.json");
215
- copyDefault("fonts.json", "fonts.json");
216
-
217
- // Copy HTML/JS template patterns to swatchkit/tokens/ (UI documentation)
218
- // Note: Token display templates (colors, typography, spacing, etc.) are
219
- // generated dynamically at build time from the JSON token files.
220
- const copyTemplate = (srcFilename, destFilename) => {
221
- const destPath = path.join(tokensUiDir, destFilename);
222
- if (!fs.existsSync(destPath)) {
223
- const srcPath = path.join(__dirname, "src/templates", srcFilename);
224
- const content = fs.readFileSync(srcPath, "utf-8");
225
- fs.writeFileSync(destPath, content.trim());
226
- console.log(`+ Pattern Template: ${destFilename}`);
273
+ const newDirs = [];
274
+ const created = [];
275
+ const changed = [];
276
+ const upToDate = [];
277
+
278
+ for (const dir of dirs) {
279
+ if (!fs.existsSync(dir)) {
280
+ newDirs.push(path.relative(cwd, dir) + "/");
227
281
  }
228
- };
282
+ }
229
283
 
230
- // Only copy non-token templates (prose is a kitchen sink, not a token display)
231
- copyTemplate("prose.html", "prose.html");
232
-
233
- // Create shared script for tokens UI
234
- const tokensScriptFile = path.join(tokensUiDir, "script.js");
235
- if (!fs.existsSync(tokensScriptFile)) {
236
- const srcPath = path.join(__dirname, "src/templates/script.js");
237
- const content = fs.readFileSync(srcPath, "utf-8");
238
- fs.writeFileSync(tokensScriptFile, content.trim());
239
- console.log(`+ Script: ${path.basename(tokensScriptFile)}`);
240
- }
241
-
242
- // Create main.css entry point
243
- if (!fs.existsSync(settings.mainCssFile)) {
244
- const srcPath = path.join(__dirname, "src/blueprints/main.css");
245
- let content = fs.readFileSync(srcPath, "utf-8");
246
-
247
- // Default: Copy the files
248
- const copyCssBlueprint = (filename) => {
249
- const src = path.join(__dirname, "src/blueprints", filename);
250
- // Ensure destination path (cssDir) exists
251
- if (!fs.existsSync(settings.cssDir)) {
252
- fs.mkdirSync(settings.cssDir, { recursive: true });
253
- }
254
- const dest = path.join(settings.cssDir, filename);
255
- // Only copy if destination doesn't exist
256
- if (fs.existsSync(src) && !fs.existsSync(dest)) {
257
- fs.copyFileSync(src, dest);
258
- console.log(`+ CSS Blueprint: ${filename}`);
284
+ for (const entry of manifest) {
285
+ const relDest = path.relative(cwd, entry.dest);
286
+ if (!fs.existsSync(entry.dest)) {
287
+ created.push(relDest);
288
+ } else {
289
+ let srcContent = fs.readFileSync(entry.src, "utf-8");
290
+ if (entry.transform) srcContent = entry.transform(srcContent);
291
+ const destContent = fs.readFileSync(entry.dest, "utf-8");
292
+ if (srcContent !== destContent) {
293
+ changed.push(relDest);
294
+ } else {
295
+ upToDate.push(relDest);
259
296
  }
260
- };
261
-
262
- // Global blueprints are now copied as a folder below
297
+ }
298
+ }
263
299
 
264
- fs.writeFileSync(settings.mainCssFile, content);
265
- console.log(`+ CSS Blueprint: main.css`);
300
+ if (newDirs.length === 0 && created.length === 0 && changed.length === 0) {
301
+ console.log("[SwatchKit] All init-managed files are up to date.");
302
+ return;
266
303
  }
267
304
 
268
- // Copy Global Styles Folder
269
- const globalSrc = path.join(__dirname, "src/blueprints/global");
270
- const globalDest = path.join(settings.cssDir, "global");
271
- if (fs.existsSync(globalSrc)) {
272
- copyDir(globalSrc, globalDest);
305
+ if (newDirs.length > 0 || created.length > 0) {
306
+ console.log("\n New (will be created):");
307
+ for (const d of newDirs) console.log(` + ${d}`);
308
+ for (const f of created) console.log(` + ${f}`);
273
309
  }
274
310
 
275
- // Copy Compositions
276
- const compositionsSrc = path.join(__dirname, "src/blueprints/compositions");
277
- const compositionsDest = path.join(settings.cssDir, "compositions");
278
- if (fs.existsSync(compositionsSrc)) {
279
- copyDir(compositionsSrc, compositionsDest);
311
+ if (changed.length > 0) {
312
+ console.log("\n Changed (differs from latest blueprint):");
313
+ for (const f of changed) console.log(` ~ ${f}`);
280
314
  }
281
315
 
282
- // Copy Utilities
283
- const utilitiesSrc = path.join(__dirname, "src/blueprints/utilities");
284
- const utilitiesDest = path.join(settings.cssDir, "utilities");
285
- if (fs.existsSync(utilitiesSrc)) {
286
- copyDir(utilitiesSrc, utilitiesDest);
316
+ if (upToDate.length > 0) {
317
+ console.log("\n Up to date:");
318
+ for (const f of upToDate) console.log(` = ${f}`);
287
319
  }
288
320
 
289
- // Copy SwatchKit UI Styles
290
- const uiSrc = path.join(__dirname, "src/blueprints/swatchkit-ui.css");
291
- const uiDest = path.join(settings.cssDir, "swatchkit-ui.css");
292
- if (fs.existsSync(uiSrc) && !fs.existsSync(uiDest)) {
293
- fs.copyFileSync(uiSrc, uiDest);
294
- console.log(`+ CSS Blueprint: swatchkit-ui.css`);
321
+ console.log("");
322
+
323
+ if (changed.length > 0 || created.length > 0 || newDirs.length > 0) {
324
+ console.log(
325
+ " Run 'swatchkit init --force' to update all files to latest blueprints.\n",
326
+ );
295
327
  }
328
+ }
296
329
 
297
- // Generate initial tokens.css
298
- // processTokens now expects the folder where tokens.css should live
299
- // We pass settings.cssDir, but processTokens internally joins 'tokens.css'
300
- // So we need to point it to css/global
301
- const tokensContext = processTokens(settings.tokensDir, path.join(settings.cssDir, "global"));
302
-
303
- if (tokensContext) {
304
- generateTokenUtilities(tokensContext, path.join(settings.cssDir, "utilities"));
330
+ // --- 5. Init Command Logic ---
331
+ function runInit(settings, options) {
332
+ const isInitialized = fs.existsSync(settings.swatchkitDir);
333
+
334
+ // --dry-run: always just report status, change nothing.
335
+ // Already initialized without --force: auto dry-run.
336
+ if (options.dryRun || (isInitialized && !options.force)) {
337
+ if (isInitialized && !options.dryRun) {
338
+ console.log("[SwatchKit] Project already initialized.");
339
+ }
340
+ reportInitStatus(settings);
341
+ return;
342
+ }
343
+
344
+ // Sanity check: internal layout template must exist
345
+ if (!fs.existsSync(settings.internalLayout)) {
346
+ console.error(
347
+ `Error: Internal layout file not found at ${settings.internalLayout}`,
348
+ );
349
+ process.exit(1);
305
350
  }
306
351
 
307
- const targetLayout = settings.projectLayout;
352
+ console.log("[SwatchKit] Scaffolding project structure...");
308
353
 
309
- if (fs.existsSync(targetLayout) && !options.force) {
310
- console.warn(`! Layout already exists: ${targetLayout} (Use --force to overwrite)`);
311
- } else {
312
- if (fs.existsSync(settings.internalLayout)) {
313
- const layoutContent = fs.readFileSync(settings.internalLayout, "utf-8");
314
- fs.writeFileSync(targetLayout, layoutContent);
315
- console.log(`+ Layout: ${path.basename(targetLayout)}`);
316
- } else {
317
- console.error(
318
- `Error: Internal layout file not found at ${settings.internalLayout}`,
319
- );
320
- process.exit(1);
354
+ // Ensure directories exist
355
+ const cwd = process.cwd();
356
+ for (const dir of getInitDirs(settings)) {
357
+ if (!fs.existsSync(dir)) {
358
+ console.log(`+ Directory: ${path.relative(cwd, dir)}/`);
359
+ fs.mkdirSync(dir, { recursive: true });
321
360
  }
322
361
  }
323
362
 
324
- // Copy preview layout template (standalone page for individual swatches)
325
- const targetPreview = settings.projectPreviewLayout;
326
- if (!fs.existsSync(targetPreview)) {
327
- if (fs.existsSync(settings.internalPreviewLayout)) {
328
- const previewContent = fs.readFileSync(
329
- settings.internalPreviewLayout,
330
- "utf-8",
331
- );
332
- fs.writeFileSync(targetPreview, previewContent);
333
- console.log(`+ Layout: ${path.basename(targetPreview)}`);
363
+ // Copy all manifest files
364
+ const manifest = buildInitManifest(settings);
365
+ for (const entry of manifest) {
366
+ const exists = fs.existsSync(entry.dest);
367
+ if (options.force || !exists) {
368
+ // Ensure parent directory exists (for CSS subdirs like compositions/)
369
+ const parentDir = path.dirname(entry.dest);
370
+ if (!fs.existsSync(parentDir)) {
371
+ fs.mkdirSync(parentDir, { recursive: true });
372
+ }
373
+
374
+ let content = fs.readFileSync(entry.src, "utf-8");
375
+ if (entry.transform) content = entry.transform(content);
376
+ fs.writeFileSync(entry.dest, content);
377
+
378
+ const label = path.relative(cwd, entry.dest);
379
+ console.log(`${exists ? "~ Updated" : "+ Created"}: ${label}`);
334
380
  }
335
381
  }
382
+
383
+ // Generate tokens.css and utility tokens.css (always — these are generated files)
384
+ const tokensContext = processTokens(
385
+ settings.tokensDir,
386
+ path.join(settings.cssDir, "global"),
387
+ );
388
+ if (tokensContext) {
389
+ generateTokenUtilities(
390
+ tokensContext,
391
+ path.join(settings.cssDir, "utilities"),
392
+ );
393
+ }
336
394
  }
337
395
 
338
- // --- 5. Build Logic ---
396
+ // --- 6. Build Logic ---
339
397
  function copyDir(src, dest, force = false) {
340
398
  if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
341
399
  const entries = fs.readdirSync(src, { withFileTypes: true });
@@ -467,7 +525,9 @@ function build(settings) {
467
525
  }
468
526
 
469
527
  // 3. Ensure dist directories exist
470
- [settings.outDir, settings.distCssDir, settings.distJsDir].forEach((dir) => {
528
+ const distDirs = [settings.outDir, settings.distJsDir];
529
+ if (settings.cssCopy) distDirs.push(settings.distCssDir);
530
+ distDirs.forEach((dir) => {
471
531
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
472
532
  });
473
533
 
@@ -490,10 +550,12 @@ function build(settings) {
490
550
  }
491
551
  }
492
552
 
493
- // 3. Copy CSS files (recursively)
494
- if (fs.existsSync(settings.cssDir)) {
553
+ // 3. Copy CSS files (recursively) — skip if cssCopy is disabled
554
+ if (settings.cssCopy && fs.existsSync(settings.cssDir)) {
495
555
  console.log("Copying static CSS assets (css/*)...");
496
556
  copyDir(settings.cssDir, settings.distCssDir, true);
557
+ } else if (!settings.cssCopy) {
558
+ console.log(`Skipping CSS copy (cssCopy: false). CSS referenced at: ${settings.cssPath}`);
497
559
  }
498
560
 
499
561
  // 4. Read swatches & JS
@@ -651,25 +713,32 @@ function build(settings) {
651
713
  sortedKeys.forEach((section) => {
652
714
  const swatches = sections[section];
653
715
  swatches.forEach((p) => {
654
- // Determine output path and relative CSS path
716
+ // Determine output path and relative CSS path.
717
+ // Preview pages need to navigate up to the swatchkit output root,
718
+ // then follow cssPath to reach the CSS files.
655
719
  let previewFile, cssPath;
656
720
  if (p.sectionSlug) {
657
721
  const sectionDir = path.join(settings.distPreviewDir, p.sectionSlug);
658
722
  if (!fs.existsSync(sectionDir))
659
723
  fs.mkdirSync(sectionDir, { recursive: true });
660
724
  previewFile = path.join(sectionDir, `${p.id}.html`);
661
- cssPath = "../../"; // preview/section/file.html -> ../../css/
725
+ cssPath = "../../" + settings.cssPath; // preview/section/file.html -> ../../ + cssPath
662
726
  } else {
663
727
  if (!fs.existsSync(settings.distPreviewDir))
664
728
  fs.mkdirSync(settings.distPreviewDir, { recursive: true });
665
729
  previewFile = path.join(settings.distPreviewDir, `${p.id}.html`);
666
- cssPath = "../"; // preview/file.html -> ../css/
730
+ cssPath = "../" + settings.cssPath; // preview/file.html -> ../ + cssPath
667
731
  }
668
732
 
733
+ // JS always lives in the swatchkit output dir, so jsPath just
734
+ // navigates back to the output root (not to the user's CSS dir).
735
+ const jsPath = p.sectionSlug ? "../../" : "../";
736
+
669
737
  const previewHtml = previewLayoutContent
670
738
  .replace("<!-- PREVIEW_TITLE -->", p.name)
671
739
  .replace("<!-- PREVIEW_CONTENT -->", p.content)
672
740
  .replaceAll("<!-- CSS_PATH -->", cssPath)
741
+ .replaceAll("<!-- JS_PATH -->", jsPath)
673
742
  .replace("<!-- HEAD_EXTRAS -->", "");
674
743
 
675
744
  fs.writeFileSync(previewFile, previewHtml);
@@ -697,6 +766,7 @@ function build(settings) {
697
766
  const finalHtml = layoutContent
698
767
  .replace("<!-- SIDEBAR_LINKS -->", sidebarLinks)
699
768
  .replace("<!-- SWATCHES -->", swatchBlocks)
769
+ .replaceAll("<!-- CSS_PATH -->", settings.cssPath)
700
770
  .replace("<!-- HEAD_EXTRAS -->", headExtras);
701
771
 
702
772
  // 9. Write output
@@ -705,7 +775,7 @@ function build(settings) {
705
775
  console.log(`Build complete! Generated ${settings.outputFile}`);
706
776
  }
707
777
 
708
- // --- 6. Watch Logic ---
778
+ // --- 7. Watch Logic ---
709
779
  function watch(settings) {
710
780
  const sourcePaths = [
711
781
  settings.swatchkitDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swatchkit",
3
- "version": "0.6.2",
3
+ "version": "0.8.0",
4
4
  "description": "A lightweight tool for creating HTML pattern libraries.",
5
5
  "main": "build.js",
6
6
  "bin": {
package/src/layout.html CHANGED
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>SwatchKit</title>
7
- <link rel="stylesheet" href="css/main.css" />
8
- <link rel="stylesheet" href="css/swatchkit-ui.css" />
7
+ <link rel="stylesheet" href="<!-- CSS_PATH -->main.css" />
8
+ <link rel="stylesheet" href="<!-- CSS_PATH -->swatchkit-ui.css" />
9
9
  <!-- HEAD_EXTRAS -->
10
10
  </head>
11
11
  <body>