@stayicon/drift-guard 0.1.0 → 0.2.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.
@@ -1,3 +1,113 @@
1
+ // src/parsers/structure-parser.ts
2
+ import * as cheerio from "cheerio";
3
+ import { createHash } from "crypto";
4
+ var SEMANTIC_TAGS = [
5
+ "header",
6
+ "nav",
7
+ "main",
8
+ "section",
9
+ "article",
10
+ "aside",
11
+ "footer",
12
+ "form",
13
+ "table",
14
+ "dialog"
15
+ ];
16
+ function shortHash(input) {
17
+ return createHash("sha256").update(input).digest("hex").slice(0, 8);
18
+ }
19
+ function computeMaxDepth($, el, depth) {
20
+ let maxDepth = depth;
21
+ el.children().each((_, child) => {
22
+ if ($(child).prop("nodeType") === 1) {
23
+ const childDepth = computeMaxDepth($, $(child), depth + 1);
24
+ if (childDepth > maxDepth) {
25
+ maxDepth = childDepth;
26
+ }
27
+ }
28
+ });
29
+ return maxDepth;
30
+ }
31
+ function computeStructureFingerprint(htmlContent) {
32
+ const $ = cheerio.load(htmlContent);
33
+ const semanticTags = {};
34
+ for (const tag of SEMANTIC_TAGS) {
35
+ const count = $(tag).length;
36
+ if (count > 0) {
37
+ semanticTags[tag] = count;
38
+ }
39
+ }
40
+ const body = $("body");
41
+ const maxDepth = body.length > 0 ? computeMaxDepth($, body, 0) : 0;
42
+ const layoutElements = [];
43
+ $("[style]").each((_, el) => {
44
+ const style = $(el).attr("style") ?? "";
45
+ if (/display\s*:\s*(flex|grid|inline-flex|inline-grid)/i.test(style)) {
46
+ const tag = ($(el).prop("tagName") ?? "div").toLowerCase();
47
+ const cls = $(el).attr("class") ?? "";
48
+ layoutElements.push(`${tag}.${cls}`);
49
+ }
50
+ });
51
+ $('[class*="flex"], [class*="grid"]').each((_, el) => {
52
+ const tag = ($(el).prop("tagName") ?? "div").toLowerCase();
53
+ const cls = $(el).attr("class") ?? "";
54
+ if (/\b(flex|grid|inline-flex|inline-grid)\b/.test(cls)) {
55
+ const key = `${tag}.${cls}`;
56
+ if (!layoutElements.includes(key)) {
57
+ layoutElements.push(key);
58
+ }
59
+ }
60
+ });
61
+ layoutElements.sort();
62
+ const layoutHash = layoutElements.length > 0 ? shortHash(layoutElements.join("|")) : "empty";
63
+ const childTags = [];
64
+ body.children().each((_, child) => {
65
+ if ($(child).prop("nodeType") === 1) {
66
+ const tag = ($(child).prop("tagName") ?? "").toLowerCase();
67
+ if (tag && tag !== "script" && tag !== "link") {
68
+ childTags.push(tag);
69
+ }
70
+ }
71
+ });
72
+ const childSequenceHash = childTags.length > 0 ? shortHash(childTags.join(",")) : "empty";
73
+ return {
74
+ semanticTags,
75
+ maxDepth,
76
+ layoutHash,
77
+ childSequenceHash
78
+ };
79
+ }
80
+ function compareStructure(original, current) {
81
+ const details = [];
82
+ if (original.maxDepth !== current.maxDepth) {
83
+ details.push(`maxDepth: ${original.maxDepth} \u2192 ${current.maxDepth}`);
84
+ }
85
+ const allTags = /* @__PURE__ */ new Set([
86
+ ...Object.keys(original.semanticTags),
87
+ ...Object.keys(current.semanticTags)
88
+ ]);
89
+ for (const tag of allTags) {
90
+ const origCount = original.semanticTags[tag] ?? 0;
91
+ const currCount = current.semanticTags[tag] ?? 0;
92
+ if (origCount !== currCount) {
93
+ if (origCount === 0) {
94
+ details.push(`<${tag}> added (${currCount})`);
95
+ } else if (currCount === 0) {
96
+ details.push(`<${tag}> removed (was ${origCount})`);
97
+ } else {
98
+ details.push(`<${tag}> count: ${origCount} \u2192 ${currCount}`);
99
+ }
100
+ }
101
+ }
102
+ if (original.layoutHash !== current.layoutHash) {
103
+ details.push(`layout elements changed (hash: ${original.layoutHash} \u2192 ${current.layoutHash})`);
104
+ }
105
+ if (original.childSequenceHash !== current.childSequenceHash) {
106
+ details.push(`body child sequence changed (hash: ${original.childSequenceHash} \u2192 ${current.childSequenceHash})`);
107
+ }
108
+ return details;
109
+ }
110
+
1
111
  // src/types/index.ts
2
112
  var DEFAULT_CONFIG = {
3
113
  cssFiles: [
@@ -77,19 +187,62 @@ var CATEGORY_MAP = {
77
187
  "align-items": "layout",
78
188
  "grid-template-columns": "layout",
79
189
  "grid-template-rows": "layout",
80
- "position": "layout"
190
+ "position": "layout",
191
+ // Visual effects
192
+ "backdrop-filter": "other",
193
+ "filter": "other",
194
+ "animation": "other",
195
+ "transition": "other"
81
196
  };
82
- function getCategory(property) {
197
+ function getCategory(property, value) {
83
198
  if (CATEGORY_MAP[property]) {
84
199
  return CATEGORY_MAP[property];
85
200
  }
86
201
  if (property.startsWith("--")) {
87
202
  const lower = property.toLowerCase();
88
- if (lower.includes("color") || lower.includes("bg") || lower.includes("text")) return "color";
89
- if (lower.includes("font") || lower.includes("size") || lower.includes("weight")) return "font";
90
- if (lower.includes("spacing") || lower.includes("margin") || lower.includes("padding") || lower.includes("gap")) return "spacing";
203
+ const colorKeywords = [
204
+ "color",
205
+ "bg",
206
+ "text",
207
+ "foreground",
208
+ "background",
209
+ // Semantic color tokens (Shadcn UI / Tailwind)
210
+ "primary",
211
+ "secondary",
212
+ "accent",
213
+ "muted",
214
+ "destructive",
215
+ "success",
216
+ "warning",
217
+ "danger",
218
+ "error",
219
+ "info",
220
+ // UI component colors
221
+ "card",
222
+ "popover",
223
+ "border",
224
+ "input",
225
+ "ring",
226
+ "sidebar",
227
+ "chart",
228
+ "glow",
229
+ // State colors
230
+ "hover",
231
+ "active",
232
+ "focus",
233
+ "disabled"
234
+ ];
235
+ if (colorKeywords.some((kw) => lower.includes(kw))) return "color";
236
+ if (lower.includes("font") || lower.includes("size") || lower.includes("weight") || lower.includes("line-height") || lower.includes("letter")) return "font";
237
+ if (lower.includes("spacing") || lower.includes("margin") || lower.includes("padding") || lower.includes("gap") || lower.includes("inset")) return "spacing";
91
238
  if (lower.includes("shadow")) return "shadow";
92
239
  if (lower.includes("radius") || lower.includes("rounded")) return "radius";
240
+ if (lower.includes("width") || lower.includes("height") || lower.includes("sidebar-width")) return "layout";
241
+ if (value) {
242
+ const trimmed = value.trim();
243
+ if (/^\d{1,3}\s+\d{1,3}%\s+\d{1,3}%$/.test(trimmed)) return "color";
244
+ if (/^(#|rgb|hsl|oklch|lch|lab|color\()/.test(trimmed)) return "color";
245
+ }
93
246
  return "other";
94
247
  }
95
248
  return null;
@@ -105,9 +258,9 @@ function parseCss(cssContent, filePath) {
105
258
  visit: "Declaration",
106
259
  enter(node) {
107
260
  const property = node.property;
108
- const category = getCategory(property);
109
- if (!category) return;
110
261
  const value = csstree.generate(node.value);
262
+ const category = getCategory(property, value);
263
+ if (!category) return;
111
264
  if (!value || ["inherit", "initial", "unset", "revert"].includes(value)) return;
112
265
  let selector = ":root";
113
266
  let parent = this.atrule ?? this.rule;
@@ -136,7 +289,7 @@ function extractCssVariables(cssContent, filePath) {
136
289
  while ((match = varRegex.exec(cssContent)) !== null) {
137
290
  const property = `--${match[1]}`;
138
291
  const value = match[2].trim();
139
- const category = getCategory(property) ?? "other";
292
+ const category = getCategory(property, value) ?? "other";
140
293
  tokens.push({
141
294
  category,
142
295
  property,
@@ -150,7 +303,7 @@ function extractCssVariables(cssContent, filePath) {
150
303
  }
151
304
 
152
305
  // src/parsers/html-parser.ts
153
- import * as cheerio from "cheerio";
306
+ import * as cheerio2 from "cheerio";
154
307
  var TRACKED_PROPERTIES = {
155
308
  "color": "color",
156
309
  "background-color": "color",
@@ -177,7 +330,12 @@ var TRACKED_PROPERTIES = {
177
330
  "display": "layout",
178
331
  "flex-direction": "layout",
179
332
  "justify-content": "layout",
180
- "align-items": "layout"
333
+ "align-items": "layout",
334
+ // Visual effects
335
+ "backdrop-filter": "other",
336
+ "filter": "other",
337
+ "animation": "other",
338
+ "transition": "other"
181
339
  };
182
340
  function parseInlineStyle(styleStr) {
183
341
  const result = {};
@@ -195,7 +353,7 @@ function parseInlineStyle(styleStr) {
195
353
  }
196
354
  function parseHtml(htmlContent, filePath) {
197
355
  const tokens = [];
198
- const $ = cheerio.load(htmlContent);
356
+ const $ = cheerio2.load(htmlContent);
199
357
  const styleBlocks = [];
200
358
  $("style").each((_, el) => {
201
359
  const text = $(el).text();
@@ -224,7 +382,7 @@ function parseHtml(htmlContent, filePath) {
224
382
  return tokens;
225
383
  }
226
384
  function extractStyleBlocks(htmlContent) {
227
- const $ = cheerio.load(htmlContent);
385
+ const $ = cheerio2.load(htmlContent);
228
386
  const blocks = [];
229
387
  $("style").each((_, el) => {
230
388
  const text = $(el).text();
@@ -234,6 +392,64 @@ function extractStyleBlocks(htmlContent) {
234
392
  });
235
393
  return blocks;
236
394
  }
395
+ function extractTailwindConfig(htmlContent, filePath) {
396
+ const tokens = [];
397
+ const $ = cheerio2.load(htmlContent);
398
+ $("script").each((_, el) => {
399
+ const scriptId = $(el).attr("id") ?? "";
400
+ const text = $(el).text();
401
+ if (!text) return;
402
+ const isTailwindConfig = scriptId.toLowerCase().includes("tailwind") || text.includes("tailwind.config");
403
+ if (!isTailwindConfig) return;
404
+ const colorsMatch = text.match(/colors\s*:\s*\{([^}]+)\}/);
405
+ if (colorsMatch) {
406
+ const colorsBlock = colorsMatch[1];
407
+ const colorRegex = /["']([^"']+)["']\s*:\s*["']([^"']+)["']/g;
408
+ let match;
409
+ while ((match = colorRegex.exec(colorsBlock)) !== null) {
410
+ tokens.push({
411
+ category: "color",
412
+ property: `--tw-${match[1]}`,
413
+ value: match[2],
414
+ selector: "[tailwind.config]",
415
+ file: filePath
416
+ });
417
+ }
418
+ }
419
+ const radiusMatch = text.match(/borderRadius\s*:\s*\{([^}]+)\}/);
420
+ if (radiusMatch) {
421
+ const radiusBlock = radiusMatch[1];
422
+ const radiusRegex = /["']([^"']+)["']\s*:\s*["']([^"']+)["']/g;
423
+ let match;
424
+ while ((match = radiusRegex.exec(radiusBlock)) !== null) {
425
+ tokens.push({
426
+ category: "radius",
427
+ property: `--tw-radius-${match[1]}`,
428
+ value: match[2],
429
+ selector: "[tailwind.config]",
430
+ file: filePath
431
+ });
432
+ }
433
+ }
434
+ const fontMatch = text.match(/fontFamily\s*:\s*\{([^}]+)\}/);
435
+ if (fontMatch) {
436
+ const fontBlock = fontMatch[1];
437
+ const fontRegex = /["']([^"']+)["']\s*:\s*\[([^\]]+)\]/g;
438
+ let match;
439
+ while ((match = fontRegex.exec(fontBlock)) !== null) {
440
+ const familyValues = match[2].split(",").map((v) => v.trim().replace(/["']/g, "")).join(", ");
441
+ tokens.push({
442
+ category: "font",
443
+ property: `--tw-font-${match[1]}`,
444
+ value: familyValues,
445
+ selector: "[tailwind.config]",
446
+ file: filePath
447
+ });
448
+ }
449
+ }
450
+ });
451
+ return tokens;
452
+ }
237
453
  function buildSelectorPath($, element) {
238
454
  const parts = [];
239
455
  const tagName = element.prop("tagName")?.toLowerCase() ?? "div";
@@ -292,6 +508,8 @@ async function scanProject(projectRoot, config, stitchHtmlPath) {
292
508
  allTokens.push(...vars);
293
509
  }
294
510
  scannedFiles.push(stitchHtmlPath);
511
+ const twTokens = extractTailwindConfig(htmlContent, stitchHtmlPath);
512
+ allTokens.push(...twTokens);
295
513
  }
296
514
  }
297
515
  const cssFiles = await fg(config.cssFiles, {
@@ -324,6 +542,8 @@ async function scanProject(projectRoot, config, stitchHtmlPath) {
324
542
  const cssTokens = parseCss(block, file);
325
543
  allTokens.push(...cssTokens);
326
544
  }
545
+ const twTokens = extractTailwindConfig(content, file);
546
+ allTokens.push(...twTokens);
327
547
  scannedFiles.push(file);
328
548
  }
329
549
  const filtered = allTokens.filter(
@@ -356,13 +576,38 @@ function buildSummary(tokens) {
356
576
  async function createSnapshot(projectRoot, stitchHtmlPath) {
357
577
  const config = loadConfig(projectRoot);
358
578
  const { tokens, files } = await scanProject(projectRoot, config, stitchHtmlPath);
579
+ let structure;
580
+ try {
581
+ let htmlForStructure = null;
582
+ if (stitchHtmlPath) {
583
+ const absPath = path.resolve(projectRoot, stitchHtmlPath);
584
+ if (fs.existsSync(absPath)) {
585
+ htmlForStructure = fs.readFileSync(absPath, "utf-8");
586
+ }
587
+ } else {
588
+ for (const file of files) {
589
+ if (file.endsWith(".html")) {
590
+ const absPath = path.join(projectRoot, file);
591
+ if (fs.existsSync(absPath)) {
592
+ htmlForStructure = fs.readFileSync(absPath, "utf-8");
593
+ break;
594
+ }
595
+ }
596
+ }
597
+ }
598
+ if (htmlForStructure) {
599
+ structure = computeStructureFingerprint(htmlForStructure);
600
+ }
601
+ } catch {
602
+ }
359
603
  const snapshot = {
360
604
  version: "1.0.0",
361
605
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
362
606
  projectRoot,
363
607
  sourceFiles: files,
364
608
  tokens,
365
- summary: buildSummary(tokens)
609
+ summary: buildSummary(tokens),
610
+ structure
366
611
  };
367
612
  return snapshot;
368
613
  }
@@ -430,6 +675,37 @@ async function detectDrift(projectRoot, snapshot, threshold = 10) {
430
675
  driftPercent: total > 0 ? Math.round(changed / total * 100 * 100) / 100 : 0
431
676
  };
432
677
  }
678
+ let structureDrift;
679
+ if (snapshot.structure) {
680
+ try {
681
+ const config2 = loadConfig(projectRoot);
682
+ const fg2 = (await import("fast-glob")).default;
683
+ const htmlFiles = await fg2(config2.htmlFiles, {
684
+ cwd: projectRoot,
685
+ ignore: config2.ignore,
686
+ absolute: false
687
+ });
688
+ let htmlContent = null;
689
+ const fs3 = await import("fs");
690
+ const path3 = await import("path");
691
+ for (const file of htmlFiles) {
692
+ const absPath = path3.join(projectRoot, file);
693
+ if (fs3.existsSync(absPath)) {
694
+ htmlContent = fs3.readFileSync(absPath, "utf-8");
695
+ break;
696
+ }
697
+ }
698
+ if (htmlContent) {
699
+ const currentStructure = computeStructureFingerprint(htmlContent);
700
+ const details = compareStructure(snapshot.structure, currentStructure);
701
+ structureDrift = {
702
+ changed: details.length > 0,
703
+ details
704
+ };
705
+ }
706
+ } catch {
707
+ }
708
+ }
433
709
  return {
434
710
  checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
435
711
  snapshotCreatedAt: snapshot.createdAt,
@@ -439,7 +715,8 @@ async function detectDrift(projectRoot, snapshot, threshold = 10) {
439
715
  threshold,
440
716
  passed: driftScore <= threshold,
441
717
  items: driftItems,
442
- categorySummary
718
+ categorySummary,
719
+ structureDrift
443
720
  };
444
721
  }
445
722
 
@@ -612,7 +889,235 @@ ${buildTokenList(snapshot)}
612
889
  `;
613
890
  }
614
891
 
892
+ // src/core/sync.ts
893
+ var CATEGORY_LABELS = {
894
+ color: "color",
895
+ font: "font",
896
+ spacing: "spacing",
897
+ shadow: "shadow",
898
+ radius: "border-radius",
899
+ layout: "layout",
900
+ other: "style"
901
+ };
902
+ function isDesignTokenProperty(property) {
903
+ if (property.startsWith("--")) return true;
904
+ if (property === "font-family") return true;
905
+ return false;
906
+ }
907
+ function normalizeTokenKey(property) {
908
+ return property.replace(/^--tw-/, "--").replace(/^--tw-font-/, "--font-").toLowerCase();
909
+ }
910
+ function driftItemsToSyncChanges(items) {
911
+ return items.map((item) => {
912
+ if (item.changeType === "deleted") {
913
+ return {
914
+ category: item.original.category,
915
+ property: item.original.property,
916
+ fromValue: item.original.value,
917
+ toValue: "",
918
+ action: "remove"
919
+ };
920
+ }
921
+ if (item.changeType === "added") {
922
+ return {
923
+ category: (item.current ?? item.original).category,
924
+ property: (item.current ?? item.original).property,
925
+ fromValue: "",
926
+ toValue: (item.current ?? item.original).value,
927
+ action: "add"
928
+ };
929
+ }
930
+ return {
931
+ category: item.original.category,
932
+ property: item.original.property,
933
+ fromValue: item.original.value,
934
+ toValue: item.current?.value ?? "",
935
+ action: "update"
936
+ };
937
+ });
938
+ }
939
+ function generateSyncPrompt(changes) {
940
+ if (changes.length === 0) {
941
+ return "";
942
+ }
943
+ const grouped = /* @__PURE__ */ new Map();
944
+ for (const change of changes) {
945
+ const existing = grouped.get(change.category) ?? [];
946
+ existing.push(change);
947
+ grouped.set(change.category, existing);
948
+ }
949
+ const lines = [
950
+ "Update the following design tokens to match the latest code changes:",
951
+ ""
952
+ ];
953
+ for (const [category, categoryChanges] of grouped) {
954
+ const label = CATEGORY_LABELS[category];
955
+ for (const change of categoryChanges) {
956
+ const propName = cleanPropertyName(change.property);
957
+ if (change.action === "update") {
958
+ lines.push(
959
+ `- Change ${label} '${propName}' from ${change.fromValue} to ${change.toValue}`
960
+ );
961
+ } else if (change.action === "add") {
962
+ lines.push(
963
+ `- Add new ${label} '${propName}' with value ${change.toValue}`
964
+ );
965
+ } else if (change.action === "remove") {
966
+ lines.push(
967
+ `- Remove ${label} '${propName}' (was ${change.fromValue})`
968
+ );
969
+ }
970
+ }
971
+ }
972
+ lines.push("");
973
+ lines.push(
974
+ "Keep all other design elements unchanged. Only modify the specified tokens."
975
+ );
976
+ return lines.join("\n");
977
+ }
978
+ function cleanPropertyName(property) {
979
+ return property.replace(/^--tw-/, "").replace(/^--tw-radius-/, "").replace(/^--tw-font-/, "").replace(/^--/, "");
980
+ }
981
+ function syncToStitch(driftItems) {
982
+ const changes = driftItemsToSyncChanges(driftItems);
983
+ const prompt = generateSyncPrompt(changes);
984
+ return {
985
+ direction: "to-stitch",
986
+ changes,
987
+ prompt: prompt || void 0,
988
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
989
+ };
990
+ }
991
+ function syncFromStitch(stitchTokens, snapshotTokens) {
992
+ const changes = [];
993
+ const stitchDesignTokens = stitchTokens.filter((t) => isDesignTokenProperty(t.property));
994
+ const snapshotDesignTokens = snapshotTokens.filter((t) => isDesignTokenProperty(t.property));
995
+ const snapshotMap = /* @__PURE__ */ new Map();
996
+ for (const token of snapshotDesignTokens) {
997
+ const key = normalizeTokenKey(token.property);
998
+ if (!snapshotMap.has(key)) {
999
+ snapshotMap.set(key, token);
1000
+ }
1001
+ }
1002
+ const stitchMap = /* @__PURE__ */ new Map();
1003
+ for (const token of stitchDesignTokens) {
1004
+ const key = normalizeTokenKey(token.property);
1005
+ if (!stitchMap.has(key)) {
1006
+ stitchMap.set(key, token);
1007
+ }
1008
+ }
1009
+ for (const [key, stitchToken] of stitchMap) {
1010
+ const snapshotToken = snapshotMap.get(key);
1011
+ if (!snapshotToken) {
1012
+ changes.push({
1013
+ category: stitchToken.category,
1014
+ property: stitchToken.property,
1015
+ fromValue: "",
1016
+ toValue: stitchToken.value,
1017
+ action: "add"
1018
+ });
1019
+ } else if (stitchToken.value !== snapshotToken.value) {
1020
+ changes.push({
1021
+ category: stitchToken.category,
1022
+ property: snapshotToken.property,
1023
+ fromValue: snapshotToken.value,
1024
+ toValue: stitchToken.value,
1025
+ action: "update"
1026
+ });
1027
+ }
1028
+ }
1029
+ if (stitchDesignTokens.length > 0) {
1030
+ for (const [key, snapshotToken] of snapshotMap) {
1031
+ const isStitchOrigin = snapshotToken.property.startsWith("--tw-") || snapshotToken.selector === "[tailwind.config]";
1032
+ if (isStitchOrigin && !stitchMap.has(key)) {
1033
+ changes.push({
1034
+ category: snapshotToken.category,
1035
+ property: snapshotToken.property,
1036
+ fromValue: snapshotToken.value,
1037
+ toValue: "",
1038
+ action: "remove"
1039
+ });
1040
+ }
1041
+ }
1042
+ }
1043
+ const patchFile = generateCssPatch(changes);
1044
+ return {
1045
+ direction: "to-code",
1046
+ changes,
1047
+ patchFile: patchFile || void 0,
1048
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1049
+ };
1050
+ }
1051
+ function applySyncChanges(changes, cssFiles) {
1052
+ const modifiedFiles = /* @__PURE__ */ new Map();
1053
+ let appliedCount = 0;
1054
+ const updateChanges = changes.filter((c) => c.action === "update");
1055
+ for (const [filename, content] of cssFiles) {
1056
+ let modified = content;
1057
+ let fileChanged = false;
1058
+ for (const change of updateChanges) {
1059
+ const propName = change.property.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1060
+ const escapedFrom = change.fromValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1061
+ const cssRegex = new RegExp(
1062
+ `(${propName}\\s*:\\s*)${escapedFrom}(\\s*[;\\n])`,
1063
+ "g"
1064
+ );
1065
+ let newContent = modified.replace(cssRegex, `$1${change.toValue}$2`);
1066
+ if (newContent === modified) {
1067
+ const twResult = applySyncToHtml(modified, change);
1068
+ if (twResult !== modified) {
1069
+ newContent = twResult;
1070
+ }
1071
+ }
1072
+ if (newContent !== modified) {
1073
+ modified = newContent;
1074
+ fileChanged = true;
1075
+ appliedCount++;
1076
+ }
1077
+ }
1078
+ if (fileChanged) {
1079
+ modifiedFiles.set(filename, modified);
1080
+ }
1081
+ }
1082
+ return { modifiedFiles, appliedCount };
1083
+ }
1084
+ function applySyncToHtml(html, change) {
1085
+ const tokenName = change.property.replace(/^--tw-/, "").replace(/^--/, "");
1086
+ const escapedFrom = change.fromValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1087
+ const regex = new RegExp(
1088
+ `("${tokenName}"\\s*:\\s*)["']${escapedFrom}["']`,
1089
+ "g"
1090
+ );
1091
+ return html.replace(regex, `$1"${change.toValue}"`);
1092
+ }
1093
+ function generateCssPatch(changes) {
1094
+ if (changes.length === 0) return "";
1095
+ const lines = [
1096
+ "/* drift-guard sync patch \u2014 apply these changes to your CSS */",
1097
+ "/* Generated by: drift-guard sync --direction to-code */",
1098
+ "",
1099
+ ":root {"
1100
+ ];
1101
+ for (const change of changes) {
1102
+ if (change.action === "remove") {
1103
+ lines.push(` /* REMOVED: ${change.property}: ${change.fromValue}; */`);
1104
+ } else {
1105
+ const comment = change.action === "update" ? ` /* was: ${change.fromValue} */` : " /* NEW */";
1106
+ lines.push(` ${change.property}: ${change.toValue};${comment}`);
1107
+ }
1108
+ }
1109
+ lines.push("}");
1110
+ return lines.join("\n");
1111
+ }
1112
+
615
1113
  export {
1114
+ parseCss,
1115
+ extractCssVariables,
1116
+ parseHtml,
1117
+ extractStyleBlocks,
1118
+ extractTailwindConfig,
1119
+ computeStructureFingerprint,
1120
+ compareStructure,
616
1121
  DEFAULT_CONFIG,
617
1122
  loadConfig,
618
1123
  saveConfig,
@@ -622,6 +1127,11 @@ export {
622
1127
  loadSnapshot,
623
1128
  detectDrift,
624
1129
  generateRules,
625
- saveRules
1130
+ saveRules,
1131
+ driftItemsToSyncChanges,
1132
+ generateSyncPrompt,
1133
+ syncToStitch,
1134
+ syncFromStitch,
1135
+ applySyncChanges
626
1136
  };
627
- //# sourceMappingURL=chunk-27T45SVD.js.map
1137
+ //# sourceMappingURL=chunk-HI6H6PCS.js.map