swatchkit 1.1.1 → 2.0.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/build.js CHANGED
@@ -175,10 +175,8 @@ function resolveSettings(cliOptions, fileConfig) {
175
175
  // Derived paths
176
176
  distCssDir: path.join(outDir, "css"),
177
177
  distTokensCssFile: path.join(outDir, "css", "tokens.css"),
178
- distJsDir: path.join(outDir, "js"),
179
178
  distPreviewDir: path.join(outDir, "preview"),
180
179
  outputFile: path.join(outDir, "index.html"),
181
- outputJsFile: path.join(outDir, "js/swatches.js"),
182
180
  tokensCssFile: path.join(cssDir, "global", "tokens.css"),
183
181
  mainCssFile: path.join(cssDir, "main.css"),
184
182
  };
@@ -231,6 +229,22 @@ function buildInitManifest(settings) {
231
229
  });
232
230
  }
233
231
 
232
+ // Hello swatch (default example in swatchkit/swatches/hello/)
233
+ for (const file of ["index.html", "README.md"]) {
234
+ manifest.push({
235
+ src: path.join(templatesDir, "hello", file),
236
+ dest: path.join(settings.swatchkitDir, "swatches", "hello", file),
237
+ });
238
+ }
239
+
240
+ // Swatches CSS folder (css/swatches/)
241
+ for (const file of ["index.css", "hello.css"]) {
242
+ manifest.push({
243
+ src: path.join(blueprintsDir, "swatches", file),
244
+ dest: path.join(settings.cssDir, "swatches", file),
245
+ });
246
+ }
247
+
234
248
  // Utility and composition display templates — walk each subfolder
235
249
  for (const section of ["utilities", "compositions"]) {
236
250
  const sectionSrc = path.join(templatesDir, section);
@@ -301,8 +315,11 @@ function getInitDirs(settings) {
301
315
  path.join(settings.swatchkitDir, "tokens"),
302
316
  path.join(settings.swatchkitDir, "utilities"),
303
317
  path.join(settings.swatchkitDir, "compositions"),
318
+ path.join(settings.swatchkitDir, "swatches"),
319
+ path.join(settings.swatchkitDir, "swatches", "hello"),
304
320
  settings.cssDir,
305
321
  path.join(settings.cssDir, "global"),
322
+ path.join(settings.cssDir, "swatches"),
306
323
  ];
307
324
  }
308
325
 
@@ -550,7 +567,26 @@ function copyDir(src, dest, force = false) {
550
567
  }
551
568
  }
552
569
 
553
- function scanSwatches(dir, scriptsCollector, exclude = []) {
570
+ // Special filenames that SwatchKit reads and treats differently — not copied verbatim.
571
+ const SWATCH_SPECIAL_FILES = new Set(["index.html", "description.html"]);
572
+
573
+ // Recursively copy a swatch's assets (all files and subdirs except index.html,
574
+ // description.html, and anything prefixed with _ or .) to destDir.
575
+ function copySwatchAssets(srcDir, destDir) {
576
+ if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
577
+ for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
578
+ if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
579
+ const src = path.join(srcDir, entry.name);
580
+ const dest = path.join(destDir, entry.name);
581
+ if (entry.isDirectory()) {
582
+ copySwatchAssets(src, dest);
583
+ } else if (!SWATCH_SPECIAL_FILES.has(entry.name)) {
584
+ fs.copyFileSync(src, dest);
585
+ }
586
+ }
587
+ }
588
+
589
+ function scanSwatches(dir, destDir, exclude = []) {
554
590
  const swatches = [];
555
591
  if (!fs.existsSync(dir)) return swatches;
556
592
 
@@ -583,23 +619,14 @@ function scanSwatches(dir, scriptsCollector, exclude = []) {
583
619
  description = fs.readFileSync(descriptionFile, "utf-8");
584
620
  }
585
621
 
586
- // Find all .js files
587
- const jsFiles = fs
588
- .readdirSync(itemPath)
589
- .filter((file) => file.endsWith(".js"));
590
-
591
- jsFiles.forEach((jsFile) => {
592
- const scriptContent = fs.readFileSync(
593
- path.join(itemPath, jsFile),
594
- "utf-8",
595
- );
596
- scriptsCollector.push(`
597
- /* --- Swatch: ${name} / File: ${jsFile} --- */
598
- (function() {
599
- ${scriptContent}
600
- })();
601
- `);
602
- });
622
+ // Copy all non-special files and subdirectories from the component
623
+ // folder into its preview output directory so index.html can reference
624
+ // them with relative paths (e.g. ./styles.css, ./script.js, ./img/).
625
+ // Files and directories prefixed with _ or . are skipped.
626
+ if (destDir) {
627
+ const swatchDestDir = path.join(destDir, item);
628
+ copySwatchAssets(itemPath, swatchDestDir);
629
+ }
603
630
  }
604
631
  }
605
632
  // Handle Single File
@@ -608,18 +635,6 @@ ${scriptContent}
608
635
  id = name;
609
636
  content = fs.readFileSync(itemPath, "utf-8");
610
637
  }
611
- // Handle Loose JS Files (e.g. script.js in tokens/)
612
- else if (item.endsWith(".js")) {
613
- const scriptContent = fs.readFileSync(itemPath, "utf-8");
614
- scriptsCollector.push(`
615
- /* --- File: ${item} --- */
616
- (function() {
617
- ${scriptContent}
618
- })();
619
- `);
620
- // Don't add to swatches list, just scripts
621
- return;
622
- }
623
638
 
624
639
  if (name && content) {
625
640
  swatches.push({ name, id, content, description: description || null });
@@ -668,7 +683,7 @@ function build(settings) {
668
683
  }
669
684
 
670
685
  // 3. Ensure dist directories exist
671
- const distDirs = [settings.outDir, settings.distJsDir];
686
+ const distDirs = [settings.outDir];
672
687
  if (settings.cssCopy) distDirs.push(settings.distCssDir);
673
688
  distDirs.forEach((dir) => {
674
689
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
@@ -701,9 +716,8 @@ function build(settings) {
701
716
  console.log(`Skipping CSS copy (cssCopy: false). CSS referenced at: ${settings.cssPath}`);
702
717
  }
703
718
 
704
- // 4. Read swatches & JS
719
+ // 4. Read swatches
705
720
  console.log("Scanning HTML patterns (swatchkit/**/*.html)...");
706
- const scripts = [];
707
721
  const sections = {}; // Map<SectionName, Array<Swatch>>
708
722
 
709
723
  if (fs.existsSync(settings.swatchkitDir)) {
@@ -723,7 +737,9 @@ function build(settings) {
723
737
  // It is a Section Container (e.g. "Utilities")
724
738
  const sectionName =
725
739
  item === "tokens" ? "Design Tokens" : toTitleCase(item);
726
- const swatches = scanSwatches(itemPath, scripts, exclude);
740
+ // Pass the preview dest dir so extra files get copied alongside each swatch
741
+ const sectionDestDir = path.join(settings.distPreviewDir, item);
742
+ const swatches = scanSwatches(itemPath, sectionDestDir, exclude);
727
743
  // Tag each swatch with its section slug for preview paths
728
744
  swatches.forEach((s) => (s.sectionSlug = item));
729
745
  if (swatches.length > 0) {
@@ -749,24 +765,19 @@ function build(settings) {
749
765
  } else if (stat.isDirectory()) {
750
766
  const indexFile = path.join(itemPath, "index.html");
751
767
  if (fs.existsSync(indexFile)) {
752
- // Component folder swatch at root
768
+ // Component folder swatch at root — copy extra files to preview dest
753
769
  const name = item;
754
770
  const content = fs.readFileSync(indexFile, "utf-8");
755
- rootSwatches.push({ name, id: name, content, sectionSlug: null });
756
-
757
- // Collect JS
758
- const jsFiles = fs
759
- .readdirSync(itemPath)
760
- .filter((f) => f.endsWith(".js"));
761
- jsFiles.forEach((jsFile) => {
762
- const scriptContent = fs.readFileSync(
763
- path.join(itemPath, jsFile),
764
- "utf-8",
765
- );
766
- scripts.push(
767
- `/* ${name}/${jsFile} */ (function(){${scriptContent}})();`,
768
- );
769
- });
771
+ const descriptionFile = path.join(itemPath, "description.html");
772
+ const description = fs.existsSync(descriptionFile)
773
+ ? fs.readFileSync(descriptionFile, "utf-8")
774
+ : null;
775
+
776
+ // Copy extra files into preview/id/
777
+ const swatchDestDir = path.join(settings.distPreviewDir, name);
778
+ copySwatchAssets(itemPath, swatchDestDir);
779
+
780
+ rootSwatches.push({ name, id: name, content, description, sectionSlug: null });
770
781
  }
771
782
  }
772
783
  });
@@ -810,11 +821,13 @@ function build(settings) {
810
821
  .replace(/"/g, "&quot;")
811
822
  .replace(/'/g, "&#039;");
812
823
 
813
- // Build preview path: preview/{section}/{name}.html or preview/{name}.html
824
+ // Build preview path: preview/{section}/{id}/ or preview/{id}/
825
+ // Each swatch is a directory with its own index.html so sibling assets
826
+ // (css, js, images, etc.) can be referenced with relative paths.
814
827
  const previewPath = p.sectionSlug
815
- ? `preview/${p.sectionSlug}/${p.id}.html`
816
- : `preview/${p.id}.html`;
817
- const previewLink = previewPath.replace(/\.html$/, '');
828
+ ? `preview/${p.sectionSlug}/${p.id}/`
829
+ : `preview/${p.id}/`;
830
+ const previewLink = previewPath;
818
831
 
819
832
  return `
820
833
  <section id="${p.id}" class="region flow">
@@ -832,23 +845,9 @@ function build(settings) {
832
845
  .join("\n");
833
846
  });
834
847
 
835
- // 6. Write JS Bundle
836
- // Always prepend the internal token display script (resolves CSS custom
837
- // property values shown in token documentation pages).
838
- const tokenDisplayScript = fs.readFileSync(
839
- path.join(__dirname, "src/templates/script.js"),
840
- "utf-8",
841
- );
842
- const internalScript = `/* --- SwatchKit: token display --- */\n(function() {\n${tokenDisplayScript}\n})();`;
843
- const allScripts = [internalScript, ...scripts];
844
- fs.writeFileSync(settings.outputJsFile, allScripts.join("\n"));
845
- if (scripts.length > 0) {
846
- console.log(
847
- `Bundled ${scripts.length} swatch scripts to ${settings.outputJsFile}`,
848
- );
849
- }
850
-
851
- // 7. Generate preview pages (standalone full-screen view of each swatch)
848
+ // 6. Generate preview pages (standalone full-screen view of each swatch)
849
+ // Each swatch gets its own directory: preview/{section}/{id}/index.html
850
+ // This allows index.html to reference sibling assets with relative paths.
852
851
  let previewLayoutContent;
853
852
  if (fs.existsSync(settings.projectPreviewLayout)) {
854
853
  previewLayoutContent = fs.readFileSync(
@@ -867,32 +866,25 @@ function build(settings) {
867
866
  sortedKeys.forEach((section) => {
868
867
  const swatches = sections[section];
869
868
  swatches.forEach((p) => {
870
- // Determine output path and relative CSS path.
871
- // Preview pages need to navigate up to the swatchkit output root,
872
- // then follow cssPath to reach the CSS files.
873
- let previewFile, cssPath;
869
+ // Each swatch is output as a directory with index.html inside.
870
+ // preview/{section}/{id}/index.html — depth: 3 levels from outDir
871
+ // preview/{id}/index.html — depth: 2 levels from outDir
872
+ let swatchDir, cssPath;
874
873
  if (p.sectionSlug) {
875
- const sectionDir = path.join(settings.distPreviewDir, p.sectionSlug);
876
- if (!fs.existsSync(sectionDir))
877
- fs.mkdirSync(sectionDir, { recursive: true });
878
- previewFile = path.join(sectionDir, `${p.id}.html`);
879
- cssPath = "../../" + settings.cssPath; // preview/section/file.html -> ../../ + cssPath
874
+ swatchDir = path.join(settings.distPreviewDir, p.sectionSlug, p.id);
875
+ cssPath = "../../../" + settings.cssPath; // preview/section/id/index.html -> ../../../ + cssPath
880
876
  } else {
881
- if (!fs.existsSync(settings.distPreviewDir))
882
- fs.mkdirSync(settings.distPreviewDir, { recursive: true });
883
- previewFile = path.join(settings.distPreviewDir, `${p.id}.html`);
884
- cssPath = "../" + settings.cssPath; // preview/file.html -> ../ + cssPath
877
+ swatchDir = path.join(settings.distPreviewDir, p.id);
878
+ cssPath = "../../" + settings.cssPath; // preview/id/index.html -> ../../ + cssPath
885
879
  }
886
880
 
887
- // JS always lives in the swatchkit output dir, so jsPath just
888
- // navigates back to the output root (not to the user's CSS dir).
889
- const jsPath = p.sectionSlug ? "../../" : "../";
881
+ if (!fs.existsSync(swatchDir)) fs.mkdirSync(swatchDir, { recursive: true });
882
+ const previewFile = path.join(swatchDir, "index.html");
890
883
 
891
884
  const previewHtml = previewLayoutContent
892
885
  .replace("<!-- PREVIEW_TITLE -->", p.name)
893
886
  .replace("<!-- PREVIEW_CONTENT -->", p.content)
894
887
  .replaceAll("<!-- CSS_PATH -->", cssPath)
895
- .replaceAll("<!-- JS_PATH -->", jsPath)
896
888
  .replace("<!-- HEAD_EXTRAS -->", "");
897
889
 
898
890
  fs.writeFileSync(previewFile, previewHtml);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swatchkit",
3
- "version": "1.1.1",
3
+ "version": "2.0.0",
4
4
  "description": "A lightweight tool for creating HTML pattern libraries.",
5
5
  "main": "build.js",
6
6
  "bin": {
@@ -2,18 +2,15 @@
2
2
  * SwatchKit Main Stylesheet
3
3
  *
4
4
  * This is your app's CSS entry point. It imports design tokens, reset,
5
- * global styles, and compositions. Edit freely this file is yours.
6
- * Note: swatchkit init --force will overwrite blueprint files (with .bak backups).
5
+ * global styles, compositions, utilities, and component swatches.
6
+ * Edit freely this file is yours.
7
+ *
8
+ * Note: swatchkit scaffold --force will overwrite blueprint files (with .bak backups).
7
9
  * The only truly auto-generated files are css/global/tokens.css and
8
10
  * css/utilities/tokens.css — do not edit those manually.
9
11
  */
10
12
 
11
- /*
12
- * Import Global Styles (Reset, Tokens, Variables, Elements)
13
- *
14
- * NOTE: Some global styles (variables & elements) are DISABLED by default.
15
- * You must open `css/global/index.css` to verify variable names and enable them.
16
- */
13
+ /* Import global styles (reset, tokens, variables, elements) */
17
14
  @import "global/index.css";
18
15
 
19
16
  /* Import layout compositions */
@@ -22,6 +19,9 @@
22
19
  /* Import utilities */
23
20
  @import "utilities/index.css";
24
21
 
22
+ /* Import swatches (component styles) */
23
+ @import "swatches/index.css";
24
+
25
25
  /* === Your App Styles === */
26
26
 
27
27
  /* Add your component styles below */
@@ -0,0 +1,11 @@
1
+ /* === Hello Swatch ===
2
+ *
3
+ * Styles for the hello swatch (swatchkit/swatches/hello/).
4
+ * Replace or delete this file once you have real components to document.
5
+ */
6
+
7
+ .hello-swatch {
8
+ font-family: var(--font-base, sans-serif);
9
+ font-size: var(--size-step-0, 1rem);
10
+ color: var(--color-primary, currentColor);
11
+ }
@@ -0,0 +1,7 @@
1
+ /* === Swatches ===
2
+ *
3
+ * Component styles for your swatches. Add a CSS file per component
4
+ * and import it here.
5
+ */
6
+
7
+ @import "hello.css";
@@ -8,6 +8,39 @@
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
10
 
11
+ // Inline script that resolves CSS custom property values and annotates
12
+ // .token-value elements with their computed value (e.g. "(#3b82f6)").
13
+ // Included directly in any generated HTML fragment that uses .token-value
14
+ // so the fragment is self-contained and doesn't depend on a shared bundle.
15
+ const TOKEN_DISPLAY_SCRIPT = `<script>
16
+ (function() {
17
+ var elements = document.querySelectorAll('.token-value');
18
+ if (!elements.length) return;
19
+ elements.forEach(function(el) {
20
+ var prop = el.getAttribute('data-var');
21
+ var computed = getComputedStyle(el).getPropertyValue(prop).trim();
22
+ if (computed) {
23
+ el.innerHTML += ' <span style="opacity:0.5;font-family:monospace;font-size:0.8em">(' + computed + ')</span>';
24
+ } else {
25
+ var temp = document.createElement('div');
26
+ temp.style.fontSize = '16px';
27
+ temp.style.lineHeight = 'var(' + prop + ')';
28
+ document.body.appendChild(temp);
29
+ var computedPx = getComputedStyle(temp).lineHeight;
30
+ document.body.removeChild(temp);
31
+ var displayValue = computedPx;
32
+ if (computedPx.endsWith('px')) {
33
+ var ratio = Math.round((parseFloat(computedPx) / 16) * 100) / 100;
34
+ displayValue = String(ratio);
35
+ }
36
+ if (displayValue) {
37
+ el.innerHTML += ' <span style="opacity:0.5;font-family:monospace;font-size:0.8em">(' + displayValue + ')</span>';
38
+ }
39
+ }
40
+ });
41
+ })();
42
+ </script>`;
43
+
11
44
  /**
12
45
  * Read and parse a JSON token file
13
46
  */
@@ -113,7 +146,8 @@ function generateTypography(tokensDir) {
113
146
  </style>
114
147
  <div class="type-ladder">
115
148
  ${steps}
116
- </div>`;
149
+ </div>
150
+ ${TOKEN_DISPLAY_SCRIPT}`;
117
151
  }
118
152
 
119
153
  /**
@@ -233,7 +267,8 @@ function generateFonts(tokensDir) {
233
267
 
234
268
  return `<div class="font-stack">
235
269
  ${stacks}
236
- </div>`;
270
+ </div>
271
+ ${TOKEN_DISPLAY_SCRIPT}`;
237
272
  }
238
273
 
239
274
  /**
@@ -294,7 +329,8 @@ ${leadings}
294
329
  color: #666;
295
330
  margin-left: 0.5rem;
296
331
  }
297
- </style>`;
332
+ </style>
333
+ ${TOKEN_DISPLAY_SCRIPT}`;
298
334
  }
299
335
 
300
336
  /**
package/src/layout.html CHANGED
@@ -20,6 +20,5 @@
20
20
  <!-- SWATCHES -->
21
21
  </main>
22
22
  </div>
23
- <script src="js/swatches.js"></script>
24
23
  </body>
25
24
  </html>
@@ -9,6 +9,5 @@
9
9
  </head>
10
10
  <body class="wrapper region">
11
11
  <!-- PREVIEW_CONTENT -->
12
- <script src="<!-- JS_PATH -->js/swatches.js"></script>
13
12
  </body>
14
13
  </html>
@@ -0,0 +1,83 @@
1
+ # Hello Swatch
2
+
3
+ This is your first swatch. It lives at `swatchkit/swatches/hello/`.
4
+
5
+ ## How swatch folders work
6
+
7
+ SwatchKit treats every folder inside `swatchkit/` that contains an `index.html`
8
+ as a swatch. The `index.html` is rendered inside a preview iframe in the pattern
9
+ library. It is an HTML fragment — no `<html>`, `<head>`, or `<body>` needed, since
10
+ SwatchKit wraps it in a full page automatically.
11
+
12
+ ## Sibling files are copied alongside
13
+
14
+ Every file in this folder **other than** `index.html` and `description.html` is
15
+ copied into the build output next to the preview page. That means your `index.html`
16
+ can reference sibling files using relative paths:
17
+
18
+ ```html
19
+ <link rel="stylesheet" href="./styles.css">
20
+ <script src="./script.js"></script>
21
+ <img src="./demo-image.png" alt="">
22
+ ```
23
+
24
+ This makes each swatch self-contained. Drop in whatever assets the demo needs.
25
+
26
+ ## description.html
27
+
28
+ If you add a `description.html` file to this folder, its contents will be
29
+ displayed above the iframe in the pattern library UI — useful for documenting
30
+ usage notes, props, or design decisions.
31
+
32
+ ## Global CSS
33
+
34
+ Component styles that belong in your real app's stylesheet (not just the demo)
35
+ live in `css/swatches/`. The `hello.css` file there is imported into `main.css`
36
+ via `css/swatches/index.css`. Add new component CSS files to that folder and
37
+ import them from `css/swatches/index.css`.
38
+
39
+ ## Opting out with underscores
40
+
41
+ Prefix any file or folder with `_` to exclude it from the build entirely —
42
+ it won't be rendered as a swatch and won't be copied to the output.
43
+
44
+ This works at every level:
45
+
46
+ - `swatchkit/swatches/_wip/` — entire swatch folder ignored
47
+ - `swatchkit/_drafts/` — entire section folder ignored
48
+ - `swatchkit/swatches/hello/_notes.md` — single file inside a swatch, not copied
49
+ - `swatchkit/swatches/hello/_fixtures/` — subdirectory inside a swatch, not copied
50
+
51
+ This is useful for work-in-progress components, private notes, test fixtures,
52
+ or any file that should live alongside a swatch but never appear in the output.
53
+
54
+ ## A note on CSS isolation
55
+
56
+ SwatchKit does not scope or isolate CSS between swatches. Every preview page
57
+ loads the same global stylesheet (`main.css` and everything it imports), so
58
+ styles defined in `css/swatches/hello.css` are active on *every* swatch's
59
+ preview page — not just this one.
60
+
61
+ This means `button { background: red }` in one swatch's CSS will affect the
62
+ `<button>` elements in every other swatch. If two swatches both style `button`
63
+ differently, the cascade determines which wins.
64
+
65
+ The fix is to scope your styles with a component-specific class:
66
+
67
+ ```css
68
+ /* Instead of this: */
69
+ button { background: red; }
70
+
71
+ /* Do this: */
72
+ .hello-swatch button { background: red; }
73
+ ```
74
+
75
+ The same applies to sibling CSS files (`./styles.css` next to `index.html`) —
76
+ they load per-page on top of the global stylesheet, so unscoped rules will
77
+ still bleed into any swatch that shares the same elements.
78
+
79
+ ## This README
80
+
81
+ This file (`README.md`) is ignored by SwatchKit's UI — it is just for you and
82
+ your team. It gets copied into the build output alongside `index.html`, but it
83
+ is not rendered anywhere in the pattern library.
@@ -0,0 +1 @@
1
+ <p class="hello-swatch">Hello from SwatchKit! Edit <code>swatchkit/swatches/hello/index.html</code> to get started.</p>
@@ -1,15 +1,12 @@
1
- // Token Value Display Script
1
+ // Token Value Display Script (reference copy — not used directly by the build)
2
2
  //
3
3
  // Shows the computed (resolved) value of CSS custom properties next to each
4
4
  // .token-value element in the token documentation pages. For example, a color
5
5
  // token swatch might display "(#3b82f6)" beside the variable name.
6
6
  //
7
- // How it reaches the browser:
8
- // 1. `swatchkit init` copies this file to swatchkit/tokens/script.js
9
- // 2. The build scans section directories for loose .js files, collects them
10
- // (along with any component-folder scripts), wraps each in an IIFE, and
11
- // concatenates them into dist/swatchkit/js/swatches.js
12
- // 3. The layout templates include a <script> tag pointing to js/swatches.js
7
+ // The minified version of this logic is inlined directly into the HTML fragments
8
+ // generated by src/generators/index.js (TOKEN_DISPLAY_SCRIPT constant), so each
9
+ // token swatch page is fully self-contained with no external script dependency.
13
10
 
14
11
  const elements = document.querySelectorAll('.token-value');
15
12
  if (elements.length > 0) {