kss-modern 1.3.0-beta.0 → 1.3.0-beta.2

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
@@ -9,7 +9,7 @@ A modern, KSS-compatible styleguide generator. Parses KSS comment blocks from CS
9
9
  - Pug template support via worker thread pool
10
10
  - Color palette and icon gallery documentation
11
11
  - Figma embed integration with light/dark theme sync
12
- - Accessibility auditing (axe-core) and HTML validation
12
+ - Accessibility auditing (axe-core) and HTML validation, per component or page-wide via `window.kssAudit()` for CI and AI agents
13
13
  - Markdown descriptions with custom components (alerts, accordions)
14
14
  - Dark mode with three-way toggle (System/Light/Dark)
15
15
  - Global search, keyboard navigation, and "Open in Editor" links
@@ -202,7 +202,7 @@ type StyleguideChange = {
202
202
  type: 'structural';
203
203
  } | {
204
204
  type: 'markup';
205
- file: string;
205
+ files: string[];
206
206
  sections: string[];
207
207
  };
208
208
  /**
@@ -202,7 +202,7 @@ type StyleguideChange = {
202
202
  type: 'structural';
203
203
  } | {
204
204
  type: 'markup';
205
- file: string;
205
+ files: string[];
206
206
  sections: string[];
207
207
  };
208
208
  /**
@@ -16,11 +16,11 @@ import githubLightDefault from '@shikijs/themes/github-light-default';
16
16
  import MarkdownItAsync from 'markdown-it-async';
17
17
  import { createHighlighterCore } from 'shiki/core';
18
18
  import { createJavaScriptRegexEngine } from 'shiki/engine/javascript';
19
+ import { existsSync, readFileSync } from 'node:fs';
19
20
  import os from 'node:os';
20
21
  import { Worker } from 'node:worker_threads';
21
22
  import fs$1 from 'node:fs/promises';
22
23
  import { objectEntries } from '@antfu/utils';
23
- import { readFileSync } from 'node:fs';
24
24
  import chokidar from 'chokidar';
25
25
  import 'pug';
26
26
  import 'consola';
@@ -217,6 +217,9 @@ function escape(str) {
217
217
  return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
218
218
  }
219
219
  const when = (condition, content) => condition ? content() : "";
220
+ function attrs(obj) {
221
+ return Object.entries(obj).filter(([, value]) => value !== null && value !== void 0 && value !== false).map(([key, value]) => value === true ? key : `${key}="${value}"`).join(" ");
222
+ }
220
223
 
221
224
  const alertRenderer = (bodyContent, props) => {
222
225
  const type = props.type || "info";
@@ -390,7 +393,7 @@ function shiftHeadingLevels(markdownContent, rootHeadingLevel) {
390
393
  }
391
394
  return markdownContent;
392
395
  }
393
- async function parseMarkdown(data) {
396
+ function getMarkdownRenderer() {
394
397
  if (!md) {
395
398
  md = MarkdownItAsync({ linkify: true, typographer: true });
396
399
  md.use(markdownItComponent, {
@@ -411,6 +414,29 @@ async function parseMarkdown(data) {
411
414
  )
412
415
  );
413
416
  }
417
+ return md;
418
+ }
419
+ const RENDER_CACHE_MAX_ENTRIES = 2e3;
420
+ const renderCache = /* @__PURE__ */ new Map();
421
+ async function renderMarkdownCached(source) {
422
+ const cached = renderCache.get(source);
423
+ if (cached !== void 0) {
424
+ return cached;
425
+ }
426
+ const promise = getMarkdownRenderer().renderAsync(source);
427
+ renderCache.set(source, promise);
428
+ promise.catch(() => {
429
+ if (renderCache.get(source) === promise)
430
+ renderCache.delete(source);
431
+ });
432
+ if (renderCache.size > RENDER_CACHE_MAX_ENTRIES) {
433
+ const oldest = renderCache.keys().next().value;
434
+ if (oldest !== void 0 && oldest !== source)
435
+ renderCache.delete(oldest);
436
+ }
437
+ return promise;
438
+ }
439
+ async function parseMarkdown(data) {
414
440
  if ("filePath" in data) {
415
441
  const doesFileExist = fs.existsSync(data.filePath);
416
442
  if (!doesFileExist) {
@@ -419,12 +445,12 @@ async function parseMarkdown(data) {
419
445
  }
420
446
  let fileContent = await fs.readFile(data.filePath, "utf8");
421
447
  fileContent = shiftHeadingLevels(fileContent, data.rootHeadingLevel);
422
- const parsedMarkdown = await md.renderAsync(fileContent);
448
+ const parsedMarkdown = await renderMarkdownCached(fileContent);
423
449
  return parsedMarkdown;
424
450
  } else {
425
451
  let fileContent = data.markdownContent.replace("Markdown:", "");
426
452
  fileContent = shiftHeadingLevels(fileContent, data.rootHeadingLevel);
427
- return await md.renderAsync(fileContent);
453
+ return await renderMarkdownCached(fileContent);
428
454
  }
429
455
  }
430
456
 
@@ -1038,8 +1064,17 @@ function getPugDependencyGraph() {
1038
1064
  }
1039
1065
  const __filename$1 = fileURLToPath(import.meta.url);
1040
1066
  const __dirname$1 = path.dirname(__filename$1);
1041
- const workerFilePath = __dirname$1.includes("dist/") ? "./pug/worker.mjs" : "./worker.ts";
1042
- const workerFilePathResolved = path.resolve(__dirname$1, workerFilePath);
1067
+ function resolveWorkerFile() {
1068
+ const candidates = ["./worker.ts", "./worker.mjs", "./pug/worker.mjs"];
1069
+ for (const candidate of candidates) {
1070
+ const resolved = path.resolve(__dirname$1, candidate);
1071
+ if (existsSync(resolved))
1072
+ return resolved;
1073
+ }
1074
+ return void 0;
1075
+ }
1076
+ const workerFilePathResolved = resolveWorkerFile();
1077
+ let warnedMissingWorker = false;
1043
1078
  const slots = Array.from({ length: MAX_POOL_SIZE }, () => ({ tail: Promise.resolve(), active: 0 }));
1044
1079
  const allWorkers = /* @__PURE__ */ new Set();
1045
1080
  function slotIndexFor(id) {
@@ -1051,6 +1086,8 @@ function slotIndexFor(id) {
1051
1086
  return (hash >>> 0) % MAX_POOL_SIZE;
1052
1087
  }
1053
1088
  function spawnWorker(index) {
1089
+ if (!workerFilePathResolved)
1090
+ throw new Error("pug compile worker file could not be located");
1054
1091
  const worker = new Worker(workerFilePathResolved, { name: `pug-worker-${index}` });
1055
1092
  allWorkers.add(worker);
1056
1093
  return worker;
@@ -1185,7 +1222,11 @@ async function compileIds(mode, contentDir, repository, ids) {
1185
1222
  if (misses.length === 0)
1186
1223
  return clonedRepository;
1187
1224
  const errors = [];
1188
- const useInline = mode === "development" && misses.length <= INLINE_THRESHOLD;
1225
+ if (!workerFilePathResolved && misses.length > INLINE_THRESHOLD && !warnedMissingWorker) {
1226
+ warnedMissingWorker = true;
1227
+ logger.warn("Pug compile worker not found; compiling markup on the main thread. Rebuilds stay correct but are slower.");
1228
+ }
1229
+ const useInline = !workerFilePathResolved || mode === "development" && misses.length <= INLINE_THRESHOLD;
1189
1230
  if (useInline) {
1190
1231
  await Promise.all(misses.map((id) => compileInline(mode, contentDir, clonedRepository, id, errors)));
1191
1232
  } else {
@@ -1209,6 +1250,66 @@ async function compilePugMarkupIncremental(mode, contentDir, repository, ids) {
1209
1250
  return compileIds(mode, contentDir, repository, wanted);
1210
1251
  }
1211
1252
 
1253
+ function createRebuildQueue(handlers) {
1254
+ let isRunning = false;
1255
+ let isStructuralPending = false;
1256
+ const pendingMarkupFiles = /* @__PURE__ */ new Set();
1257
+ let idleResolvers = [];
1258
+ const hasPendingWork = () => isStructuralPending || pendingMarkupFiles.size > 0;
1259
+ const releaseIdleWaiters = () => {
1260
+ const resolvers = idleResolvers;
1261
+ idleResolvers = [];
1262
+ resolvers.forEach((resolve) => resolve());
1263
+ };
1264
+ const runNext = async () => {
1265
+ if (isStructuralPending) {
1266
+ isStructuralPending = false;
1267
+ pendingMarkupFiles.clear();
1268
+ await handlers.onStructural();
1269
+ return;
1270
+ }
1271
+ const changedFiles = [...pendingMarkupFiles];
1272
+ pendingMarkupFiles.clear();
1273
+ await handlers.onMarkup(changedFiles);
1274
+ };
1275
+ const drain = async () => {
1276
+ if (isRunning)
1277
+ return;
1278
+ isRunning = true;
1279
+ try {
1280
+ while (hasPendingWork()) {
1281
+ try {
1282
+ await runNext();
1283
+ } catch (error) {
1284
+ handlers.onError?.(error);
1285
+ }
1286
+ }
1287
+ } finally {
1288
+ isRunning = false;
1289
+ releaseIdleWaiters();
1290
+ }
1291
+ };
1292
+ return {
1293
+ requestStructural: () => {
1294
+ isStructuralPending = true;
1295
+ void drain();
1296
+ },
1297
+ requestMarkup: (changedFiles) => {
1298
+ for (const file of changedFiles) {
1299
+ pendingMarkupFiles.add(file);
1300
+ }
1301
+ void drain();
1302
+ },
1303
+ whenIdle: () => {
1304
+ if (!isRunning && !hasPendingWork())
1305
+ return Promise.resolve();
1306
+ return new Promise((resolve) => {
1307
+ idleResolvers.push(resolve);
1308
+ });
1309
+ }
1310
+ };
1311
+ }
1312
+
1212
1313
  const TRANSIENT_FS_ERROR_CODES = /* @__PURE__ */ new Set(["ENOENT", "EBUSY", "EPERM", "EACCES"]);
1213
1314
  function isTransientFsError(error) {
1214
1315
  if (typeof error !== "object" || error === null)
@@ -1253,7 +1354,7 @@ async function generateFullPageFile(data) {
1253
1354
  <meta name="theme-color" media="(prefers-color-scheme: light)" content="${data.theme.light}">
1254
1355
  <meta name="theme-color" media="(prefers-color-scheme: dark)" content="${data.theme.dark}">
1255
1356
  ` : `<meta name="theme-color" content="${data.theme}">`}
1256
- <script type="module" src="/styleguide-assets/fullpage-DpAT8aw3.js"><\/script>
1357
+ <script type="module" src="/styleguide-assets/fullpage-D-fU_bAa.js"><\/script>
1257
1358
  ${computedStyleTags}
1258
1359
  </head>
1259
1360
  <body${data.page.bodyclass ? ` class="${data.page.bodyclass}"` : ""}>
@@ -1265,6 +1366,9 @@ async function generateFullPageFile(data) {
1265
1366
  await logicalWriteFile(data.filePath, content);
1266
1367
  }
1267
1368
 
1369
+ function getSectionTitleLine(section) {
1370
+ return section.source.css.line + 1;
1371
+ }
1268
1372
  function getHasSectionExternalFullpage(section) {
1269
1373
  return section.markup.length > 0 && (section.icons === void 0 || section.icons.length === 0) && (section.colors === void 0 || section.colors.length === 0);
1270
1374
  }
@@ -1540,10 +1644,17 @@ function getMainContentSectionWrapper(section, html) {
1540
1644
  }
1541
1645
  return `<p class="mt-2${section.sectionLevel === "second" ? " text-xl" : ""}">${section.description}</p>`;
1542
1646
  };
1647
+ const sourceAttributes = attrs({
1648
+ "data-section-reference": sanitizeSpecialCharacters(section.id),
1649
+ "data-source-file": sanitizeSpecialCharacters(section.source.css.file),
1650
+ "data-source-line": getSectionTitleLine(section),
1651
+ "data-markup-file": section.source.markup?.file ? sanitizeSpecialCharacters(section.source.markup.file) : void 0
1652
+ });
1543
1653
  return `
1544
- <section
1545
- id="section-${sectionSanitizeId(section.id)}"
1654
+ <section
1655
+ id="section-${sectionSanitizeId(section.id)}"
1546
1656
  class="styleguide-section border-b px-4 py-10 border-b-styleguide-border scroll-mt-[50px] md:px-10"
1657
+ ${sourceAttributes}
1547
1658
  >
1548
1659
  <div class="flex items-center justify-between gap-6">
1549
1660
  <a class="relative group" href="#section-${sectionSanitizeId(section.id)}">
@@ -1580,7 +1691,7 @@ function getMainContentRegular(section, config) {
1580
1691
  if (config.mode === "development" && config.launchInEditor) {
1581
1692
  const computedRootPath = config.launchInEditor && typeof config.launchInEditor === "object" && "rootDir" in config.launchInEditor ? config.launchInEditor.rootDir : process.cwd();
1582
1693
  const cssFilePath = ensureStartingSlash(path.join(computedRootPath, section.source.css.file));
1583
- const cssLineNumber = section.source.css.line + 1;
1694
+ const cssLineNumber = getSectionTitleLine(section);
1584
1695
  openInEditorPaths.css = {
1585
1696
  vscode: `vscode://file//${cssFilePath}:${cssLineNumber}`,
1586
1697
  phpstorm: `phpstorm://open?file=${cssFilePath}&line=${cssLineNumber}`
@@ -1602,7 +1713,7 @@ function getMainContentRegular(section, config) {
1602
1713
  };
1603
1714
  const renderOpenInEditorLink = (editorType, contentType, link) => `
1604
1715
  <a
1605
- class="hidden editor-${editorType}-only @md:inline-flex items-center group/${editorType} gap-1.5 p-4 desktop-device-only cursor-pointer active:scale-90 transition hover:text-styleguide-highlight duration-200"
1716
+ class="pointer-events-auto hidden editor-${editorType}-only @md:inline-flex items-center group/${editorType} gap-1.5 p-4 desktop-device-only cursor-pointer active:scale-90 transition hover:text-styleguide-highlight duration-200"
1606
1717
  href="${link}"
1607
1718
  >
1608
1719
  <span class="hidden @2xl:inline-block">Open ${contentType}</span>
@@ -1635,47 +1746,59 @@ function getMainContentRegular(section, config) {
1635
1746
  </div>
1636
1747
 
1637
1748
  <!-- Code -->
1638
- <details class="group @container">
1639
- <summary class="flex cursor-pointer justify-between items-center rounded-b-2xl px-6 text-sm font-light bg-styleguide-bg">
1640
- <span class="flex gap-2 items-center py-4 select-none">
1641
- <svg class="h-4 w-4 group-open:rotate-90 transition-transform" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor">
1642
- <path fill-rule="evenodd" d="M6.22 4.22a.75.75 0 0 1 1.06 0l3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06L8.94 8 6.22 5.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd"/>
1643
- </svg>
1749
+ <div class="relative @container">
1750
+ <details class="group">
1751
+ <summary class="flex cursor-pointer items-center rounded-b-2xl px-6 text-sm font-light bg-styleguide-bg">
1752
+ <span class="flex gap-2 items-center py-4 select-none">
1753
+ <svg class="h-4 w-4 group-open:rotate-90 transition-transform" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor">
1754
+ <path fill-rule="evenodd" d="M6.22 4.22a.75.75 0 0 1 1.06 0l3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06L8.94 8 6.22 5.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd"/>
1755
+ </svg>
1756
+
1757
+ <span class="group-open:hidden">Show code</span>
1758
+ <span class="group-open:block hidden">Hide code</span>
1759
+ </span>
1760
+ </summary>
1761
+
1762
+ <div class="border-t p-6 text-sm bg-styleguide-bg-highlight border-styleguide-border">
1763
+ <div
1764
+ id="code-fullpage-${sectionSanitizeId(section.id)}"
1765
+ class="overflow-x-auto w-full code-highlight"
1766
+ data-source-code="${encodeURIComponent(sourceCode)}"
1767
+ data-source-lang="html"
1768
+ ></div>
1769
+ </div>
1770
+ </details>
1644
1771
 
1645
- <span class="group-open:hidden">Show code</span>
1646
- <span class="group-open:block hidden">Hide code</span>
1647
- </span>
1648
-
1649
- <span class="flex items-center">
1772
+ <span class="pointer-events-none absolute top-0 right-0 flex items-center pr-6 text-sm font-light">
1650
1773
  ${section.figma ? `<a
1651
- class="inline-flex items-center group/figma gap-1.5 p-4 desktop-device-only cursor-pointer active:scale-90 transition hover:text-styleguide-highlight duration-200"
1774
+ class="pointer-events-auto inline-flex items-center group/figma gap-1.5 p-4 desktop-device-only cursor-pointer active:scale-90 transition hover:text-styleguide-highlight duration-200"
1652
1775
  href="${computeFigmaExternalLink(section.figma)}"
1653
1776
  target="_blank"
1654
1777
  >
1655
1778
  <span class="hidden @2xl:inline-block">Open in</span>
1656
1779
  <span class="sr-only">Figma</span>
1657
- <img
1780
+ <img
1658
1781
  src="styleguide-assets/icons/figma.svg"
1659
- width="600"
1660
- height="600"
1661
- class="size-4 saturate-0 group-hover/figma:saturate-100 transition"
1662
- alt="Figma Logo"
1663
- aria-hidden="true"
1782
+ width="600"
1783
+ height="600"
1784
+ class="size-4 saturate-0 group-hover/figma:saturate-100 transition"
1785
+ alt="Figma Logo"
1786
+ aria-hidden="true"
1664
1787
  >
1665
1788
  </a>` : ""}
1666
-
1789
+
1667
1790
  ${openInEditorPaths.html ? `
1668
1791
  ${renderOpenInEditorLink("phpstorm", openInEditorPaths.html.phpstorm.includes(".pug") ? "Pug" : "HTML", openInEditorPaths.html.phpstorm)}
1669
1792
  ${renderOpenInEditorLink("vscode", openInEditorPaths.html.phpstorm.includes(".pug") ? "Pug" : "HTML", openInEditorPaths.html.vscode)}
1670
1793
  ` : ""}
1671
-
1794
+
1672
1795
  ${openInEditorPaths.css ? `
1673
1796
  ${renderOpenInEditorLink("phpstorm", "CSS", openInEditorPaths.css.phpstorm)}
1674
1797
  ${renderOpenInEditorLink("vscode", "CSS", openInEditorPaths.css.vscode)}
1675
1798
  ` : ""}
1676
1799
 
1677
1800
  <button
1678
- class="inline-flex items-center gap-1.5 p-4 cursor-pointer active:scale-90 transition hover:text-styleguide-highlight duration-200"
1801
+ class="pointer-events-auto inline-flex items-center gap-1.5 p-4 cursor-pointer active:scale-90 transition hover:text-styleguide-highlight duration-200"
1679
1802
  type="button"
1680
1803
  data-clipboard-value="${encodeURIComponent(sourceCode)}"
1681
1804
  data-clipboard-uri-encoded="true"
@@ -1690,7 +1813,7 @@ function getMainContentRegular(section, config) {
1690
1813
  </button>
1691
1814
 
1692
1815
  <button
1693
- class="inline-flex items-center gap-1.5 p-4 cursor-pointer active:scale-90 transition hover:text-styleguide-highlight duration-200"
1816
+ class="pointer-events-auto inline-flex items-center gap-1.5 p-4 cursor-pointer active:scale-90 transition hover:text-styleguide-highlight duration-200"
1694
1817
  type="button"
1695
1818
  data-code-audit-iframe="preview-fullpage-${sectionSanitizeId(section.id)}"
1696
1819
  aria-controls="code-audit-dialog"
@@ -1699,10 +1822,10 @@ function getMainContentRegular(section, config) {
1699
1822
  <svg class="h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122.88 122.88" aria-hidden="true"><title>accessibility</title><path fill="currentcolor" d="M61.44,0A61.46,61.46,0,1,1,18,18,61.21,61.21,0,0,1,61.44,0Zm-.39,74.18L52.1,98.91a4.94,4.94,0,0,1-2.58,2.83A5,5,0,0,1,42.7,95.5l6.24-17.28a26.3,26.3,0,0,0,1.17-4,40.64,40.64,0,0,0,.54-4.18c.24-2.53.41-5.27.54-7.9s.22-5.18.29-7.29c.09-2.63-.62-2.8-2.73-3.3l-.44-.1-18-3.39A5,5,0,0,1,27.08,46a5,5,0,0,1,5.05-7.74l19.34,3.63c.77.07,1.52.16,2.31.25a57.64,57.64,0,0,0,7.18.53A81.13,81.13,0,0,0,69.9,42c.9-.1,1.75-.21,2.6-.29l18.25-3.42A5,5,0,0,1,94.5,39a5,5,0,0,1,1.3,7,5,5,0,0,1-3.21,2.09L75.15,51.37c-.58.13-1.1.22-1.56.29-1.82.31-2.72.47-2.61,3.06.08,1.89.31,4.15.61,6.51.35,2.77.81,5.71,1.29,8.4.31,1.77.6,3.19,1,4.55s.79,2.75,1.39,4.42l6.11,16.9a5,5,0,0,1-6.82,6.24,4.94,4.94,0,0,1-2.58-2.83L63,74.23,62,72.4l-1,1.78Zm.39-53.52a8.83,8.83,0,1,1-6.24,2.59,8.79,8.79,0,0,1,6.24-2.59Zm36.35,4.43a51.42,51.42,0,1,0,15,36.35,51.27,51.27,0,0,0-15-36.35Z"/></svg>
1700
1823
  <span class="hidden md:inline-block">Audit</span>
1701
1824
  </button>
1702
-
1825
+
1703
1826
  ${getHasSectionExternalFullpage(section) ? `
1704
1827
  <a
1705
- class="hidden [.styleguide-section--large_&]:inline-block -mr-2 px-2 py-4 group/externallink hover:text-styleguide-highlight focus:text-styleguide-highlight"
1828
+ class="pointer-events-auto hidden [.styleguide-section--large_&]:inline-block -mr-2 px-2 py-4 group/externallink hover:text-styleguide-highlight focus:text-styleguide-highlight"
1706
1829
  href="/${section.fullpageFileName}"
1707
1830
  target="_blank"
1708
1831
  title="Open ${section.header} in fullpage"
@@ -1714,17 +1837,7 @@ function getMainContentRegular(section, config) {
1714
1837
  </a>
1715
1838
  ` : ""}
1716
1839
  </span>
1717
- </summary>
1718
-
1719
- <div class="border-t p-6 text-sm bg-styleguide-bg-highlight border-styleguide-border">
1720
- <div
1721
- id="code-fullpage-${sectionSanitizeId(section.id)}"
1722
- class="overflow-x-auto w-full code-highlight"
1723
- data-source-code="${encodeURIComponent(sourceCode)}"
1724
- data-source-lang="html"
1725
- ></div>
1726
- </div>
1727
- </details>
1840
+ </div>
1728
1841
  </div>
1729
1842
 
1730
1843
  ${section.modifiers?.length > 0 ? `
@@ -2060,7 +2173,7 @@ async function generatePreviewFile(data) {
2060
2173
  <meta name="theme-color" media="(prefers-color-scheme: light)" content="${data.theme.light}">
2061
2174
  <meta name="theme-color" media="(prefers-color-scheme: dark)" content="${data.theme.dark}">
2062
2175
  ` : `<meta name="theme-color" content="${data.theme}">`}
2063
- <link rel="stylesheet" type="text/css" href="/styleguide-assets/kss-modern-Bw6-exnr.css">
2176
+ <link rel="stylesheet" type="text/css" href="/styleguide-assets/kss-modern-BJ971T05.css">
2064
2177
  <script>
2065
2178
  //#region client/lib/iframe-resize.ts
2066
2179
  var e = [
@@ -2223,7 +2336,7 @@ document.addEventListener("DOMContentLoaded", () => {
2223
2336
  //#endregion
2224
2337
  ;
2225
2338
  <\/script>
2226
- <script type="module" src="/styleguide-assets/preview-BNQ_kTZ_.js"><\/script>
2339
+ <script type="module" src="/styleguide-assets/preview-DEipbLLB.js"><\/script>
2227
2340
  <link rel="preload" href="/styleguide-assets/fonts/geist-mono-latin-400-normal.woff2" as="font" type="font/woff2" crossorigin="anonymous">
2228
2341
  <link rel="preload" href="/styleguide-assets/fonts/geist-mono-latin-600-normal.woff2" as="font" type="font/woff2" crossorigin="anonymous">
2229
2342
  <link rel="preload" href="/styleguide-assets/fonts/geist-mono-latin-300-normal.woff2" as="font" type="font/woff2" crossorigin="anonymous">
@@ -2278,6 +2391,26 @@ document.addEventListener("DOMContentLoaded", () => {
2278
2391
  await logicalWriteFile(data.filePath, content);
2279
2392
  }
2280
2393
 
2394
+ function batchDebounce(fn, windowMs) {
2395
+ let timer;
2396
+ let buffer = [];
2397
+ return (item) => {
2398
+ buffer.push(item);
2399
+ if (timer !== void 0) {
2400
+ clearTimeout(timer);
2401
+ }
2402
+ timer = setTimeout(() => {
2403
+ const batch = buffer;
2404
+ buffer = [];
2405
+ timer = void 0;
2406
+ fn(batch);
2407
+ }, windowMs);
2408
+ };
2409
+ }
2410
+
2411
+ const BATCH_SETTLE_MS = 300;
2412
+ const POLLING_INTERVAL_MS = 300;
2413
+ const USE_POLLING = process.env.FORCE_POLLING === "1" || existsSync("/.dockerenv") || existsSync("/run/.containerenv") || process.env.container !== void 0;
2281
2414
  const VALID_CSS_FILE_TYPES = [".css", ".scss", ".sass", ".less"];
2282
2415
  const MARKUP_FILE_TYPES = [".pug", ".html"];
2283
2416
  function isCssFile(filePath) {
@@ -2308,11 +2441,33 @@ function tryReadFileSync(filePath) {
2308
2441
  throw error;
2309
2442
  }
2310
2443
  }
2444
+ function planRebuilds(changes) {
2445
+ const structural = changes.some((change) => change.category === "structural");
2446
+ if (structural) {
2447
+ return { structural: true, markupFiles: [] };
2448
+ }
2449
+ const markupFiles = [...new Set(
2450
+ changes.filter((change) => change.category === "markup").map((change) => change.path)
2451
+ )];
2452
+ return { structural: false, markupFiles };
2453
+ }
2311
2454
  function watchStyleguideForChanges(watchPath, handlers) {
2312
2455
  if (typeof handlers.onStructuralChange !== "function" || typeof handlers.onMarkupChange !== "function") {
2313
2456
  throw new TypeError("styleguide watch requires onStructuralChange and onMarkupChange callbacks");
2314
2457
  }
2315
2458
  const regexFileContents = /* @__PURE__ */ new Map();
2459
+ const dispatch = batchDebounce((changes) => {
2460
+ const { structural, markupFiles } = planRebuilds(changes);
2461
+ if (structural) {
2462
+ handlers.onStructuralChange();
2463
+ return;
2464
+ }
2465
+ if (markupFiles.length > 0) {
2466
+ handlers.onMarkupChange(markupFiles);
2467
+ }
2468
+ }, BATCH_SETTLE_MS);
2469
+ const requestStructural = () => dispatch({ category: "structural" });
2470
+ const requestMarkup = (filePath) => dispatch({ category: "markup", path: path.resolve(filePath) });
2316
2471
  const handleCssAdd = (filePath) => {
2317
2472
  const contents = tryReadFileSync(filePath);
2318
2473
  if (contents === void 0)
@@ -2322,7 +2477,7 @@ function watchStyleguideForChanges(watchPath, handlers) {
2322
2477
  return;
2323
2478
  }
2324
2479
  regexFileContents.set(filePath, currentFileMatches);
2325
- handlers.onStructuralChange();
2480
+ requestStructural();
2326
2481
  };
2327
2482
  const handleCssChange = (filePath) => {
2328
2483
  const previousFileMatches = regexFileContents.get(filePath);
@@ -2336,47 +2491,48 @@ function watchStyleguideForChanges(watchPath, handlers) {
2336
2491
  if (currentFileMatches === null) {
2337
2492
  return;
2338
2493
  }
2339
- handlers.onStructuralChange();
2494
+ requestStructural();
2340
2495
  return;
2341
2496
  }
2342
2497
  if (matchArraysEqual(previousFileMatches, currentFileMatches)) {
2343
2498
  return;
2344
2499
  }
2345
2500
  regexFileContents.set(filePath, currentFileMatches);
2346
- handlers.onStructuralChange();
2501
+ requestStructural();
2347
2502
  };
2348
2503
  const handleCssUnlink = (filePath) => {
2349
2504
  regexFileContents.delete(filePath);
2350
- handlers.onStructuralChange();
2505
+ requestStructural();
2506
+ };
2507
+ const routeEvent = (filePath, handleCss) => {
2508
+ if (isCssFile(filePath))
2509
+ handleCss(filePath);
2510
+ else if (isMarkdownFile(filePath))
2511
+ requestStructural();
2512
+ else if (isMarkupFile(filePath))
2513
+ requestMarkup(filePath);
2351
2514
  };
2352
2515
  const validFileTypes = [...VALID_CSS_FILE_TYPES, ".md", ...MARKUP_FILE_TYPES];
2353
2516
  return chokidar.watch(watchPath, {
2354
2517
  ignoreInitial: true,
2518
+ usePolling: USE_POLLING,
2519
+ interval: POLLING_INTERVAL_MS,
2520
+ // Wait until a file has stopped being written before emitting — collapses the
2521
+ // multi-event sequence an atomic-save editor (JetBrains, vim with
2522
+ // backupcopy=auto) produces for a single save, and guarantees the read in the
2523
+ // CSS handlers sees complete content rather than a half-written file.
2524
+ awaitWriteFinish: {
2525
+ stabilityThreshold: 100,
2526
+ pollInterval: 20
2527
+ },
2355
2528
  // @ts-expect-error - chokidar types seem to be incomplete, ignore
2356
2529
  ignored: (filePath, stats) => {
2357
2530
  return stats?.isFile() && !validFileTypes.some((type) => filePath.endsWith(type));
2358
2531
  }
2359
- }).on("add", (filePath) => {
2360
- if (isCssFile(filePath))
2361
- handleCssAdd(filePath);
2362
- else if (isMarkdownFile(filePath))
2363
- handlers.onStructuralChange();
2364
- else if (isMarkupFile(filePath))
2365
- handlers.onMarkupChange(path.resolve(filePath));
2366
- }).on("change", (filePath) => {
2367
- if (isCssFile(filePath))
2368
- handleCssChange(filePath);
2369
- else if (isMarkdownFile(filePath))
2370
- handlers.onStructuralChange();
2371
- else if (isMarkupFile(filePath))
2372
- handlers.onMarkupChange(path.resolve(filePath));
2373
- }).on("unlink", (filePath) => {
2374
- if (isCssFile(filePath))
2375
- handleCssUnlink(filePath);
2376
- else if (isMarkdownFile(filePath))
2377
- handlers.onStructuralChange();
2378
- else if (isMarkupFile(filePath))
2379
- handlers.onMarkupChange(path.resolve(filePath));
2532
+ }).on("add", (filePath) => routeEvent(filePath, handleCssAdd)).on("change", (filePath) => routeEvent(filePath, handleCssChange)).on("unlink", (filePath) => routeEvent(filePath, handleCssUnlink)).on("error", (error) => {
2533
+ if (isTransientFsError(error))
2534
+ return;
2535
+ logger.error("Styleguide watcher error (ignored to keep the dev server running):", error);
2380
2536
  });
2381
2537
  }
2382
2538
 
@@ -2589,6 +2745,19 @@ async function buildContext(config) {
2589
2745
  ownerLocationById
2590
2746
  };
2591
2747
  }
2748
+ const ASSETS_MARKER_FILE = ".kss-modern-assets";
2749
+ function getAssetsBuildId(theme) {
2750
+ return `kss-modern-BJ971T05.css
2751
+ ${JSON.stringify(theme)}
2752
+ `;
2753
+ }
2754
+ async function readAssetsBuildId(markerPath) {
2755
+ try {
2756
+ return await fs.readFile(markerPath, "utf-8");
2757
+ } catch {
2758
+ return void 0;
2759
+ }
2760
+ }
2592
2761
  async function copyStyleguideAssets(config) {
2593
2762
  const __filename = fileURLToPath(import.meta.url);
2594
2763
  const __dirname = path.dirname(__filename);
@@ -2600,11 +2769,16 @@ async function copyStyleguideAssets(config) {
2600
2769
  };
2601
2770
  const assetsDirectoryPath = findAssetsDirectoryPath();
2602
2771
  const assetsDirectoryOutputPath = path.join(config.outDir, "styleguide-assets");
2603
- const isAssetsDirectoryAlreadyCopied = await fs.exists(assetsDirectoryOutputPath) && (await fs.readdir(assetsDirectoryOutputPath)).length > 0;
2604
- if (!isAssetsDirectoryAlreadyCopied) {
2605
- await fs.copy(assetsDirectoryPath, assetsDirectoryOutputPath);
2606
- await generateFaviconFiles(assetsDirectoryOutputPath, config.theme);
2772
+ const markerPath = path.join(assetsDirectoryOutputPath, ASSETS_MARKER_FILE);
2773
+ const expectedBuildId = getAssetsBuildId(config.theme);
2774
+ const currentBuildId = await readAssetsBuildId(markerPath);
2775
+ if (currentBuildId === expectedBuildId) {
2776
+ return;
2607
2777
  }
2778
+ await fs.remove(assetsDirectoryOutputPath);
2779
+ await fs.copy(assetsDirectoryPath, assetsDirectoryOutputPath);
2780
+ await generateFaviconFiles(assetsDirectoryOutputPath, config.theme);
2781
+ await fs.writeFile(markerPath, expectedBuildId);
2608
2782
  }
2609
2783
  async function buildAll(config) {
2610
2784
  if (config.mode === "production" && await fs.exists(config.outDir)) {
@@ -2700,35 +2874,37 @@ async function watchStyleguide(config, onChange, onError) {
2700
2874
  }
2701
2875
  let context = initialBuild.context;
2702
2876
  const contentDirPath = config.contentDir.endsWith("/") ? config.contentDir : `${config.contentDir}/`;
2703
- watchStyleguideForChanges(contentDirPath, {
2704
- // css/scss/md edits can change section structure -> full rebuild + fresh context
2705
- onStructuralChange: () => {
2706
- (async () => {
2707
- const localBuild = await buildAll(config);
2708
- context = localBuild.context;
2709
- if (onChange)
2710
- onChange({ type: "structural" });
2711
- if (onError && localBuild.errors) {
2712
- onError(localBuild.errors);
2713
- }
2714
- })().catch((error) => {
2715
- logger.error("Error during rebuild:", error);
2716
- });
2877
+ const queue = createRebuildQueue({
2878
+ // css/scss/md/yaml edits can change section structure -> full rebuild + fresh context
2879
+ onStructural: async () => {
2880
+ const localBuild = await buildAll(config);
2881
+ context = localBuild.context;
2882
+ if (onChange)
2883
+ onChange({ type: "structural" });
2884
+ if (onError && localBuild.errors) {
2885
+ onError(localBuild.errors);
2886
+ }
2717
2887
  },
2718
- // a .pug/.html source edit -> recompile + rewrite only the sections that depend on it
2719
- onMarkupChange: (changedFile) => {
2720
- (async () => {
2721
- const affected = getPugDependencyGraph().getAffectedSections(changedFile);
2722
- if (affected.length === 0)
2723
- return;
2724
- await rebuildSections(config, context, affected);
2725
- if (onChange)
2726
- onChange({ type: "markup", file: changedFile, sections: affected });
2727
- })().catch((error) => {
2728
- logger.error("Error during incremental rebuild:", error);
2729
- });
2888
+ // .pug/.html source edits -> recompile + rewrite only the sections that depend on them
2889
+ onMarkup: async (changedFiles) => {
2890
+ const dependencyGraph = getPugDependencyGraph();
2891
+ const affected = [...new Set(
2892
+ changedFiles.flatMap((changedFile) => dependencyGraph.getAffectedSections(changedFile))
2893
+ )];
2894
+ if (affected.length === 0)
2895
+ return;
2896
+ await rebuildSections(config, context, affected);
2897
+ if (onChange)
2898
+ onChange({ type: "markup", files: changedFiles, sections: affected });
2899
+ },
2900
+ onError: (error) => {
2901
+ logger.error("Error during rebuild:", error);
2730
2902
  }
2731
2903
  });
2904
+ watchStyleguideForChanges(contentDirPath, {
2905
+ onStructuralChange: queue.requestStructural,
2906
+ onMarkupChange: queue.requestMarkup
2907
+ });
2732
2908
  }
2733
2909
 
2734
2910
  export { buildAll, buildStyleguide, logger, parseStyleguide, rebuildSections, watchStyleguide };