@controlfront/detect 0.0.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 (35) hide show
  1. package/bin/cfb.js +202 -0
  2. package/package.json +64 -0
  3. package/src/commands/baseline.js +198 -0
  4. package/src/commands/init.js +309 -0
  5. package/src/commands/login.js +71 -0
  6. package/src/commands/logout.js +44 -0
  7. package/src/commands/scan.js +1547 -0
  8. package/src/commands/snapshot.js +191 -0
  9. package/src/commands/sync.js +127 -0
  10. package/src/config/baseUrl.js +49 -0
  11. package/src/data/tailwind-core-spec.js +149 -0
  12. package/src/engine/runRules.js +210 -0
  13. package/src/lib/collectDeclaredTokensAuto.js +67 -0
  14. package/src/lib/collectTokenMatches.js +330 -0
  15. package/src/lib/collectTokenMatches.js.regex +252 -0
  16. package/src/lib/loadRules.js +73 -0
  17. package/src/rules/core/no-hardcoded-colors.js +28 -0
  18. package/src/rules/core/no-hardcoded-spacing.js +29 -0
  19. package/src/rules/core/no-inline-styles.js +28 -0
  20. package/src/utils/authorId.js +106 -0
  21. package/src/utils/buildAIContributions.js +224 -0
  22. package/src/utils/buildBlameData.js +388 -0
  23. package/src/utils/buildDeclaredCssVars.js +185 -0
  24. package/src/utils/buildDeclaredJson.js +214 -0
  25. package/src/utils/buildFileChanges.js +372 -0
  26. package/src/utils/buildRuntimeUsage.js +337 -0
  27. package/src/utils/detectDeclaredDrift.js +59 -0
  28. package/src/utils/extractImports.js +178 -0
  29. package/src/utils/fileExtensions.js +65 -0
  30. package/src/utils/generateInsights.js +332 -0
  31. package/src/utils/getAllFiles.js +63 -0
  32. package/src/utils/getCommitMetaData.js +102 -0
  33. package/src/utils/getLine.js +14 -0
  34. package/src/utils/resolveProjectForFolder/index.js +47 -0
  35. package/src/utils/twClassify.js +138 -0
@@ -0,0 +1,191 @@
1
+ /**
2
+ * snapshot.js
3
+ *
4
+ * Snapshot orchestrator.
5
+ *
6
+ * Responsibilities:
7
+ * - determine which files to scan
8
+ * - collect raw per-file indicators
9
+ *
10
+ * This file does NOT:
11
+ * - compute baselines
12
+ * - compare snapshots
13
+ * - infer drift, deltas, volatility, or causality
14
+ * - compute per-file characteristics
15
+ */
16
+
17
+ import { collectFileIndicators } from "../fileCharacteristics/collectFileIndicators.js";
18
+ import getAllFiles from "../utils/getAllFiles.js";
19
+ import getCommitMetaData from "../utils/getCommitMetaData.js";
20
+ import { buildBlameData } from "../utils/buildBlameData.js";
21
+ import { buildFileChanges } from "../utils/buildFileChanges.js";
22
+ import { buildDependencyMap } from "../utils/extractImports.js";
23
+ import { generateAuthorId } from "../utils/authorId.js";
24
+
25
+ /**
26
+ * Run a snapshot scan at the current commit.
27
+ *
28
+ * @param {{ files?: string[] }} options
29
+ */
30
+ export async function runSnapshot(options = {}) {
31
+ const repoRoot = process.cwd();
32
+
33
+ // 1. Resolve files to scan
34
+ const files =
35
+ options.files && options.files.length > 0
36
+ ? options.files
37
+ : getAllFiles(repoRoot);
38
+
39
+ // 2. Collect raw indicators
40
+ const fileIndicators = await collectFileIndicators(files, { repoRoot });
41
+
42
+ // 2a. Build dependency map (imports + imported_by)
43
+ console.log("📦 Building dependency map...");
44
+ // Use the actual files from fileIndicators to ensure key matching
45
+ const indicatorFiles = Object.keys(fileIndicators);
46
+ const dependencyMap = buildDependencyMap(indicatorFiles, repoRoot);
47
+
48
+ console.log(`📦 Dependency map built for ${indicatorFiles.length} files`);
49
+
50
+ // Debug: find a JS/TS file sample
51
+ const jsTsFile = indicatorFiles.find(f =>
52
+ /\.(jsx?|tsx?|mjs|cjs)$/i.test(f)
53
+ );
54
+
55
+ if (jsTsFile && dependencyMap[jsTsFile]) {
56
+ console.log(`📦 Sample JS/TS file: ${jsTsFile}`);
57
+ console.log(` imports: ${dependencyMap[jsTsFile].imports.length}`);
58
+ console.log(` imported_by: ${dependencyMap[jsTsFile].imported_by.length}`);
59
+ if (dependencyMap[jsTsFile].imports.length > 0) {
60
+ console.log(` first import: ${dependencyMap[jsTsFile].imports[0]}`);
61
+ }
62
+ }
63
+
64
+ // 3. Assemble snapshot per file with indicators only
65
+ const filesSnapshot = {};
66
+
67
+ for (const [file, { indicators }] of Object.entries(fileIndicators)) {
68
+ const deps = dependencyMap[file] || { imports: [], imported_by: [] };
69
+
70
+ filesSnapshot[file] = {
71
+ indicators: { ...indicators },
72
+ imports: deps.imports,
73
+ imported_by: deps.imported_by,
74
+ };
75
+ }
76
+
77
+ // 4. Attach commit metadata
78
+ const commit = getCommitMetaData();
79
+
80
+ // Generate consistent author ID
81
+ const commitAuthorId = generateAuthorId({
82
+ email: commit.author_email,
83
+ name: commit.author,
84
+ });
85
+
86
+ // 4a. Build per-changed-file unified-0 diffs (+line ranges) and scoped blame stats (step 2)
87
+ // `buildBlameData` will derive the parent SHA (single-parent only) and skip merge/root commits.
88
+ const changedFilePaths = Array.isArray(commit.changed_files)
89
+ ? commit.changed_files
90
+ .map((cf) =>
91
+ (cf && typeof cf.path === "string" && cf.path) ||
92
+ (cf && typeof cf.file === "string" && cf.file) ||
93
+ (cf && typeof cf.to === "string" && cf.to) ||
94
+ (cf && typeof cf.from === "string" && cf.from) ||
95
+ null
96
+ )
97
+ .filter(Boolean)
98
+ : [];
99
+
100
+ let diffData = { status: "skipped", files: {} };
101
+ try {
102
+ if (commit && typeof commit.sha === "string" && commit.sha && changedFilePaths.length > 0) {
103
+ diffData = buildBlameData({
104
+ repoRoot,
105
+ commitSha: commit.sha,
106
+ filePaths: changedFilePaths,
107
+ commitTimestampIso: commit.timestamp || null,
108
+ });
109
+ }
110
+ } catch (_err) {
111
+ diffData = { status: "error", files: {} };
112
+ }
113
+
114
+ // Enrich changed_files with indicator context from the full snapshot.
115
+ // This keeps `files` as the full snapshot (baseline-safe) while making
116
+ // `changed_files` useful for downstream rollups and charts.
117
+ const enrichedChangedFiles = Array.isArray(commit.changed_files)
118
+ ? commit.changed_files.map((cf) => {
119
+ const path =
120
+ (cf && typeof cf.path === "string" && cf.path) ||
121
+ (cf && typeof cf.file === "string" && cf.file) ||
122
+ (cf && typeof cf.to === "string" && cf.to) ||
123
+ (cf && typeof cf.from === "string" && cf.from) ||
124
+ null;
125
+
126
+ const indicators =
127
+ path && filesSnapshot[path] && filesSnapshot[path].indicators
128
+ ? filesSnapshot[path].indicators
129
+ : null;
130
+
131
+ const imports =
132
+ path && filesSnapshot[path] && filesSnapshot[path].imports
133
+ ? filesSnapshot[path].imports
134
+ : [];
135
+
136
+ const importedBy =
137
+ path && filesSnapshot[path] && filesSnapshot[path].imported_by
138
+ ? filesSnapshot[path].imported_by
139
+ : [];
140
+
141
+ const diffForFile = path && diffData && diffData.files ? diffData.files[path] : null;
142
+
143
+ return {
144
+ ...cf,
145
+
146
+ // Stamp current-commit context onto each changed file entry.
147
+ // This is required for accurate cross-commit file sequencing.
148
+ commit_sha: commit.sha || null,
149
+ commit_timestamp: commit.timestamp || null,
150
+ commit_author: commit.author || null,
151
+ commit_author_email: commit.author_email || null,
152
+ commit_author_id: commitAuthorId,
153
+
154
+ indicators,
155
+ imports,
156
+ imported_by: importedBy,
157
+ diff_unified0: diffForFile ? diffForFile.diff_unified0 : null,
158
+ diff_ranges: diffForFile ? diffForFile.ranges : [],
159
+ blame_stats: diffForFile ? diffForFile.blame_stats : null,
160
+ };
161
+ })
162
+ : [];
163
+
164
+ // 4b. Build per-commit file change events (file-centric rows) for downstream sequencing.
165
+ // NOTE: This is a pure transform; persistence happens via the existing snapshots API.
166
+ const fileChanges = buildFileChanges({
167
+ projectId: options.projectId || options.project_id || null,
168
+ commit,
169
+ changedFiles: enrichedChangedFiles,
170
+ });
171
+
172
+ // 5. Emit snapshot payload
173
+ return {
174
+ commit_sha: commit.sha,
175
+ commit_author: commit.author,
176
+ author_email: commit.author_email || null,
177
+ commit_author_email: commit.author_email || null,
178
+ commit_author_id: commitAuthorId,
179
+ commit_branch: commit.branch,
180
+ commit_timestamp: commit.timestamp,
181
+ commit_message: commit.message || "",
182
+ diff_status: diffData && diffData.status ? diffData.status : "skipped",
183
+ blame_line_coverage_pct:
184
+ diffData && typeof diffData.line_coverage_pct === "number" ? diffData.line_coverage_pct : null,
185
+ changed_files: enrichedChangedFiles,
186
+ file_changes: fileChanges,
187
+ files: filesSnapshot,
188
+ };
189
+ }
190
+
191
+ export default runSnapshot;
@@ -0,0 +1,127 @@
1
+ import fs from "fs";
2
+ import fetch from "node-fetch";
3
+ import os from "os";
4
+ import path from "path";
5
+
6
+ import open from "open";
7
+ import { makeUrl, resolveBaseUrl } from "../config/baseUrl.js";
8
+
9
+ import { runInit } from "./init.js";
10
+ import { runLogin } from "./login.js";
11
+
12
+ export async function runSync({
13
+ openWeb = true,
14
+ projectInfo,
15
+ token,
16
+ changedFiles,
17
+ slug,
18
+ } = {}) {
19
+ const configPath = path.join(os.homedir(), ".cf", "config.json");
20
+ const reportPath = path.resolve(process.cwd(), "scan-report.json");
21
+ const insightsPath = path.resolve(process.cwd(), "insights.json");
22
+
23
+ if (!fs.existsSync(configPath)) {
24
+ console.log("🔐 No config found. Launching login flow...");
25
+ await runLogin();
26
+ await runInit();
27
+ }
28
+
29
+ if (!fs.existsSync(reportPath)) {
30
+ console.error("❌ scan-report.json not found. Run `cf scan` first.");
31
+ process.exit(1);
32
+ }
33
+
34
+ const resolvedWorkspace =
35
+ projectInfo?.workspace ||
36
+ (projectInfo?.project?.workspace_id
37
+ ? { id: projectInfo.project.workspace_id, name: "Unknown" }
38
+ : undefined);
39
+
40
+ if (!token) {
41
+ console.error("❌ Missing token. Please run `cf login`.");
42
+ process.exit(1);
43
+ }
44
+
45
+ if (!resolvedWorkspace?.id) {
46
+ console.error("❌ Missing workspace info. Please run `cf init`.");
47
+ process.exit(1);
48
+ }
49
+
50
+ if (!projectInfo?.project?.id) {
51
+ console.error("❌ Missing project info. Please run `cf init`.");
52
+ process.exit(1);
53
+ }
54
+
55
+ const scan = JSON.parse(fs.readFileSync(reportPath, "utf8"));
56
+
57
+ // IMPORTANT: when syncing an updated scan over an existing drift row, we must
58
+ // not send an `id` field back to the server. If we do, an overwrite/upsert can
59
+ // update the primary key and break stable URLs.
60
+ //
61
+ // We keep the rest of the payload intact; the server should decide whether to
62
+ // insert or update based on its own keys.
63
+ const scanForSync = { ...scan };
64
+ delete scanForSync.id;
65
+
66
+ console.log(
67
+ `📡 Syncing scan to ${resolvedWorkspace.name} / ${projectInfo.project.name}...`
68
+ );
69
+
70
+ try {
71
+ const res = await fetch(makeUrl(`/api/scans`), {
72
+ method: "POST",
73
+ headers: {
74
+ "Content-Type": "application/json",
75
+ Authorization: `Bearer ${token}`,
76
+ },
77
+ body: JSON.stringify({
78
+ scan: {
79
+ ...scanForSync,
80
+ workspace_id: resolvedWorkspace.id,
81
+ project_id: projectInfo.project.id,
82
+ ...(changedFiles ? { changed_files: changedFiles } : {}),
83
+ slug: slug || "manual",
84
+ },
85
+ }),
86
+ });
87
+ const text = await res.text();
88
+ let data;
89
+ try {
90
+ data = JSON.parse(text);
91
+ } catch {
92
+ console.error(`❌ Server response is not valid JSON:\n${text}`);
93
+ return;
94
+ }
95
+ if (!res.ok) {
96
+ console.error(
97
+ `❌ Sync failed with status ${res.status}:\n${JSON.stringify(
98
+ data,
99
+ null,
100
+ 2
101
+ )}`
102
+ );
103
+ return;
104
+ }
105
+ console.log("✅ Scan synced successfully to ControlFront.");
106
+ if (data?.id && data?.workspace_id && data?.project_id) {
107
+ const url = `${resolveBaseUrl()}/workspaces/${
108
+ data.workspace_id
109
+ }/projects/${data.project_id}/baseline`;
110
+ console.log(`👀 View scan report: ${url}`);
111
+
112
+ if (openWeb) {
113
+ try {
114
+ await open(url);
115
+ } catch (err) {
116
+ console.error("❌ Failed to open browser:", err.message);
117
+ }
118
+ }
119
+ }
120
+
121
+ if (!openWeb) {
122
+ console.log("🚫 Web app not opened (per --no-web-open flag).");
123
+ }
124
+ } catch (err) {
125
+ console.error("❌ Sync failed:", err.message);
126
+ }
127
+ }
@@ -0,0 +1,49 @@
1
+ import dotenv from "dotenv";
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import { fileURLToPath } from "url";
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+
10
+ // Load .env from CLI root (one directory up from /src/config/)
11
+ dotenv.config({
12
+ path: path.join(__dirname, "..", "..", ".env"),
13
+ override: true,
14
+ });
15
+
16
+ export function getStoredEnvironment() {
17
+ try {
18
+ const configPath = path.join(os.homedir(), ".cf", "config.json");
19
+ if (fs.existsSync(configPath)) {
20
+ const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
21
+ return config.environment || "dev";
22
+ }
23
+ } catch (e) {
24
+ // ignore errors
25
+ }
26
+ return "dev";
27
+ }
28
+
29
+ export function resolveBaseUrlForEnv(env) {
30
+ const url = env === "prod"
31
+ ? process.env.CF_APP_URL_PROD
32
+ : process.env.CF_APP_URL_DEV;
33
+
34
+ if (url && url.trim()) {
35
+ return url.trim();
36
+ }
37
+
38
+ // Fallback defaults
39
+ return env === "prod" ? "https://app.controlfront.io" : "http://localhost:3000";
40
+ }
41
+
42
+ export function resolveBaseUrl() {
43
+ const env = getStoredEnvironment();
44
+ return resolveBaseUrlForEnv(env);
45
+ }
46
+
47
+ export function makeUrl(pathAndQuery = "") {
48
+ return new URL(pathAndQuery, resolveBaseUrl() + "/").toString();
49
+ }
@@ -0,0 +1,149 @@
1
+ // Tailwind v3.x+ core patterns (no plugins). Keep this in JS so we can use regexes & helpers.
2
+ // Covers: variants, breakpoints, colors/shades, sizing scale, radii, borders, typography,
3
+ // layout, effects, transforms, interactivity, tables, accessibility, etc.
4
+
5
+ const BREAKPOINTS = ["sm","md","lg","xl","2xl"];
6
+ const STATE_VARIANTS = [
7
+ "hover","focus","focus-visible","active","disabled","visited","first","last","odd","even",
8
+ "checked","indeterminate","focus-within","required","valid","invalid","read-only","empty",
9
+ "placeholder","placeholder-shown","autofill","selection","target","open"
10
+ ];
11
+ const GROUP_VARIANTS = [
12
+ "group-hover","group-focus","group-active","group-disabled","group-aria-[\\w:-]+","peer-hover",
13
+ "peer-focus","peer-active","peer-disabled","peer-checked","peer-invalid","peer-aria-[\\w:-]+"
14
+ ];
15
+ // Tailwind also supports arbitrary variants in brackets like [@supports(display:grid)]:, etc.
16
+ // We’ll just strip any leading "{variant}:" chunks before matching a base utility.
17
+
18
+ export const VARIANT_PREFIX_RE = new RegExp(
19
+ // responsive + state + dark + rtl/ltr + print + motion + forced-colors + data-* + aria-* + arbitrary [...]
20
+ `^(?:(${[
21
+ ...BREAKPOINTS,
22
+ "dark","rtl","ltr","motion-safe","motion-reduce","portrait","landscape",
23
+ "print","contrast-more","contrast-less","forced-colors",
24
+ ...STATE_VARIANTS,
25
+ ].join("|")})|(?:data-[\\w-]+)|(?:aria-[\\w-]+)|(?:group\\/(?:[\\w-]+))|(?:group)|(?:peer)|(?:\$begin:math:display$.+?\\$end:math:display$)):`,
26
+ "g"
27
+ );
28
+
29
+ // Colors from the default palette (Tailwind v3)
30
+ const COLORS = [
31
+ "inherit","current","transparent","black","white",
32
+ "slate","gray","zinc","neutral","stone",
33
+ "red","orange","amber","yellow","lime","green","emerald","teal","cyan","sky","blue",
34
+ "indigo","violet","purple","fuchsia","pink","rose"
35
+ ];
36
+ const SHADES = ["50","100","200","300","400","500","600","700","800","900","950"];
37
+
38
+ // Spacing scale (subset but representative of Tailwind defaults, expanded to cover common sizes)
39
+ const SPACING_KEYS = [
40
+ "px","0","0\\.5","1","1\\.5","2","2\\.5","3","3\\.5","4","5","6","7","8","9","10",
41
+ "11","12","14","16","20","24","28","32","36","40","44","48","52","56","60","64","72","80","96"
42
+ ];
43
+
44
+ // Fractions for width/height
45
+ const FRACTIONS = ["1\\/2","1\\/3","2\\/3","1\\/4","2\\/4","3\\/4","1\\/5","2\\/5","3\\/5","4\\/5","1\\/6","2\\/6","3\\/6","4\\/6","5\\/6","full","screen","min","max","fit","auto"];
46
+
47
+ // Font sizes
48
+ const FONT_SIZES = ["xs","sm","base","lg","xl","2xl","3xl","4xl","5xl","6xl","7xl","8xl","9xl"];
49
+
50
+ // Border radii
51
+ const RADII = ["none","sm","", "md","lg","xl","2xl","3xl","full"]; // "" is default ("rounded")
52
+
53
+ // Border widths
54
+ const BORDER_W = ["0","2","4","8",""];
55
+
56
+ // Z-index keywords
57
+ const ZINDEX = ["0","10","20","30","40","50","auto"];
58
+
59
+ // Opacity
60
+ const OPACITY = ["0","5","10","20","25","30","40","50","60","70","75","80","90","95","100"];
61
+
62
+ // Object fit/position, overflow, display, position, etc. are constant words, we’ll regex by namespace.
63
+
64
+ const COLOR_CLASS = (ns) =>
65
+ new RegExp(`^${ns}-(?:${COLORS.join("|")})(?:-(?:${SHADES.join("|")}))?$`);
66
+
67
+ const SCALE_CLASS = (ns) =>
68
+ new RegExp(`^${ns}-(?:${SPACING_KEYS.join("|")})$`);
69
+
70
+ const FRACTION_CLASS = (ns) =>
71
+ new RegExp(`^${ns}-(?:${FRACTIONS.join("|")})$`);
72
+
73
+ const SIMPLE_SET = (ns, set) =>
74
+ new RegExp(`^${ns}-(?:${set.join("|")})$`);
75
+
76
+ const EXACT = (...names) => new RegExp(`^(?:${names.map(n=>n.replace(/[.*+?^${}()|[\\]\\\\]/g,"\\$&")).join("|")})$`);
77
+
78
+ export const TAILWIND_PATTERNS = [
79
+ // Display & Position
80
+ EXACT("block","inline-block","inline","flex","inline-flex","grid","inline-grid","contents","hidden"),
81
+ EXACT("static","fixed","absolute","relative","sticky"),
82
+ // Flex & Grid
83
+ EXACT("flex-row","flex-row-reverse","flex-col","flex-col-reverse","flex-wrap","flex-nowrap","flex-wrap-reverse"),
84
+ EXACT("items-start","items-center","items-end","items-baseline","items-stretch"),
85
+ EXACT("justify-start","justify-center","justify-end","justify-between","justify-around","justify-evenly","content-center","content-between","content-around","content-evenly"),
86
+ EXACT("self-auto","self-start","self-center","self-end","self-stretch","place-content-center","place-items-center","place-self-center"),
87
+ // Spacing
88
+ SCALE_CLASS("p"), SCALE_CLASS("px"), SCALE_CLASS("py"), SCALE_CLASS("pt"), SCALE_CLASS("pr"), SCALE_CLASS("pb"), SCALE_CLASS("pl"),
89
+ SCALE_CLASS("m"), SCALE_CLASS("mx"), SCALE_CLASS("my"), SCALE_CLASS("mt"), SCALE_CLASS("mr"), SCALE_CLASS("mb"), SCALE_CLASS("ml"),
90
+ SCALE_CLASS("gap"), SCALE_CLASS("space-x"), SCALE_CLASS("space-y"),
91
+ // Sizing
92
+ SCALE_CLASS("h"), SCALE_CLASS("w"), FRACTION_CLASS("h"), FRACTION_CLASS("w"),
93
+ // Extended sizing utilities
94
+ SCALE_CLASS("min-h"), SCALE_CLASS("min-w"),
95
+ SCALE_CLASS("max-h"), SCALE_CLASS("max-w"),
96
+ SIMPLE_SET("max-w", [
97
+ "0","none","xs","sm","md","lg","xl","2xl","3xl","4xl","5xl","6xl","7xl",
98
+ "full","min","max","fit","prose",
99
+ "screen-sm","screen-md","screen-lg","screen-xl","screen-2xl"
100
+ ]),
101
+ SIMPLE_SET("min-h", ["0","full","screen","min","max","fit"]),
102
+ SIMPLE_SET("max-h", ["0","full","screen","min","max","fit"]),
103
+ SIMPLE_SET("min-w", ["0","full","min","max","fit"]),
104
+ // Typography
105
+ SIMPLE_SET("text", FONT_SIZES),
106
+ // Recognize all standard Tailwind font-size utilities (including text-md, text-base, etc.)
107
+ /^text-(?:xs|sm|base|md|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl)$/,
108
+ EXACT("antialiased","subpixel-antialiased"),
109
+ EXACT("font-thin","font-extralight","font-light","font-normal","font-medium","font-semibold","font-bold","font-extrabold","font-black"),
110
+ EXACT("italic","not-italic"),
111
+ EXACT("uppercase","lowercase","capitalize","normal-case","truncate","break-words","break-normal"),
112
+ // Colors
113
+ COLOR_CLASS("text"), COLOR_CLASS("bg"), COLOR_CLASS("border"), COLOR_CLASS("divide"),
114
+ // Border radius & width & style
115
+ new RegExp(`^rounded(?:-(?:${RADII.join("|")}))?$`),
116
+ SIMPLE_SET("border", BORDER_W), EXACT("border","border-x","border-y","border-t","border-r","border-b","border-l"),
117
+ EXACT("border-solid","border-dashed","border-dotted","border-double","border-none"),
118
+ // Effects
119
+ EXACT("shadow","shadow-sm","shadow-md","shadow-lg","shadow-xl","shadow-2xl","shadow-inner","shadow-none"),
120
+ SIMPLE_SET("opacity", OPACITY),
121
+ // Transforms & Transitions
122
+ EXACT("transform","transform-gpu","transform-none"),
123
+ EXACT("transition","transition-none","transition-all","transition-colors","transition-opacity","transition-shadow","transition-transform"),
124
+ SIMPLE_SET("duration", ["75","100","150","200","300","500","700","1000"]),
125
+ SIMPLE_SET("delay", ["75","100","150","200","300","500","700","1000"]),
126
+ SIMPLE_SET("ease", ["linear","in","out","in-out"]),
127
+ // Layout helpers
128
+ EXACT("container","sr-only","not-sr-only","overflow-hidden","overflow-auto","overflow-scroll","overflow-x-hidden","overflow-y-hidden","overflow-x-auto","overflow-y-auto"),
129
+ // Z-index
130
+ SIMPLE_SET("z", ZINDEX),
131
+ // Cursors & pointer events
132
+ EXACT("pointer-events-none","pointer-events-auto","cursor-default","cursor-pointer","cursor-not-allowed"),
133
+ // Tables
134
+ EXACT("table-auto","table-fixed","border-collapse","border-separate","caption-top","caption-bottom")
135
+ ];
136
+
137
+ // Arbitrary value syntax (always Tailwind, but not default-scale):
138
+ export const ARBITRARY_RE = /-\\[(?:.|\\n)+?\\]$/;
139
+
140
+ // Known base namespaces (for quick prefix sniffing)
141
+ export const KNOWN_PREFIXES = [
142
+ "p","px","py","pt","pr","pb","pl",
143
+ "m","mx","my","mt","mr","mb","ml",
144
+ "text","bg","border","divide","ring","outline",
145
+ "rounded","grid","col","row","gap","justify","items","content","place",
146
+ "h","w","min-h","min-w","max-h","max-w",
147
+ "shadow","opacity","z","duration","delay","ease","transition",
148
+ "overflow","cursor","pointer-events","table","caption","container",
149
+ ];