@frontfriend/tailwind 3.0.3 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +94 -1
  2. package/dist/browser.mjs +2 -0
  3. package/dist/browser.mjs.map +7 -0
  4. package/dist/cli.js +63 -41
  5. package/dist/components-config-schema.d.ts +2 -0
  6. package/dist/components-config-schema.js +2 -0
  7. package/dist/components-config-schema.js.map +7 -0
  8. package/dist/ffdc.d.ts +3 -1
  9. package/dist/ffdc.js +1 -1
  10. package/dist/ffdc.js.map +4 -4
  11. package/dist/index.js +3 -3
  12. package/dist/index.js.map +4 -4
  13. package/dist/index.mjs +1 -1
  14. package/dist/index.mjs.map +3 -3
  15. package/dist/lib/core/api-client.js +2 -1
  16. package/dist/lib/core/api-client.js.map +4 -4
  17. package/dist/lib/core/cache-manager.js +11 -2
  18. package/dist/lib/core/component-downloader.js +3 -2
  19. package/dist/lib/core/component-downloader.js.map +4 -4
  20. package/dist/lib/core/components-config-schema.js +2 -0
  21. package/dist/lib/core/components-config-schema.js.map +7 -0
  22. package/dist/lib/core/constants.js +1 -1
  23. package/dist/lib/core/constants.js.map +2 -2
  24. package/dist/lib/core/credentials.js +3 -0
  25. package/dist/lib/core/credentials.js.map +7 -0
  26. package/dist/lib/core/default-config-data.js +2 -0
  27. package/dist/lib/core/default-config-data.js.map +7 -0
  28. package/dist/lib/core/default-config.js +2 -0
  29. package/dist/lib/core/default-config.js.map +7 -0
  30. package/dist/lib/core/env-utils.js +1 -1
  31. package/dist/lib/core/env-utils.js.map +3 -3
  32. package/dist/lib/core/file-utils.js +1 -1
  33. package/dist/lib/core/file-utils.js.map +3 -3
  34. package/dist/lib/core/local-token-reader.js +1 -1
  35. package/dist/lib/core/local-token-reader.js.map +3 -3
  36. package/dist/lib/core/path-utils.js +1 -1
  37. package/dist/lib/core/path-utils.js.map +3 -3
  38. package/dist/lib/core/token-processor.js +3 -1
  39. package/dist/lib/core/token-processor.js.map +3 -3
  40. package/dist/next.js +1 -1
  41. package/dist/next.js.map +4 -4
  42. package/dist/types/index.d.ts +107 -11
  43. package/dist/vite.js +8 -8
  44. package/dist/vite.js.map +4 -4
  45. package/dist/vite.mjs +1 -1
  46. package/dist/vite.mjs.map +3 -3
  47. package/package.json +15 -5
  48. package/scripts/build.js +21 -4
  49. package/scripts/update-default-config-data.js +589 -0
  50. package/src/theme.css +13 -0
package/scripts/build.js CHANGED
@@ -2,6 +2,9 @@ const esbuild = require('esbuild');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
  const { glob } = require('glob');
5
+ const { writeDefaultConfigData } = require('./update-default-config-data');
6
+
7
+ writeDefaultConfigData();
5
8
 
6
9
  // Clean dist directory
7
10
  const distDir = path.join(__dirname, '..', 'dist');
@@ -72,7 +75,14 @@ export default function frontfriend(): Plugin;`
72
75
  * @param config - Design configuration object
73
76
  * @returns Processed configuration
74
77
  */
75
- export function ffdc(config: Record<string, any> | null | undefined): Record<string, any>;`
78
+ export function ffdc(config: Record<string, any> | null | undefined): Record<string, any>;
79
+ export const componentsConfigSchema: Record<string, any>;
80
+ export function isCSSClassString(value: string): boolean;`
81
+ },
82
+ {
83
+ path: 'components-config-schema.d.ts',
84
+ content: `export const componentsConfigSchema: Record<string, any>;
85
+ export function isCSSClassString(value: string): boolean;`
76
86
  },
77
87
  {
78
88
  path: 'lib/react/utils.d.ts',
@@ -149,11 +159,15 @@ const filesToBundle = [
149
159
  { input: 'lib/core/token-processor.js', output: 'lib/core/token-processor.js' },
150
160
  { input: 'lib/core/errors.js', output: 'lib/core/errors.js' },
151
161
  { input: 'lib/core/constants.js', output: 'lib/core/constants.js' },
162
+ { input: 'lib/core/credentials.js', output: 'lib/core/credentials.js' },
152
163
  { input: 'lib/core/component-downloader.js', output: 'lib/core/component-downloader.js' },
153
164
  { input: 'lib/core/local-token-reader.js', output: 'lib/core/local-token-reader.js' },
154
165
  { input: 'lib/core/file-utils.js', output: 'lib/core/file-utils.js' },
155
166
  { input: 'lib/core/path-utils.js', output: 'lib/core/path-utils.js' },
156
167
  { input: 'lib/core/env-utils.js', output: 'lib/core/env-utils.js' },
168
+ { input: 'lib/core/components-config-schema.js', output: 'lib/core/components-config-schema.js' },
169
+ { input: 'lib/core/default-config-data.js', output: 'lib/core/default-config-data.js' },
170
+ { input: 'lib/core/default-config.js', output: 'lib/core/default-config.js' },
157
171
 
158
172
  // React/Vue utilities
159
173
  { input: 'lib/react/utils.mjs', output: 'lib/react/utils.mjs', format: 'esm' },
@@ -161,11 +175,13 @@ const filesToBundle = [
161
175
 
162
176
  // Main files
163
177
  { input: 'ffdc.js', output: 'ffdc.js' },
178
+ { input: 'lib/core/components-config-schema.js', output: 'components-config-schema.js' },
164
179
  { input: 'runtime.js', output: 'runtime.js' },
165
180
  // CLI will be handled separately to preserve shebang
166
181
  { input: 'next.js', output: 'next.js' },
167
182
  { input: 'vite.js', output: 'vite.js' },
168
183
  { input: 'vite.mjs', output: 'vite.mjs', format: 'esm' },
184
+ { input: 'browser.mjs', output: 'browser.mjs', format: 'esm', platform: 'browser' },
169
185
  ];
170
186
 
171
187
  // Files to copy without bundling (dynamic loaders)
@@ -190,7 +206,7 @@ async function build() {
190
206
  minify: true,
191
207
  sourcemap: true,
192
208
  format: file.format || 'cjs',
193
- platform: 'node',
209
+ platform: file.platform || 'node',
194
210
  target: 'node14',
195
211
  outfile: path.join(distDir, file.output),
196
212
  external: [
@@ -268,7 +284,8 @@ async function build() {
268
284
  './lib/core/errors',
269
285
  './lib/core/env-utils',
270
286
  './lib/core/file-utils',
271
- './lib/core/path-utils'
287
+ './lib/core/path-utils',
288
+ './lib/core/default-config'
272
289
  ],
273
290
  });
274
291
 
@@ -355,4 +372,4 @@ async function build() {
355
372
  build().catch(error => {
356
373
  console.error('Build failed:', error);
357
374
  process.exit(1);
358
- });
375
+ });
@@ -0,0 +1,589 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ const packageRoot = path.join(__dirname, '..');
6
+ const repoRoot = path.join(packageRoot, '..', '..');
7
+ const sourcePath = path.join(repoRoot, 'apps', 'saas', 'components', 'code-editor', 'setup', 'components-config.json');
8
+ const outputPath = path.join(packageRoot, 'lib', 'core', 'default-config-data.js');
9
+ const SYSTEM_WORKSPACE_SLUG = 'system-templates';
10
+ const MASTER_DS_NAME = 'Master Design System';
11
+
12
+ const REGISTRY_ALIASES = {
13
+ alertDialog: ['alertdialog'],
14
+ avatarGroup: ['avatargroup'],
15
+ checkboxField: ['checkboxfield'],
16
+ datePicker: ['datepicker'],
17
+ dateInput: ['dateinput'],
18
+ dropdown: ['dropdownmenu'],
19
+ fileuploader: ['fileupload'],
20
+ multiSelect: ['multiselect'],
21
+ pdfViewer: ['pdfviewer'],
22
+ radiogroupeField: ['radiogroupfield'],
23
+ timePicker: ['timepicker'],
24
+ inputOTP: ['inputotp'],
25
+ };
26
+
27
+ const DERIVED_FROM_MASTER = {
28
+ phoneInput: 'input',
29
+ phoneinput: 'input',
30
+ inputNumber: 'input',
31
+ inputnumber: 'input',
32
+ dateSlider: 'calendar',
33
+ dateslider: 'calendar',
34
+ tabBar: 'actionbar',
35
+ tabbar: 'actionbar',
36
+ };
37
+
38
+ const SUPPLEMENTAL_CONFIG = {
39
+ multiSelect: {
40
+ createNew: 'flex flex-col gap-2 items-center',
41
+ createNewButton: 'w-fit',
42
+ error: 'px-3 py-1.5 text-destructive text-xs',
43
+ empty: 'px-2 py-1.5 text-xs text-muted-foreground',
44
+ },
45
+ multiselect: {
46
+ createNew: 'flex flex-col gap-2 items-center',
47
+ createNewButton: 'w-fit',
48
+ error: 'px-3 py-1.5 text-destructive text-xs',
49
+ empty: 'px-2 py-1.5 text-xs text-muted-foreground',
50
+ },
51
+ phoneInput: {
52
+ wrapper: 'flex gap-4',
53
+ selectorWrapper: 'flex items-center',
54
+ nativeSelect: 'base-select',
55
+ trigger: { layout: 'flex items-center' },
56
+ dialCode: 'ml-2',
57
+ triggerIcon: 'ml-2 h-4 w-4',
58
+ flagIcon: '-mr-2 h-5 w-5',
59
+ content: 'w-[300px] p-0',
60
+ item: 'gap-2',
61
+ itemLabel: 'flex-1 text-sm',
62
+ input: 'w-full',
63
+ flag: 'bg-muted flex h-4 w-6 overflow-hidden rounded-sm',
64
+ },
65
+ phoneinput: {
66
+ wrapper: 'flex gap-4',
67
+ selectorWrapper: 'flex items-center',
68
+ nativeSelect: 'base-select',
69
+ trigger: { layout: 'flex items-center' },
70
+ dialCode: 'ml-2',
71
+ triggerIcon: 'ml-2 h-4 w-4',
72
+ flagIcon: '-mr-2 h-5 w-5',
73
+ content: 'w-[300px] p-0',
74
+ item: 'gap-2',
75
+ itemLabel: 'flex-1 text-sm',
76
+ input: 'w-full',
77
+ flag: 'bg-muted flex h-4 w-6 overflow-hidden rounded-sm',
78
+ },
79
+ spinner: {
80
+ root: 'animate-spin',
81
+ track: 'opacity-20',
82
+ },
83
+ tabs: {
84
+ list: { solidRoot: 'relative' },
85
+ },
86
+ table: {
87
+ container: 'w-full',
88
+ },
89
+ sheet: {
90
+ srOnly: 'sr-only',
91
+ },
92
+ themepicker: {
93
+ icon: {
94
+ light: 'h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90',
95
+ dark: 'absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0',
96
+ },
97
+ srOnly: 'sr-only',
98
+ },
99
+ themeprovider: {
100
+ themePicker: { srOnly: 'sr-only' },
101
+ },
102
+ toast: {
103
+ toaster: 'toaster group',
104
+ },
105
+ textarea: {
106
+ noResize: 'resize-none',
107
+ },
108
+ input: {
109
+ textCenter: 'text-center',
110
+ },
111
+ inputNumber: {
112
+ textCenter: 'text-center',
113
+ },
114
+ inputnumber: {
115
+ textCenter: 'text-center',
116
+ },
117
+ datePicker: {
118
+ selectContent: 'z-50',
119
+ fullWidth: 'w-full',
120
+ },
121
+ datepicker: {
122
+ selectContent: 'z-50',
123
+ fullWidth: 'w-full',
124
+ },
125
+ fileuploader: {
126
+ cursor: 'cursor-pointer',
127
+ borderless: 'border-0',
128
+ },
129
+ fileupload: {
130
+ cursor: 'cursor-pointer',
131
+ borderless: 'border-0',
132
+ },
133
+ sidebar: {
134
+ srOnly: 'sr-only',
135
+ root: { foreground: 'text-sidebar-foreground' },
136
+ },
137
+ segmented: {
138
+ trigger: 'tab-trigger',
139
+ activeTrigger: 'tab-trigger-active',
140
+ disabled: 'pointer-events-none opacity-50',
141
+ },
142
+ calendar: {
143
+ customRoot: 'p-3 rounded-md font-primary',
144
+ header: 'flex justify-center pt-1 relative items-center',
145
+ content: 'grid grid-cols-3 gap-2',
146
+ options: 'cursor-pointer',
147
+ selectGroup: 'flex items-center gap-2',
148
+ nativeSelect: 'h-8 rounded-md border border-input bg-background px-2 text-sm',
149
+ range: {
150
+ root: 'p-3 rounded-md font-primary',
151
+ container: 'flex flex-col gap-4 sm:flex-row',
152
+ row: 'flex w-full mt-2',
153
+ navSpacer: 'h-7 w-7',
154
+ },
155
+ },
156
+ dateSlider: {
157
+ customRootSlider: 'w-full',
158
+ header: 'flex justify-center pt-1 relative items-center',
159
+ cell: 'text-center text-sm p-0 relative',
160
+ selectContent: 'z-50',
161
+ buttons: { props: { variant: 'ghost' } },
162
+ dialog: {
163
+ false: 'hidden',
164
+ content: 'grid gap-4',
165
+ body: 'grid gap-4',
166
+ range: 'grid gap-4',
167
+ },
168
+ },
169
+ dateslider: {
170
+ customRootSlider: 'w-full',
171
+ header: 'flex justify-center pt-1 relative items-center',
172
+ cell: 'text-center text-sm p-0 relative',
173
+ selectContent: 'z-50',
174
+ buttons: { props: { variant: 'ghost' } },
175
+ dialog: {
176
+ false: 'hidden',
177
+ content: 'grid gap-4',
178
+ body: 'grid gap-4',
179
+ range: 'grid gap-4',
180
+ },
181
+ },
182
+ };
183
+
184
+ const MINIMAL_V4_FALLBACKS = {
185
+ avatarGroup: {
186
+ root: 'flex flex-row relative h-10 w-full font-primary',
187
+ avatar: 'relative -ml-2 first:ml-0',
188
+ },
189
+ avatargroup: {
190
+ root: 'flex flex-row relative h-10 w-full font-primary',
191
+ avatar: 'relative -ml-2 first:ml-0',
192
+ },
193
+ timePicker: {
194
+ trigger: 'w-full',
195
+ input: 'w-full',
196
+ wrapper: 'grid gap-4',
197
+ popover: {
198
+ content: 'w-auto p-4',
199
+ },
200
+ select: {
201
+ root: 'grid gap-2',
202
+ label: 'text-sm font-medium text-foreground',
203
+ container: 'flex flex-col gap-2',
204
+ content: 'max-h-48 overflow-y-auto',
205
+ },
206
+ },
207
+ timepicker: {
208
+ trigger: 'w-full',
209
+ input: 'w-full',
210
+ wrapper: 'grid gap-4',
211
+ popover: {
212
+ content: 'w-auto p-4',
213
+ },
214
+ select: {
215
+ root: 'grid gap-2',
216
+ label: 'text-sm font-medium text-foreground',
217
+ container: 'flex flex-col gap-2',
218
+ content: 'max-h-48 overflow-y-auto',
219
+ },
220
+ },
221
+ collapsible: {
222
+ root: 'w-full',
223
+ trigger: 'inline-flex items-center justify-between gap-2',
224
+ content: 'overflow-hidden',
225
+ },
226
+ icon: { root: 'shrink-0' },
227
+ resizable: {
228
+ root: 'flex',
229
+ panel: 'relative',
230
+ handle: 'bg-border focus-visible:ring-ring relative flex w-px items-center justify-center',
231
+ },
232
+ stepper: {
233
+ root: 'flex gap-2',
234
+ item: 'flex items-center gap-2',
235
+ trigger: 'inline-flex items-center gap-2',
236
+ indicator: 'flex size-8 items-center justify-center rounded-full border border-border',
237
+ separator: 'bg-border h-px flex-1',
238
+ title: 'font-medium text-foreground',
239
+ description: 'text-muted-foreground text-sm',
240
+ },
241
+ themeprovider: { root: 'contents' },
242
+ themepicker: {
243
+ root: 'inline-flex items-center gap-2 rounded-md border border-border bg-background p-1',
244
+ },
245
+ toggle: {
246
+ root: 'inline-flex items-center justify-center rounded-md text-sm font-medium hover:bg-accent hover:text-accent-foreground',
247
+ variant: {
248
+ default: 'bg-transparent',
249
+ outline: 'border border-input bg-transparent',
250
+ },
251
+ size: {
252
+ default: 'h-9 px-3',
253
+ sm: 'h-8 px-2',
254
+ lg: 'h-10 px-4',
255
+ },
256
+ },
257
+ };
258
+
259
+ const DIRECT_CLASS_MAP = {
260
+ 'border-transperent': 'border-transparent',
261
+ 'hover:interactive-brand-mid-hover': 'hover:text-primary/90',
262
+
263
+ 'bg-white/10': 'bg-background',
264
+ 'bg-transparent': 'bg-transparent',
265
+ 'border-transparent': 'border-transparent',
266
+
267
+ 'bg-overlay': 'bg-black/80',
268
+ 'bg-overlay-mid': 'bg-black/80',
269
+ 'bg-overlay-strong': 'bg-black/80',
270
+
271
+ 'bg-layer-control': 'bg-background',
272
+ 'bg-layer-below': 'bg-background',
273
+ 'bg-layer-raised': 'bg-card',
274
+ 'bg-layer-surface': 'bg-card',
275
+ 'bg-layer-dialog': 'bg-background',
276
+ 'bg-layer-popover': 'bg-popover',
277
+
278
+ 'bg-disabled': 'opacity-50',
279
+ 'text-disabled': 'opacity-50',
280
+ 'fill-disabled': 'opacity-50',
281
+ 'border-disabled': 'opacity-50',
282
+ };
283
+
284
+ const COLOR_FAMILIES = new Set([
285
+ 'brand',
286
+ 'interactive',
287
+ 'neutral',
288
+ 'negative',
289
+ 'positive',
290
+ 'warning',
291
+ 'info',
292
+ 'highlight',
293
+ 'inverse',
294
+ ]);
295
+
296
+ const ON_COLOR_FAMILIES = new Set([
297
+ 'onbrand',
298
+ 'oninteractive',
299
+ 'onnegative',
300
+ 'onpositive',
301
+ 'onwarning',
302
+ 'oninfo',
303
+ 'onhighlight',
304
+ ]);
305
+
306
+ const FORBIDDEN_DEFAULT_CLASS_PATTERNS = [
307
+ /(?:^|:)bg-brand(?:-|$)/,
308
+ /(?:^|:)text-brand(?:-|$)/,
309
+ /(?:^|:)text-onbrand(?:-|$)/,
310
+ /(?:^|:)(?:text|fill)-icon-(?:brand|onbrand)(?:-|$)/,
311
+ /(?:^|:)border-brand(?:-|$)/,
312
+ /(?:^|:)bg-layer(?:-|$)/,
313
+ /(?:^|:)bg-disabled$/,
314
+ /(?:^|:)text-disabled$/,
315
+ /(?:^|:)fill-disabled$/,
316
+ /(?:^|:)border-disabled$/,
317
+ /transperent/,
318
+ ];
319
+
320
+ function clone(value) {
321
+ return JSON.parse(JSON.stringify(value));
322
+ }
323
+
324
+ function isPlainObject(value) {
325
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
326
+ }
327
+
328
+ function deepMerge(base, override) {
329
+ if (!isPlainObject(base) || !isPlainObject(override)) {
330
+ return clone(override);
331
+ }
332
+
333
+ const merged = clone(base);
334
+ for (const [key, value] of Object.entries(override)) {
335
+ merged[key] = key in merged ? deepMerge(merged[key], value) : clone(value);
336
+ }
337
+ return merged;
338
+ }
339
+
340
+ function splitVariants(token) {
341
+ let depth = 0;
342
+ let lastColon = -1;
343
+ for (let index = 0; index < token.length; index += 1) {
344
+ const char = token[index];
345
+ if (char === '[') depth += 1;
346
+ if (char === ']') depth = Math.max(0, depth - 1);
347
+ if (char === ':' && depth === 0) lastColon = index;
348
+ }
349
+
350
+ if (lastColon === -1) {
351
+ return { prefix: '', base: token };
352
+ }
353
+ return {
354
+ prefix: token.slice(0, lastColon + 1),
355
+ base: token.slice(lastColon + 1),
356
+ };
357
+ }
358
+
359
+ function withVariantTone(prefix, baseClass) {
360
+ const variants = prefix.split(':').filter(Boolean);
361
+ const isActiveLike = variants.some((variant) =>
362
+ variant === 'hover' ||
363
+ variant === 'active' ||
364
+ variant.includes('hover') ||
365
+ variant.includes('active') ||
366
+ variant.includes('selected') ||
367
+ variant.includes('checked') ||
368
+ variant.includes('highlighted') ||
369
+ variant.includes('expanded')
370
+ );
371
+ const isDisabled = variants.some((variant) => variant.includes('disabled'));
372
+
373
+ if (isDisabled) return baseClass;
374
+ if (!isActiveLike) return baseClass;
375
+ if (baseClass === 'bg-primary') return 'bg-primary/90';
376
+ if (baseClass === 'bg-secondary') return 'bg-secondary/80';
377
+ if (baseClass === 'bg-muted') return 'bg-muted/80';
378
+ if (baseClass === 'bg-accent') return 'bg-accent/80';
379
+ if (baseClass === 'bg-destructive') return 'bg-destructive/90';
380
+ if (baseClass === 'text-primary') return 'text-primary/90';
381
+ if (baseClass === 'border-primary') return 'border-primary/80';
382
+ return baseClass;
383
+ }
384
+
385
+ function mapBgClass(base, prefix) {
386
+ const match = base.match(/^!?bg-([a-z]+)(?:-[a-z]+)*(?:-(hover|active|selected|disabled))?$/);
387
+ if (!match) return null;
388
+ const important = base.startsWith('!') ? '!' : '';
389
+ const family = match[1];
390
+ if (!COLOR_FAMILIES.has(family)) return null;
391
+
392
+ let mapped;
393
+ if (family === 'brand' || family === 'interactive') mapped = 'bg-primary';
394
+ else if (family === 'neutral') mapped = base.includes('-low') || base.includes('-subtle') ? 'bg-muted' : 'bg-accent';
395
+ else if (family === 'negative') mapped = 'bg-destructive';
396
+ else mapped = 'bg-primary';
397
+
398
+ return important + withVariantTone(prefix, mapped);
399
+ }
400
+
401
+ function mapTextClass(base, prefix) {
402
+ const match = base.match(/^!?(?:text|fill)-(?:(icon)-)?([a-z]+)(?:-[a-z]+)*(?:-(hover|active|selected|disabled))?$/);
403
+ if (!match) return null;
404
+ const important = base.startsWith('!') ? '!' : '';
405
+ const family = match[2];
406
+ if (!COLOR_FAMILIES.has(family) && !ON_COLOR_FAMILIES.has(family)) return null;
407
+
408
+ let mapped;
409
+ if (family === 'brand' || family === 'interactive') mapped = 'text-primary';
410
+ else if (family === 'neutral' || family === 'inverse') mapped = base.includes('-mid') || base.includes('-strong') ? 'text-foreground' : 'text-muted-foreground';
411
+ else if (family === 'negative') mapped = 'text-destructive';
412
+ else if (ON_COLOR_FAMILIES.has(family)) mapped = family === 'onnegative' ? 'text-destructive-foreground' : 'text-primary-foreground';
413
+ else mapped = 'text-primary';
414
+
415
+ return important + withVariantTone(prefix, mapped);
416
+ }
417
+
418
+ function mapBorderClass(base, prefix) {
419
+ const match = base.match(/^!?border-([a-z]+)(?:-[a-z]+)*(?:-(hover|active|selected|disabled))?$/);
420
+ if (!match) return null;
421
+ const important = base.startsWith('!') ? '!' : '';
422
+ const family = match[1];
423
+ if (!COLOR_FAMILIES.has(family)) return null;
424
+
425
+ let mapped;
426
+ if (family === 'brand' || family === 'interactive') mapped = 'border-primary';
427
+ else if (family === 'negative') mapped = 'border-destructive';
428
+ else mapped = 'border-border';
429
+ return important + withVariantTone(prefix, mapped);
430
+ }
431
+
432
+ function neutralizeClassToken(token) {
433
+ if (!token) return token;
434
+ if (DIRECT_CLASS_MAP[token]) return DIRECT_CLASS_MAP[token];
435
+
436
+ const { prefix, base } = splitVariants(token);
437
+ const mappedBase = DIRECT_CLASS_MAP[base]
438
+ || mapBgClass(base, prefix)
439
+ || mapTextClass(base, prefix)
440
+ || mapBorderClass(base, prefix)
441
+ || base;
442
+
443
+ return `${prefix}${mappedBase}`;
444
+ }
445
+
446
+ function neutralizeClassString(value) {
447
+ return value
448
+ .split(/\s+/)
449
+ .filter(Boolean)
450
+ .map(neutralizeClassToken)
451
+ .join(' ');
452
+ }
453
+
454
+ function neutralizeComponentsConfig(value) {
455
+ if (typeof value === 'string') return neutralizeClassString(value);
456
+ if (Array.isArray(value)) return value.map(neutralizeComponentsConfig);
457
+ if (isPlainObject(value)) {
458
+ return Object.fromEntries(
459
+ Object.entries(value).map(([key, nested]) => [key, neutralizeComponentsConfig(nested)])
460
+ );
461
+ }
462
+ return value;
463
+ }
464
+
465
+ function collectClassTokens(value, tokens = []) {
466
+ if (typeof value === 'string') {
467
+ tokens.push(...value.split(/\s+/).filter(Boolean));
468
+ } else if (Array.isArray(value)) {
469
+ value.forEach((item) => collectClassTokens(item, tokens));
470
+ } else if (isPlainObject(value)) {
471
+ Object.values(value).forEach((item) => collectClassTokens(item, tokens));
472
+ }
473
+ return tokens;
474
+ }
475
+
476
+ function findForbiddenDefaultClasses(config) {
477
+ return [...new Set(
478
+ collectClassTokens(config).filter((token) =>
479
+ FORBIDDEN_DEFAULT_CLASS_PATTERNS.some((pattern) => pattern.test(token))
480
+ )
481
+ )].sort();
482
+ }
483
+
484
+ function buildDefaultConfigData(rawMasterConfig) {
485
+ const masterConfig = neutralizeComponentsConfig(rawMasterConfig);
486
+ const generated = clone(masterConfig);
487
+
488
+ for (const [sourceKey, aliases] of Object.entries(REGISTRY_ALIASES)) {
489
+ if (!masterConfig[sourceKey]) continue;
490
+ for (const alias of aliases) {
491
+ generated[alias] = clone(masterConfig[sourceKey]);
492
+ }
493
+ }
494
+
495
+ for (const [targetKey, sourceKey] of Object.entries(DERIVED_FROM_MASTER)) {
496
+ if (masterConfig[sourceKey]) {
497
+ generated[targetKey] = clone(masterConfig[sourceKey]);
498
+ }
499
+ }
500
+
501
+ for (const [key, config] of Object.entries(MINIMAL_V4_FALLBACKS)) {
502
+ generated[key] = clone(config);
503
+ }
504
+
505
+ for (const [key, config] of Object.entries(SUPPLEMENTAL_CONFIG)) {
506
+ generated[key] = key in generated ? deepMerge(generated[key], config) : clone(config);
507
+ }
508
+
509
+ const forbidden = findForbiddenDefaultClasses(generated);
510
+ if (forbidden.length > 0) {
511
+ throw new Error(`Default config still contains FrontFriend-only classes: ${forbidden.join(', ')}`);
512
+ }
513
+
514
+ return generated;
515
+ }
516
+
517
+ function generateDefaultConfigData() {
518
+ const masterConfig = JSON.parse(fs.readFileSync(sourcePath, 'utf8'));
519
+ return buildDefaultConfigData(masterConfig);
520
+ }
521
+
522
+ function formatModule(config) {
523
+ return `// Generated from apps/saas/components/code-editor/setup/components-config.json as the neutral bundled\n` +
524
+ `// Master Design System fallback for Tailwind v4 registry components.\n` +
525
+ `// Run \`node packages/frontfriend-tailwind/scripts/update-default-config-data.js\` after Master DS changes.\n` +
526
+ `// Do not edit this file by hand.\n\n` +
527
+ `module.exports = ${JSON.stringify(config, null, 2)};\n`;
528
+ }
529
+
530
+ function writeDefaultConfigData() {
531
+ const config = generateDefaultConfigData();
532
+ fs.writeFileSync(outputPath, formatModule(config));
533
+ return config;
534
+ }
535
+
536
+ async function readMasterComponentsConfigFromDb() {
537
+ const { PrismaClient } = require('@prisma/client');
538
+ const prisma = new PrismaClient();
539
+ try {
540
+ const master = await prisma.designSystem.findFirst({
541
+ where: {
542
+ name: MASTER_DS_NAME,
543
+ workspace: { slug: SYSTEM_WORKSPACE_SLUG },
544
+ },
545
+ include: { latestVersion: true },
546
+ });
547
+
548
+ const componentsConfig = master?.latestVersion?.componentsConfig || master?.componentsConfig;
549
+ if (!componentsConfig) {
550
+ throw new Error('Master Design System componentsConfig not found. Run apps/saas/scripts/init-master-design-system.js first.');
551
+ }
552
+ return componentsConfig;
553
+ } finally {
554
+ await prisma.$disconnect();
555
+ }
556
+ }
557
+
558
+ async function writeDefaultConfigDataFromMasterDb() {
559
+ const masterConfig = await readMasterComponentsConfigFromDb();
560
+ const config = buildDefaultConfigData(masterConfig);
561
+ fs.writeFileSync(outputPath, formatModule(config));
562
+ return config;
563
+ }
564
+
565
+ if (require.main === module) {
566
+ (async () => {
567
+ const fromMasterDb = process.argv.includes('--from-master-db');
568
+ const config = fromMasterDb ? await writeDefaultConfigDataFromMasterDb() : writeDefaultConfigData();
569
+ const source = fromMasterDb ? 'Master Design System DB' : path.relative(repoRoot, sourcePath);
570
+ console.log(`Wrote ${path.relative(repoRoot, outputPath)} (${Object.keys(config).length} component keys) from ${source}`);
571
+ })().catch((error) => {
572
+ console.error(error.message);
573
+ process.exit(1);
574
+ });
575
+ }
576
+
577
+ module.exports = {
578
+ FORBIDDEN_DEFAULT_CLASS_PATTERNS,
579
+ buildDefaultConfigData,
580
+ collectClassTokens,
581
+ findForbiddenDefaultClasses,
582
+ generateDefaultConfigData,
583
+ readMasterComponentsConfigFromDb,
584
+ neutralizeClassString,
585
+ neutralizeClassToken,
586
+ neutralizeComponentsConfig,
587
+ writeDefaultConfigData,
588
+ writeDefaultConfigDataFromMasterDb,
589
+ };
package/src/theme.css ADDED
@@ -0,0 +1,13 @@
1
+ /*
2
+ * Frontfriend Tailwind v4 Theme Placeholder
3
+ *
4
+ * `frontfriend init` generates the real CSS-first theme at:
5
+ * node_modules/.cache/frontfriend/theme.css
6
+ *
7
+ * The Frontfriend Next.js and Vite integrations alias this package entrypoint
8
+ * to the generated cache file so apps can import a stable path:
9
+ * @import "@frontfriend/tailwind/theme.css";
10
+ *
11
+ * If this placeholder appears in your compiled CSS, run `npx frontfriend init`
12
+ * and make sure the Frontfriend Next.js/Vite integration is enabled.
13
+ */