@pure-ds/core 0.6.10 → 0.7.1

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.
Files changed (132) hide show
  1. package/.github/copilot-instructions.md +6 -1
  2. package/custom-elements.json +803 -16
  3. package/dist/types/pds.d.ts +1 -0
  4. package/dist/types/public/assets/js/pds-ask.d.ts +2 -0
  5. package/dist/types/public/assets/js/pds-ask.d.ts.map +1 -0
  6. package/dist/types/public/assets/js/pds-auto-definer.d.ts +14 -0
  7. package/dist/types/public/assets/js/pds-auto-definer.d.ts.map +1 -0
  8. package/dist/types/public/assets/js/pds-autocomplete.d.ts +79 -0
  9. package/dist/types/public/assets/js/pds-autocomplete.d.ts.map +1 -0
  10. package/dist/types/public/assets/js/pds-enhancers.d.ts +7 -0
  11. package/dist/types/public/assets/js/pds-enhancers.d.ts.map +1 -0
  12. package/dist/types/public/assets/js/pds-manager.d.ts +98 -1
  13. package/dist/types/public/assets/js/pds-manager.d.ts.map +1 -1
  14. package/dist/types/public/assets/js/pds-toast.d.ts +8 -0
  15. package/dist/types/public/assets/js/pds-toast.d.ts.map +1 -0
  16. package/dist/types/public/assets/js/pds.d.ts.map +1 -1
  17. package/dist/types/public/assets/pds/components/pds-drawer.d.ts +1 -143
  18. package/dist/types/public/assets/pds/components/pds-drawer.d.ts.map +1 -1
  19. package/dist/types/public/assets/pds/components/pds-form.d.ts.map +1 -1
  20. package/dist/types/public/assets/pds/components/pds-icon.d.ts.map +1 -1
  21. package/dist/types/public/assets/pds/components/pds-live-converter.d.ts +8 -0
  22. package/dist/types/public/assets/pds/components/pds-live-converter.d.ts.map +1 -0
  23. package/dist/types/public/assets/pds/components/pds-live-importer.d.ts +2 -0
  24. package/dist/types/public/assets/pds/components/pds-live-importer.d.ts.map +1 -0
  25. package/dist/types/public/assets/pds/components/pds-live-template-canvas.d.ts +2 -0
  26. package/dist/types/public/assets/pds/components/pds-live-template-canvas.d.ts.map +1 -0
  27. package/dist/types/public/assets/pds/components/pds-omnibox.d.ts.map +1 -1
  28. package/dist/types/public/assets/pds/components/pds-richtext.d.ts.map +1 -1
  29. package/dist/types/public/assets/pds/components/pds-scrollrow.d.ts +1 -63
  30. package/dist/types/public/assets/pds/components/pds-scrollrow.d.ts.map +1 -1
  31. package/dist/types/public/assets/pds/components/pds-splitpanel.d.ts +1 -89
  32. package/dist/types/public/assets/pds/components/pds-splitpanel.d.ts.map +1 -1
  33. package/dist/types/public/assets/pds/components/pds-theme.d.ts +1 -22
  34. package/dist/types/public/assets/pds/components/pds-theme.d.ts.map +1 -1
  35. package/dist/types/public/assets/pds/components/pds-toaster.d.ts +1 -1
  36. package/dist/types/public/assets/pds/components/pds-toaster.d.ts.map +1 -1
  37. package/dist/types/public/assets/pds/components/pds-treeview.d.ts +37 -0
  38. package/dist/types/public/assets/pds/components/pds-treeview.d.ts.map +1 -0
  39. package/dist/types/public/assets/pds/components/pds-upload.d.ts.map +1 -1
  40. package/dist/types/src/js/common/ask.d.ts.map +1 -1
  41. package/dist/types/src/js/common/toast.d.ts +8 -0
  42. package/dist/types/src/js/common/toast.d.ts.map +1 -1
  43. package/dist/types/src/js/pds-ask.d.ts +2 -0
  44. package/dist/types/src/js/pds-ask.d.ts.map +1 -0
  45. package/dist/types/src/js/pds-auto-definer.d.ts +2 -0
  46. package/dist/types/src/js/pds-auto-definer.d.ts.map +1 -0
  47. package/dist/types/src/js/pds-autocomplete.d.ts +2 -0
  48. package/dist/types/src/js/pds-autocomplete.d.ts.map +1 -0
  49. package/dist/types/src/js/pds-core/pds-enhancers.d.ts.map +1 -1
  50. package/dist/types/src/js/pds-core/pds-generator.d.ts.map +1 -1
  51. package/dist/types/src/js/pds-core/pds-live.d.ts +2 -1
  52. package/dist/types/src/js/pds-core/pds-live.d.ts.map +1 -1
  53. package/dist/types/src/js/pds-core/pds-start-helpers.d.ts.map +1 -1
  54. package/dist/types/src/js/pds-enhancers.d.ts +2 -0
  55. package/dist/types/src/js/pds-enhancers.d.ts.map +1 -0
  56. package/dist/types/src/js/pds-live-manager/conversion-service.d.ts +66 -0
  57. package/dist/types/src/js/pds-live-manager/conversion-service.d.ts.map +1 -0
  58. package/dist/types/src/js/pds-live-manager/import-contract.d.ts +15 -0
  59. package/dist/types/src/js/pds-live-manager/import-contract.d.ts.map +1 -0
  60. package/dist/types/src/js/pds-live-manager/import-history-service.d.ts +32 -0
  61. package/dist/types/src/js/pds-live-manager/import-history-service.d.ts.map +1 -0
  62. package/dist/types/src/js/pds-live-manager/import-service.d.ts +21 -0
  63. package/dist/types/src/js/pds-live-manager/import-service.d.ts.map +1 -0
  64. package/dist/types/src/js/pds-live-manager/template-service.d.ts +17 -0
  65. package/dist/types/src/js/pds-live-manager/template-service.d.ts.map +1 -0
  66. package/dist/types/src/js/pds-manager.d.ts +4 -0
  67. package/dist/types/src/js/pds-toast.d.ts +2 -0
  68. package/dist/types/src/js/pds-toast.d.ts.map +1 -0
  69. package/dist/types/src/js/pds.d.ts.map +1 -1
  70. package/package.json +11 -5
  71. package/packages/pds-cli/README.md +60 -0
  72. package/packages/pds-cli/bin/pds-import.js +176 -0
  73. package/packages/pds-cli/bin/pds-static.js +27 -1
  74. package/packages/pds-cli/bin/postinstall.mjs +17 -8
  75. package/packages/pds-cli/bin/templates/bootstrap/pds.config.js +1 -5
  76. package/packages/pds-cli/bin/templates/bootstrap/public/index.html +2 -1
  77. package/packages/pds-cli/bin/templates/starter-templates.js +1 -3
  78. package/public/assets/js/app.js +9 -163
  79. package/public/assets/js/pds-ask.js +25 -0
  80. package/public/assets/js/pds-auto-definer.js +1 -0
  81. package/public/assets/js/pds-autocomplete.js +7 -0
  82. package/public/assets/js/pds-enhancers.js +1 -0
  83. package/public/assets/js/pds-manager.js +370 -267
  84. package/public/assets/js/pds-toast.js +1 -0
  85. package/public/assets/js/pds.js +2 -32
  86. package/public/assets/pds/components/pds-calendar.js +2 -2
  87. package/public/assets/pds/components/pds-drawer.js +1 -1
  88. package/public/assets/pds/components/pds-form.js +7 -6
  89. package/public/assets/pds/components/pds-icon.js +12 -9
  90. package/public/assets/pds/components/pds-live-converter.js +47 -0
  91. package/public/assets/pds/components/pds-live-edit.js +758 -44
  92. package/public/assets/pds/components/pds-live-importer.js +773 -0
  93. package/public/assets/pds/components/pds-live-template-canvas.js +172 -0
  94. package/public/assets/pds/components/pds-omnibox.js +147 -3
  95. package/public/assets/pds/components/pds-richtext.js +2 -0
  96. package/public/assets/pds/components/pds-scrollrow.js +61 -2
  97. package/public/assets/pds/components/pds-splitpanel.js +3 -1
  98. package/public/assets/pds/components/pds-theme.js +2 -0
  99. package/public/assets/pds/components/pds-toaster.js +52 -5
  100. package/public/assets/pds/components/pds-treeview.js +974 -0
  101. package/public/assets/pds/components/pds-upload.js +2 -0
  102. package/public/assets/pds/core/pds-ask.js +25 -0
  103. package/public/assets/pds/core/pds-auto-definer.js +1 -0
  104. package/public/assets/pds/core/pds-autocomplete.js +7 -0
  105. package/public/assets/pds/core/pds-enhancers.js +1 -0
  106. package/public/assets/pds/core/pds-manager.js +3646 -0
  107. package/public/assets/pds/core/pds-toast.js +1 -0
  108. package/public/assets/pds/core.js +2 -0
  109. package/public/assets/pds/custom-elements.json +803 -16
  110. package/public/assets/pds/pds-css-complete.json +7 -2
  111. package/public/assets/pds/templates/commerce-scroll-explorer.html +115 -0
  112. package/public/assets/pds/templates/content-brand-showcase.html +110 -0
  113. package/public/assets/pds/templates/feedback-ops-dashboard.html +91 -0
  114. package/public/assets/pds/templates/release-readiness-radar.html +69 -0
  115. package/public/assets/pds/templates/support-command-center.html +92 -0
  116. package/public/assets/pds/templates/templates.json +53 -0
  117. package/public/assets/pds/templates/workspace-settings-lab.html +131 -0
  118. package/public/assets/pds/vscode-custom-data.json +54 -4
  119. package/readme.md +38 -1
  120. package/src/js/pds-core/pds-config.js +9 -9
  121. package/src/js/pds-core/pds-enhancers.js +146 -0
  122. package/src/js/pds-core/pds-generator.js +170 -29
  123. package/src/js/pds-core/pds-live.js +456 -13
  124. package/src/js/pds-core/pds-start-helpers.js +5 -1
  125. package/src/js/pds-live-manager/conversion-service.js +3135 -0
  126. package/src/js/pds-live-manager/import-contract.js +57 -0
  127. package/src/js/pds-live-manager/import-history-service.js +145 -0
  128. package/src/js/pds-live-manager/import-service.js +255 -0
  129. package/src/js/pds-live-manager/tailwind-conversion-rules.json +383 -0
  130. package/src/js/pds-live-manager/template-service.js +170 -0
  131. package/src/js/pds.d.ts +1 -0
  132. package/src/js/pds.js +192 -12
@@ -0,0 +1,3135 @@
1
+ import { createImportResult } from "./import-contract.js";
2
+ import tailwindRulebookJson from "./tailwind-conversion-rules.json" with { type: "json" };
3
+ import { PDS } from "../pds.js";
4
+
5
+ const RULEBOOK_JSON_PATH = "src/js/pds-live-manager/tailwind-conversion-rules.json";
6
+ const BREAKPOINT_ORDER = ["base", "sm", "md", "lg", "xl", "2xl"];
7
+
8
+ function hydrateRulebook(rawRulebook = {}) {
9
+ const ignoredPatterns = Array.isArray(rawRulebook.ignoredPatterns)
10
+ ? rawRulebook.ignoredPatterns.map((rule) => ({
11
+ ...rule,
12
+ pattern: rule?.pattern instanceof RegExp ? rule.pattern : new RegExp(String(rule?.pattern || "")),
13
+ }))
14
+ : [];
15
+
16
+ const nonPdsClassPatterns = Array.isArray(rawRulebook.nonPdsClassPatterns)
17
+ ? rawRulebook.nonPdsClassPatterns.map((pattern) => (
18
+ pattern instanceof RegExp ? pattern : new RegExp(String(pattern || ""))
19
+ ))
20
+ : [];
21
+
22
+ return {
23
+ ...rawRulebook,
24
+ ignoredPatterns,
25
+ nonPdsClassPatterns,
26
+ };
27
+ }
28
+
29
+ export const TAILWIND_TO_PDS_RULES = hydrateRulebook(tailwindRulebookJson);
30
+ const RULEBOOK_VERSION = TAILWIND_TO_PDS_RULES.version || "tw2pds-layout-v4";
31
+
32
+ const DIRECT_MAP = new Map(
33
+ TAILWIND_TO_PDS_RULES.directMappings.map((rule) => [rule.tw, rule])
34
+ );
35
+
36
+ const GAP_MAP = new Map(Object.entries(TAILWIND_TO_PDS_RULES.gapScaleMap || {}));
37
+
38
+ const MAX_WIDTH_MAP = new Map(Object.entries(TAILWIND_TO_PDS_RULES.maxWidthMap || {}));
39
+
40
+ const NON_PDS_CLASS_PATTERNS = TAILWIND_TO_PDS_RULES.nonPdsClassPatterns || [];
41
+
42
+ const TABLE_STRICT_TAGS = new Set(TAILWIND_TO_PDS_RULES.neverFallbackTags || []);
43
+
44
+ const IMPORT_STYLE_BASE_RULES = { ...(TAILWIND_TO_PDS_RULES.importStyleRules || {}) };
45
+
46
+ const TW_SIZE_SCALE = TAILWIND_TO_PDS_RULES.tailwindSizeScale || {};
47
+
48
+ const TW_SHADE_SCALE = Array.isArray(TAILWIND_TO_PDS_RULES.tailwindShadeScale)
49
+ ? TAILWIND_TO_PDS_RULES.tailwindShadeScale.map((value) => String(value)).filter(Boolean)
50
+ : ["50", "100", "200", "300", "400", "500", "600", "700", "800", "900"];
51
+
52
+ const DEFAULT_TW_SHADE = TW_SHADE_SCALE.includes(String(TAILWIND_TO_PDS_RULES.defaultTailwindShade || ""))
53
+ ? String(TAILWIND_TO_PDS_RULES.defaultTailwindShade)
54
+ : "500";
55
+
56
+ const DEFAULT_FONT_SCALE = 1.2;
57
+
58
+ const KNOWN_TW_PREFIXES = [
59
+ "container",
60
+ "grid",
61
+ "flex",
62
+ "gap",
63
+ "space",
64
+ "items",
65
+ "justify",
66
+ "content",
67
+ "place",
68
+ "self",
69
+ "col",
70
+ "row",
71
+ "w",
72
+ "h",
73
+ "min",
74
+ "max",
75
+ "p",
76
+ "m",
77
+ "rounded",
78
+ "border",
79
+ "ring",
80
+ "outline",
81
+ "shadow",
82
+ "bg",
83
+ "text",
84
+ "font",
85
+ "leading",
86
+ "tracking",
87
+ "uppercase",
88
+ "lowercase",
89
+ "capitalize",
90
+ "overflow",
91
+ "whitespace",
92
+ "truncate",
93
+ "object",
94
+ "aspect",
95
+ "opacity",
96
+ "blur",
97
+ "backdrop",
98
+ "transition",
99
+ "duration",
100
+ "ease",
101
+ "delay",
102
+ "animate",
103
+ "hidden",
104
+ "block",
105
+ "inline",
106
+ "absolute",
107
+ "relative",
108
+ "fixed",
109
+ "sticky",
110
+ "size",
111
+ ];
112
+
113
+ function removeAttributeFromTagAttributes(attrs = "", name = "") {
114
+ if (!attrs || !name) return attrs;
115
+ const attrRegex = new RegExp(`\\s${name}\\s*=\\s*("[^"]*"|'[^']*'|[^\\s>]+)`, "gi");
116
+ return String(attrs).replace(attrRegex, "");
117
+ }
118
+
119
+ function applyLabelNestingRule(sourceHtml = "", summary = null) {
120
+ let html = String(sourceHtml || "");
121
+ let nestingCount = 0;
122
+
123
+ html = html.replace(
124
+ /<label([^>]*?)\sfor\s*=\s*(["'])([^"']+)\2([^>]*)>([\s\S]*?)<\/label>\s*<(input)([^>]*?)\sid\s*=\s*(["'])([^"']+)\8([^>]*?)>/gi,
125
+ (match, labelAttrsBefore, _q1, forValue, labelAttrsAfter, labelInner, inputTag, inputAttrsBefore, _q2, inputId, inputAttrsAfter) => {
126
+ if (forValue !== inputId) return match;
127
+ const nextLabelAttrs = removeAttributeFromTagAttributes(`${labelAttrsBefore || ""}${labelAttrsAfter || ""}`, "for");
128
+ const inputMarkup = `<${inputTag}${inputAttrsBefore || ""} id="${inputId}"${inputAttrsAfter || ""}>`;
129
+ nestingCount += 1;
130
+ return `<label${nextLabelAttrs}>${labelInner}${inputMarkup}</label>`;
131
+ }
132
+ );
133
+
134
+ html = html.replace(
135
+ /<label([^>]*?)\sfor\s*=\s*(["'])([^"']+)\2([^>]*)>([\s\S]*?)<\/label>\s*<(select|textarea)([^>]*?)\sid\s*=\s*(["'])([^"']+)\8([^>]*)>([\s\S]*?)<\/\6>/gi,
136
+ (match, labelAttrsBefore, _q1, forValue, labelAttrsAfter, labelInner, controlTag, controlAttrsBefore, _q2, controlId, controlAttrsAfter, controlInner) => {
137
+ if (forValue !== controlId) return match;
138
+ const nextLabelAttrs = removeAttributeFromTagAttributes(`${labelAttrsBefore || ""}${labelAttrsAfter || ""}`, "for");
139
+ const controlMarkup = `<${controlTag}${controlAttrsBefore || ""} id="${controlId}"${controlAttrsAfter || ""}>${controlInner}</${controlTag}>`;
140
+ nestingCount += 1;
141
+ return `<label${nextLabelAttrs}>${labelInner}${controlMarkup}</label>`;
142
+ }
143
+ );
144
+
145
+ if (summary && nestingCount > 0) {
146
+ summary.labelNestingCount += nestingCount;
147
+ addNote(summary, `Nested ${nestingCount} label/control pairs.`);
148
+ recordRule(summary, "intent.form.nested-label");
149
+ }
150
+
151
+ return html;
152
+ }
153
+
154
+ function toImportClassName(token = "", breakpoint = "base") {
155
+ const normalized = String(token || "")
156
+ .toLowerCase()
157
+ .replace(/[^a-z0-9]+/g, "-")
158
+ .replace(/^-+|-+$/g, "") || "rule";
159
+
160
+ const bpPart = breakpoint && breakpoint !== "base" ? `${breakpoint}-` : "";
161
+ return `import-${bpPart}${normalized}`;
162
+ }
163
+
164
+ function registerImportStyle(summary, token, declaration, breakpoint = "base", pseudo = "") {
165
+ if (!summary || !token || !declaration) return "";
166
+ const className = toImportClassName(token, breakpoint);
167
+ if (pseudo) {
168
+ const pseudoBucketKey = `${breakpoint}|${pseudo}`;
169
+ if (!summary.importPseudoStyles.has(pseudoBucketKey)) {
170
+ summary.importPseudoStyles.set(pseudoBucketKey, new Map());
171
+ }
172
+ summary.importPseudoStyles.get(pseudoBucketKey).set(className, declaration);
173
+ } else if (breakpoint === "base") {
174
+ summary.importBaseStyles.set(className, declaration);
175
+ } else {
176
+ if (!summary.importResponsiveStyles.has(breakpoint)) {
177
+ summary.importResponsiveStyles.set(breakpoint, new Map());
178
+ }
179
+ summary.importResponsiveStyles.get(breakpoint).set(className, declaration);
180
+ }
181
+ summary.importedStyleCount += 1;
182
+ return className;
183
+ }
184
+
185
+ function normalizeArbitraryValue(raw = "") {
186
+ return String(raw || "").trim().replace(/_/g, " ");
187
+ }
188
+
189
+ function isSafeCssValue(value = "") {
190
+ if (!value) return false;
191
+ if (/[;{}]/.test(value)) return false;
192
+ return /^[-#(),.%/\sa-zA-Z0-9]+$/.test(value);
193
+ }
194
+
195
+ function resolveTailwindSizeValue(rawToken = "") {
196
+ const token = String(rawToken || "").trim();
197
+ if (!token) return null;
198
+
199
+ const arbitrary = token.match(/^\[([^\]]+)\]$/);
200
+ if (arbitrary) {
201
+ const value = normalizeArbitraryValue(arbitrary[1]);
202
+ return isSafeCssValue(value) ? value : null;
203
+ }
204
+
205
+ return TW_SIZE_SCALE[token] || null;
206
+ }
207
+
208
+ function resolveTwShade(shade = "") {
209
+ const parsed = Number(shade);
210
+ if (!Number.isFinite(parsed)) return DEFAULT_TW_SHADE;
211
+
212
+ const exact = String(parsed);
213
+ if (TW_SHADE_SCALE.includes(exact)) return exact;
214
+
215
+ return TW_SHADE_SCALE.reduce((closest, candidate) => {
216
+ const closestDistance = Math.abs(Number(closest) - parsed);
217
+ const candidateDistance = Math.abs(Number(candidate) - parsed);
218
+ return candidateDistance < closestDistance ? candidate : closest;
219
+ }, DEFAULT_TW_SHADE);
220
+ }
221
+
222
+ function mapTailwindColorToPdsColorVar(family = "", shade = "500") {
223
+ const twFamily = String(family || "").toLowerCase();
224
+ const mappedShade = resolveTwShade(shade);
225
+
226
+ if (["blue", "sky", "indigo", "cyan"].includes(twFamily)) {
227
+ return `var(--color-primary-${mappedShade})`;
228
+ }
229
+ if (["purple", "violet", "fuchsia"].includes(twFamily)) {
230
+ return `var(--color-accent-${mappedShade})`;
231
+ }
232
+ if (["green", "emerald", "lime", "teal"].includes(twFamily)) {
233
+ return `var(--color-success-${mappedShade})`;
234
+ }
235
+ if (["yellow", "amber", "warning"].includes(twFamily)) {
236
+ return `var(--color-warning-${mappedShade})`;
237
+ }
238
+ if (["red", "rose", "pink", "orange"].includes(twFamily)) {
239
+ return `var(--color-danger-${mappedShade})`;
240
+ }
241
+ if (["slate", "gray", "zinc", "neutral", "stone"].includes(twFamily)) {
242
+ return `var(--color-gray-${mappedShade})`;
243
+ }
244
+
245
+ return "";
246
+ }
247
+
248
+ function resolveArbitraryColorValue(raw = "") {
249
+ const value = normalizeArbitraryValue(raw);
250
+ if (!isSafeCssValue(value)) return "";
251
+ if (/^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(value)) return value;
252
+ if (/^(?:rgb|hsl)a?\([^)]*\)$/.test(value)) return value;
253
+ return "";
254
+ }
255
+
256
+ function parseClassTokens(value = "") {
257
+ return String(value || "")
258
+ .split(/\s+/)
259
+ .map((token) => token.trim())
260
+ .filter(Boolean);
261
+ }
262
+
263
+ function withClassInAttrs(attrs = "", className = "") {
264
+ if (!className) return attrs;
265
+ const text = String(attrs || "");
266
+ const classMatch = text.match(/\sclass\s*=\s*(["'])(.*?)\1/i);
267
+ if (!classMatch) {
268
+ return `${text} class="${className}"`;
269
+ }
270
+
271
+ const quote = classMatch[1] || '"';
272
+ const values = parseClassTokens(classMatch[2]);
273
+ if (!values.includes(className)) values.push(className);
274
+ const next = ` class=${quote}${values.join(" ")}${quote}`;
275
+ return text.replace(classMatch[0], next);
276
+ }
277
+
278
+ function hasAttribute(attrs = "", attr = "") {
279
+ if (!attr) return false;
280
+ return new RegExp(`\\s${attr}\\s*=`, "i").test(String(attrs || ""));
281
+ }
282
+
283
+ function humanizeIconName(iconName = "") {
284
+ const text = String(iconName || "").replace(/[-_]+/g, " ").trim();
285
+ if (!text) return "Icon button";
286
+ return text.replace(/(^|\s)([a-z])/g, (_m, pre, ch) => `${pre}${ch.toUpperCase()}`);
287
+ }
288
+
289
+ function normalizeIconOnlyButtons(sourceHtml = "", summary = null) {
290
+ const input = String(sourceHtml || "");
291
+ if (!input) return input;
292
+
293
+ return input.replace(
294
+ /<(button|a)([^>]*)>\s*(<pds-icon\b[^>]*><\/pds-icon>)\s*<\/\1>/gi,
295
+ (fullMatch, tagName, attrs, iconMarkup) => {
296
+ let nextAttrs = withClassInAttrs(attrs, "icon-only");
297
+ if (!hasAttribute(nextAttrs, "aria-label")) {
298
+ const iconNameMatch = String(iconMarkup).match(/\sicon\s*=\s*(["'])(.*?)\1/i);
299
+ const iconName = iconNameMatch ? String(iconNameMatch[2] || "") : "";
300
+ const label = humanizeIconName(iconName);
301
+ nextAttrs += ` aria-label="${label}"`;
302
+ }
303
+
304
+ if (summary) {
305
+ summary.intentHits += 1;
306
+ recordRule(summary, "intent.component.button.icon-only-markup");
307
+ }
308
+
309
+ return `<${tagName}${nextAttrs}>${iconMarkup}</${tagName}>`;
310
+ }
311
+ );
312
+ }
313
+
314
+ function normalizeMetricTextParagraphs(sourceHtml = "", summary = null) {
315
+ const input = String(sourceHtml || "");
316
+ if (!input) return input;
317
+
318
+ let convertedCount = 0;
319
+
320
+ const output = input.replace(/<p([^>]*?)>([\s\S]*?)<\/p>/gi, (fullMatch, attrs, inner) => {
321
+ const classMatch = String(attrs || "").match(/\sclass\s*=\s*(["'])(.*?)\1/i);
322
+ if (!classMatch) return fullMatch;
323
+
324
+ const classTokens = parseClassTokens(classMatch[2] || "");
325
+ const hasImportText = classTokens.some((token) => /^import-text-/.test(String(token || "")));
326
+ const isLikelyMetricLine = classTokens.includes("text-muted") || classTokens.some((token) => /^import-font-/.test(String(token || "")));
327
+ if (!hasImportText || !isLikelyMetricLine) return fullMatch;
328
+
329
+ convertedCount += 1;
330
+ return `<div${attrs}>${inner}</div>`;
331
+ });
332
+
333
+ if (summary && convertedCount > 0) {
334
+ summary.intentHits += 1;
335
+ recordRule(summary, "intent.typography.metric-paragraph-to-div");
336
+ addNote(summary, `Normalized ${convertedCount} metric text paragraph tag(s) to div.`);
337
+ }
338
+
339
+ return output;
340
+ }
341
+
342
+ function removeClassFromAttrs(attrs = "", className = "") {
343
+ if (!className) return attrs;
344
+ const text = String(attrs || "");
345
+ const classMatch = text.match(/\sclass\s*=\s*(["'])(.*?)\1/i);
346
+ if (!classMatch) return text;
347
+
348
+ const quote = classMatch[1] || '"';
349
+ const values = parseClassTokens(classMatch[2]).filter((token) => token !== className);
350
+ if (values.length === 0) {
351
+ return text.replace(classMatch[0], "");
352
+ }
353
+
354
+ const next = ` class=${quote}${values.join(" ")}${quote}`;
355
+ return text.replace(classMatch[0], next);
356
+ }
357
+
358
+ function updateClassTokensInAttrs(attrs = "", mapper = (tokens) => tokens) {
359
+ const text = String(attrs || "");
360
+ const classMatch = text.match(/\sclass\s*=\s*(["'])(.*?)\1/i);
361
+ if (!classMatch) return text;
362
+
363
+ const quote = classMatch[1] || '"';
364
+ const currentTokens = parseClassTokens(classMatch[2]);
365
+ const nextTokensRaw = mapper(Array.from(currentTokens));
366
+ const nextTokens = Array.isArray(nextTokensRaw)
367
+ ? nextTokensRaw.filter(Boolean)
368
+ : currentTokens;
369
+
370
+ if (nextTokens.length === 0) {
371
+ return text.replace(classMatch[0], "");
372
+ }
373
+
374
+ const next = ` class=${quote}${nextTokens.join(" ")}${quote}`;
375
+ return text.replace(classMatch[0], next);
376
+ }
377
+
378
+ function normalizeMetricPairStackContainers(sourceHtml = "", summary = null) {
379
+ const input = String(sourceHtml || "");
380
+ if (!input) return input;
381
+
382
+ let convertedCount = 0;
383
+
384
+ const output = input.replace(
385
+ /<(div|section|article|aside)([^>]*)>\s*<(p|div)([^>]*)>[\s\S]*?<\/\3>\s*<(p|div)([^>]*)>[\s\S]*?<\/\5>\s*<\/\1>/gi,
386
+ (fullMatch, tag, attrs, firstTag, firstAttrs, secondTag, secondAttrs) => {
387
+ const classMatch = String(attrs || "").match(/\sclass\s*=\s*(["'])(.*?)\1/i);
388
+ if (!classMatch) return fullMatch;
389
+
390
+ const containerClasses = parseClassTokens(classMatch[2]);
391
+ if (!containerClasses.includes("stack-sm")) return fullMatch;
392
+
393
+ const firstClassMatch = String(firstAttrs || "").match(/\sclass\s*=\s*(["'])(.*?)\1/i);
394
+ const secondClassMatch = String(secondAttrs || "").match(/\sclass\s*=\s*(["'])(.*?)\1/i);
395
+ if (!firstClassMatch || !secondClassMatch) return fullMatch;
396
+
397
+ const firstClasses = parseClassTokens(firstClassMatch[2]);
398
+ const secondClasses = parseClassTokens(secondClassMatch[2]);
399
+ const hasMetricTypographyPair =
400
+ firstClasses.some((token) => /^import-text-/.test(String(token || ""))) &&
401
+ secondClasses.some((token) => /^import-text-/.test(String(token || "")));
402
+
403
+ if (!hasMetricTypographyPair) return fullMatch;
404
+
405
+ const nextAttrs = removeClassFromAttrs(attrs, "stack-sm");
406
+ convertedCount += 1;
407
+ return fullMatch.replace(`<${tag}${attrs}>`, `<${tag}${nextAttrs}>`);
408
+ }
409
+ );
410
+
411
+ if (summary && convertedCount > 0) {
412
+ summary.intentHits += 1;
413
+ recordRule(summary, "intent.typography.metric-pair-no-stack");
414
+ addNote(summary, `Removed stack-sm from ${convertedCount} metric text pair container(s).`);
415
+ }
416
+
417
+ return output;
418
+ }
419
+
420
+ function resolveTypographyFromConfig(configInput = {}) {
421
+ if (!configInput || typeof configInput !== "object") return {};
422
+ const directTypography = configInput.typography;
423
+ if (directTypography && typeof directTypography === "object") {
424
+ return directTypography;
425
+ }
426
+ const nestedTypography = configInput.design?.typography;
427
+ if (nestedTypography && typeof nestedTypography === "object") {
428
+ return nestedTypography;
429
+ }
430
+ return {};
431
+ }
432
+
433
+ function resolveFontScale(configInput = {}) {
434
+ const typography = resolveTypographyFromConfig(configInput);
435
+ const parsed = Number(typography.fontScale);
436
+ if (!Number.isFinite(parsed)) return DEFAULT_FONT_SCALE;
437
+ return Math.max(1, Math.min(2, parsed));
438
+ }
439
+
440
+ function resolveSemanticHeadingTag(sizeToken = "", fontScale = DEFAULT_FONT_SCALE) {
441
+ const baseRankBySize = {
442
+ "4xl": 1,
443
+ "3xl": 2,
444
+ "2xl": 3,
445
+ xl: 4,
446
+ };
447
+ const baseRank = baseRankBySize[sizeToken];
448
+ if (!baseRank) return "";
449
+
450
+ const normalizedScale = Number.isFinite(Number(fontScale))
451
+ ? Math.max(1, Math.min(2, Number(fontScale)))
452
+ : DEFAULT_FONT_SCALE;
453
+ const rankShift = Math.max(-1, Math.min(1, Math.round((normalizedScale - DEFAULT_FONT_SCALE) / 0.25)));
454
+ const adjustedRank = baseRank - rankShift;
455
+ if (adjustedRank < 1) return "h1";
456
+ if (adjustedRank > 4) return "";
457
+ return `h${adjustedRank}`;
458
+ }
459
+
460
+ function normalizeSemanticTypography(sourceHtml = "", summary = null, options = {}) {
461
+ const input = String(sourceHtml || "");
462
+ if (!input) return input;
463
+
464
+ const fontScale = resolveFontScale(options.config || {});
465
+ let headingCount = 0;
466
+ let strongCount = 0;
467
+
468
+ const output = input.replace(/<(p|div|span)([^>]*)>([\s\S]*?)<\/\1>/gi, (fullMatch, tag, attrs, inner) => {
469
+ const classMatch = String(attrs || "").match(/\sclass\s*=\s*(["'])(.*?)\1/i);
470
+ if (!classMatch) return fullMatch;
471
+
472
+ const classTokens = parseClassTokens(classMatch[2]);
473
+ const hasImportBold = classTokens.includes("import-font-bold");
474
+ if (!hasImportBold) return fullMatch;
475
+
476
+ const textSizeToken = classTokens.find((token) => /^import-text-(?:4xl|3xl|2xl|xl)$/.test(String(token || ""))) || "";
477
+ const sizeMatch = textSizeToken.match(/^import-text-(4xl|3xl|2xl|xl)$/);
478
+
479
+ if (sizeMatch) {
480
+ const headingTag = resolveSemanticHeadingTag(sizeMatch[1], fontScale);
481
+ if (!headingTag) {
482
+ return fullMatch;
483
+ }
484
+ const nextAttrs = updateClassTokensInAttrs(attrs, (tokens) => (
485
+ tokens.filter((token) => token !== "import-font-bold" && token !== textSizeToken)
486
+ ));
487
+ headingCount += 1;
488
+ return `<${headingTag}${nextAttrs}>${inner}</${headingTag}>`;
489
+ }
490
+
491
+ const hasNestedBlock = /<\/?(?:div|p|section|article|aside|main|header|footer|ul|ol|li|table|tr|td|th|h[1-6])\b/i.test(inner);
492
+ const alreadyStrong = /<\/?(?:strong|b)\b/i.test(inner);
493
+ if (hasNestedBlock || alreadyStrong) return fullMatch;
494
+
495
+ const nextAttrs = removeClassFromAttrs(attrs, "import-font-bold");
496
+ strongCount += 1;
497
+ return `<${tag}${nextAttrs}><strong>${inner}</strong></${tag}>`;
498
+ });
499
+
500
+ if (summary) {
501
+ if (headingCount > 0) {
502
+ summary.intentHits += 1;
503
+ recordRule(summary, "intent.typography.semantic-heading-from-scale");
504
+ addNote(summary, `Converted ${headingCount} bold display text node(s) to semantic heading tags (fontScale=${Number(fontScale).toFixed(2)}).`);
505
+ }
506
+ if (strongCount > 0) {
507
+ summary.intentHits += 1;
508
+ recordRule(summary, "intent.typography.bold-to-strong");
509
+ addNote(summary, `Wrapped ${strongCount} bold text node(s) in strong tags.`);
510
+ }
511
+ }
512
+
513
+ return output;
514
+ }
515
+
516
+ function resolvePseudoVariant(variants = []) {
517
+ if (!Array.isArray(variants) || variants.length === 0) return "";
518
+ const nonBreakpoint = variants.filter((variant) => !BREAKPOINT_ORDER.includes(variant));
519
+ if (nonBreakpoint.length === 0) return "";
520
+ if (nonBreakpoint.length > 1) return "";
521
+ const candidate = nonBreakpoint[0];
522
+ if (["hover", "focus", "active"].includes(candidate)) return candidate;
523
+ return "";
524
+ }
525
+
526
+ function resolveImportStyleToken(baseToken, breakpoint = "base", variants = []) {
527
+ const pseudo = resolvePseudoVariant(variants);
528
+ const declaration = IMPORT_STYLE_BASE_RULES[baseToken];
529
+ if (declaration) {
530
+ return {
531
+ declaration,
532
+ breakpoint,
533
+ pseudo,
534
+ ruleId: "fallback.import-style",
535
+ };
536
+ }
537
+
538
+ const gapMatch = String(baseToken).match(/^gap-(\d+)$/);
539
+ if (gapMatch) {
540
+ const gapScale = {
541
+ 0: "var(--spacing-0)",
542
+ 1: "var(--spacing-1)",
543
+ 2: "var(--spacing-2)",
544
+ 3: "var(--spacing-3)",
545
+ 4: "var(--spacing-4)",
546
+ 5: "var(--spacing-5)",
547
+ 6: "var(--spacing-6)",
548
+ 7: "var(--spacing-7)",
549
+ 8: "var(--spacing-8)",
550
+ 10: "var(--spacing-10)",
551
+ 12: "var(--spacing-12)",
552
+ };
553
+ const size = Number(gapMatch[1]);
554
+ if (gapScale[size]) {
555
+ return {
556
+ declaration: `gap:${gapScale[size]}`,
557
+ breakpoint,
558
+ pseudo,
559
+ ruleId: "fallback.import-style.gap-scale",
560
+ };
561
+ }
562
+ }
563
+
564
+ const marginMatch = String(baseToken).match(/^(mt|mb|my)-(.+)$/);
565
+ if (marginMatch) {
566
+ const axis = marginMatch[1];
567
+ const rawValue = marginMatch[2];
568
+ const value = resolveTailwindSizeValue(rawValue);
569
+ if (value) {
570
+ let marginDeclaration = "";
571
+ if (axis === "mt") {
572
+ marginDeclaration = `margin-top:${value}`;
573
+ } else if (axis === "mb") {
574
+ marginDeclaration = `margin-bottom:${value}`;
575
+ } else {
576
+ marginDeclaration = `margin-top:${value};margin-bottom:${value}`;
577
+ }
578
+
579
+ return {
580
+ declaration: marginDeclaration,
581
+ breakpoint,
582
+ pseudo,
583
+ ruleId: "fallback.import-style.margin-scale",
584
+ };
585
+ }
586
+ }
587
+
588
+ const minWidthArbitrary = String(baseToken).match(/^min-w-\[([^\]]+)\]$/);
589
+ if (minWidthArbitrary) {
590
+ const value = normalizeArbitraryValue(minWidthArbitrary[1]);
591
+ if (isSafeCssValue(value)) {
592
+ return {
593
+ declaration: `min-width:${value}`,
594
+ breakpoint,
595
+ pseudo,
596
+ ruleId: "fallback.import-style.min-width-arbitrary",
597
+ };
598
+ }
599
+ }
600
+
601
+ const maxWidthArbitrary = String(baseToken).match(/^max-w-\[([^\]]+)\]$/);
602
+ if (maxWidthArbitrary) {
603
+ const value = normalizeArbitraryValue(maxWidthArbitrary[1]);
604
+ if (isSafeCssValue(value)) {
605
+ return {
606
+ declaration: `max-width:${value}`,
607
+ breakpoint,
608
+ pseudo,
609
+ ruleId: "fallback.import-style.max-width-arbitrary",
610
+ };
611
+ }
612
+ }
613
+
614
+ const minHeightArbitrary = String(baseToken).match(/^min-h-\[([^\]]+)\]$/);
615
+ if (minHeightArbitrary) {
616
+ const value = normalizeArbitraryValue(minHeightArbitrary[1]);
617
+ if (isSafeCssValue(value)) {
618
+ return {
619
+ declaration: `min-height:${value}`,
620
+ breakpoint,
621
+ pseudo,
622
+ ruleId: "fallback.import-style.min-height-arbitrary",
623
+ };
624
+ }
625
+ }
626
+
627
+ const gridRowsArbitrary = String(baseToken).match(/^grid-rows-\[([^\]]+)\]$/);
628
+ if (gridRowsArbitrary) {
629
+ const value = normalizeArbitraryValue(gridRowsArbitrary[1]);
630
+ if (isSafeCssValue(value)) {
631
+ return {
632
+ declaration: `grid-template-rows:${value}`,
633
+ breakpoint,
634
+ pseudo,
635
+ ruleId: "fallback.import-style.grid-rows-arbitrary",
636
+ };
637
+ }
638
+ }
639
+
640
+ const sizeMatch = String(baseToken).match(/^size-(.+)$/);
641
+ if (sizeMatch) {
642
+ const value = resolveTailwindSizeValue(sizeMatch[1]);
643
+ if (value) {
644
+ return {
645
+ declaration: `width:${value};height:${value}`,
646
+ breakpoint,
647
+ pseudo,
648
+ ruleId: "fallback.import-style.size-scale",
649
+ };
650
+ }
651
+ }
652
+
653
+ const widthMatch = String(baseToken).match(/^w-(.+)$/);
654
+ if (widthMatch) {
655
+ const value = resolveTailwindSizeValue(widthMatch[1]);
656
+ if (value) {
657
+ return {
658
+ declaration: `width:${value}`,
659
+ breakpoint,
660
+ pseudo,
661
+ ruleId: "fallback.import-style.width-scale",
662
+ };
663
+ }
664
+ }
665
+
666
+ const heightMatch = String(baseToken).match(/^h-(.+)$/);
667
+ if (heightMatch) {
668
+ const value = resolveTailwindSizeValue(heightMatch[1]);
669
+ if (value) {
670
+ return {
671
+ declaration: `height:${value}`,
672
+ breakpoint,
673
+ pseudo,
674
+ ruleId: "fallback.import-style.height-scale",
675
+ };
676
+ }
677
+ }
678
+
679
+ const textSizeMap = {
680
+ xs: "var(--font-size-xs)",
681
+ sm: "var(--font-size-sm)",
682
+ base: "var(--font-size-md)",
683
+ lg: "var(--font-size-lg)",
684
+ xl: "var(--font-size-xl)",
685
+ "2xl": "var(--font-size-2xl)",
686
+ "3xl": "var(--font-size-3xl)",
687
+ "4xl": "var(--font-size-4xl)",
688
+ };
689
+ const textSizeMatch = String(baseToken).match(/^text-(xs|sm|base|lg|xl|2xl|3xl|4xl)$/);
690
+ if (textSizeMatch) {
691
+ const size = textSizeMap[textSizeMatch[1]];
692
+ return {
693
+ declaration: `font-size:${size}`,
694
+ breakpoint,
695
+ pseudo,
696
+ ruleId: "fallback.import-style.text-size",
697
+ };
698
+ }
699
+
700
+ const fontWeightMap = {
701
+ normal: "400",
702
+ medium: "500",
703
+ semibold: "600",
704
+ bold: "700",
705
+ extrabold: "800",
706
+ black: "900",
707
+ };
708
+ const fontWeightMatch = String(baseToken).match(/^font-(normal|medium|semibold|bold|extrabold|black)$/);
709
+ if (fontWeightMatch) {
710
+ return {
711
+ declaration: `font-weight:${fontWeightMap[fontWeightMatch[1]]}`,
712
+ breakpoint,
713
+ pseudo,
714
+ ruleId: "fallback.import-style.font-weight",
715
+ };
716
+ }
717
+
718
+ const lineHeightMap = {
719
+ none: "1",
720
+ tight: "1.25",
721
+ snug: "1.375",
722
+ normal: "1.5",
723
+ relaxed: "1.625",
724
+ loose: "2",
725
+ };
726
+ const lineHeightMatch = String(baseToken).match(/^leading-(none|tight|snug|normal|relaxed|loose)$/);
727
+ if (lineHeightMatch) {
728
+ return {
729
+ declaration: `line-height:${lineHeightMap[lineHeightMatch[1]]}`,
730
+ breakpoint,
731
+ pseudo,
732
+ ruleId: "fallback.import-style.line-height",
733
+ };
734
+ }
735
+
736
+ const trackingMap = {
737
+ tighter: "-0.05em",
738
+ tight: "-0.025em",
739
+ normal: "0em",
740
+ wide: "0.025em",
741
+ wider: "0.05em",
742
+ widest: "0.1em",
743
+ };
744
+ const trackingMatch = String(baseToken).match(/^tracking-(tighter|tight|normal|wide|wider|widest)$/);
745
+ if (trackingMatch) {
746
+ return {
747
+ declaration: `letter-spacing:${trackingMap[trackingMatch[1]]}`,
748
+ breakpoint,
749
+ pseudo,
750
+ ruleId: "fallback.import-style.tracking",
751
+ };
752
+ }
753
+
754
+ const bgBlackAlphaMatch = String(baseToken).match(/^bg-black\/(\d{1,3})$/);
755
+ if (bgBlackAlphaMatch) {
756
+ const alpha = Math.max(0, Math.min(100, Number(bgBlackAlphaMatch[1])));
757
+ return {
758
+ declaration: `background-color:color-mix(in srgb, var(--color-gray-900) ${alpha}%, transparent)`,
759
+ breakpoint,
760
+ pseudo,
761
+ ruleId: "fallback.import-style.overlay-alpha",
762
+ };
763
+ }
764
+
765
+ if (baseToken === "text-white") {
766
+ return {
767
+ declaration: "color:var(--color-gray-50)",
768
+ breakpoint,
769
+ pseudo,
770
+ ruleId: "fallback.import-style.text-inverse",
771
+ };
772
+ }
773
+
774
+ const semanticBgMatch = String(baseToken).match(/^bg-(primary|secondary|accent)$/);
775
+ if (semanticBgMatch) {
776
+ const semanticBgVarMap = {
777
+ primary: "var(--color-primary-fill)",
778
+ secondary: "var(--color-gray-500)",
779
+ accent: "var(--color-accent-500)",
780
+ };
781
+ const colorVar = semanticBgVarMap[semanticBgMatch[1]];
782
+ if (colorVar) {
783
+ return {
784
+ declaration: `background-color:${colorVar}`,
785
+ breakpoint,
786
+ pseudo,
787
+ ruleId: "fallback.import-style.bg-semantic",
788
+ };
789
+ }
790
+ }
791
+
792
+ const bgColorMatch = String(baseToken).match(/^bg-([a-z]+)-(\d{2,3})$/);
793
+ if (bgColorMatch) {
794
+ const colorVar = mapTailwindColorToPdsColorVar(bgColorMatch[1], bgColorMatch[2]);
795
+ if (colorVar) {
796
+ return {
797
+ declaration: `background-color:${colorVar}`,
798
+ breakpoint,
799
+ pseudo,
800
+ ruleId: "fallback.import-style.bg-tokenized",
801
+ };
802
+ }
803
+ }
804
+
805
+ const bgArbitraryMatch = String(baseToken).match(/^bg-\[([^\]]+)\]$/);
806
+ if (bgArbitraryMatch) {
807
+ const colorValue = resolveArbitraryColorValue(bgArbitraryMatch[1]);
808
+ if (colorValue) {
809
+ return {
810
+ declaration: `background-color:${colorValue}`,
811
+ breakpoint,
812
+ pseudo,
813
+ ruleId: "fallback.import-style.bg-arbitrary",
814
+ };
815
+ }
816
+ }
817
+
818
+ const textColorMatch = String(baseToken).match(/^text-([a-z]+)-(\d{2,3})$/);
819
+ if (textColorMatch) {
820
+ const colorVar = mapTailwindColorToPdsColorVar(textColorMatch[1], textColorMatch[2]);
821
+ if (colorVar) {
822
+ return {
823
+ declaration: `color:${colorVar}`,
824
+ breakpoint,
825
+ pseudo,
826
+ ruleId: "fallback.import-style.text-tokenized",
827
+ };
828
+ }
829
+ }
830
+
831
+ const textArbitraryMatch = String(baseToken).match(/^text-\[([^\]]+)\]$/);
832
+ if (textArbitraryMatch) {
833
+ const colorValue = resolveArbitraryColorValue(textArbitraryMatch[1]);
834
+ if (colorValue) {
835
+ return {
836
+ declaration: `color:${colorValue}`,
837
+ breakpoint,
838
+ pseudo,
839
+ ruleId: "fallback.import-style.text-arbitrary",
840
+ };
841
+ }
842
+ }
843
+
844
+ const roundedMatch = String(baseToken).match(/^rounded(?:-([trbl]{1,2}))?(?:-(none|xs|sm|md|lg|xl|2xl|3xl|full))?$/);
845
+ if (roundedMatch) {
846
+ const corner = roundedMatch[1] || "";
847
+ const size = roundedMatch[2] || "sm";
848
+ const radiusValue = size === "none" ? "0" : `var(--radius-${size})`;
849
+ const cornerMap = {
850
+ t: ["top-left", "top-right"],
851
+ b: ["bottom-left", "bottom-right"],
852
+ l: ["top-left", "bottom-left"],
853
+ r: ["top-right", "bottom-right"],
854
+ tl: ["top-left"],
855
+ tr: ["top-right"],
856
+ bl: ["bottom-left"],
857
+ br: ["bottom-right"],
858
+ };
859
+
860
+ if (!corner) {
861
+ return {
862
+ declaration: `border-radius:${radiusValue}`,
863
+ breakpoint,
864
+ pseudo,
865
+ ruleId: "fallback.import-style.rounded",
866
+ };
867
+ }
868
+
869
+ const targets = cornerMap[corner] || [];
870
+ const declarationText = targets
871
+ .map((targetCorner) => `border-${targetCorner}-radius:${radiusValue}`)
872
+ .join(";");
873
+
874
+ if (declarationText) {
875
+ return {
876
+ declaration: declarationText,
877
+ breakpoint,
878
+ pseudo,
879
+ ruleId: "fallback.import-style.rounded",
880
+ };
881
+ }
882
+ }
883
+
884
+ return null;
885
+ }
886
+
887
+ function formatBreakpointLength(value, fallbackPx) {
888
+ if (typeof value === "number" && Number.isFinite(value)) return `${value}px`;
889
+ if (typeof value === "string" && value.trim()) return value.trim();
890
+ return `${fallbackPx}px`;
891
+ }
892
+
893
+ function resolveBreakpoints(configInput = {}) {
894
+ const config = configInput?.design && typeof configInput.design === "object"
895
+ ? configInput.design
896
+ : configInput;
897
+
898
+ const bp = config?.layout?.breakpoints || {};
899
+ return {
900
+ sm: formatBreakpointLength(bp.sm, 640),
901
+ md: formatBreakpointLength(bp.md, 768),
902
+ lg: formatBreakpointLength(bp.lg, 1024),
903
+ xl: formatBreakpointLength(bp.xl, 1280),
904
+ };
905
+ }
906
+
907
+ function generateImportStyleSheetText(summary, breakpoints) {
908
+ const baseRules = Array.from(summary.importBaseStyles.entries()).map(
909
+ ([className, declaration]) => `.${className}{${declaration};}`
910
+ );
911
+
912
+ const responsiveRules = [];
913
+ for (const [bp, map] of summary.importResponsiveStyles.entries()) {
914
+ const minWidth = breakpoints?.[bp];
915
+ if (!minWidth || !map?.size) continue;
916
+ const body = Array.from(map.entries())
917
+ .map(([className, declaration]) => `.${className}{${declaration};}`)
918
+ .join("\n");
919
+ responsiveRules.push(`@media (min-width: ${minWidth}) {\n${body}\n}`);
920
+ }
921
+
922
+ for (const [bucketKey, map] of summary.importPseudoStyles.entries()) {
923
+ const [bp, pseudo] = String(bucketKey).split("|");
924
+ if (!pseudo || !map?.size) continue;
925
+ const body = Array.from(map.entries())
926
+ .map(([className, declaration]) => `.${className}:${pseudo}{${declaration};}`)
927
+ .join("\n");
928
+ if (!body) continue;
929
+ if (bp === "base") {
930
+ responsiveRules.push(body);
931
+ continue;
932
+ }
933
+ const minWidth = breakpoints?.[bp];
934
+ if (!minWidth) continue;
935
+ responsiveRules.push(`@media (min-width: ${minWidth}) {\n${body}\n}`);
936
+ }
937
+
938
+ const allRules = [...baseRules, ...responsiveRules].filter(Boolean).join("\n");
939
+ if (!allRules.trim()) return "";
940
+
941
+ return [
942
+ "/* pds-import: generated fallback styles for unmapped Tailwind utilities */",
943
+ allRules,
944
+ ].join("\n");
945
+ }
946
+
947
+ function injectImportStyleBlock(html = "", cssText = "") {
948
+ if (!cssText || !cssText.trim()) return html;
949
+ const styleBlock = `<style data-pds-import="tailwind-fallback">\n${cssText}\n</style>`;
950
+
951
+ if (/<head[^>]*>/i.test(html)) {
952
+ return html.replace(/<head([^>]*)>/i, `<head$1>\n${styleBlock}`);
953
+ }
954
+
955
+ return `${styleBlock}\n${html}`;
956
+ }
957
+
958
+ function isTailwindLike(token = "") {
959
+ if (!token) return false;
960
+ if (token.includes(":")) return true;
961
+ if (token.includes("[")) return true;
962
+ const firstSegment = token.split("-")[0];
963
+ return KNOWN_TW_PREFIXES.includes(firstSegment);
964
+ }
965
+
966
+ function parseVariantToken(token = "") {
967
+ const parts = String(token).split(":");
968
+ if (parts.length === 1) {
969
+ return { breakpoint: "base", base: parts[0], variants: [] };
970
+ }
971
+ const base = parts[parts.length - 1];
972
+ const variants = parts.slice(0, -1);
973
+ const breakpoint = variants.find((variant) => BREAKPOINT_ORDER.includes(variant)) || "base";
974
+ return { breakpoint, base, variants };
975
+ }
976
+
977
+ function createConversionSummary() {
978
+ return {
979
+ totalTailwind: 0,
980
+ mapped: 0,
981
+ ignored: 0,
982
+ policySkipped: 0,
983
+ unknown: 0,
984
+ intentHits: 0,
985
+ unknownTokens: new Map(),
986
+ notes: [],
987
+ appliedRules: new Set(),
988
+ importBaseStyles: new Map(),
989
+ importResponsiveStyles: new Map(),
990
+ importPseudoStyles: new Map(),
991
+ importedStyleCount: 0,
992
+ labelNestingCount: 0,
993
+ removedAtomicSpacingCount: 0,
994
+ removedAtomicPositioningCount: 0,
995
+ };
996
+ }
997
+
998
+ function parseTailwindRuntimePreflightHints(cssText = "") {
999
+ const compact = String(cssText || "")
1000
+ .toLowerCase()
1001
+ .replace(/\s+/g, "");
1002
+
1003
+ const hasListReset =
1004
+ compact.includes("menu,ol,ul{list-style:none") ||
1005
+ compact.includes("ol,ul,menu{list-style:none") ||
1006
+ compact.includes("ul,ol,menu{list-style:none");
1007
+
1008
+ const hasAnchorReset = compact.includes("a{color:inherit;text-decoration:inherit");
1009
+
1010
+ return {
1011
+ listReset: hasListReset,
1012
+ anchorReset: hasAnchorReset,
1013
+ };
1014
+ }
1015
+
1016
+ function parseTailwindRuntimeScriptHints(scriptSrc = "") {
1017
+ const src = String(scriptSrc || "").toLowerCase();
1018
+ const isTailwindCdn = src.includes("cdn.tailwindcss.com");
1019
+ if (!isTailwindCdn) {
1020
+ return {
1021
+ listReset: false,
1022
+ anchorReset: false,
1023
+ };
1024
+ }
1025
+
1026
+ return {
1027
+ listReset: true,
1028
+ anchorReset: true,
1029
+ };
1030
+ }
1031
+
1032
+ function extractTailwindRuntimeContext(sourceHtml = "", summary = null) {
1033
+ let html = String(sourceHtml || "");
1034
+ const hints = {
1035
+ listReset: false,
1036
+ anchorReset: false,
1037
+ strippedRuntimeCssBlocks: 0,
1038
+ strippedRuntimeScripts: 0,
1039
+ };
1040
+
1041
+ html = html.replace(/<style([^>]*)>([\s\S]*?)<\/style>/gi, (fullMatch, attrs, cssBody) => {
1042
+ const cssText = String(cssBody || "");
1043
+ const isTailwindRuntime = /tailwindcss\s+v\d/i.test(cssText) || /\*\s*!\s*tailwindcss/i.test(cssText);
1044
+ if (!isTailwindRuntime) return fullMatch;
1045
+
1046
+ const parsedHints = parseTailwindRuntimePreflightHints(cssText);
1047
+ hints.listReset = hints.listReset || parsedHints.listReset;
1048
+ hints.anchorReset = hints.anchorReset || parsedHints.anchorReset;
1049
+ hints.strippedRuntimeCssBlocks += 1;
1050
+ return "";
1051
+ });
1052
+
1053
+ html = html.replace(
1054
+ /<script([^>]*?)src\s*=\s*(?:(['"])([^"']*cdn\.tailwindcss\.com[^"']*)\2|([^\s>]*cdn\.tailwindcss\.com[^\s>]*))([^>]*)><\/script>/gi,
1055
+ (_match, _before, _quote, quotedSrc, unquotedSrc) => {
1056
+ const scriptSrc = quotedSrc || unquotedSrc || "";
1057
+ const scriptHints = parseTailwindRuntimeScriptHints(scriptSrc);
1058
+ hints.listReset = hints.listReset || scriptHints.listReset;
1059
+ hints.anchorReset = hints.anchorReset || scriptHints.anchorReset;
1060
+ hints.strippedRuntimeScripts += 1;
1061
+ return "";
1062
+ }
1063
+ );
1064
+
1065
+ if (summary && (hints.strippedRuntimeCssBlocks > 0 || hints.strippedRuntimeScripts > 0)) {
1066
+ recordRule(summary, "intent.preflight.tailwind-runtime-detected");
1067
+ if (hints.strippedRuntimeCssBlocks > 0) {
1068
+ addNote(
1069
+ summary,
1070
+ `Detected and stripped ${hints.strippedRuntimeCssBlocks} Tailwind runtime style block(s).`
1071
+ );
1072
+ }
1073
+ }
1074
+
1075
+ if (summary && hints.strippedRuntimeScripts > 0) {
1076
+ addNote(
1077
+ summary,
1078
+ `Removed ${hints.strippedRuntimeScripts} Tailwind CDN script reference(s).`
1079
+ );
1080
+ }
1081
+
1082
+ return { html, hints };
1083
+ }
1084
+
1085
+ function addNote(summary, message) {
1086
+ if (!summary || !message) return;
1087
+ if (!summary.notes.includes(message)) {
1088
+ summary.notes.push(message);
1089
+ }
1090
+ }
1091
+
1092
+ function addUnknownToken(summary, token) {
1093
+ const current = summary.unknownTokens.get(token) || 0;
1094
+ summary.unknownTokens.set(token, current + 1);
1095
+ }
1096
+
1097
+ function resolveConversionPolicy(configInput = {}) {
1098
+ const config = configInput?.design && typeof configInput.design === "object"
1099
+ ? configInput.design
1100
+ : configInput;
1101
+
1102
+ const utilities = config?.layout?.utilities || {};
1103
+ return {
1104
+ grid: utilities.grid !== false,
1105
+ flex: utilities.flex !== false,
1106
+ spacing: utilities.spacing !== false,
1107
+ container: utilities.container !== false,
1108
+ };
1109
+ }
1110
+
1111
+ function allowsRule(policy, gate) {
1112
+ if (!gate) return true;
1113
+ return policy?.[gate] !== false;
1114
+ }
1115
+
1116
+ function parseGridColumns(base) {
1117
+ const match = String(base).match(/^grid-cols-(\d+)$/);
1118
+ if (!match) return null;
1119
+ return Number(match[1]);
1120
+ }
1121
+
1122
+ function chooseGridAutoClass(gridByBreakpoint = {}) {
1123
+ const values = BREAKPOINT_ORDER
1124
+ .map((bp) => ({ bp, cols: gridByBreakpoint[bp] }))
1125
+ .filter((entry) => Number.isFinite(entry.cols));
1126
+
1127
+ if (values.length < 2) return null;
1128
+
1129
+ if (values.length === 2) {
1130
+ const [first, second] = values;
1131
+ const isClassicOneToTwo = first.bp === "base" && first.cols === 1 && second.cols === 2;
1132
+ if (isClassicOneToTwo) return "grid-auto-lg";
1133
+
1134
+ const isBaseSingleToExplicitBreakpoint =
1135
+ first.bp === "base" && first.cols === 1 && second.cols >= 3;
1136
+ if (isBaseSingleToExplicitBreakpoint) return null;
1137
+
1138
+ if (first.cols < second.cols) {
1139
+ if (second.cols >= 4) return "grid-auto-md";
1140
+ if (second.cols >= 2) return "grid-auto-lg";
1141
+ }
1142
+
1143
+ return null;
1144
+ }
1145
+
1146
+ let strictlyIncreasing = true;
1147
+ for (let index = 1; index < values.length; index += 1) {
1148
+ if (values[index].cols <= values[index - 1].cols) {
1149
+ strictlyIncreasing = false;
1150
+ break;
1151
+ }
1152
+ }
1153
+
1154
+ if (!strictlyIncreasing) return null;
1155
+
1156
+ const largest = values[values.length - 1]?.cols || 0;
1157
+ if (largest >= 4) return "grid-auto-md";
1158
+ if (largest >= 3) return "grid-auto-sm";
1159
+ return null;
1160
+ }
1161
+
1162
+ function mapNeutralTextColorToSemanticClass(baseToken = "") {
1163
+ const match = String(baseToken).match(/^text-(gray|slate|zinc|neutral|stone)-(\d{2,3})$/);
1164
+ if (!match) return "";
1165
+ const shade = Number(match[2]);
1166
+ if (!Number.isFinite(shade)) return "";
1167
+ if (shade >= 400 && shade <= 600) return "text-muted";
1168
+ return "";
1169
+ }
1170
+
1171
+ function resolveResponsiveGridUtilityClass(breakpoint = "", cols = 0) {
1172
+ if (!breakpoint || !Number.isFinite(cols)) return "";
1173
+ const map = {
1174
+ sm: {
1175
+ 2: "sm:grid-cols-2",
1176
+ },
1177
+ md: {
1178
+ 3: "md:grid-cols-3",
1179
+ },
1180
+ lg: {
1181
+ 4: "lg:grid-cols-4",
1182
+ },
1183
+ };
1184
+ return map?.[breakpoint]?.[cols] || "";
1185
+ }
1186
+
1187
+ function mapSpaceYTokenToStackClass(token = "") {
1188
+ const parsed = parseVariantToken(token);
1189
+ const base = String(parsed?.base || "");
1190
+ const sizeMatch = base.match(/^space-y-(\d+)$/);
1191
+ if (!sizeMatch) return "stack-md";
1192
+
1193
+ const step = Number(sizeMatch[1]);
1194
+ if (!Number.isFinite(step)) return "stack-md";
1195
+ if (step <= 1) return "stack-xs";
1196
+ if (step <= 2) return "stack-sm";
1197
+ if (step <= 4) return "stack-md";
1198
+ return "stack-lg";
1199
+ }
1200
+
1201
+ function hasGapUtilityClass(mapped = new Set()) {
1202
+ return Array.from(mapped).some((className) => {
1203
+ const value = String(className || "");
1204
+ return (
1205
+ /^gap-(?:xs|sm|md|lg|xl)$/.test(value) ||
1206
+ /^gap-[0-9]+$/.test(value) ||
1207
+ /^import-(?:sm-|md-|lg-|xl-)?gap-/.test(value)
1208
+ );
1209
+ });
1210
+ }
1211
+
1212
+ function hasStackUtilityClass(mapped = new Set()) {
1213
+ return Array.from(mapped).some((className) => /^stack-(?:xs|sm|md|lg|xl)$/.test(String(className || "")));
1214
+ }
1215
+
1216
+ function hasGridSizingClass(mapped = new Set()) {
1217
+ return Array.from(mapped).some((className) => {
1218
+ const value = String(className || "");
1219
+ return (
1220
+ /^grid-cols-\d+$/.test(value) ||
1221
+ /^grid-auto-(?:sm|md|lg|xl)$/.test(value) ||
1222
+ /^(?:sm|md|lg|xl):grid-cols-\d+$/.test(value) ||
1223
+ /^import-(?:sm-|md-|lg-|xl-)?grid-cols-\d+$/.test(value)
1224
+ );
1225
+ });
1226
+ }
1227
+
1228
+ function listTopUnknownTokens(summary, limit = 12) {
1229
+ return Array.from(summary.unknownTokens.entries())
1230
+ .sort((a, b) => b[1] - a[1])
1231
+ .slice(0, limit)
1232
+ .map(([token]) => token);
1233
+ }
1234
+
1235
+ function recordRule(summary, ruleId) {
1236
+ if (!summary || !ruleId) return;
1237
+ summary.appliedRules.add(ruleId);
1238
+ }
1239
+
1240
+ function hasToken(sourceTokens = [], pattern) {
1241
+ if (!Array.isArray(sourceTokens) || !pattern) return false;
1242
+ return sourceTokens.some((token) => pattern.test(String(token)));
1243
+ }
1244
+
1245
+ function parseTailwindButtonHeight(sourceTokens = []) {
1246
+ for (const token of sourceTokens) {
1247
+ const parsed = parseVariantToken(token);
1248
+ if (parsed.breakpoint !== "base") continue;
1249
+ const match = String(parsed.base).match(/^h-(.+)$/);
1250
+ if (!match) continue;
1251
+ const value = resolveTailwindSizeValue(match[1]);
1252
+ if (!value || value === "auto") continue;
1253
+ const pxMatch = String(value).match(/^(-?\d+(?:\.\d+)?)rem$/);
1254
+ if (pxMatch) {
1255
+ const rem = Number(pxMatch[1]);
1256
+ if (Number.isFinite(rem)) return rem * 16;
1257
+ }
1258
+ }
1259
+ return null;
1260
+ }
1261
+
1262
+ function detectButtonIntent(sourceTokens = [], tagName = "") {
1263
+ const isNativeButton = tagName === "button";
1264
+ const hasBgSignal = hasToken(sourceTokens, /^bg-/);
1265
+ const hasHoverBgSignal = hasToken(sourceTokens, /^hover:bg-/);
1266
+ const hasBorderSignal = hasToken(sourceTokens, /^border/);
1267
+ const hasShadowSignal = hasToken(sourceTokens, /^shadow/);
1268
+ const hasCursorPointer = sourceTokens.includes("cursor-pointer");
1269
+ const hasRoundedSignal = hasToken(sourceTokens, /^rounded/);
1270
+ const hasWidthSignal = hasToken(sourceTokens, /^(?:min-w|max-w|w)-/);
1271
+ const hasTextColorSignal = hasToken(sourceTokens, /^text-(?:white|black|\[[^\]]+\]|[a-z]+-\d{2,3})$/);
1272
+ const hasCtaSignal = hasBgSignal || hasHoverBgSignal || hasShadowSignal;
1273
+ const isAnchorButtonLike =
1274
+ tagName === "a" &&
1275
+ (hasCtaSignal || hasBorderSignal || hasCursorPointer || (hasRoundedSignal && hasWidthSignal));
1276
+
1277
+ const shouldNormalize = isNativeButton || isAnchorButtonLike;
1278
+ if (!shouldNormalize) {
1279
+ return {
1280
+ shouldNormalize: false,
1281
+ variant: "none",
1282
+ size: "base",
1283
+ iconOnly: false,
1284
+ };
1285
+ }
1286
+
1287
+ let variant = "none";
1288
+ if (hasBorderSignal && !hasBgSignal && !hasHoverBgSignal) {
1289
+ variant = "outline";
1290
+ } else if (hasCtaSignal || (hasBgSignal && hasTextColorSignal)) {
1291
+ variant = "primary";
1292
+ }
1293
+
1294
+ const hasCompactIconSignal =
1295
+ sourceTokens.includes("rounded-full") &&
1296
+ (sourceTokens.includes("p-2") || sourceTokens.includes("p-1") || sourceTokens.includes("p-2.5"));
1297
+ const hasIconSizeSignal = hasToken(sourceTokens, /^size-(?:6|7|8|9|10|11|12)$/);
1298
+ const iconOnly = hasCompactIconSignal || hasIconSizeSignal;
1299
+
1300
+ const buttonHeightPx = parseTailwindButtonHeight(sourceTokens);
1301
+ const hasSmallText = sourceTokens.includes("text-sm") || sourceTokens.includes("text-xs");
1302
+ const hasLargeText = sourceTokens.includes("text-lg") || sourceTokens.includes("text-xl");
1303
+
1304
+ let size = "base";
1305
+ if ((buttonHeightPx && buttonHeightPx <= 40) || hasSmallText) {
1306
+ size = "sm";
1307
+ } else if ((buttonHeightPx && buttonHeightPx >= 48) || hasLargeText) {
1308
+ size = "lg";
1309
+ }
1310
+
1311
+ return {
1312
+ shouldNormalize: true,
1313
+ variant,
1314
+ size,
1315
+ iconOnly,
1316
+ };
1317
+ }
1318
+
1319
+ function mapBadgeVariantFromColorFamily(family = "") {
1320
+ const key = String(family || "").toLowerCase();
1321
+ if (["green", "emerald", "lime", "teal"].includes(key)) return "badge-success";
1322
+ if (["blue", "sky", "cyan", "indigo"].includes(key)) return "badge-info";
1323
+ if (["yellow", "amber", "orange"].includes(key)) return "badge-warning";
1324
+ if (["red", "rose", "pink"].includes(key)) return "badge-danger";
1325
+ if (["gray", "slate", "zinc", "neutral", "stone"].includes(key)) return "badge-secondary";
1326
+ if (["purple", "violet", "fuchsia", "primary", "accent"].includes(key)) return "badge-primary";
1327
+ return "badge-secondary";
1328
+ }
1329
+
1330
+ function detectBadgeIntent(sourceTokens = [], tagName = "", buttonIntent = { shouldNormalize: false }) {
1331
+ if (buttonIntent?.shouldNormalize) {
1332
+ return {
1333
+ shouldNormalize: false,
1334
+ variantClass: "",
1335
+ outline: false,
1336
+ sizeClass: "",
1337
+ pastel: null,
1338
+ };
1339
+ }
1340
+
1341
+ if (["button", "a", "input", "select", "textarea"].includes(tagName)) {
1342
+ return {
1343
+ shouldNormalize: false,
1344
+ variantClass: "",
1345
+ outline: false,
1346
+ sizeClass: "",
1347
+ pastel: null,
1348
+ };
1349
+ }
1350
+
1351
+ if (sourceTokens.some((token) => /^badge(?:-|$)/.test(String(token)))) {
1352
+ return {
1353
+ shouldNormalize: false,
1354
+ variantClass: "",
1355
+ outline: false,
1356
+ sizeClass: "",
1357
+ pastel: null,
1358
+ };
1359
+ }
1360
+
1361
+ const baseTokens = sourceTokens
1362
+ .map((token) => parseVariantToken(token))
1363
+ .filter((parsed) => parsed.breakpoint === "base")
1364
+ .map((parsed) => String(parsed.base));
1365
+
1366
+ const hasRounded = baseTokens.some((token) => /^rounded(?:-|$)/.test(token));
1367
+ const hasPx = baseTokens.some((token) => /^px-/.test(token));
1368
+ const hasPy = baseTokens.some((token) => /^py-/.test(token));
1369
+ const hasCompactPadding = hasPx && hasPy;
1370
+ const hasSmallText = baseTokens.includes("text-xs") || baseTokens.includes("text-sm");
1371
+ const hasLargeText = baseTokens.includes("text-lg") || baseTokens.includes("text-xl");
1372
+
1373
+ const bgMatch = baseTokens
1374
+ .map((token) => token.match(/^bg-([a-z]+)-(\d{2,3})(?:\/\d{1,3})?$/))
1375
+ .find(Boolean);
1376
+ const textMatch = baseTokens
1377
+ .map((token) => token.match(/^text-([a-z]+)-(\d{2,3})(?:\/\d{1,3})?$/))
1378
+ .find(Boolean);
1379
+ const borderMatch = baseTokens
1380
+ .map((token) => token.match(/^border-([a-z]+)-(\d{2,3})$/))
1381
+ .find(Boolean);
1382
+
1383
+ const bgShade = Number(bgMatch?.[2]);
1384
+ const textShade = Number(textMatch?.[2]);
1385
+ const hasBgTone = Boolean(bgMatch && Number.isFinite(bgShade) && bgShade <= 300);
1386
+ const hasBorder = baseTokens.some((token) => /^border(?:-|$)/.test(token));
1387
+ const hasStatusColorSignal = Boolean(bgMatch || textMatch || borderMatch);
1388
+
1389
+ const confidenceSignals = [hasRounded, hasCompactPadding, hasSmallText, hasBgTone || hasBorder].filter(Boolean).length;
1390
+ const shouldNormalize = hasStatusColorSignal && confidenceSignals >= 3;
1391
+ if (!shouldNormalize) {
1392
+ return {
1393
+ shouldNormalize: false,
1394
+ variantClass: "",
1395
+ outline: false,
1396
+ sizeClass: "",
1397
+ pastel: null,
1398
+ };
1399
+ }
1400
+
1401
+ const family = (bgMatch && bgMatch[1]) || (textMatch && textMatch[1]) || (borderMatch && borderMatch[1]) || "";
1402
+ const mappedVariantClass = mapBadgeVariantFromColorFamily(family);
1403
+ const outline = hasBorder && !hasBgTone;
1404
+ const sizeClass = hasSmallText ? "badge-sm" : hasLargeText ? "badge-lg" : "";
1405
+ const pastel = hasBgTone
1406
+ ? {
1407
+ family,
1408
+ bgShade: Number.isFinite(bgShade) ? bgShade : 200,
1409
+ textShade: Number.isFinite(textShade) ? textShade : 700,
1410
+ }
1411
+ : null;
1412
+ const variantClass = pastel ? "" : mappedVariantClass;
1413
+
1414
+ return {
1415
+ shouldNormalize: true,
1416
+ variantClass,
1417
+ outline,
1418
+ sizeClass,
1419
+ pastel,
1420
+ };
1421
+ }
1422
+
1423
+ function mapTailwindBgFamilyToSurfaceClass(family = "", shade = 0) {
1424
+ const key = String(family || "").toLowerCase();
1425
+ const numericShade = Number(shade);
1426
+
1427
+ if (key === "white") return "surface-base";
1428
+ if (["gray", "slate", "zinc", "neutral", "stone"].includes(key)) {
1429
+ if (Number.isFinite(numericShade) && numericShade <= 100) return "surface-base";
1430
+ return "surface-subtle";
1431
+ }
1432
+ if (["blue", "sky", "cyan", "indigo", "primary", "info"].includes(key)) return "surface-info";
1433
+ if (["purple", "violet", "fuchsia", "accent"].includes(key)) return "surface-primary";
1434
+ if (["green", "emerald", "lime", "teal", "success"].includes(key)) return "surface-success";
1435
+ if (["yellow", "amber", "orange", "warning"].includes(key)) return "surface-warning";
1436
+ if (["red", "rose", "pink", "danger"].includes(key)) return "surface-danger";
1437
+ return "surface-base";
1438
+ }
1439
+
1440
+ function detectCardIntent(sourceTokens = [], tagName = "", buttonIntent = { shouldNormalize: false }, badgeIntent = { shouldNormalize: false }) {
1441
+ if (buttonIntent?.shouldNormalize || badgeIntent?.shouldNormalize) {
1442
+ return {
1443
+ shouldNormalize: false,
1444
+ cardVariantClass: "",
1445
+ surfaceClass: "",
1446
+ };
1447
+ }
1448
+
1449
+ const validTags = new Set(["div", "section", "article", "aside", "li"]);
1450
+ if (!validTags.has(tagName)) {
1451
+ return {
1452
+ shouldNormalize: false,
1453
+ cardVariantClass: "",
1454
+ surfaceClass: "",
1455
+ };
1456
+ }
1457
+
1458
+ if (sourceTokens.some((token) => /^card(?:-|$)/.test(String(token)))) {
1459
+ return {
1460
+ shouldNormalize: false,
1461
+ cardVariantClass: "",
1462
+ surfaceClass: "",
1463
+ };
1464
+ }
1465
+
1466
+ const baseTokens = sourceTokens
1467
+ .map((token) => parseVariantToken(token))
1468
+ .filter((parsed) => parsed.breakpoint === "base")
1469
+ .map((parsed) => String(parsed.base));
1470
+
1471
+ const hasRounded = baseTokens.some((token) => /^rounded(?:-|$)/.test(token));
1472
+ const hasBorder = baseTokens.some((token) => /^border(?:$|-)/.test(token));
1473
+ const hasShadow = baseTokens.some((token) => /^shadow(?:$|-)/.test(token));
1474
+ const hasPadding = baseTokens.some((token) => /^(?:p|px|py|pt|pb|pl|pr)-/.test(token));
1475
+ const bgMatch = baseTokens
1476
+ .map((token) => token.match(/^bg-([a-z]+)-?(\d{2,3})?$/))
1477
+ .find(Boolean);
1478
+ const hasBackground = baseTokens.includes("bg-white") || Boolean(bgMatch);
1479
+
1480
+ const confidenceSignals = [hasRounded, hasBorder || hasShadow, hasBackground, hasPadding].filter(Boolean).length;
1481
+ const shouldNormalize = confidenceSignals >= 3;
1482
+ if (!shouldNormalize) {
1483
+ return {
1484
+ shouldNormalize: false,
1485
+ cardVariantClass: "",
1486
+ surfaceClass: "",
1487
+ };
1488
+ }
1489
+
1490
+ let cardVariantClass = "card-basic";
1491
+ if (hasShadow) {
1492
+ cardVariantClass = "card-elevated";
1493
+ } else if (hasBorder) {
1494
+ cardVariantClass = "card-outlined";
1495
+ }
1496
+
1497
+ let surfaceClass = "";
1498
+ if (hasShadow) {
1499
+ surfaceClass = "surface-elevated";
1500
+ } else if (bgMatch) {
1501
+ surfaceClass = mapTailwindBgFamilyToSurfaceClass(bgMatch[1], bgMatch[2]);
1502
+ } else if (hasBackground) {
1503
+ surfaceClass = "surface-base";
1504
+ }
1505
+
1506
+ return {
1507
+ shouldNormalize: true,
1508
+ cardVariantClass,
1509
+ surfaceClass,
1510
+ };
1511
+ }
1512
+
1513
+ function buildClassReplacement({
1514
+ tagName,
1515
+ originalClassValue,
1516
+ policy,
1517
+ summary,
1518
+ preflightHints = {},
1519
+ }) {
1520
+ if (TABLE_STRICT_TAGS.has(tagName)) {
1521
+ recordRule(summary, "table.strict-tags.no-classes");
1522
+ return "";
1523
+ }
1524
+
1525
+ const sourceTokens = String(originalClassValue)
1526
+ .split(/\s+/)
1527
+ .filter(Boolean);
1528
+
1529
+ const buttonIntent = detectButtonIntent(sourceTokens, tagName);
1530
+ const badgeIntent = detectBadgeIntent(sourceTokens, tagName, buttonIntent);
1531
+ const cardIntent = detectCardIntent(sourceTokens, tagName, buttonIntent, badgeIntent);
1532
+ const isHeadingTag = /^h[1-6]$/.test(tagName);
1533
+ const isIconLikeElement =
1534
+ ["i", "svg"].includes(tagName) ||
1535
+ sourceTokens.some((token) => /^fa(?:[a-z-]+)?$/i.test(String(token || "")) || /^fa-/.test(String(token || "")));
1536
+
1537
+ const mapped = new Set();
1538
+ const gridByBreakpoint = {};
1539
+ const flexByBreakpoint = {};
1540
+ let sawSpaceY = false;
1541
+ let firstSpaceYToken = "";
1542
+ let firstSpaceYStackClass = "";
1543
+ let sawSpaceX = false;
1544
+ let firstSpaceXToken = "";
1545
+
1546
+ sourceTokens.forEach((token) => {
1547
+ const parsed = parseVariantToken(token);
1548
+ const base = parsed.base;
1549
+
1550
+ if (NON_PDS_CLASS_PATTERNS.some((pattern) => pattern.test(base))) {
1551
+ summary.ignored += 1;
1552
+ recordRule(summary, "cleanup.non-pds-class");
1553
+ return;
1554
+ }
1555
+
1556
+ const isTwToken = isTailwindLike(token) || isTailwindLike(base);
1557
+
1558
+ if (isTwToken) {
1559
+ summary.totalTailwind += 1;
1560
+ }
1561
+
1562
+ if (/^space-y-/.test(base)) {
1563
+ sawSpaceY = true;
1564
+ firstSpaceYToken = firstSpaceYToken || token;
1565
+ firstSpaceYStackClass = firstSpaceYStackClass || mapSpaceYTokenToStackClass(token);
1566
+ summary.ignored += 1;
1567
+ recordRule(summary, "layout.spacing.space-y-to-stack");
1568
+ return;
1569
+ }
1570
+
1571
+ if (/^space-x-/.test(base)) {
1572
+ const sizeMatch = String(base).match(/^space-x-(\d+)$/);
1573
+ if (sizeMatch) {
1574
+ const twGapToken = `gap-${sizeMatch[1]}`;
1575
+ const mappedGap = GAP_MAP.get(twGapToken);
1576
+ if (mappedGap && allowsRule(policy, "spacing")) {
1577
+ mapped.add(mappedGap);
1578
+ sawSpaceX = true;
1579
+ firstSpaceXToken = firstSpaceXToken || token;
1580
+ summary.mapped += 1;
1581
+ summary.intentHits += 1;
1582
+ recordRule(summary, "layout.spacing.space-x-to-gap");
1583
+ return;
1584
+ }
1585
+ }
1586
+ summary.ignored += 1;
1587
+ recordRule(summary, "style.spacing.atomic");
1588
+ return;
1589
+ }
1590
+
1591
+ if (/^grid-cols-\d+$/.test(base) && parsed.breakpoint !== "base") {
1592
+ const cols = parseGridColumns(base);
1593
+ if (Number.isFinite(cols) && allowsRule(policy, "grid")) {
1594
+ gridByBreakpoint[parsed.breakpoint] = cols;
1595
+ summary.mapped += 1;
1596
+ recordRule(summary, "intent.layout.responsive-grid-to-auto");
1597
+ return;
1598
+ }
1599
+ if (!allowsRule(policy, "grid")) {
1600
+ summary.policySkipped += 1;
1601
+ addNote(summary, "Skipped responsive grid mapping because layout.utilities.grid=false.");
1602
+ return;
1603
+ }
1604
+ }
1605
+
1606
+ if (/^flex-(?:row|col)$/.test(base) && parsed.breakpoint !== "base") {
1607
+ if (allowsRule(policy, "flex")) {
1608
+ flexByBreakpoint[parsed.breakpoint] = base;
1609
+ summary.mapped += 1;
1610
+ recordRule(summary, "intent.layout.mobile-stack");
1611
+ return;
1612
+ }
1613
+ summary.policySkipped += 1;
1614
+ addNote(summary, "Skipped responsive flex mapping because layout.utilities.flex=false.");
1615
+ return;
1616
+ }
1617
+
1618
+ if (/^grid-cols-\d+$/.test(base) && parsed.breakpoint === "base") {
1619
+ const cols = parseGridColumns(base);
1620
+ if (Number.isFinite(cols) && allowsRule(policy, "grid")) {
1621
+ gridByBreakpoint.base = cols;
1622
+ }
1623
+ }
1624
+
1625
+ const directRule = DIRECT_MAP.get(base);
1626
+ if (directRule && parsed.breakpoint === "base") {
1627
+ if (!allowsRule(policy, directRule.gate)) {
1628
+ summary.policySkipped += 1;
1629
+ addNote(summary, `Skipped ${base} because layout.utilities.${directRule.gate}=false.`);
1630
+ return;
1631
+ }
1632
+ directRule.pds.forEach((nextClass) => {
1633
+ if (nextClass) mapped.add(nextClass);
1634
+ });
1635
+ summary.mapped += 1;
1636
+ recordRule(summary, directRule.id);
1637
+ return;
1638
+ }
1639
+
1640
+ if (GAP_MAP.has(base) && parsed.breakpoint === "base") {
1641
+ if (!allowsRule(policy, "spacing")) {
1642
+ summary.policySkipped += 1;
1643
+ addNote(summary, "Skipped gap utility because layout.utilities.spacing=false.");
1644
+ return;
1645
+ }
1646
+ mapped.add(GAP_MAP.get(base));
1647
+ summary.mapped += 1;
1648
+ recordRule(summary, "layout.spacing.gap-scale");
1649
+ return;
1650
+ }
1651
+
1652
+ if (MAX_WIDTH_MAP.has(base) && parsed.breakpoint === "base") {
1653
+ if (!allowsRule(policy, "container")) {
1654
+ summary.policySkipped += 1;
1655
+ addNote(summary, "Skipped max-width utility because layout.utilities.container=false.");
1656
+ return;
1657
+ }
1658
+ mapped.add(MAX_WIDTH_MAP.get(base));
1659
+ summary.mapped += 1;
1660
+ recordRule(summary, "layout.container.max-width");
1661
+ return;
1662
+ }
1663
+
1664
+ if (buttonIntent.shouldNormalize && isTwToken) {
1665
+ const parsedBase = String(base || "");
1666
+
1667
+ if (parsed.breakpoint === "base" && ["flex-1", "grow", "flex-grow"].includes(parsedBase)) {
1668
+ mapped.add("grow");
1669
+ summary.mapped += 1;
1670
+ summary.intentHits += 1;
1671
+ recordRule(summary, "intent.component.button.layout-grow");
1672
+ return;
1673
+ }
1674
+
1675
+ const buttonStyleTokenPattern =
1676
+ /^(?:bg-|text-(?!center$|left$|right$)|font-|leading-|tracking-|rounded|ring|border|shadow|outline|transition|duration|ease|delay|animate|p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr|w-|h-|min-|max-|size-|overflow)/;
1677
+ const skipNonStructuralButtonToken =
1678
+ buttonStyleTokenPattern.test(parsedBase) || parsedBase.startsWith("hover:");
1679
+
1680
+ if (skipNonStructuralButtonToken) {
1681
+ summary.ignored += 1;
1682
+ recordRule(summary, "intent.component.button.normalize");
1683
+ return;
1684
+ }
1685
+ }
1686
+
1687
+ if (isHeadingTag) {
1688
+ const headingTypographyOrColorPattern =
1689
+ /^(?:text-(?:xs|sm|base|lg|xl|\dxl|white|black|\[[^\]]+\]|[a-z]+-\d{2,3})|font-|leading-|tracking-|uppercase|lowercase|capitalize)/;
1690
+ if (headingTypographyOrColorPattern.test(base)) {
1691
+ summary.ignored += 1;
1692
+ summary.intentHits += 1;
1693
+ recordRule(summary, "intent.typography.heading-semantic");
1694
+ return;
1695
+ }
1696
+ }
1697
+
1698
+ if (badgeIntent.shouldNormalize && isTwToken) {
1699
+ const parsedBase = String(base || "");
1700
+ const badgeStyleTokenPattern =
1701
+ /^(?:bg-|text-(?!center$|left$|right$)|font-|leading-|tracking-|rounded|ring|border|shadow|outline|transition|duration|ease|delay|animate|p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr|w-|h-|min-|max-|size-|overflow)/;
1702
+ if (badgeStyleTokenPattern.test(parsedBase) || parsedBase.startsWith("hover:")) {
1703
+ summary.ignored += 1;
1704
+ recordRule(summary, "intent.component.badge.normalize");
1705
+ return;
1706
+ }
1707
+ }
1708
+
1709
+ if (cardIntent.shouldNormalize && isTwToken) {
1710
+ const parsedBase = String(base || "");
1711
+ const cardStyleTokenPattern =
1712
+ /^(?:bg-|text-(?!center$|left$|right$)|font-|leading-|tracking-|rounded|ring|border|shadow|outline|transition|duration|ease|delay|animate|p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr)/;
1713
+ if (cardStyleTokenPattern.test(parsedBase) || parsedBase.startsWith("hover:")) {
1714
+ summary.ignored += 1;
1715
+ recordRule(summary, "intent.component.card.normalize");
1716
+ return;
1717
+ }
1718
+ }
1719
+
1720
+ const neutralTextSemanticClass = mapNeutralTextColorToSemanticClass(base);
1721
+ if (neutralTextSemanticClass && parsed.breakpoint === "base") {
1722
+ mapped.add(neutralTextSemanticClass);
1723
+ summary.mapped += 1;
1724
+ summary.intentHits += 1;
1725
+ recordRule(summary, "intent.typography.text-neutral-to-muted");
1726
+ return;
1727
+ }
1728
+
1729
+ const isTextColorToken = /^text-(?:white|black|[a-z]+-\d{2,3}|\[[^\]]+\])$/.test(base);
1730
+ if (isTextColorToken) {
1731
+ const shouldPreserveColorUtility =
1732
+ isIconLikeElement ||
1733
+ (tagName === "a" && !buttonIntent.shouldNormalize);
1734
+
1735
+ if (shouldPreserveColorUtility) {
1736
+ const iconColorImportStyle = resolveImportStyleToken(base, parsed.breakpoint, parsed.variants);
1737
+ if (iconColorImportStyle) {
1738
+ const className = registerImportStyle(
1739
+ summary,
1740
+ `${tagName}-color-${base}`,
1741
+ iconColorImportStyle.declaration,
1742
+ iconColorImportStyle.breakpoint,
1743
+ iconColorImportStyle.pseudo
1744
+ );
1745
+ if (className) {
1746
+ mapped.add(className);
1747
+ summary.mapped += 1;
1748
+ summary.intentHits += 1;
1749
+ recordRule(summary, isIconLikeElement ? "intent.icon.color-preserve" : "intent.typography.link-active-preserve");
1750
+ return;
1751
+ }
1752
+ }
1753
+ }
1754
+
1755
+ summary.ignored += 1;
1756
+ recordRule(summary, "style.color");
1757
+ return;
1758
+ }
1759
+
1760
+ const importStyle = resolveImportStyleToken(base, parsed.breakpoint, parsed.variants);
1761
+ if (importStyle) {
1762
+ const className = registerImportStyle(
1763
+ summary,
1764
+ base,
1765
+ importStyle.declaration,
1766
+ importStyle.breakpoint,
1767
+ importStyle.pseudo
1768
+ );
1769
+ if (className) {
1770
+ mapped.add(className);
1771
+ summary.mapped += 1;
1772
+ summary.intentHits += 1;
1773
+ recordRule(summary, importStyle.ruleId);
1774
+ if (parsed.breakpoint !== "base") {
1775
+ addNote(summary, `Generated responsive import fallback for ${token}.`);
1776
+ }
1777
+ return;
1778
+ }
1779
+ }
1780
+
1781
+ for (const ignoreRule of TAILWIND_TO_PDS_RULES.ignoredPatterns) {
1782
+ if (ignoreRule.pattern.test(base)) {
1783
+ summary.ignored += 1;
1784
+ recordRule(summary, ignoreRule.id);
1785
+ if (ignoreRule.id === "style.spacing.atomic") {
1786
+ summary.removedAtomicSpacingCount += 1;
1787
+ }
1788
+ if (ignoreRule.id === "style.positioning.atomic") {
1789
+ summary.removedAtomicPositioningCount += 1;
1790
+ }
1791
+ return;
1792
+ }
1793
+ }
1794
+
1795
+ if (isTwToken) {
1796
+ summary.unknown += 1;
1797
+ addUnknownToken(summary, token);
1798
+ return;
1799
+ }
1800
+
1801
+ mapped.add(token);
1802
+ });
1803
+
1804
+ if (sawSpaceY && allowsRule(policy, "spacing")) {
1805
+ mapped.add(firstSpaceYStackClass || "stack-md");
1806
+ summary.mapped += 1;
1807
+ summary.intentHits += 1;
1808
+ addNote(summary, `Mapped ${firstSpaceYToken} to ${firstSpaceYStackClass || "stack-md"}.`);
1809
+ }
1810
+
1811
+ if (sawSpaceX && allowsRule(policy, "spacing")) {
1812
+ addNote(summary, `Mapped ${firstSpaceXToken} to gap utility.`);
1813
+ }
1814
+
1815
+ const responsiveAuto = chooseGridAutoClass(gridByBreakpoint);
1816
+ if (responsiveAuto && allowsRule(policy, "grid")) {
1817
+ mapped.delete("grid-cols-1");
1818
+ mapped.delete("grid-cols-2");
1819
+ mapped.delete("grid-cols-3");
1820
+ mapped.delete("grid-cols-4");
1821
+ mapped.delete("grid-cols-6");
1822
+ mapped.add("grid");
1823
+ mapped.add(responsiveAuto);
1824
+ summary.intentHits += 1;
1825
+ recordRule(summary, "intent.layout.responsive-grid-to-auto");
1826
+ addNote(summary, `Collapsed responsive grid columns to ${responsiveAuto}.`);
1827
+ } else if (allowsRule(policy, "grid")) {
1828
+ const responsiveGridBreakpoints = BREAKPOINT_ORDER.filter(
1829
+ (bp) => bp !== "base" && Number.isFinite(gridByBreakpoint[bp])
1830
+ );
1831
+
1832
+ responsiveGridBreakpoints.forEach((bp) => {
1833
+ const cols = gridByBreakpoint[bp];
1834
+ const utilityClass = resolveResponsiveGridUtilityClass(bp, cols);
1835
+
1836
+ if (utilityClass) {
1837
+ mapped.add("grid");
1838
+ mapped.add(utilityClass);
1839
+ summary.intentHits += 1;
1840
+ recordRule(summary, "intent.layout.responsive-grid-to-auto");
1841
+ addNote(summary, `Mapped ${bp}:grid-cols-${cols} to ${utilityClass}.`);
1842
+ return;
1843
+ }
1844
+
1845
+ const fallbackClass = registerImportStyle(
1846
+ summary,
1847
+ `grid-cols-${cols}`,
1848
+ `grid-template-columns:repeat(${cols}, minmax(0, 1fr))`,
1849
+ bp
1850
+ );
1851
+ if (fallbackClass) {
1852
+ mapped.add("grid");
1853
+ mapped.add(fallbackClass);
1854
+ summary.intentHits += 1;
1855
+ recordRule(summary, "fallback.import-style.grid-cols-responsive");
1856
+ addNote(summary, `Mapped ${bp}:grid-cols-${cols} to responsive import fallback for exact columns.`);
1857
+ }
1858
+ });
1859
+ }
1860
+
1861
+ if (
1862
+ allowsRule(policy, "flex") &&
1863
+ sourceTokens.includes("flex-col") &&
1864
+ (flexByBreakpoint.md === "flex-row" || flexByBreakpoint.lg === "flex-row")
1865
+ ) {
1866
+ mapped.delete("flex-col");
1867
+ mapped.delete("flex-row");
1868
+ mapped.add("mobile-stack");
1869
+ summary.intentHits += 1;
1870
+ recordRule(summary, "intent.layout.mobile-stack");
1871
+ addNote(summary, "Mapped flex-col + breakpoint flex-row to mobile-stack.");
1872
+ }
1873
+
1874
+ const hasFlexLayoutClass = mapped.has("flex") || mapped.has("inline-flex");
1875
+ if (hasFlexLayoutClass && allowsRule(policy, "spacing")) {
1876
+ const hasExplicitSpacing = hasGapUtilityClass(mapped) || hasStackUtilityClass(mapped) || sawSpaceX || sawSpaceY;
1877
+ if (!hasExplicitSpacing) {
1878
+ mapped.add("gap-sm");
1879
+ summary.intentHits += 1;
1880
+ recordRule(summary, "layout.spacing.flex-min-gap");
1881
+ addNote(summary, "Added gap-sm fallback for flex container without explicit spacing.");
1882
+ }
1883
+ }
1884
+
1885
+ const hadGridColsIntent = sourceTokens.some((token) => /^grid-cols-\d+$/.test(parseVariantToken(token).base));
1886
+ if (hadGridColsIntent && mapped.has("grid") && !hasGridSizingClass(mapped)) {
1887
+ const safetyAuto = chooseGridAutoClass(gridByBreakpoint);
1888
+ if (safetyAuto) {
1889
+ mapped.add(safetyAuto);
1890
+ summary.intentHits += 1;
1891
+ recordRule(summary, "intent.layout.responsive-grid-to-auto");
1892
+ addNote(summary, `Applied grid safety fallback ${safetyAuto} to avoid bare grid output.`);
1893
+ } else if (Number.isFinite(gridByBreakpoint.base) && gridByBreakpoint.base > 1) {
1894
+ mapped.add(`grid-cols-${gridByBreakpoint.base}`);
1895
+ summary.intentHits += 1;
1896
+ recordRule(summary, "intent.layout.grid-safety-fallback");
1897
+ addNote(summary, `Applied grid safety fallback grid-cols-${gridByBreakpoint.base} to preserve explicit grid intent.`);
1898
+ } else {
1899
+ mapped.add("mobile-stack");
1900
+ summary.intentHits += 1;
1901
+ recordRule(summary, "intent.layout.grid-safety-fallback.mobile-stack");
1902
+ addNote(summary, "Applied mobile-stack safety fallback to avoid bare grid output when explicit grid intent was present.");
1903
+ }
1904
+ }
1905
+
1906
+ const hasSurfaceSignal = sourceTokens.some((token) => /^(?:bg-white|shadow|shadow-md|shadow-lg)$/.test(token));
1907
+ const hasCardShapeSignal = sourceTokens.some((token) => /^rounded/.test(token));
1908
+ const isContainerTag = ["div", "section", "article", "li", "aside"].includes(tagName);
1909
+ if (isContainerTag && hasSurfaceSignal && hasCardShapeSignal) {
1910
+ mapped.add("card");
1911
+ if (!mapped.has("surface-elevated") && sourceTokens.some((token) => /^shadow/.test(token))) {
1912
+ mapped.add("surface-elevated");
1913
+ }
1914
+ if (!mapped.has("surface-base") && sourceTokens.includes("bg-white")) {
1915
+ mapped.add("surface-base");
1916
+ }
1917
+ summary.intentHits += 1;
1918
+ recordRule(summary, "intent.component.card");
1919
+ }
1920
+
1921
+ const isButtonLike = tagName === "button" || tagName === "a";
1922
+ if (isButtonLike) {
1923
+ const hasPrimarySignal = sourceTokens.some((token) => /^bg-(?:[a-z]+-)?[4567]00$/.test(token)) && sourceTokens.includes("text-white");
1924
+ const hasOutlineSignal = sourceTokens.some((token) => /^border/.test(token)) && !hasPrimarySignal;
1925
+ const hasCompactSignal = sourceTokens.includes("p-2") && sourceTokens.includes("rounded-full");
1926
+
1927
+ if (hasPrimarySignal) {
1928
+ mapped.delete("surface-base");
1929
+ mapped.delete("surface-elevated");
1930
+ mapped.add("btn-primary");
1931
+ summary.intentHits += 1;
1932
+ recordRule(summary, "intent.component.button.primary");
1933
+ } else if (hasOutlineSignal) {
1934
+ mapped.add("btn-outline");
1935
+ summary.intentHits += 1;
1936
+ recordRule(summary, "intent.component.button.outline");
1937
+ }
1938
+
1939
+ if (hasCompactSignal) {
1940
+ mapped.add("icon-only");
1941
+ recordRule(summary, "intent.component.button.icon-only");
1942
+ }
1943
+ }
1944
+
1945
+ if (buttonIntent.shouldNormalize) {
1946
+ for (const className of Array.from(mapped)) {
1947
+ if (String(className).startsWith("import-")) {
1948
+ mapped.delete(className);
1949
+ }
1950
+ }
1951
+
1952
+ const buttonStructuralClasses = [
1953
+ "flex",
1954
+ "inline-flex",
1955
+ "items-start",
1956
+ "items-center",
1957
+ "items-end",
1958
+ "justify-start",
1959
+ "justify-center",
1960
+ "justify-end",
1961
+ "justify-between",
1962
+ "shrink",
1963
+ "self-start",
1964
+ "self-center",
1965
+ "self-end",
1966
+ "cursor-pointer",
1967
+ "truncate",
1968
+ "overflow-hidden",
1969
+ "whitespace-nowrap",
1970
+ "surface-base",
1971
+ "surface-elevated",
1972
+ "surface-subtle",
1973
+ "card",
1974
+ ];
1975
+ buttonStructuralClasses.forEach((name) => mapped.delete(name));
1976
+
1977
+ if (buttonIntent.variant === "primary") {
1978
+ mapped.add("btn-primary");
1979
+ recordRule(summary, "intent.component.button.primary");
1980
+ } else if (buttonIntent.variant === "outline") {
1981
+ mapped.add("btn-outline");
1982
+ recordRule(summary, "intent.component.button.outline");
1983
+ }
1984
+
1985
+ if (buttonIntent.size === "sm") {
1986
+ mapped.add("btn-sm");
1987
+ recordRule(summary, "intent.component.button.size-sm");
1988
+ } else if (buttonIntent.size === "lg") {
1989
+ mapped.add("btn-lg");
1990
+ recordRule(summary, "intent.component.button.size-lg");
1991
+ }
1992
+
1993
+ if (buttonIntent.iconOnly) {
1994
+ mapped.add("icon-only");
1995
+ recordRule(summary, "intent.component.button.icon-only");
1996
+ }
1997
+
1998
+ summary.intentHits += 1;
1999
+ recordRule(summary, "intent.component.button.normalize");
2000
+ }
2001
+
2002
+ if (badgeIntent.shouldNormalize) {
2003
+ for (const className of Array.from(mapped)) {
2004
+ if (String(className).startsWith("import-")) {
2005
+ mapped.delete(className);
2006
+ }
2007
+ }
2008
+
2009
+ const badgeStructuralClasses = [
2010
+ "flex",
2011
+ "inline-flex",
2012
+ "items-start",
2013
+ "items-center",
2014
+ "items-end",
2015
+ "justify-start",
2016
+ "justify-center",
2017
+ "justify-end",
2018
+ "justify-between",
2019
+ "grow",
2020
+ "shrink",
2021
+ "self-start",
2022
+ "self-center",
2023
+ "self-end",
2024
+ "cursor-pointer",
2025
+ "truncate",
2026
+ "overflow-hidden",
2027
+ "whitespace-nowrap",
2028
+ "text-muted",
2029
+ "surface-base",
2030
+ "surface-elevated",
2031
+ "surface-subtle",
2032
+ "card",
2033
+ ];
2034
+ badgeStructuralClasses.forEach((name) => mapped.delete(name));
2035
+
2036
+ mapped.add("badge");
2037
+ if (badgeIntent.variantClass) {
2038
+ mapped.add(badgeIntent.variantClass);
2039
+ }
2040
+ if (badgeIntent.outline) {
2041
+ mapped.add("badge-outline");
2042
+ }
2043
+ if (badgeIntent.sizeClass) {
2044
+ mapped.add(badgeIntent.sizeClass);
2045
+ }
2046
+
2047
+ if (badgeIntent.pastel && badgeIntent.pastel.family) {
2048
+ const bgVar = mapTailwindColorToPdsColorVar(
2049
+ badgeIntent.pastel.family,
2050
+ String(badgeIntent.pastel.bgShade || 200)
2051
+ );
2052
+ const textVar = mapTailwindColorToPdsColorVar(
2053
+ badgeIntent.pastel.family,
2054
+ String(badgeIntent.pastel.textShade || 700)
2055
+ );
2056
+
2057
+ if (bgVar && textVar) {
2058
+ const pastelToken = `badge-pastel-${badgeIntent.pastel.family}-${badgeIntent.pastel.bgShade}-${badgeIntent.pastel.textShade}`;
2059
+ const pastelClass = registerImportStyle(
2060
+ summary,
2061
+ pastelToken,
2062
+ `background-color:${bgVar};color:${textVar}`,
2063
+ "base"
2064
+ );
2065
+ if (pastelClass) {
2066
+ mapped.add(pastelClass);
2067
+ recordRule(summary, "intent.component.badge.pastel-preserve");
2068
+ addNote(summary, `Preserved pastel badge tone using ${pastelClass}.`);
2069
+ }
2070
+ }
2071
+ }
2072
+
2073
+ summary.intentHits += 1;
2074
+ recordRule(summary, "intent.component.badge.normalize");
2075
+ addNote(summary, "Normalized badge/pill utility cluster to PDS badge classes.");
2076
+ }
2077
+
2078
+ if (cardIntent.shouldNormalize) {
2079
+ for (const className of Array.from(mapped)) {
2080
+ if (String(className).startsWith("import-")) {
2081
+ mapped.delete(className);
2082
+ }
2083
+ }
2084
+
2085
+ const cardStructuralClasses = [
2086
+ "surface-base",
2087
+ "surface-subtle",
2088
+ "surface-elevated",
2089
+ "surface-sunken",
2090
+ "surface-overlay",
2091
+ "surface-inverse",
2092
+ "surface-primary",
2093
+ "surface-secondary",
2094
+ "surface-success",
2095
+ "surface-warning",
2096
+ "surface-danger",
2097
+ "surface-info",
2098
+ "card-basic",
2099
+ "card-elevated",
2100
+ "card-outlined",
2101
+ "card-interactive",
2102
+ ];
2103
+ cardStructuralClasses.forEach((name) => mapped.delete(name));
2104
+
2105
+ mapped.add("card");
2106
+ if (cardIntent.cardVariantClass) {
2107
+ mapped.add(cardIntent.cardVariantClass);
2108
+ }
2109
+ if (cardIntent.surfaceClass) {
2110
+ mapped.add(cardIntent.surfaceClass);
2111
+ }
2112
+
2113
+ summary.intentHits += 1;
2114
+ recordRule(summary, "intent.component.card.normalize");
2115
+ addNote(summary, "Normalized card utility cluster to PDS card/surface classes.");
2116
+ }
2117
+
2118
+ if (tagName === "a" && !buttonIntent.shouldNormalize) {
2119
+ const hasLinkSignal = sourceTokens.some(
2120
+ (token) => token.includes("hover:text") || token === "transition-colors"
2121
+ );
2122
+ if (hasLinkSignal) {
2123
+ const linkResetClass = registerImportStyle(summary, "link-reset", "text-decoration:none");
2124
+ if (linkResetClass) {
2125
+ mapped.add(linkResetClass);
2126
+ }
2127
+ summary.intentHits += 1;
2128
+ recordRule(summary, "intent.typography.link-treatment");
2129
+ }
2130
+ }
2131
+
2132
+ if (tagName === "footer") {
2133
+ const hasFooterSurfaceSignal = mapped.has("surface-base") || sourceTokens.some((token) => /^bg-/.test(token));
2134
+ if (hasFooterSurfaceSignal) {
2135
+ mapped.delete("surface-base");
2136
+ mapped.delete("surface-subtle");
2137
+ mapped.add("surface-inverse");
2138
+ summary.intentHits += 1;
2139
+ recordRule(summary, "intent.surface.footer-inverse");
2140
+ }
2141
+ }
2142
+
2143
+ if (preflightHints?.listReset && ["ul", "ol", "menu"].includes(tagName)) {
2144
+ const listResetClass = registerImportStyle(summary, "list-reset", "list-style:none;margin:0;padding:0");
2145
+ if (listResetClass) {
2146
+ mapped.add(listResetClass);
2147
+ summary.intentHits += 1;
2148
+ recordRule(summary, "intent.preflight.list-reset");
2149
+ }
2150
+ }
2151
+
2152
+ if (preflightHints?.anchorReset && tagName === "a" && !buttonIntent.shouldNormalize) {
2153
+ const anchorResetClass = registerImportStyle(
2154
+ summary,
2155
+ "anchor-reset",
2156
+ "color:inherit;text-decoration:inherit"
2157
+ );
2158
+ if (anchorResetClass) {
2159
+ mapped.add(anchorResetClass);
2160
+ summary.intentHits += 1;
2161
+ recordRule(summary, "intent.preflight.anchor-reset");
2162
+ }
2163
+ }
2164
+
2165
+ const fallbackContainerTags = new Set(["div", "section", "article", "aside", "nav", "main", "header", "footer", "form", "fieldset", "ul", "ol", "li"]);
2166
+ const hadContainerStructuralLayoutSignal = sourceTokens.some((token) => {
2167
+ const baseToken = parseVariantToken(token).base;
2168
+ return /^(?:flex|grid|container|gap-|space-[xy]-|items-|justify-|content-|place-|self-|w-|h-|min-|max-)/.test(baseToken);
2169
+ });
2170
+ if (mapped.size === 0 && fallbackContainerTags.has(tagName) && hadContainerStructuralLayoutSignal) {
2171
+ mapped.add("stack-sm");
2172
+ addNote(summary, `Added stack-sm fallback for <${tagName}> with unmapped classes.`);
2173
+ }
2174
+
2175
+ return Array.from(mapped).join(" ");
2176
+ }
2177
+
2178
+ function convertHtmlWithRules(html = "", options = {}) {
2179
+ const source = String(html || "");
2180
+ const policy = resolveConversionPolicy(options.config || {});
2181
+ const breakpoints = resolveBreakpoints(options.config || {});
2182
+ const summary = createConversionSummary();
2183
+ const extracted = extractTailwindRuntimeContext(source, summary);
2184
+ const normalizedSource = applyLabelNestingRule(extracted.html, summary);
2185
+
2186
+ const convertedHtmlRaw = normalizedSource.replace(
2187
+ /<([a-zA-Z][\w:-]*)([^>]*?)\sclass\s*=\s*(["'])(.*?)\3([^>]*)>/gs,
2188
+ (fullMatch, tagName, beforeAttrs, quote, classValue, afterAttrs) => {
2189
+ const nextClassValue = buildClassReplacement({
2190
+ tagName: String(tagName || "").toLowerCase(),
2191
+ originalClassValue: classValue,
2192
+ policy,
2193
+ summary,
2194
+ preflightHints: extracted.hints,
2195
+ });
2196
+
2197
+ const compact = String(nextClassValue || "").trim();
2198
+ if (!compact) {
2199
+ return `<${tagName}${beforeAttrs}${afterAttrs}>`;
2200
+ }
2201
+
2202
+ return `<${tagName}${beforeAttrs} class=${quote}${compact}${quote}${afterAttrs}>`;
2203
+ }
2204
+ );
2205
+
2206
+ const postProcessedHtml = normalizeSemanticTypography(
2207
+ normalizeMetricPairStackContainers(
2208
+ normalizeMetricTextParagraphs(
2209
+ normalizeIconOnlyButtons(convertedHtmlRaw, summary),
2210
+ summary
2211
+ ),
2212
+ summary
2213
+ ),
2214
+ summary,
2215
+ { config: options.config || {} }
2216
+ );
2217
+ const importCss = generateImportStyleSheetText(summary, breakpoints);
2218
+ const convertedHtml = injectImportStyleBlock(postProcessedHtml, importCss);
2219
+ if (importCss) {
2220
+ addNote(summary, `Generated ${summary.importedStyleCount} import-* fallback style mappings.`);
2221
+ }
2222
+
2223
+ if (summary.removedAtomicSpacingCount > 0 || summary.removedAtomicPositioningCount > 0) {
2224
+ addNote(
2225
+ summary,
2226
+ `Removed atomic utilities by policy: spacing=${summary.removedAtomicSpacingCount}, positioning=${summary.removedAtomicPositioningCount}.`
2227
+ );
2228
+ }
2229
+
2230
+ const unknownTokens = listTopUnknownTokens(summary, 16);
2231
+ const handled = summary.mapped + summary.ignored + summary.policySkipped;
2232
+ const baseCoverage = summary.totalTailwind > 0 ? handled / summary.totalTailwind : 1;
2233
+ const unknownRatio = summary.totalTailwind > 0 ? summary.unknown / summary.totalTailwind : 0;
2234
+
2235
+ const confidenceRaw =
2236
+ 0.42 +
2237
+ baseCoverage * 0.45 +
2238
+ Math.min(summary.intentHits, 4) * 0.025 -
2239
+ unknownRatio * 0.18;
2240
+ const confidence = Math.max(0.15, Math.min(0.96, Number(confidenceRaw.toFixed(2))));
2241
+
2242
+ const commentLines = [
2243
+ `pds-import: rulebook=${RULEBOOK_VERSION} confidence=${Math.round(confidence * 100)}%`,
2244
+ `pds-import: tailwind=${summary.totalTailwind} mapped=${summary.mapped} ignored=${summary.ignored} policySkipped=${summary.policySkipped} unknown=${summary.unknown}`,
2245
+ ];
2246
+
2247
+ if (unknownTokens.length) {
2248
+ commentLines.push(`pds-import: unknown-tailwind=${unknownTokens.join(", ")}`);
2249
+ }
2250
+
2251
+ if (summary.notes.length) {
2252
+ commentLines.push(`pds-import: notes=${summary.notes.join(" | ")}`);
2253
+ }
2254
+
2255
+ const annotatedHtml = `<!-- ${commentLines.join(" -->\n<!-- ")} -->\n${convertedHtml}`;
2256
+
2257
+ const issues = [];
2258
+ if (summary.unknown > 0) {
2259
+ issues.push({
2260
+ severity: "warning",
2261
+ message: `Converted with ${summary.unknown} unknown Tailwind utilities requiring manual review.`,
2262
+ });
2263
+ }
2264
+ if (summary.policySkipped > 0) {
2265
+ issues.push({
2266
+ severity: "info",
2267
+ message: `Skipped ${summary.policySkipped} utility mappings due to PDS config policy.`,
2268
+ });
2269
+ }
2270
+ if (unknownTokens.length) {
2271
+ issues.push({
2272
+ severity: "info",
2273
+ message: `Top unknown utilities: ${unknownTokens.slice(0, 8).join(", ")}`,
2274
+ });
2275
+ }
2276
+
2277
+ return {
2278
+ html: annotatedHtml,
2279
+ confidence,
2280
+ issues,
2281
+ meta: {
2282
+ rulebookVersion: RULEBOOK_VERSION,
2283
+ coverage: {
2284
+ tailwind: summary.totalTailwind,
2285
+ mapped: summary.mapped,
2286
+ ignored: summary.ignored,
2287
+ policySkipped: summary.policySkipped,
2288
+ unknown: summary.unknown,
2289
+ importedStyles: summary.importedStyleCount,
2290
+ nestedLabelPairs: summary.labelNestingCount,
2291
+ },
2292
+ unknownTailwindTokens: unknownTokens,
2293
+ notes: summary.notes,
2294
+ appliedRules: Array.from(summary.appliedRules),
2295
+ policy,
2296
+ importStyleSheetInjected: Boolean(importCss),
2297
+ breakpoints,
2298
+ },
2299
+ };
2300
+ }
2301
+
2302
+ export function describeTailwindConversionRules() {
2303
+ return {
2304
+ rulesJsonPath: RULEBOOK_JSON_PATH,
2305
+ ...TAILWIND_TO_PDS_RULES,
2306
+ directMappings: TAILWIND_TO_PDS_RULES.directMappings.map((rule) => ({
2307
+ id: rule.id,
2308
+ tw: rule.tw,
2309
+ pds: rule.pds,
2310
+ gate: rule.gate || null,
2311
+ })),
2312
+ ignoredPatterns: TAILWIND_TO_PDS_RULES.ignoredPatterns.map((rule) => ({
2313
+ id: rule.id,
2314
+ pattern: String(rule.pattern),
2315
+ reason: rule.reason,
2316
+ })),
2317
+ };
2318
+ }
2319
+
2320
+ function inferPrimaryColor(text) {
2321
+ const hex = String(text || "").match(/#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})\b/);
2322
+ return hex ? hex[0] : null;
2323
+ }
2324
+
2325
+ function escapeHtml(value) {
2326
+ return String(value || "")
2327
+ .replaceAll("&", "&amp;")
2328
+ .replaceAll("<", "&lt;")
2329
+ .replaceAll(">", "&gt;")
2330
+ .replaceAll('"', "&quot;")
2331
+ .replaceAll("'", "&#39;");
2332
+ }
2333
+
2334
+ function isLikelyHtml(value) {
2335
+ return /<\s*[a-z][^>]*>/i.test(String(value || ""));
2336
+ }
2337
+
2338
+ function toPxNumber(value) {
2339
+ const text = String(value || "").trim().toLowerCase();
2340
+ if (!text) return null;
2341
+ const numeric = Number.parseFloat(text);
2342
+ if (!Number.isFinite(numeric)) return null;
2343
+ if (text.endsWith("rem") || text.endsWith("em")) return numeric * 16;
2344
+ if (text.endsWith("px") || /^[0-9.\-]+$/.test(text)) return numeric;
2345
+ return null;
2346
+ }
2347
+
2348
+ function normalizeCssColor(value) {
2349
+ const text = String(value || "").trim();
2350
+ if (!text) return "";
2351
+ const hexMatch = text.match(/#(?:[0-9a-f]{3,8})\b/i);
2352
+ if (hexMatch) return hexMatch[0].toLowerCase();
2353
+ const rgbMatch = text.match(/rgba?\([^)]*\)/i);
2354
+ if (rgbMatch) return rgbMatch[0];
2355
+ const hslMatch = text.match(/hsla?\([^)]*\)/i);
2356
+ if (hslMatch) return hslMatch[0];
2357
+ return "";
2358
+ }
2359
+
2360
+ function resolveCssVariableValue(varName = "") {
2361
+ const name = String(varName || "").trim();
2362
+ if (!name || typeof window === "undefined" || typeof document === "undefined") return "";
2363
+ const root = document.documentElement;
2364
+ if (!root) return "";
2365
+ const computed = window.getComputedStyle(root);
2366
+ return String(computed.getPropertyValue(name) || "").trim();
2367
+ }
2368
+
2369
+ function resolveColorFromMaybeVar(value = "") {
2370
+ const text = String(value || "").trim();
2371
+ const direct = normalizeCssColor(text);
2372
+ if (direct) return direct;
2373
+
2374
+ const varMatch = text.match(/var\(\s*(--[^\s,)]+)\s*(?:,[^)]+)?\)/i);
2375
+ if (!varMatch) return "";
2376
+ const resolved = resolveCssVariableValue(varMatch[1]);
2377
+ return normalizeCssColor(resolved);
2378
+ }
2379
+
2380
+ function resolveTailwindBackgroundTokenToColor(token = "") {
2381
+ const raw = String(token || "").trim();
2382
+ if (!raw) return "";
2383
+ const baseToken = raw.split(":").pop() || raw;
2384
+
2385
+ if (baseToken === "bg-white") return "#ffffff";
2386
+ if (baseToken === "bg-black") return "#000000";
2387
+
2388
+ const alphaBlack = baseToken.match(/^bg-black\/(\d{1,3})$/i);
2389
+ if (alphaBlack) {
2390
+ const alpha = Math.max(0, Math.min(100, Number(alphaBlack[1]))) / 100;
2391
+ return `rgba(0,0,0,${alpha})`;
2392
+ }
2393
+
2394
+ const arbitrary = baseToken.match(/^bg-\[([^\]]+)\]$/i);
2395
+ if (arbitrary) {
2396
+ return normalizeCssColor(arbitrary[1]);
2397
+ }
2398
+
2399
+ const familyShade = baseToken.match(/^bg-([a-z]+)-(\d{2,3})$/i);
2400
+ if (!familyShade) return "";
2401
+
2402
+ const colorVar = mapTailwindColorToPdsColorVar(familyShade[1], familyShade[2]);
2403
+ if (!colorVar) return "";
2404
+ return resolveColorFromMaybeVar(colorVar);
2405
+ }
2406
+
2407
+ function extractTailwindBackgroundColorsFromClass(className = "") {
2408
+ const tokens = String(className || "")
2409
+ .split(/\s+/)
2410
+ .map((token) => token.trim())
2411
+ .filter(Boolean);
2412
+
2413
+ return tokens
2414
+ .map((token) => resolveTailwindBackgroundTokenToColor(token))
2415
+ .filter(Boolean);
2416
+ }
2417
+
2418
+ function parseCssRuleBlocks(cssText = "") {
2419
+ const blocks = [];
2420
+ const input = String(cssText || "");
2421
+ const regex = /([^{}]+)\{([^{}]*)\}/g;
2422
+ let match = regex.exec(input);
2423
+ while (match) {
2424
+ const selector = String(match[1] || "").trim();
2425
+ const body = String(match[2] || "").trim();
2426
+ if (selector && body) {
2427
+ blocks.push({ selector, body });
2428
+ }
2429
+ match = regex.exec(input);
2430
+ }
2431
+ return blocks;
2432
+ }
2433
+
2434
+ function isRootLikeSelector(selector = "") {
2435
+ const text = String(selector || "").toLowerCase();
2436
+ if (!text) return false;
2437
+ return /(^|\s|,)(html|body|:root|main)(\s|,|$)|#app\b|#root\b|\.app\b|\.page\b/.test(text);
2438
+ }
2439
+
2440
+ function parseRgbColor(value = "") {
2441
+ const match = String(value || "")
2442
+ .trim()
2443
+ .match(/rgba?\(\s*([0-9.]+)\s*,\s*([0-9.]+)\s*,\s*([0-9.]+)(?:\s*,\s*([0-9.]+))?\s*\)/i);
2444
+ if (!match) return null;
2445
+ const r = Number.parseFloat(match[1]);
2446
+ const g = Number.parseFloat(match[2]);
2447
+ const b = Number.parseFloat(match[3]);
2448
+ const a = match[4] == null ? 1 : Number.parseFloat(match[4]);
2449
+ if (![r, g, b, a].every((num) => Number.isFinite(num))) return null;
2450
+ return {
2451
+ r: Math.max(0, Math.min(255, r)),
2452
+ g: Math.max(0, Math.min(255, g)),
2453
+ b: Math.max(0, Math.min(255, b)),
2454
+ a: Math.max(0, Math.min(1, a)),
2455
+ };
2456
+ }
2457
+
2458
+ function parseHexColor(value = "") {
2459
+ const match = String(value || "").trim().match(/^#([0-9a-f]{3,8})$/i);
2460
+ if (!match) return null;
2461
+ const raw = match[1].toLowerCase();
2462
+ if (raw.length === 3) {
2463
+ const [r, g, b] = raw.split("");
2464
+ return {
2465
+ r: Number.parseInt(`${r}${r}`, 16),
2466
+ g: Number.parseInt(`${g}${g}`, 16),
2467
+ b: Number.parseInt(`${b}${b}`, 16),
2468
+ a: 1,
2469
+ };
2470
+ }
2471
+ if (raw.length === 6 || raw.length === 8) {
2472
+ return {
2473
+ r: Number.parseInt(raw.slice(0, 2), 16),
2474
+ g: Number.parseInt(raw.slice(2, 4), 16),
2475
+ b: Number.parseInt(raw.slice(4, 6), 16),
2476
+ a: raw.length === 8 ? Number.parseInt(raw.slice(6, 8), 16) / 255 : 1,
2477
+ };
2478
+ }
2479
+ return null;
2480
+ }
2481
+
2482
+ function colorToRgba(value = "") {
2483
+ const normalized = normalizeCssColor(value);
2484
+ if (!normalized) return null;
2485
+ if (normalized.startsWith("#")) return parseHexColor(normalized);
2486
+ if (normalized.startsWith("rgb")) return parseRgbColor(normalized);
2487
+ return null;
2488
+ }
2489
+
2490
+ function relativeLuminance(color) {
2491
+ if (!color) return null;
2492
+ const channel = (value) => {
2493
+ const n = Number(value) / 255;
2494
+ return n <= 0.03928 ? n / 12.92 : ((n + 0.055) / 1.055) ** 2.4;
2495
+ };
2496
+ const r = channel(color.r);
2497
+ const g = channel(color.g);
2498
+ const b = channel(color.b);
2499
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
2500
+ }
2501
+
2502
+ function isTransparentColor(value = "") {
2503
+ const text = String(value || "").trim().toLowerCase();
2504
+ if (!text) return true;
2505
+ if (text === "transparent") return true;
2506
+ const rgba = colorToRgba(text);
2507
+ if (rgba && Number.isFinite(rgba.a)) return rgba.a <= 0.04;
2508
+ return false;
2509
+ }
2510
+
2511
+ function chooseBackgroundColor(rootCandidates = [], fallbackCandidates = []) {
2512
+ const normalizedRoot = rootCandidates
2513
+ .map((value) => normalizeCssColor(value))
2514
+ .filter((value) => value && !isTransparentColor(value));
2515
+ const rootWinner = pickMostFrequent(normalizedRoot);
2516
+ if (rootWinner) {
2517
+ return { color: rootWinner, source: "root" };
2518
+ }
2519
+
2520
+ const normalizedFallback = fallbackCandidates
2521
+ .map((value) => normalizeCssColor(value))
2522
+ .filter((value) => value && !isTransparentColor(value));
2523
+
2524
+ const brightFallback = normalizedFallback.filter((value) => {
2525
+ const rgba = colorToRgba(value);
2526
+ const lum = relativeLuminance(rgba);
2527
+ return Number.isFinite(lum) ? lum >= 0.72 : false;
2528
+ });
2529
+
2530
+ const brightWinner = pickMostFrequent(brightFallback);
2531
+ if (brightWinner) {
2532
+ return { color: brightWinner, source: "fallback-bright" };
2533
+ }
2534
+
2535
+ const fallbackWinner = pickMostFrequent(normalizedFallback);
2536
+ if (fallbackWinner) {
2537
+ return { color: fallbackWinner, source: "fallback" };
2538
+ }
2539
+
2540
+ return { color: "", source: "none" };
2541
+ }
2542
+
2543
+ function parseCssDeclarations(text, out = new Map()) {
2544
+ const input = String(text || "");
2545
+ const regex = /([a-z-]+)\s*:\s*([^;{}]+)/gi;
2546
+ let match = regex.exec(input);
2547
+ while (match) {
2548
+ const prop = String(match[1] || "").trim().toLowerCase();
2549
+ const value = String(match[2] || "").trim();
2550
+ if (prop && value) {
2551
+ if (!out.has(prop)) out.set(prop, []);
2552
+ out.get(prop).push(value);
2553
+ }
2554
+ match = regex.exec(input);
2555
+ }
2556
+ return out;
2557
+ }
2558
+
2559
+ function collectHtmlSignals(rawInput = "") {
2560
+ const input = String(rawInput || "");
2561
+ const declarations = new Map();
2562
+ const colorValues = [];
2563
+ const rootBackgroundColors = [];
2564
+ const rootClassBackgroundColors = [];
2565
+ const classBackgroundColors = [];
2566
+ const buttonBackgroundColors = [];
2567
+ const classTokens = [];
2568
+ const textChunks = [];
2569
+
2570
+ const colorRegex = /#(?:[0-9a-f]{3,8})\b|rgba?\([^)]*\)|hsla?\([^)]*\)/gi;
2571
+ const pushColors = (text) => {
2572
+ const matches = String(text || "").match(colorRegex) || [];
2573
+ matches.forEach((item) => {
2574
+ const normalized = normalizeCssColor(item);
2575
+ if (normalized) colorValues.push(normalized);
2576
+ });
2577
+ };
2578
+
2579
+ if (typeof DOMParser !== "undefined" && isLikelyHtml(input)) {
2580
+ try {
2581
+ const parser = new DOMParser();
2582
+ const doc = parser.parseFromString(input, "text/html");
2583
+ const styles = Array.from(doc.querySelectorAll("style")).map((node) => node.textContent || "");
2584
+ styles.forEach((styleText) => {
2585
+ parseCssDeclarations(styleText, declarations);
2586
+ pushColors(styleText);
2587
+ parseCssRuleBlocks(styleText).forEach((block) => {
2588
+ if (!isRootLikeSelector(block.selector)) return;
2589
+ const rootDeclarations = parseCssDeclarations(block.body, new Map());
2590
+ const rootValues = getDeclarationValues(rootDeclarations, ["background", "background-color"])
2591
+ .map((value) => normalizeCssColor(value))
2592
+ .filter(Boolean);
2593
+ rootBackgroundColors.push(...rootValues);
2594
+ });
2595
+ });
2596
+
2597
+ const inlineNodes = Array.from(doc.querySelectorAll("[style]"));
2598
+ inlineNodes.forEach((node) => {
2599
+ const inlineStyle = node.getAttribute("style") || "";
2600
+ parseCssDeclarations(inlineStyle, declarations);
2601
+ pushColors(inlineStyle);
2602
+ });
2603
+
2604
+ const rootInlineSelectors = ["html", "body", "main", "#app", "#root", ".app", ".page"];
2605
+ rootInlineSelectors.forEach((selector) => {
2606
+ const rootNode = doc.querySelector(selector);
2607
+ if (!rootNode) return;
2608
+ const inlineStyle = rootNode.getAttribute("style") || "";
2609
+ if (!inlineStyle) return;
2610
+ const rootDeclarations = parseCssDeclarations(inlineStyle, new Map());
2611
+ const rootValues = getDeclarationValues(rootDeclarations, ["background", "background-color"])
2612
+ .map((value) => normalizeCssColor(value))
2613
+ .filter(Boolean);
2614
+ rootBackgroundColors.push(...rootValues);
2615
+
2616
+ const classColors = extractTailwindBackgroundColorsFromClass(rootNode.getAttribute("class") || "");
2617
+ rootClassBackgroundColors.push(...classColors);
2618
+ });
2619
+
2620
+ const classNodes = Array.from(doc.querySelectorAll("[class]"));
2621
+ classNodes.forEach((node) => {
2622
+ const tokens = parseClassTokens(node.getAttribute("class") || "");
2623
+ classTokens.push(...tokens);
2624
+ const classColors = extractTailwindBackgroundColorsFromClass(node.getAttribute("class") || "");
2625
+ classBackgroundColors.push(...classColors);
2626
+
2627
+ const tagName = String(node.tagName || "").toLowerCase();
2628
+ const isButtonLike = tagName === "button" || tagName === "a";
2629
+ const hasBgSignal = tokens.some((token) => /^bg-/.test(String(parseVariantToken(token).base || "")));
2630
+ if (isButtonLike && hasBgSignal && classColors.length) {
2631
+ buttonBackgroundColors.push(...classColors);
2632
+ }
2633
+ });
2634
+
2635
+ const text = doc.body?.textContent || "";
2636
+ if (text.trim()) textChunks.push(text);
2637
+ pushColors(doc.documentElement?.outerHTML || input);
2638
+ } catch (error) {
2639
+ parseCssDeclarations(input, declarations);
2640
+ pushColors(input);
2641
+ textChunks.push(input);
2642
+ }
2643
+ } else {
2644
+ parseCssDeclarations(input, declarations);
2645
+ pushColors(input);
2646
+ textChunks.push(input);
2647
+ }
2648
+
2649
+ return {
2650
+ declarations,
2651
+ colorValues,
2652
+ rootBackgroundColors,
2653
+ rootClassBackgroundColors,
2654
+ classBackgroundColors,
2655
+ buttonBackgroundColors,
2656
+ classTokens,
2657
+ textCorpus: textChunks.join("\n"),
2658
+ };
2659
+ }
2660
+
2661
+ function pickMostFrequent(values = []) {
2662
+ const counts = new Map();
2663
+ values.forEach((value) => {
2664
+ const key = String(value || "").trim();
2665
+ if (!key) return;
2666
+ counts.set(key, (counts.get(key) || 0) + 1);
2667
+ });
2668
+ let winner = "";
2669
+ let winnerCount = -1;
2670
+ counts.forEach((count, key) => {
2671
+ if (count > winnerCount) {
2672
+ winner = key;
2673
+ winnerCount = count;
2674
+ }
2675
+ });
2676
+ return winner;
2677
+ }
2678
+
2679
+ function getDeclarationValues(map, propNames = []) {
2680
+ return propNames.flatMap((name) => map.get(name) || []);
2681
+ }
2682
+
2683
+ function getSchemaNodeByPath(schema, path) {
2684
+ if (!schema || !path) return null;
2685
+ const segments = String(path).split(".").filter(Boolean);
2686
+ let node = schema;
2687
+ for (const segment of segments) {
2688
+ if (!node || node.type !== "object" || !node.properties || typeof node.properties !== "object") {
2689
+ return null;
2690
+ }
2691
+ node = node.properties[segment];
2692
+ }
2693
+ return node || null;
2694
+ }
2695
+
2696
+ function getInferenceContext(config = {}) {
2697
+ const design = config && typeof config === "object" ? config : {};
2698
+ const relations = PDS?.configRelations && typeof PDS.configRelations === "object"
2699
+ ? PDS.configRelations
2700
+ : {};
2701
+ const allowedPaths = new Set(Object.keys(relations));
2702
+
2703
+ let schema = null;
2704
+ if (typeof PDS?.buildConfigFormSchema === "function") {
2705
+ try {
2706
+ schema = PDS.buildConfigFormSchema(design)?.schema || null;
2707
+ } catch (error) {
2708
+ schema = null;
2709
+ }
2710
+ }
2711
+ if (!schema && PDS?.configFormSchema?.schema) {
2712
+ schema = PDS.configFormSchema.schema;
2713
+ }
2714
+
2715
+ return { design, schema, allowedPaths };
2716
+ }
2717
+
2718
+ function coerceValueForNode(node, value) {
2719
+ if (!node) return value;
2720
+
2721
+ if (Array.isArray(node.oneOf) && node.oneOf.length) {
2722
+ const options = node.oneOf
2723
+ .map((item) => item?.const)
2724
+ .filter((item) => item !== undefined && item !== null);
2725
+ if (options.length) {
2726
+ if (typeof value === "string") {
2727
+ const matched = options.find(
2728
+ (option) => String(option).toLowerCase() === value.toLowerCase()
2729
+ );
2730
+ if (matched !== undefined) return matched;
2731
+ }
2732
+ if (typeof value === "number") {
2733
+ const numeric = options
2734
+ .map((option) => Number(option))
2735
+ .filter((option) => Number.isFinite(option));
2736
+ if (numeric.length) {
2737
+ return numeric.reduce((best, current) => (
2738
+ Math.abs(current - value) < Math.abs(best - value) ? current : best
2739
+ ), numeric[0]);
2740
+ }
2741
+ }
2742
+ return options[0];
2743
+ }
2744
+ }
2745
+
2746
+ if (node.type === "number" || node.type === "integer") {
2747
+ const parsed = Number(value);
2748
+ if (!Number.isFinite(parsed)) return undefined;
2749
+ return node.type === "integer" ? Math.round(parsed) : parsed;
2750
+ }
2751
+
2752
+ if (node.type === "boolean") {
2753
+ return Boolean(value);
2754
+ }
2755
+
2756
+ if (node.type === "string") {
2757
+ return String(value || "").trim();
2758
+ }
2759
+
2760
+ return value;
2761
+ }
2762
+
2763
+ function setPatchPath(target, path, value) {
2764
+ const segments = String(path || "").split(".").filter(Boolean);
2765
+ if (!segments.length) return;
2766
+ let current = target;
2767
+ for (let index = 0; index < segments.length; index += 1) {
2768
+ const segment = segments[index];
2769
+ if (index === segments.length - 1) {
2770
+ current[segment] = value;
2771
+ return;
2772
+ }
2773
+ if (!current[segment] || typeof current[segment] !== "object" || Array.isArray(current[segment])) {
2774
+ current[segment] = {};
2775
+ }
2776
+ current = current[segment];
2777
+ }
2778
+ }
2779
+
2780
+ function maybeSetInferredValue(state, path, value) {
2781
+ if (value === undefined || value === null || value === "") return;
2782
+ if (state.allowedPaths.size && !state.allowedPaths.has(path)) return;
2783
+
2784
+ const node = getSchemaNodeByPath(state.schema, path);
2785
+ const nextValue = coerceValueForNode(node, value);
2786
+ if (nextValue === undefined || nextValue === null || nextValue === "") return;
2787
+
2788
+ setPatchPath(state.patch, path, nextValue);
2789
+ state.inferredPaths.add(path);
2790
+ }
2791
+
2792
+ function collectMedianPx(values = []) {
2793
+ const nums = values
2794
+ .map((item) => toPxNumber(item))
2795
+ .filter((item) => Number.isFinite(item));
2796
+ if (!nums.length) return null;
2797
+ nums.sort((a, b) => a - b);
2798
+ const middle = Math.floor(nums.length / 2);
2799
+ if (nums.length % 2) return nums[middle];
2800
+ return (nums[middle - 1] + nums[middle]) / 2;
2801
+ }
2802
+
2803
+ function inferFontFamily(values = []) {
2804
+ const cleaned = values
2805
+ .map((item) => String(item || "").split(",")[0] || "")
2806
+ .map((item) => item.trim().replace(/^['"]|['"]$/g, ""))
2807
+ .filter(Boolean);
2808
+ return pickMostFrequent(cleaned);
2809
+ }
2810
+
2811
+ function inferBorderWidthKeyword(pxValue) {
2812
+ const size = Number(pxValue);
2813
+ if (!Number.isFinite(size)) return "thin";
2814
+ if (size <= 0.75) return "hairline";
2815
+ if (size <= 1.5) return "thin";
2816
+ if (size <= 2.5) return "medium";
2817
+ return "thick";
2818
+ }
2819
+
2820
+ function resolveTailwindRoundedTokenPx(token = "") {
2821
+ const baseToken = String(parseVariantToken(token).base || "").toLowerCase();
2822
+ const match = baseToken.match(/^rounded(?:-[trbl]{1,2})?(?:-(none|xs|sm|md|lg|xl|2xl|3xl|full))?$/);
2823
+ if (!match) return null;
2824
+
2825
+ const size = match[1] || "DEFAULT";
2826
+ const map = {
2827
+ none: 0,
2828
+ xs: 2,
2829
+ sm: 4,
2830
+ DEFAULT: 6,
2831
+ md: 8,
2832
+ lg: 12,
2833
+ xl: 16,
2834
+ "2xl": 24,
2835
+ "3xl": 32,
2836
+ };
2837
+
2838
+ if (size === "full") return null;
2839
+ return Number.isFinite(map[size]) ? map[size] : null;
2840
+ }
2841
+
2842
+ function inferRadiusPxFromTailwindClasses(classTokens = []) {
2843
+ const radiusValues = classTokens
2844
+ .map((token) => resolveTailwindRoundedTokenPx(token))
2845
+ .filter((value) => Number.isFinite(value));
2846
+ if (!radiusValues.length) return null;
2847
+ radiusValues.sort((a, b) => a - b);
2848
+ const middle = Math.floor(radiusValues.length / 2);
2849
+ if (radiusValues.length % 2) return radiusValues[middle];
2850
+ return (radiusValues[middle - 1] + radiusValues[middle]) / 2;
2851
+ }
2852
+
2853
+ export function inferPdsDesignFromHtml(input = {}) {
2854
+ const html = String(input.html || "");
2855
+ if (!html.trim()) {
2856
+ return createImportResult({
2857
+ source: "html-inference",
2858
+ type: String(input.sourceType || "design-inference"),
2859
+ confidence: 0,
2860
+ issues: [{ severity: "warning", message: "No HTML or guideline text provided for design extraction." }],
2861
+ designPatch: {},
2862
+ meta: {
2863
+ extractedPathCount: 0,
2864
+ extractedPaths: [],
2865
+ },
2866
+ });
2867
+ }
2868
+
2869
+ const context = getInferenceContext(input.config || {});
2870
+ const signals = collectHtmlSignals(html);
2871
+ const state = {
2872
+ patch: {},
2873
+ inferredPaths: new Set(),
2874
+ allowedPaths: context.allowedPaths,
2875
+ schema: context.schema,
2876
+ };
2877
+
2878
+ const textColors = getDeclarationValues(signals.declarations, ["color"])
2879
+ .map((value) => normalizeCssColor(value))
2880
+ .filter(Boolean);
2881
+ const backgroundColors = getDeclarationValues(signals.declarations, ["background", "background-color"])
2882
+ .map((value) => normalizeCssColor(value))
2883
+ .filter(Boolean);
2884
+ const allColors = [...backgroundColors, ...textColors, ...signals.colorValues].filter(Boolean);
2885
+ const uniqueColors = Array.from(new Set(allColors));
2886
+ const explicitRootBackgroundCandidates = [
2887
+ ...(signals.rootBackgroundColors || []),
2888
+ ];
2889
+ const rootClassBackgroundCandidates = [
2890
+ ...(signals.rootClassBackgroundColors || []),
2891
+ ];
2892
+ const rootBackgroundCandidates = explicitRootBackgroundCandidates.length
2893
+ ? explicitRootBackgroundCandidates
2894
+ : rootClassBackgroundCandidates;
2895
+ const fallbackBackgroundCandidates = [
2896
+ ...backgroundColors,
2897
+ ...(signals.classBackgroundColors || []),
2898
+ ];
2899
+ const backgroundPick = chooseBackgroundColor(rootBackgroundCandidates, fallbackBackgroundCandidates);
2900
+ const inferredBackground = backgroundPick.color;
2901
+ maybeSetInferredValue(state, "colors.background", inferredBackground || backgroundColors[0] || uniqueColors[0]);
2902
+
2903
+ const paletteColors = uniqueColors.filter((color) => color && color !== inferredBackground);
2904
+ const buttonPrimary = pickMostFrequent(signals.buttonBackgroundColors || []);
2905
+ const inferredPrimary = buttonPrimary || paletteColors[0] || uniqueColors[0];
2906
+ const remainingPalette = paletteColors.filter((color) => color && color !== inferredPrimary);
2907
+ maybeSetInferredValue(state, "colors.primary", inferredPrimary);
2908
+ maybeSetInferredValue(state, "colors.secondary", remainingPalette[0] || inferredPrimary || uniqueColors[0]);
2909
+ maybeSetInferredValue(state, "colors.accent", remainingPalette[1] || remainingPalette[0] || inferredPrimary || uniqueColors[0]);
2910
+
2911
+ const families = getDeclarationValues(signals.declarations, ["font-family"]);
2912
+ const fontFamily = inferFontFamily(families);
2913
+ maybeSetInferredValue(state, "typography.fontFamilyBody", fontFamily);
2914
+ maybeSetInferredValue(state, "typography.fontFamilyHeadings", fontFamily);
2915
+ maybeSetInferredValue(state, "typography.fontFamilyMono", /mono|code/i.test(signals.textCorpus) ? "JetBrains Mono" : "");
2916
+
2917
+ const fontSizes = getDeclarationValues(signals.declarations, ["font-size"]);
2918
+ const medianFontSizePx = collectMedianPx(fontSizes);
2919
+ maybeSetInferredValue(state, "typography.baseSize", medianFontSizePx);
2920
+
2921
+ const spacingValues = getDeclarationValues(signals.declarations, [
2922
+ "padding",
2923
+ "padding-top",
2924
+ "padding-right",
2925
+ "padding-bottom",
2926
+ "padding-left",
2927
+ "margin",
2928
+ "margin-top",
2929
+ "margin-right",
2930
+ "margin-bottom",
2931
+ "margin-left",
2932
+ "gap",
2933
+ "row-gap",
2934
+ "column-gap",
2935
+ ]);
2936
+ const medianSpacing = collectMedianPx(spacingValues);
2937
+ maybeSetInferredValue(state, "spatialRhythm.baseUnit", medianSpacing);
2938
+ maybeSetInferredValue(state, "spatialRhythm.inputPadding", medianSpacing);
2939
+ maybeSetInferredValue(state, "spatialRhythm.buttonPadding", medianSpacing);
2940
+
2941
+ const radiusValues = getDeclarationValues(signals.declarations, ["border-radius"]);
2942
+ const borderRadius = collectMedianPx(radiusValues) || inferRadiusPxFromTailwindClasses(signals.classTokens || []);
2943
+ maybeSetInferredValue(state, "shape.radiusSize", borderRadius);
2944
+
2945
+ const borderWidthValues = getDeclarationValues(signals.declarations, [
2946
+ "border-width",
2947
+ "border-top-width",
2948
+ "border-right-width",
2949
+ "border-bottom-width",
2950
+ "border-left-width",
2951
+ ]);
2952
+ const borderWidth = collectMedianPx(borderWidthValues);
2953
+ maybeSetInferredValue(state, "shape.borderWidth", inferBorderWidthKeyword(borderWidth));
2954
+
2955
+ const maxWidthValues = getDeclarationValues(signals.declarations, ["max-width"]);
2956
+ const maxWidth = collectMedianPx(maxWidthValues);
2957
+ maybeSetInferredValue(state, "layout.containerMaxWidth", maxWidth);
2958
+ maybeSetInferredValue(state, "layout.maxWidth", maxWidth);
2959
+
2960
+ const minHeightValues = getDeclarationValues(signals.declarations, ["min-height", "height"]);
2961
+ const minHeight = collectMedianPx(minHeightValues);
2962
+ maybeSetInferredValue(state, "layout.buttonMinHeight", minHeight);
2963
+ maybeSetInferredValue(state, "layout.inputMinHeight", minHeight);
2964
+
2965
+ const transitionValues = getDeclarationValues(signals.declarations, ["transition-duration"]);
2966
+ const transitionMs = collectMedianPx(transitionValues.map((value) => {
2967
+ const text = String(value || "").trim().toLowerCase();
2968
+ const number = Number.parseFloat(text);
2969
+ if (!Number.isFinite(number)) return null;
2970
+ if (text.endsWith("ms")) return number;
2971
+ if (text.endsWith("s")) return number * 1000;
2972
+ return number;
2973
+ }));
2974
+ maybeSetInferredValue(state, "behavior.transitionSpeed", transitionMs);
2975
+
2976
+ const shadowValues = getDeclarationValues(signals.declarations, ["box-shadow"]);
2977
+ const hasShadow = shadowValues.length > 0;
2978
+ maybeSetInferredValue(state, "layers.baseShadowOpacity", hasShadow ? 0.2 : 0.08);
2979
+
2980
+ const extractedPaths = Array.from(state.inferredPaths);
2981
+ const categoryCoverage = extractedPaths.reduce((acc, path) => {
2982
+ const category = path.split(".")[0];
2983
+ acc[category] = (acc[category] || 0) + 1;
2984
+ return acc;
2985
+ }, {});
2986
+
2987
+ const confidence = extractedPaths.length
2988
+ ? Math.min(0.92, 0.35 + extractedPaths.length * 0.02)
2989
+ : 0.25;
2990
+
2991
+ return createImportResult({
2992
+ source: "html-inference",
2993
+ type: String(input.sourceType || "design-inference"),
2994
+ confidence,
2995
+ issues: extractedPaths.length
2996
+ ? []
2997
+ : [{ severity: "warning", message: "Could not infer enough design signals from input." }],
2998
+ designPatch: state.patch,
2999
+ meta: {
3000
+ extractedPathCount: extractedPaths.length,
3001
+ extractedPaths,
3002
+ categoryCoverage,
3003
+ colorSampleSize: uniqueColors.length,
3004
+ backgroundInference: {
3005
+ source: backgroundPick.source,
3006
+ candidates: {
3007
+ root: rootBackgroundCandidates.length,
3008
+ declaration: backgroundColors.length,
3009
+ classBased: (signals.classBackgroundColors || []).length,
3010
+ },
3011
+ },
3012
+ },
3013
+ });
3014
+ }
3015
+
3016
+ export function convertHtmlLikeInputToPdsTemplate(input = {}) {
3017
+ const raw = String(input.input || "").trim();
3018
+ const sourceType = String(input.sourceType || "unknown");
3019
+
3020
+ if (!raw) {
3021
+ return createImportResult({
3022
+ source: sourceType,
3023
+ type: sourceType,
3024
+ confidence: 0,
3025
+ issues: [{ severity: "error", message: "No input provided." }],
3026
+ meta: {
3027
+ conversionMode: "none",
3028
+ },
3029
+ });
3030
+ }
3031
+
3032
+ if (isLikelyHtml(raw)) {
3033
+ const converted = convertTailwindHtmlToPds({ html: raw, config: input.config || {} });
3034
+ return createImportResult({
3035
+ source: sourceType,
3036
+ type: sourceType,
3037
+ confidence: converted.confidence,
3038
+ issues: converted.issues,
3039
+ template: converted.template,
3040
+ meta: {
3041
+ ...(converted.meta || {}),
3042
+ conversionMode: "html-to-pds",
3043
+ },
3044
+ });
3045
+ }
3046
+
3047
+ return createImportResult({
3048
+ source: sourceType,
3049
+ type: sourceType,
3050
+ confidence: 0.48,
3051
+ issues: [{ severity: "info", message: "Input is not HTML; generated text-based preview template." }],
3052
+ template: {
3053
+ id: `${sourceType}-text-import`,
3054
+ name: "Imported Guideline Text",
3055
+ html: `<article class="card surface-base stack-sm"><h3>Imported Guidelines</h3><pre>${escapeHtml(raw)}</pre></article>`,
3056
+ },
3057
+ meta: {
3058
+ conversionMode: "text-preview",
3059
+ },
3060
+ });
3061
+ }
3062
+
3063
+ export function convertTailwindHtmlToPds(input = {}) {
3064
+ const html = String(input.html || "").trim();
3065
+ if (!html) {
3066
+ return createImportResult({
3067
+ source: "tailwind",
3068
+ type: "tailwind-html",
3069
+ confidence: 0,
3070
+ issues: [{ severity: "error", message: "No HTML provided." }],
3071
+ });
3072
+ }
3073
+
3074
+ const conversion = convertHtmlWithRules(html, { config: input.config || {} });
3075
+
3076
+ return createImportResult({
3077
+ source: "tailwind",
3078
+ type: "tailwind-html",
3079
+ confidence: conversion.confidence,
3080
+ issues: conversion.issues,
3081
+ template: {
3082
+ id: "tailwind-import",
3083
+ name: "Converted Tailwind Markup",
3084
+ html: conversion.html,
3085
+ },
3086
+ meta: conversion.meta,
3087
+ });
3088
+ }
3089
+
3090
+ export function convertBrandGuidelinesToPatch(input = {}) {
3091
+ const text = String(input.text || "").trim();
3092
+ if (!text) {
3093
+ return createImportResult({
3094
+ source: "brand",
3095
+ type: "brand-guidelines",
3096
+ confidence: 0,
3097
+ issues: [{ severity: "error", message: "No brand guideline text provided." }],
3098
+ });
3099
+ }
3100
+
3101
+ const primary = inferPrimaryColor(text);
3102
+ const patch = {
3103
+ colors: {},
3104
+ typography: {},
3105
+ };
3106
+ const issues = [];
3107
+
3108
+ if (primary) {
3109
+ patch.colors.primary = primary;
3110
+ } else {
3111
+ issues.push({
3112
+ severity: "warning",
3113
+ message: "No HEX color found; primary color was not inferred.",
3114
+ });
3115
+ }
3116
+
3117
+ if (/serif/i.test(text)) patch.typography.fontFamilyBody = "Georgia, serif";
3118
+ if (/sans[-\s]?serif/i.test(text)) patch.typography.fontFamilyBody = "Inter, Arial, sans-serif";
3119
+ if (/mono|monospace/i.test(text)) patch.typography.fontFamilyMono = "JetBrains Mono, monospace";
3120
+
3121
+ const confidence = primary ? 0.68 : 0.52;
3122
+
3123
+ return createImportResult({
3124
+ source: "brand",
3125
+ type: "brand-guidelines",
3126
+ confidence,
3127
+ issues,
3128
+ designPatch: patch,
3129
+ meta: {
3130
+ inferred: {
3131
+ primaryColor: primary,
3132
+ },
3133
+ },
3134
+ });
3135
+ }