@bitovi/vybit 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +155 -0
- package/loader.mjs +11 -0
- package/overlay/dist/.gitkeep +0 -0
- package/overlay/dist/overlay.js +1547 -0
- package/package.json +57 -0
- package/panel/dist/assets/index-BUKLf5aN.css +1 -0
- package/panel/dist/assets/index-Cr2RD_Gn.js +549 -0
- package/panel/dist/index.html +25 -0
- package/server/app.ts +117 -0
- package/server/index.ts +57 -0
- package/server/mcp-tools.ts +356 -0
- package/server/queue.ts +281 -0
- package/server/tailwind-adapter.ts +17 -0
- package/server/tailwind-v3.ts +159 -0
- package/server/tailwind-v4.ts +160 -0
- package/server/tailwind.ts +50 -0
- package/server/tests/server.integration.test.ts +698 -0
- package/server/websocket.ts +130 -0
- package/shared/types.ts +304 -0
package/server/queue.ts
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// In-memory patch queue with draft + commits model
|
|
2
|
+
|
|
3
|
+
import { EventEmitter } from 'node:events';
|
|
4
|
+
import type { Patch, PatchStatus, PatchSummary, Commit, CommitStatus, CommitSummary } from '../shared/types.js';
|
|
5
|
+
|
|
6
|
+
const emitter = new EventEmitter();
|
|
7
|
+
|
|
8
|
+
function toSummary(p: Patch): PatchSummary {
|
|
9
|
+
return {
|
|
10
|
+
id: p.id,
|
|
11
|
+
kind: p.kind,
|
|
12
|
+
elementKey: p.elementKey,
|
|
13
|
+
status: p.status,
|
|
14
|
+
originalClass: p.originalClass,
|
|
15
|
+
newClass: p.newClass,
|
|
16
|
+
property: p.property,
|
|
17
|
+
timestamp: p.timestamp,
|
|
18
|
+
component: p.component,
|
|
19
|
+
errorMessage: p.errorMessage,
|
|
20
|
+
message: p.message,
|
|
21
|
+
image: p.image,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function toCommitSummary(c: Commit): CommitSummary {
|
|
26
|
+
return {
|
|
27
|
+
id: c.id,
|
|
28
|
+
status: c.status,
|
|
29
|
+
timestamp: c.timestamp,
|
|
30
|
+
patches: c.patches.map(toSummary),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Mutable draft: accumulates patches as the user stages them (preserves insertion order)
|
|
35
|
+
const draftPatches: Patch[] = [];
|
|
36
|
+
|
|
37
|
+
// Finalized commits
|
|
38
|
+
const commits: Commit[] = [];
|
|
39
|
+
|
|
40
|
+
export function addPatch(patch: Patch): Patch {
|
|
41
|
+
// Dedup by ID first — if an identical PATCH_STAGED arrives twice (e.g. from
|
|
42
|
+
// two overlays connected to the same server) just ignore the duplicate.
|
|
43
|
+
if (draftPatches.some(p => p.id === patch.id)) {
|
|
44
|
+
return patch;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (patch.kind === 'class-change') {
|
|
48
|
+
// Dedup: if a staged patch exists for the same elementKey+property, replace it
|
|
49
|
+
const existingIdx = draftPatches.findIndex(
|
|
50
|
+
p => p.kind === 'class-change' && p.elementKey === patch.elementKey && p.property === patch.property && p.status === 'staged'
|
|
51
|
+
);
|
|
52
|
+
if (existingIdx !== -1) {
|
|
53
|
+
draftPatches.splice(existingIdx, 1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Message patches are always appended (no dedup)
|
|
57
|
+
draftPatches.push(patch);
|
|
58
|
+
return patch;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function commitDraft(ids: string[]): Commit {
|
|
62
|
+
const idSet = new Set(ids);
|
|
63
|
+
const commitPatches: Patch[] = [];
|
|
64
|
+
|
|
65
|
+
// Extract matching patches from draft, preserving order
|
|
66
|
+
for (let i = draftPatches.length - 1; i >= 0; i--) {
|
|
67
|
+
if (idSet.has(draftPatches[i].id) && draftPatches[i].status === 'staged') {
|
|
68
|
+
draftPatches[i].status = 'committed';
|
|
69
|
+
commitPatches.unshift(draftPatches[i]);
|
|
70
|
+
draftPatches.splice(i, 1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const commit: Commit = {
|
|
75
|
+
id: crypto.randomUUID(),
|
|
76
|
+
patches: commitPatches,
|
|
77
|
+
status: 'committed',
|
|
78
|
+
timestamp: new Date().toISOString(),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Set commitId on each patch
|
|
82
|
+
for (const p of commit.patches) {
|
|
83
|
+
p.commitId = commit.id;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
commits.push(commit);
|
|
87
|
+
if (commitPatches.length > 0) emitter.emit('committed');
|
|
88
|
+
return commit;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** @deprecated Use commitDraft instead. Backward compat shim. */
|
|
92
|
+
export function commitPatches(ids: string[]): number {
|
|
93
|
+
const commit = commitDraft(ids);
|
|
94
|
+
return commit.patches.length;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Returns the oldest Commit with status 'committed', or null. */
|
|
98
|
+
export function getNextCommitted(): Commit | null {
|
|
99
|
+
return commits.find(c => c.status === 'committed') ?? null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function markCommitImplementing(commitId: string): void {
|
|
103
|
+
const commit = commits.find(c => c.id === commitId);
|
|
104
|
+
if (!commit) return;
|
|
105
|
+
commit.status = 'implementing';
|
|
106
|
+
for (const p of commit.patches) {
|
|
107
|
+
if (p.status === 'committed') p.status = 'implementing';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface PatchResult {
|
|
112
|
+
patchId: string;
|
|
113
|
+
success: boolean;
|
|
114
|
+
error?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function markCommitImplemented(commitId: string, results: PatchResult[]): void {
|
|
118
|
+
const commit = commits.find(c => c.id === commitId);
|
|
119
|
+
if (!commit) return;
|
|
120
|
+
|
|
121
|
+
// Apply results to class-change patches
|
|
122
|
+
for (const result of results) {
|
|
123
|
+
const patch = commit.patches.find(p => p.id === result.patchId);
|
|
124
|
+
if (!patch) continue;
|
|
125
|
+
patch.status = result.success ? 'implemented' : 'error';
|
|
126
|
+
if (result.error) patch.errorMessage = result.error;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Message patches are always "implemented" (informational, no action needed)
|
|
130
|
+
for (const patch of commit.patches) {
|
|
131
|
+
if (patch.kind === 'message') patch.status = 'implemented';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const classChanges = commit.patches.filter(p => p.kind === 'class-change');
|
|
135
|
+
const allSucceeded = classChanges.every(p => p.status === 'implemented');
|
|
136
|
+
const allFailed = classChanges.every(p => p.status === 'error');
|
|
137
|
+
|
|
138
|
+
commit.status = classChanges.length === 0 ? 'implemented' // message-only commit
|
|
139
|
+
: allSucceeded ? 'implemented'
|
|
140
|
+
: allFailed ? 'error'
|
|
141
|
+
: 'partial';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Legacy: mark individual patch IDs as implementing (backward compat for old MCP tools). */
|
|
145
|
+
export function markImplementing(ids: string[]): number {
|
|
146
|
+
const idSet = new Set(ids);
|
|
147
|
+
let moved = 0;
|
|
148
|
+
for (const commit of commits) {
|
|
149
|
+
for (const p of commit.patches) {
|
|
150
|
+
if (idSet.has(p.id) && p.status === 'committed') {
|
|
151
|
+
p.status = 'implementing';
|
|
152
|
+
moved++;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// If all patches in commit are implementing, update commit status
|
|
156
|
+
if (commit.status === 'committed' && commit.patches.every(p => p.status !== 'committed')) {
|
|
157
|
+
commit.status = 'implementing';
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return moved;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Legacy: mark individual patch IDs as implemented (backward compat). */
|
|
164
|
+
export function markImplemented(ids: string[]): number {
|
|
165
|
+
const idSet = new Set(ids);
|
|
166
|
+
let moved = 0;
|
|
167
|
+
for (const commit of commits) {
|
|
168
|
+
for (const p of commit.patches) {
|
|
169
|
+
if (idSet.has(p.id) && (p.status === 'committed' || p.status === 'implementing')) {
|
|
170
|
+
p.status = 'implemented';
|
|
171
|
+
moved++;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Auto-succeed message patches if all class-changes done
|
|
175
|
+
const classChanges = commit.patches.filter(p => p.kind === 'class-change');
|
|
176
|
+
if (classChanges.length > 0 && classChanges.every(p => p.status === 'implemented')) {
|
|
177
|
+
for (const p of commit.patches) {
|
|
178
|
+
if (p.kind === 'message' && p.status !== 'implemented') p.status = 'implemented';
|
|
179
|
+
}
|
|
180
|
+
commit.status = 'implemented';
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return moved;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function getByStatus(status: PatchStatus): Patch[] {
|
|
187
|
+
const result: Patch[] = [];
|
|
188
|
+
// Draft patches
|
|
189
|
+
for (const p of draftPatches) {
|
|
190
|
+
if (p.status === status) result.push(p);
|
|
191
|
+
}
|
|
192
|
+
// Commit patches
|
|
193
|
+
for (const commit of commits) {
|
|
194
|
+
for (const p of commit.patches) {
|
|
195
|
+
if (p.status === status) result.push(p);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function getCounts(): { staged: number; committed: number; implementing: number; implemented: number } {
|
|
202
|
+
const counts = { staged: 0, committed: 0, implementing: 0, implemented: 0 };
|
|
203
|
+
for (const p of draftPatches) {
|
|
204
|
+
if (p.status in counts) counts[p.status as keyof typeof counts]++;
|
|
205
|
+
}
|
|
206
|
+
for (const commit of commits) {
|
|
207
|
+
for (const p of commit.patches) {
|
|
208
|
+
if (p.status in counts) counts[p.status as keyof typeof counts]++;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return counts;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Build the full QUEUE_UPDATE payload */
|
|
215
|
+
export function getQueueUpdate() {
|
|
216
|
+
// Count commits by status
|
|
217
|
+
let committedCount = 0;
|
|
218
|
+
let implementingCount = 0;
|
|
219
|
+
let implementedCount = 0;
|
|
220
|
+
let partialCount = 0;
|
|
221
|
+
let errorCount = 0;
|
|
222
|
+
for (const c of commits) {
|
|
223
|
+
switch (c.status) {
|
|
224
|
+
case 'committed': committedCount++; break;
|
|
225
|
+
case 'implementing': implementingCount++; break;
|
|
226
|
+
case 'implemented': implementedCount++; break;
|
|
227
|
+
case 'partial': partialCount++; break;
|
|
228
|
+
case 'error': errorCount++; break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
draftCount: draftPatches.length,
|
|
234
|
+
committedCount,
|
|
235
|
+
implementingCount,
|
|
236
|
+
implementedCount,
|
|
237
|
+
partialCount,
|
|
238
|
+
errorCount,
|
|
239
|
+
draft: draftPatches.map(toSummary),
|
|
240
|
+
commits: commits.map(toCommitSummary),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** @deprecated Use getQueueUpdate instead. Backward compat shim. */
|
|
245
|
+
export function getPatchUpdate() {
|
|
246
|
+
const allPatches: Patch[] = [...draftPatches];
|
|
247
|
+
for (const c of commits) allPatches.push(...c.patches);
|
|
248
|
+
const counts = getCounts();
|
|
249
|
+
return {
|
|
250
|
+
...counts,
|
|
251
|
+
patches: {
|
|
252
|
+
staged: allPatches.filter(p => p.status === 'staged').map(toSummary),
|
|
253
|
+
committed: allPatches.filter(p => p.status === 'committed').map(toSummary),
|
|
254
|
+
implementing: allPatches.filter(p => p.status === 'implementing').map(toSummary),
|
|
255
|
+
implemented: allPatches.filter(p => p.status === 'implemented').map(toSummary),
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function discardDraftPatch(id: string): boolean {
|
|
261
|
+
// Remove ALL patches with this ID (guards against any duplicates that
|
|
262
|
+
// slipped through addPatch before the ID-dedup was in place).
|
|
263
|
+
const before = draftPatches.length;
|
|
264
|
+
const remaining = draftPatches.filter(p => p.id !== id);
|
|
265
|
+
draftPatches.length = 0;
|
|
266
|
+
draftPatches.push(...remaining);
|
|
267
|
+
return remaining.length < before;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function clearAll(): { staged: number; committed: number; implementing: number; implemented: number } {
|
|
271
|
+
const counts = getCounts();
|
|
272
|
+
draftPatches.length = 0;
|
|
273
|
+
commits.length = 0;
|
|
274
|
+
return counts;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Subscribe to commit events. Returns an unsubscribe function. */
|
|
278
|
+
export function onCommitted(listener: () => void): () => void {
|
|
279
|
+
emitter.on('committed', listener);
|
|
280
|
+
return () => { emitter.off('committed', listener); };
|
|
281
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Shared interface for Tailwind version adapters.
|
|
2
|
+
// Both v3 and v4 adapters implement this contract so the rest of
|
|
3
|
+
// the server code is version-agnostic.
|
|
4
|
+
|
|
5
|
+
export interface TailwindThemeSubset {
|
|
6
|
+
spacing: Record<string, string>;
|
|
7
|
+
colors: Record<string, unknown>;
|
|
8
|
+
fontSize: Record<string, unknown>;
|
|
9
|
+
fontWeight: Record<string, unknown>;
|
|
10
|
+
borderRadius: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TailwindAdapter {
|
|
14
|
+
readonly version: 3 | 4;
|
|
15
|
+
resolveTailwindConfig(): Promise<TailwindThemeSubset>;
|
|
16
|
+
generateCssForClasses(classes: string[]): Promise<string>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Tailwind v3 adapter — uses resolveConfig + PostCSS from the target project's tailwindcss v3.
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync } from "fs";
|
|
4
|
+
import { resolve } from "path";
|
|
5
|
+
import { createRequire } from "module";
|
|
6
|
+
import type { TailwindAdapter, TailwindThemeSubset } from "./tailwind-adapter.js";
|
|
7
|
+
|
|
8
|
+
let cached: TailwindThemeSubset | null = null;
|
|
9
|
+
|
|
10
|
+
const defaultTheme: TailwindThemeSubset = {
|
|
11
|
+
spacing: {
|
|
12
|
+
"0": "0px", px: "1px",
|
|
13
|
+
"0.5": "0.125rem", "1": "0.25rem", "1.5": "0.375rem", "2": "0.5rem",
|
|
14
|
+
"2.5": "0.625rem", "3": "0.75rem", "3.5": "0.875rem", "4": "1rem",
|
|
15
|
+
"5": "1.25rem", "6": "1.5rem", "7": "1.75rem", "8": "2rem",
|
|
16
|
+
"9": "2.25rem", "10": "2.5rem", "11": "2.75rem", "12": "3rem",
|
|
17
|
+
"14": "3.5rem", "16": "4rem", "20": "5rem", "24": "6rem",
|
|
18
|
+
"28": "7rem", "32": "8rem", "36": "9rem", "40": "10rem",
|
|
19
|
+
"44": "11rem", "48": "12rem", "52": "13rem", "56": "14rem",
|
|
20
|
+
"60": "15rem", "64": "16rem", "72": "18rem", "80": "20rem", "96": "24rem",
|
|
21
|
+
},
|
|
22
|
+
colors: {
|
|
23
|
+
inherit: "inherit", current: "currentColor", transparent: "transparent",
|
|
24
|
+
black: "#000", white: "#fff",
|
|
25
|
+
slate: { "50": "#f8fafc", "100": "#f1f5f9", "200": "#e2e8f0", "300": "#cbd5e1", "400": "#94a3b8", "500": "#64748b", "600": "#475569", "700": "#334155", "800": "#1e293b", "900": "#0f172a", "950": "#020617" },
|
|
26
|
+
gray: { "50": "#f9fafb", "100": "#f3f4f6", "200": "#e5e7eb", "300": "#d1d5db", "400": "#9ca3af", "500": "#6b7280", "600": "#4b5563", "700": "#374151", "800": "#1f2937", "900": "#111827", "950": "#030712" },
|
|
27
|
+
zinc: { "50": "#fafafa", "100": "#f4f4f5", "200": "#e4e4e7", "300": "#d4d4d8", "400": "#a1a1aa", "500": "#71717a", "600": "#52525b", "700": "#3f3f46", "800": "#27272a", "900": "#18181b", "950": "#09090b" },
|
|
28
|
+
neutral: { "50": "#fafafa", "100": "#f5f5f5", "200": "#e5e5e5", "300": "#d4d4d4", "400": "#a3a3a3", "500": "#737373", "600": "#525252", "700": "#404040", "800": "#262626", "900": "#171717", "950": "#0a0a0a" },
|
|
29
|
+
stone: { "50": "#fafaf9", "100": "#f5f5f4", "200": "#e7e5e4", "300": "#d6d3d1", "400": "#a8a29e", "500": "#78716c", "600": "#57534e", "700": "#44403c", "800": "#292524", "900": "#1c1917", "950": "#0c0a09" },
|
|
30
|
+
red: { "50": "#fef2f2", "100": "#fee2e2", "200": "#fecaca", "300": "#fca5a5", "400": "#f87171", "500": "#ef4444", "600": "#dc2626", "700": "#b91c1c", "800": "#991b1b", "900": "#7f1d1d", "950": "#450a0a" },
|
|
31
|
+
orange: { "50": "#fff7ed", "100": "#ffedd5", "200": "#fed7aa", "300": "#fdba74", "400": "#fb923c", "500": "#f97316", "600": "#ea580c", "700": "#c2410c", "800": "#9a3412", "900": "#7c2d12", "950": "#431407" },
|
|
32
|
+
amber: { "50": "#fffbeb", "100": "#fef3c7", "200": "#fde68a", "300": "#fcd34d", "400": "#fbbf24", "500": "#f59e0b", "600": "#d97706", "700": "#b45309", "800": "#92400e", "900": "#78350f", "950": "#451a03" },
|
|
33
|
+
yellow: { "50": "#fefce8", "100": "#fef9c3", "200": "#fef08a", "300": "#fde047", "400": "#facc15", "500": "#eab308", "600": "#ca8a04", "700": "#a16207", "800": "#854d0e", "900": "#713f12", "950": "#422006" },
|
|
34
|
+
lime: { "50": "#f7fee7", "100": "#ecfccb", "200": "#d9f99d", "300": "#bef264", "400": "#a3e635", "500": "#84cc16", "600": "#65a30d", "700": "#4d7c0f", "800": "#3f6212", "900": "#365314", "950": "#1a2e05" },
|
|
35
|
+
green: { "50": "#f0fdf4", "100": "#dcfce7", "200": "#bbf7d0", "300": "#86efac", "400": "#4ade80", "500": "#22c55e", "600": "#16a34a", "700": "#15803d", "800": "#166534", "900": "#14532d", "950": "#052e16" },
|
|
36
|
+
emerald: { "50": "#ecfdf5", "100": "#d1fae5", "200": "#a7f3d0", "300": "#6ee7b7", "400": "#34d399", "500": "#10b981", "600": "#059669", "700": "#047857", "800": "#065f46", "900": "#064e3b", "950": "#022c22" },
|
|
37
|
+
teal: { "50": "#f0fdfa", "100": "#ccfbf1", "200": "#99f6e4", "300": "#5eead4", "400": "#2dd4bf", "500": "#14b8a6", "600": "#0d9488", "700": "#0f766e", "800": "#115e59", "900": "#134e4a", "950": "#042f2e" },
|
|
38
|
+
cyan: { "50": "#ecfeff", "100": "#cffafe", "200": "#a5f3fc", "300": "#67e8f9", "400": "#22d3ee", "500": "#06b6d4", "600": "#0891b2", "700": "#0e7490", "800": "#155e75", "900": "#164e63", "950": "#083344" },
|
|
39
|
+
sky: { "50": "#f0f9ff", "100": "#e0f2fe", "200": "#bae6fd", "300": "#7dd3fc", "400": "#38bdf8", "500": "#0ea5e9", "600": "#0284c7", "700": "#0369a1", "800": "#075985", "900": "#0c4a6e", "950": "#082f49" },
|
|
40
|
+
blue: { "50": "#eff6ff", "100": "#dbeafe", "200": "#bfdbfe", "300": "#93c5fd", "400": "#60a5fa", "500": "#3b82f6", "600": "#2563eb", "700": "#1d4ed8", "800": "#1e40af", "900": "#1e3a8a", "950": "#172554" },
|
|
41
|
+
indigo: { "50": "#eef2ff", "100": "#e0e7ff", "200": "#c7d2fe", "300": "#a5b4fc", "400": "#818cf8", "500": "#6366f1", "600": "#4f46e5", "700": "#4338ca", "800": "#3730a3", "900": "#312e81", "950": "#1e1b4b" },
|
|
42
|
+
violet: { "50": "#f5f3ff", "100": "#ede9fe", "200": "#ddd6fe", "300": "#c4b5fd", "400": "#a78bfa", "500": "#8b5cf6", "600": "#7c3aed", "700": "#6d28d9", "800": "#5b21b6", "900": "#4c1d95", "950": "#2e1065" },
|
|
43
|
+
purple: { "50": "#faf5ff", "100": "#f3e8ff", "200": "#e9d5ff", "300": "#d8b4fe", "400": "#c084fc", "500": "#a855f7", "600": "#9333ea", "700": "#7e22ce", "800": "#6b21a8", "900": "#581c87", "950": "#3b0764" },
|
|
44
|
+
fuchsia: { "50": "#fdf4ff", "100": "#fae8ff", "200": "#f5d0fe", "300": "#f0abfc", "400": "#e879f9", "500": "#d946ef", "600": "#c026d3", "700": "#a21caf", "800": "#86198f", "900": "#701a75", "950": "#4a044e" },
|
|
45
|
+
pink: { "50": "#fdf2f8", "100": "#fce7f3", "200": "#fbcfe8", "300": "#f9a8d4", "400": "#f472b6", "500": "#ec4899", "600": "#db2777", "700": "#be185d", "800": "#9d174d", "900": "#831843", "950": "#500724" },
|
|
46
|
+
rose: { "50": "#fff1f2", "100": "#ffe4e6", "200": "#fecdd3", "300": "#fda4af", "400": "#fb7185", "500": "#f43f5e", "600": "#e11d48", "700": "#be123c", "800": "#9f1239", "900": "#881337", "950": "#4c0519" },
|
|
47
|
+
},
|
|
48
|
+
fontSize: {
|
|
49
|
+
xs: ["0.75rem", { lineHeight: "1rem" }],
|
|
50
|
+
sm: ["0.875rem", { lineHeight: "1.25rem" }],
|
|
51
|
+
base: ["1rem", { lineHeight: "1.5rem" }],
|
|
52
|
+
lg: ["1.125rem", { lineHeight: "1.75rem" }],
|
|
53
|
+
xl: ["1.25rem", { lineHeight: "1.75rem" }],
|
|
54
|
+
"2xl": ["1.5rem", { lineHeight: "2rem" }],
|
|
55
|
+
"3xl": ["1.875rem", { lineHeight: "2.25rem" }],
|
|
56
|
+
"4xl": ["2.25rem", { lineHeight: "2.5rem" }],
|
|
57
|
+
"5xl": ["3rem", { lineHeight: "1" }],
|
|
58
|
+
"6xl": ["3.75rem", { lineHeight: "1" }],
|
|
59
|
+
"7xl": ["4.5rem", { lineHeight: "1" }],
|
|
60
|
+
"8xl": ["6rem", { lineHeight: "1" }],
|
|
61
|
+
"9xl": ["8rem", { lineHeight: "1" }],
|
|
62
|
+
},
|
|
63
|
+
fontWeight: {
|
|
64
|
+
thin: "100", extralight: "200", light: "300", normal: "400",
|
|
65
|
+
medium: "500", semibold: "600", bold: "700", extrabold: "800", black: "900",
|
|
66
|
+
},
|
|
67
|
+
borderRadius: {
|
|
68
|
+
none: "0px", sm: "0.125rem", DEFAULT: "0.25rem", md: "0.375rem",
|
|
69
|
+
lg: "0.5rem", xl: "0.75rem", "2xl": "1rem", "3xl": "1.5rem", full: "9999px",
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export class TailwindV3Adapter implements TailwindAdapter {
|
|
74
|
+
readonly version = 3 as const;
|
|
75
|
+
|
|
76
|
+
async resolveTailwindConfig(): Promise<TailwindThemeSubset> {
|
|
77
|
+
if (cached) return cached;
|
|
78
|
+
|
|
79
|
+
const cwd = process.cwd();
|
|
80
|
+
const jsPath = resolve(cwd, "tailwind.config.js");
|
|
81
|
+
const tsPath = resolve(cwd, "tailwind.config.ts");
|
|
82
|
+
const cjsPath = resolve(cwd, "tailwind.config.cjs");
|
|
83
|
+
const configPath = existsSync(jsPath)
|
|
84
|
+
? jsPath
|
|
85
|
+
: existsSync(tsPath)
|
|
86
|
+
? tsPath
|
|
87
|
+
: existsSync(cjsPath)
|
|
88
|
+
? cjsPath
|
|
89
|
+
: null;
|
|
90
|
+
|
|
91
|
+
if (configPath) {
|
|
92
|
+
try {
|
|
93
|
+
const req = createRequire(resolve(cwd, "package.json"));
|
|
94
|
+
// @ts-expect-error — tailwindcss resolveConfig lacks NodeNext-compatible exports
|
|
95
|
+
const resolveConfig = (await import(req.resolve("tailwindcss/resolveConfig"))).default;
|
|
96
|
+
const userConfig = (await import(configPath)).default;
|
|
97
|
+
const full = resolveConfig(userConfig);
|
|
98
|
+
const theme = full.theme ?? {};
|
|
99
|
+
|
|
100
|
+
cached = {
|
|
101
|
+
spacing: (theme.spacing as Record<string, string>) ?? defaultTheme.spacing,
|
|
102
|
+
colors: (theme.colors as Record<string, unknown>) ?? defaultTheme.colors,
|
|
103
|
+
fontSize: (theme.fontSize as Record<string, unknown>) ?? defaultTheme.fontSize,
|
|
104
|
+
fontWeight: (theme.fontWeight as Record<string, unknown>) ?? defaultTheme.fontWeight,
|
|
105
|
+
borderRadius: (theme.borderRadius as Record<string, string>) ?? defaultTheme.borderRadius,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
console.error(`[tailwind] v3 resolved config from ${configPath}`);
|
|
109
|
+
return cached;
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.error("[tailwind] v3 failed to resolve config, using defaults:", err);
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
console.error("[tailwind] v3 no tailwind.config found — using defaults.");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
cached = defaultTheme;
|
|
118
|
+
return cached;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async generateCssForClasses(classes: string[]): Promise<string> {
|
|
122
|
+
const cwd = process.cwd();
|
|
123
|
+
const req = createRequire(resolve(cwd, "package.json"));
|
|
124
|
+
|
|
125
|
+
const jsPath = resolve(cwd, "tailwind.config.js");
|
|
126
|
+
const tsPath = resolve(cwd, "tailwind.config.ts");
|
|
127
|
+
const cjsPath = resolve(cwd, "tailwind.config.cjs");
|
|
128
|
+
const configPath = existsSync(jsPath)
|
|
129
|
+
? jsPath
|
|
130
|
+
: existsSync(tsPath)
|
|
131
|
+
? tsPath
|
|
132
|
+
: existsSync(cjsPath)
|
|
133
|
+
? cjsPath
|
|
134
|
+
: null;
|
|
135
|
+
|
|
136
|
+
let userConfig: object = {};
|
|
137
|
+
if (configPath) {
|
|
138
|
+
try {
|
|
139
|
+
userConfig = (await import(configPath)).default;
|
|
140
|
+
} catch {
|
|
141
|
+
// fall through to empty config
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Load postcss and tailwindcss as a PostCSS plugin from the target project
|
|
146
|
+
const postcss = (await import(req.resolve("postcss"))).default;
|
|
147
|
+
const tailwindPlugin = (await import(req.resolve("tailwindcss"))).default;
|
|
148
|
+
|
|
149
|
+
const result = await postcss([
|
|
150
|
+
tailwindPlugin({
|
|
151
|
+
...userConfig,
|
|
152
|
+
content: [], // skip file scanning
|
|
153
|
+
safelist: classes, // generate only the requested classes
|
|
154
|
+
}),
|
|
155
|
+
]).process("@tailwind utilities;", { from: undefined });
|
|
156
|
+
|
|
157
|
+
return result.css;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Tailwind v4 adapter — uses compile() / build() from the target project's tailwindcss v4.
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from "fs";
|
|
4
|
+
import { resolve, dirname } from "path";
|
|
5
|
+
import { createRequire } from "module";
|
|
6
|
+
import type { TailwindAdapter, TailwindThemeSubset } from "./tailwind-adapter.js";
|
|
7
|
+
|
|
8
|
+
// Cached compiler instance (from target project's tailwindcss)
|
|
9
|
+
let compilerCache: { build: (classes: string[]) => string } | null = null;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get a Tailwind v4 compile() function from the target project's node_modules.
|
|
13
|
+
*/
|
|
14
|
+
async function getCompile(): Promise<(css: string, opts: any) => Promise<{ build: (classes: string[]) => string }>> {
|
|
15
|
+
const cwd = process.cwd();
|
|
16
|
+
const req = createRequire(resolve(cwd, "package.json"));
|
|
17
|
+
const tw = await import(req.resolve("tailwindcss"));
|
|
18
|
+
// Handle CJS/ESM interop: compile may be on tw directly or on tw.default
|
|
19
|
+
const mod = tw.default ?? tw;
|
|
20
|
+
const compile = mod.compile ?? mod.default?.compile;
|
|
21
|
+
if (typeof compile !== "function") {
|
|
22
|
+
throw new Error("Could not find compile() in target project's tailwindcss");
|
|
23
|
+
}
|
|
24
|
+
return compile;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* loadStylesheet callback for Tailwind v4 compile().
|
|
29
|
+
* Resolves @import "tailwindcss" and other stylesheet imports from the target project.
|
|
30
|
+
*/
|
|
31
|
+
function makeLoadStylesheet(cwd: string) {
|
|
32
|
+
const req = createRequire(resolve(cwd, "package.json"));
|
|
33
|
+
return async (id: string, base: string) => {
|
|
34
|
+
let resolved: string;
|
|
35
|
+
if (id === "tailwindcss") {
|
|
36
|
+
resolved = req.resolve("tailwindcss/index.css");
|
|
37
|
+
} else {
|
|
38
|
+
resolved = req.resolve(id, { paths: [base || cwd] });
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
content: readFileSync(resolved, "utf8"),
|
|
42
|
+
base: dirname(resolved),
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Initialize the Tailwind v4 compiler for the target project.
|
|
49
|
+
*/
|
|
50
|
+
async function getCompiler(): Promise<{ build: (classes: string[]) => string }> {
|
|
51
|
+
if (compilerCache) return compilerCache;
|
|
52
|
+
|
|
53
|
+
const cwd = process.cwd();
|
|
54
|
+
const compile = await getCompile();
|
|
55
|
+
|
|
56
|
+
const result = await compile('@import "tailwindcss";', {
|
|
57
|
+
loadStylesheet: makeLoadStylesheet(cwd),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
compilerCache = result;
|
|
61
|
+
console.error("[tailwind] Initialized Tailwind v4 compiler from target project");
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Classes we probe to extract theme values
|
|
66
|
+
const HUES = [
|
|
67
|
+
"slate", "gray", "zinc", "neutral", "stone",
|
|
68
|
+
"red", "orange", "amber", "yellow", "lime",
|
|
69
|
+
"green", "emerald", "teal", "cyan", "sky",
|
|
70
|
+
"blue", "indigo", "violet", "purple", "fuchsia",
|
|
71
|
+
"pink", "rose",
|
|
72
|
+
];
|
|
73
|
+
const SHADES = ["50", "100", "200", "300", "400", "500", "600", "700", "800", "900", "950"];
|
|
74
|
+
const SPECIAL_COLORS = ["black", "white", "transparent"];
|
|
75
|
+
|
|
76
|
+
const SPACING_KEYS = [
|
|
77
|
+
"0", "px", "0.5", "1", "1.5", "2", "2.5", "3", "3.5", "4", "5", "6", "7",
|
|
78
|
+
"8", "9", "10", "11", "12", "14", "16", "20", "24", "28", "32", "36", "40",
|
|
79
|
+
"44", "48", "52", "56", "60", "64", "72", "80", "96",
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
const FONT_SIZE_KEYS = ["xs", "sm", "base", "lg", "xl", "2xl", "3xl", "4xl", "5xl", "6xl", "7xl", "8xl", "9xl"];
|
|
83
|
+
const FONT_WEIGHT_KEYS = ["thin", "extralight", "light", "normal", "medium", "semibold", "bold", "extrabold", "black"];
|
|
84
|
+
const BORDER_RADIUS_KEYS = ["none", "sm", "", "md", "lg", "xl", "2xl", "3xl", "full"];
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Extract --var definitions from compiled CSS output.
|
|
88
|
+
*/
|
|
89
|
+
function extractVars(css: string, prefix: string): Map<string, string> {
|
|
90
|
+
const vars = new Map<string, string>();
|
|
91
|
+
const regex = new RegExp(`^\\s*--${prefix}-([\\w.-]+):\\s*([^;]+);`, "gm");
|
|
92
|
+
let match;
|
|
93
|
+
while ((match = regex.exec(css)) !== null) {
|
|
94
|
+
vars.set(match[1], match[2].trim());
|
|
95
|
+
}
|
|
96
|
+
return vars;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export class TailwindV4Adapter implements TailwindAdapter {
|
|
100
|
+
readonly version = 4 as const;
|
|
101
|
+
|
|
102
|
+
async resolveTailwindConfig(): Promise<TailwindThemeSubset> {
|
|
103
|
+
const compiler = await getCompiler();
|
|
104
|
+
|
|
105
|
+
// Probe color classes to extract theme variable definitions
|
|
106
|
+
const probeClasses: string[] = [];
|
|
107
|
+
for (const h of HUES) {
|
|
108
|
+
for (const s of SHADES) probeClasses.push(`bg-${h}-${s}`);
|
|
109
|
+
}
|
|
110
|
+
for (const s of SPECIAL_COLORS) probeClasses.push(`bg-${s}`);
|
|
111
|
+
|
|
112
|
+
const css = compiler.build(probeClasses);
|
|
113
|
+
|
|
114
|
+
// --- Colors (extracted from CSS custom properties) ---
|
|
115
|
+
const colorVars = extractVars(css, "color");
|
|
116
|
+
const colors: Record<string, unknown> = {};
|
|
117
|
+
for (const [name, value] of colorVars) {
|
|
118
|
+
const dashIdx = name.lastIndexOf("-");
|
|
119
|
+
if (dashIdx > 0) {
|
|
120
|
+
const hue = name.substring(0, dashIdx);
|
|
121
|
+
const shade = name.substring(dashIdx + 1);
|
|
122
|
+
if (/^\d+$/.test(shade)) {
|
|
123
|
+
if (!colors[hue]) colors[hue] = {};
|
|
124
|
+
(colors[hue] as Record<string, string>)[shade] = value;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
colors[name] = value;
|
|
129
|
+
}
|
|
130
|
+
if (!colors["transparent"]) colors["transparent"] = "transparent";
|
|
131
|
+
|
|
132
|
+
// --- Spacing (v4 uses calc(var(--spacing) * N)) ---
|
|
133
|
+
const spacing: Record<string, string> = {};
|
|
134
|
+
for (const k of SPACING_KEYS) {
|
|
135
|
+
spacing[k] = k === "px" ? "1px" : k === "0" ? "0px" : `calc(var(--spacing) * ${k})`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// --- Font size, weight, border radius (static scales in v4) ---
|
|
139
|
+
const fontSize: Record<string, unknown> = {};
|
|
140
|
+
for (const k of FONT_SIZE_KEYS) fontSize[k] = k;
|
|
141
|
+
|
|
142
|
+
const fontWeight: Record<string, unknown> = {};
|
|
143
|
+
for (const k of FONT_WEIGHT_KEYS) fontWeight[k] = k;
|
|
144
|
+
|
|
145
|
+
const borderRadius: Record<string, string> = {};
|
|
146
|
+
for (const k of BORDER_RADIUS_KEYS) borderRadius[k || "DEFAULT"] = k || "DEFAULT";
|
|
147
|
+
|
|
148
|
+
const result: TailwindThemeSubset = { spacing, colors, fontSize, fontWeight, borderRadius };
|
|
149
|
+
console.error("[tailwind] v4 resolved theme:", {
|
|
150
|
+
colors: Object.keys(colors).length + " entries",
|
|
151
|
+
spacing: Object.keys(spacing).length + " entries",
|
|
152
|
+
});
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async generateCssForClasses(classes: string[]): Promise<string> {
|
|
157
|
+
const compiler = await getCompiler();
|
|
158
|
+
return compiler.build(classes);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Tailwind adapter factory — auto-detects v3 vs v4 from the target project's
|
|
2
|
+
// installed tailwindcss version and delegates to the appropriate adapter.
|
|
3
|
+
|
|
4
|
+
import { readFileSync } from "fs";
|
|
5
|
+
import { resolve } from "path";
|
|
6
|
+
import { createRequire } from "module";
|
|
7
|
+
import type { TailwindAdapter, TailwindThemeSubset } from "./tailwind-adapter.js";
|
|
8
|
+
|
|
9
|
+
export type { TailwindThemeSubset };
|
|
10
|
+
|
|
11
|
+
let adapterCache: TailwindAdapter | null = null;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Detect which major version of tailwindcss is installed in the target project.
|
|
15
|
+
*/
|
|
16
|
+
function detectTailwindVersion(): 3 | 4 {
|
|
17
|
+
const cwd = process.cwd();
|
|
18
|
+
const req = createRequire(resolve(cwd, "package.json"));
|
|
19
|
+
const pkgPath = req.resolve("tailwindcss/package.json");
|
|
20
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
21
|
+
const major = parseInt(pkg.version.split(".")[0], 10);
|
|
22
|
+
return major >= 4 ? 4 : 3;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function getAdapter(): Promise<TailwindAdapter> {
|
|
26
|
+
if (adapterCache) return adapterCache;
|
|
27
|
+
const version = detectTailwindVersion();
|
|
28
|
+
if (version === 3) {
|
|
29
|
+
const { TailwindV3Adapter } = await import("./tailwind-v3.js");
|
|
30
|
+
adapterCache = new TailwindV3Adapter();
|
|
31
|
+
} else {
|
|
32
|
+
const { TailwindV4Adapter } = await import("./tailwind-v4.js");
|
|
33
|
+
adapterCache = new TailwindV4Adapter();
|
|
34
|
+
}
|
|
35
|
+
console.error(`[tailwind] Using Tailwind v${version} adapter`);
|
|
36
|
+
return adapterCache;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Expose the detected version for the /api/info endpoint. */
|
|
40
|
+
export async function getTailwindVersion(): Promise<3 | 4> {
|
|
41
|
+
return (await getAdapter()).version;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function resolveTailwindConfig(): Promise<TailwindThemeSubset> {
|
|
45
|
+
return (await getAdapter()).resolveTailwindConfig();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function generateCssForClasses(classes: string[]): Promise<string> {
|
|
49
|
+
return (await getAdapter()).generateCssForClasses(classes);
|
|
50
|
+
}
|