@frontfriend/tailwind 3.0.4 → 4.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 (51) 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 +47 -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 +13 -8
  44. package/dist/vite.js.map +4 -4
  45. package/dist/vite.mjs +6 -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/master-components-config.json +953 -0
  50. package/scripts/update-default-config-data.js +690 -0
  51. package/src/theme.css +9 -0
@@ -0,0 +1,690 @@
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
+ // Vendored snapshot of the Master Design System componentsConfig. Lives inside
8
+ // the package so the bundled fallback can be regenerated without depending on
9
+ // apps/saas (which no longer ships the code-editor setup config). Refresh it
10
+ // from the canonical source with `--from-master-db`.
11
+ const sourcePath = path.join(packageRoot, 'scripts', 'master-components-config.json');
12
+ const outputPath = path.join(packageRoot, 'lib', 'core', 'default-config-data.js');
13
+ const SYSTEM_WORKSPACE_SLUG = 'system-templates';
14
+ const MASTER_DS_NAME = 'Master Design System';
15
+
16
+ const REGISTRY_ALIASES = {
17
+ alertDialog: ['alertdialog'],
18
+ avatarGroup: ['avatargroup'],
19
+ checkboxField: ['checkboxfield'],
20
+ datePicker: ['datepicker'],
21
+ dateInput: ['dateinput'],
22
+ dropdown: ['dropdownmenu'],
23
+ fileuploader: ['fileupload'],
24
+ multiSelect: ['multiselect'],
25
+ pdfViewer: ['pdfviewer'],
26
+ radiogroupeField: ['radiogroupfield'],
27
+ timePicker: ['timepicker'],
28
+ inputOTP: ['inputotp'],
29
+ };
30
+
31
+ const DERIVED_FROM_MASTER = {
32
+ phoneInput: 'input',
33
+ phoneinput: 'input',
34
+ inputNumber: 'input',
35
+ inputnumber: 'input',
36
+ dateSlider: 'calendar',
37
+ dateslider: 'calendar',
38
+ tabBar: 'actionbar',
39
+ tabbar: 'actionbar',
40
+ };
41
+
42
+ const SUPPLEMENTAL_CONFIG = {
43
+ multiSelect: {
44
+ createNew: 'flex flex-col gap-2 items-center',
45
+ createNewButton: 'w-fit',
46
+ error: 'px-3 py-1.5 text-destructive text-xs',
47
+ empty: 'px-2 py-1.5 text-xs text-muted-foreground',
48
+ },
49
+ multiselect: {
50
+ createNew: 'flex flex-col gap-2 items-center',
51
+ createNewButton: 'w-fit',
52
+ error: 'px-3 py-1.5 text-destructive text-xs',
53
+ empty: 'px-2 py-1.5 text-xs text-muted-foreground',
54
+ },
55
+ phoneInput: {
56
+ wrapper: 'flex gap-4',
57
+ selectorWrapper: 'flex items-center',
58
+ nativeSelect: 'base-select',
59
+ trigger: { layout: 'flex items-center' },
60
+ dialCode: 'ml-2',
61
+ triggerIcon: 'ml-2 h-4 w-4',
62
+ flagIcon: '-mr-2 h-5 w-5',
63
+ content: 'w-[300px] p-0',
64
+ item: 'gap-2',
65
+ itemLabel: 'flex-1 text-sm',
66
+ input: 'w-full',
67
+ flag: 'bg-muted flex h-4 w-6 overflow-hidden rounded-sm',
68
+ },
69
+ phoneinput: {
70
+ wrapper: 'flex gap-4',
71
+ selectorWrapper: 'flex items-center',
72
+ nativeSelect: 'base-select',
73
+ trigger: { layout: 'flex items-center' },
74
+ dialCode: 'ml-2',
75
+ triggerIcon: 'ml-2 h-4 w-4',
76
+ flagIcon: '-mr-2 h-5 w-5',
77
+ content: 'w-[300px] p-0',
78
+ item: 'gap-2',
79
+ itemLabel: 'flex-1 text-sm',
80
+ input: 'w-full',
81
+ flag: 'bg-muted flex h-4 w-6 overflow-hidden rounded-sm',
82
+ },
83
+ spinner: {
84
+ root: 'animate-spin',
85
+ track: 'opacity-20',
86
+ },
87
+ tabs: {
88
+ list: { solidRoot: 'relative' },
89
+ },
90
+ table: {
91
+ container: 'w-full',
92
+ },
93
+ sheet: {
94
+ srOnly: 'sr-only',
95
+ },
96
+ themepicker: {
97
+ icon: {
98
+ light: 'h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90',
99
+ dark: 'absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0',
100
+ },
101
+ srOnly: 'sr-only',
102
+ },
103
+ themeprovider: {
104
+ themePicker: { srOnly: 'sr-only' },
105
+ },
106
+ toast: {
107
+ toaster: 'toaster group',
108
+ },
109
+ textarea: {
110
+ noResize: 'resize-none',
111
+ },
112
+ input: {
113
+ textCenter: 'text-center',
114
+ },
115
+ inputNumber: {
116
+ textCenter: 'text-center',
117
+ },
118
+ inputnumber: {
119
+ textCenter: 'text-center',
120
+ },
121
+ datePicker: {
122
+ selectContent: 'z-50',
123
+ fullWidth: 'w-full',
124
+ },
125
+ datepicker: {
126
+ selectContent: 'z-50',
127
+ fullWidth: 'w-full',
128
+ },
129
+ fileuploader: {
130
+ cursor: 'cursor-pointer',
131
+ borderless: 'border-0',
132
+ },
133
+ fileupload: {
134
+ cursor: 'cursor-pointer',
135
+ borderless: 'border-0',
136
+ },
137
+ sidebar: {
138
+ srOnly: 'sr-only',
139
+ root: { foreground: 'text-sidebar-foreground' },
140
+ },
141
+ segmented: {
142
+ trigger: 'tab-trigger',
143
+ activeTrigger: 'tab-trigger-active',
144
+ disabled: 'pointer-events-none opacity-50',
145
+ },
146
+ calendar: {
147
+ customRoot: 'p-3 rounded-md font-primary',
148
+ header: 'flex justify-center pt-1 relative items-center',
149
+ content: 'grid grid-cols-3 gap-2',
150
+ options: 'cursor-pointer',
151
+ selectGroup: 'flex items-center gap-2',
152
+ nativeSelect: 'h-8 rounded-md border border-input bg-background px-2 text-sm',
153
+ range: {
154
+ root: 'p-3 rounded-md font-primary',
155
+ container: 'flex flex-col gap-4 sm:flex-row',
156
+ row: 'flex w-full mt-2',
157
+ navSpacer: 'h-7 w-7',
158
+ },
159
+ },
160
+ dateSlider: {
161
+ customRootSlider: 'w-full',
162
+ header: 'flex justify-center pt-1 relative items-center',
163
+ cell: 'text-center text-sm p-0 relative',
164
+ selectContent: 'z-50',
165
+ buttons: { props: { variant: 'ghost' } },
166
+ dialog: {
167
+ false: 'hidden',
168
+ content: 'grid gap-4',
169
+ body: 'grid gap-4',
170
+ range: 'grid gap-4',
171
+ },
172
+ },
173
+ dateslider: {
174
+ customRootSlider: 'w-full',
175
+ header: 'flex justify-center pt-1 relative items-center',
176
+ cell: 'text-center text-sm p-0 relative',
177
+ selectContent: 'z-50',
178
+ buttons: { props: { variant: 'ghost' } },
179
+ dialog: {
180
+ false: 'hidden',
181
+ content: 'grid gap-4',
182
+ body: 'grid gap-4',
183
+ range: 'grid gap-4',
184
+ },
185
+ },
186
+ };
187
+
188
+ const MINIMAL_V4_FALLBACKS = {
189
+ avatarGroup: {
190
+ root: 'flex flex-row relative h-10 w-full font-primary',
191
+ avatar: 'relative -ml-2 first:ml-0',
192
+ },
193
+ avatargroup: {
194
+ root: 'flex flex-row relative h-10 w-full font-primary',
195
+ avatar: 'relative -ml-2 first:ml-0',
196
+ },
197
+ timePicker: {
198
+ trigger: 'w-full',
199
+ input: 'w-full',
200
+ wrapper: 'grid gap-4',
201
+ popover: {
202
+ content: 'w-auto p-4',
203
+ },
204
+ select: {
205
+ root: 'grid gap-2',
206
+ label: 'text-sm font-medium text-foreground',
207
+ container: 'flex flex-col gap-2',
208
+ content: 'max-h-48 overflow-y-auto',
209
+ },
210
+ },
211
+ timepicker: {
212
+ trigger: 'w-full',
213
+ input: 'w-full',
214
+ wrapper: 'grid gap-4',
215
+ popover: {
216
+ content: 'w-auto p-4',
217
+ },
218
+ select: {
219
+ root: 'grid gap-2',
220
+ label: 'text-sm font-medium text-foreground',
221
+ container: 'flex flex-col gap-2',
222
+ content: 'max-h-48 overflow-y-auto',
223
+ },
224
+ },
225
+ collapsible: {
226
+ root: 'w-full',
227
+ trigger: 'inline-flex items-center justify-between gap-2',
228
+ content: 'overflow-hidden',
229
+ },
230
+ icon: { root: 'shrink-0' },
231
+ resizable: {
232
+ root: 'flex',
233
+ panel: 'relative',
234
+ handle: 'bg-border focus-visible:ring-ring relative flex w-px items-center justify-center',
235
+ },
236
+ stepper: {
237
+ root: 'flex gap-2',
238
+ item: 'flex items-center gap-2',
239
+ trigger: 'inline-flex items-center gap-2',
240
+ indicator: 'flex size-8 items-center justify-center rounded-full border border-border',
241
+ separator: 'bg-border h-px flex-1',
242
+ title: 'font-medium text-foreground',
243
+ description: 'text-muted-foreground text-sm',
244
+ },
245
+ themeprovider: { root: 'contents' },
246
+ themepicker: {
247
+ root: 'inline-flex items-center gap-2 rounded-md border border-border bg-background p-1',
248
+ },
249
+ toggle: {
250
+ root: 'inline-flex items-center justify-center rounded-md text-sm font-medium hover:bg-accent hover:text-accent-foreground',
251
+ variant: {
252
+ default: 'bg-transparent',
253
+ outline: 'border border-input bg-transparent',
254
+ },
255
+ size: {
256
+ default: 'h-9 px-3',
257
+ sm: 'h-8 px-2',
258
+ lg: 'h-10 px-4',
259
+ },
260
+ },
261
+ };
262
+
263
+ // Class values re-authored from the shadcn/ui new-york-v4 sources
264
+ // (clone: apps/v4/registry/new-york-v4/ui/{button,badge,input}.tsx). These are
265
+ // deep-merged on top of the neutralized Master DS so the out-of-the-box fallback
266
+ // renders like a stock shadcn install: token-driven radius (rounded-md), the
267
+ // shadcn size scale (default h-9 / sm h-8 / lg h-10 / icon size-9), and shadcn
268
+ // variant treatments. shadcn's cva variant/size buckets are mapped into the
269
+ // FrontFriend component envelope keys (e.g. shadcn `default` -> FF `main`,
270
+ // shadcn `outline` -> FF `tertiary`). FrontFriend-only buckets (ghostmain,
271
+ // linkdestructive, badge success/warning/subtle, ...) are left to the
272
+ // neutralized Master DS values and survive the merge untouched.
273
+ const SHADCN_DEFAULTS = {
274
+ button: {
275
+ root:
276
+ 'font-primary group inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20',
277
+ variant: {
278
+ main: 'bg-primary text-primary-foreground hover:bg-primary/90',
279
+ destructive:
280
+ 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20',
281
+ tertiary:
282
+ 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground',
283
+ secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
284
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
285
+ ghostmain: 'text-primary hover:bg-accent hover:text-primary/90',
286
+ link: 'text-primary underline-offset-4 hover:underline',
287
+ linkdestructive: 'text-destructive underline-offset-4 hover:underline',
288
+ },
289
+ size: {
290
+ xs: 'h-6 gap-1 px-2 text-xs',
291
+ sm: 'h-8 gap-1.5 px-3',
292
+ default: 'h-9 px-4 py-2',
293
+ lg: 'h-10 px-6',
294
+ icon: 'size-9',
295
+ },
296
+ icon: {
297
+ size: { sm: '4', default: '4', lg: '4' },
298
+ color: {
299
+ main: 'text-primary-foreground',
300
+ secondary: 'text-secondary-foreground',
301
+ tertiary: 'text-foreground group-hover:text-accent-foreground',
302
+ destructive: 'text-white',
303
+ ghost: 'text-foreground group-hover:text-accent-foreground',
304
+ ghostmain: 'text-primary',
305
+ link: 'text-primary',
306
+ linkdestructive: 'text-destructive',
307
+ },
308
+ },
309
+ iconPosition: {
310
+ default: {
311
+ small: 'w-8 px-0',
312
+ default: 'w-9 px-0',
313
+ large: 'w-10 px-0',
314
+ },
315
+ },
316
+ },
317
+ badge: {
318
+ root:
319
+ 'font-primary inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive [&>svg]:pointer-events-none [&>svg]:size-3',
320
+ variant: {
321
+ main: 'bg-primary border-transparent text-primary-foreground [&>svg]:text-primary-foreground',
322
+ secondary:
323
+ 'bg-secondary border-transparent text-secondary-foreground [&>svg]:text-secondary-foreground',
324
+ destructive:
325
+ 'bg-destructive border-transparent text-white [&>svg]:text-white',
326
+ tertiary: 'bg-transparent border-border text-foreground [&>svg]:text-foreground',
327
+ },
328
+ },
329
+ input: {
330
+ root:
331
+ 'font-primary flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base md:text-sm shadow-xs transition-[color,box-shadow] outline-none text-foreground placeholder:text-muted-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive disabled:cursor-not-allowed disabled:opacity-50',
332
+ },
333
+ };
334
+
335
+ const DIRECT_CLASS_MAP = {
336
+ 'border-transperent': 'border-transparent',
337
+ 'hover:interactive-brand-mid-hover': 'hover:text-primary/90',
338
+
339
+ 'bg-white/10': 'bg-background',
340
+ 'bg-transparent': 'bg-transparent',
341
+ 'border-transparent': 'border-transparent',
342
+
343
+ 'bg-overlay': 'bg-black/80',
344
+ 'bg-overlay-mid': 'bg-black/80',
345
+ 'bg-overlay-strong': 'bg-black/80',
346
+
347
+ 'bg-layer-control': 'bg-background',
348
+ 'bg-layer-below': 'bg-background',
349
+ 'bg-layer-raised': 'bg-card',
350
+ 'bg-layer-surface': 'bg-card',
351
+ 'bg-layer-dialog': 'bg-background',
352
+ 'bg-layer-popover': 'bg-popover',
353
+
354
+ 'bg-disabled': 'opacity-50',
355
+ 'text-disabled': 'opacity-50',
356
+ 'fill-disabled': 'opacity-50',
357
+ 'border-disabled': 'opacity-50',
358
+ };
359
+
360
+ const COLOR_FAMILIES = new Set([
361
+ 'brand',
362
+ 'interactive',
363
+ 'neutral',
364
+ 'negative',
365
+ 'positive',
366
+ 'warning',
367
+ 'info',
368
+ 'highlight',
369
+ 'inverse',
370
+ ]);
371
+
372
+ const ON_COLOR_FAMILIES = new Set([
373
+ 'onbrand',
374
+ 'oninteractive',
375
+ 'onnegative',
376
+ 'onpositive',
377
+ 'onwarning',
378
+ 'oninfo',
379
+ 'onhighlight',
380
+ ]);
381
+
382
+ const FORBIDDEN_DEFAULT_CLASS_PATTERNS = [
383
+ /(?:^|:)bg-brand(?:-|$)/,
384
+ /(?:^|:)text-brand(?:-|$)/,
385
+ /(?:^|:)text-onbrand(?:-|$)/,
386
+ /(?:^|:)(?:text|fill)-icon-(?:brand|onbrand)(?:-|$)/,
387
+ /(?:^|:)border-brand(?:-|$)/,
388
+ /(?:^|:)bg-layer(?:-|$)/,
389
+ /(?:^|:)bg-disabled$/,
390
+ /(?:^|:)text-disabled$/,
391
+ /(?:^|:)fill-disabled$/,
392
+ /(?:^|:)border-disabled$/,
393
+ /transperent/,
394
+ // Fixed radius that ignores --radius; must use the rounded-md/lg/xl scale.
395
+ /(?:^|:)rounded(?:-(?:t|b|l|r|tl|tr|bl|br|ss|se|es|ee|s|e))?$/,
396
+ ];
397
+
398
+ function clone(value) {
399
+ return JSON.parse(JSON.stringify(value));
400
+ }
401
+
402
+ function isPlainObject(value) {
403
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
404
+ }
405
+
406
+ function deepMerge(base, override) {
407
+ if (!isPlainObject(base) || !isPlainObject(override)) {
408
+ return clone(override);
409
+ }
410
+
411
+ const merged = clone(base);
412
+ for (const [key, value] of Object.entries(override)) {
413
+ merged[key] = key in merged ? deepMerge(merged[key], value) : clone(value);
414
+ }
415
+ return merged;
416
+ }
417
+
418
+ function splitVariants(token) {
419
+ let depth = 0;
420
+ let lastColon = -1;
421
+ for (let index = 0; index < token.length; index += 1) {
422
+ const char = token[index];
423
+ if (char === '[') depth += 1;
424
+ if (char === ']') depth = Math.max(0, depth - 1);
425
+ if (char === ':' && depth === 0) lastColon = index;
426
+ }
427
+
428
+ if (lastColon === -1) {
429
+ return { prefix: '', base: token };
430
+ }
431
+ return {
432
+ prefix: token.slice(0, lastColon + 1),
433
+ base: token.slice(lastColon + 1),
434
+ };
435
+ }
436
+
437
+ function withVariantTone(prefix, baseClass) {
438
+ const variants = prefix.split(':').filter(Boolean);
439
+ const isActiveLike = variants.some((variant) =>
440
+ variant === 'hover' ||
441
+ variant === 'active' ||
442
+ variant.includes('hover') ||
443
+ variant.includes('active') ||
444
+ variant.includes('selected') ||
445
+ variant.includes('checked') ||
446
+ variant.includes('highlighted') ||
447
+ variant.includes('expanded')
448
+ );
449
+ const isDisabled = variants.some((variant) => variant.includes('disabled'));
450
+
451
+ if (isDisabled) return baseClass;
452
+ if (!isActiveLike) return baseClass;
453
+ if (baseClass === 'bg-primary') return 'bg-primary/90';
454
+ if (baseClass === 'bg-secondary') return 'bg-secondary/80';
455
+ if (baseClass === 'bg-muted') return 'bg-muted/80';
456
+ if (baseClass === 'bg-accent') return 'bg-accent/80';
457
+ if (baseClass === 'bg-destructive') return 'bg-destructive/90';
458
+ if (baseClass === 'text-primary') return 'text-primary/90';
459
+ if (baseClass === 'border-primary') return 'border-primary/80';
460
+ return baseClass;
461
+ }
462
+
463
+ function mapBgClass(base, prefix) {
464
+ const match = base.match(/^!?bg-([a-z]+)(?:-[a-z]+)*(?:-(hover|active|selected|disabled))?$/);
465
+ if (!match) return null;
466
+ const important = base.startsWith('!') ? '!' : '';
467
+ const family = match[1];
468
+ if (!COLOR_FAMILIES.has(family)) return null;
469
+
470
+ let mapped;
471
+ if (family === 'brand' || family === 'interactive') mapped = 'bg-primary';
472
+ else if (family === 'neutral') mapped = base.includes('-low') || base.includes('-subtle') ? 'bg-muted' : 'bg-accent';
473
+ else if (family === 'negative') mapped = 'bg-destructive';
474
+ else mapped = 'bg-primary';
475
+
476
+ return important + withVariantTone(prefix, mapped);
477
+ }
478
+
479
+ function mapTextClass(base, prefix) {
480
+ const match = base.match(/^!?(?:text|fill)-(?:(icon)-)?([a-z]+)(?:-[a-z]+)*(?:-(hover|active|selected|disabled))?$/);
481
+ if (!match) return null;
482
+ const important = base.startsWith('!') ? '!' : '';
483
+ const family = match[2];
484
+ if (!COLOR_FAMILIES.has(family) && !ON_COLOR_FAMILIES.has(family)) return null;
485
+
486
+ let mapped;
487
+ if (family === 'brand' || family === 'interactive') mapped = 'text-primary';
488
+ else if (family === 'neutral' || family === 'inverse') mapped = base.includes('-mid') || base.includes('-strong') ? 'text-foreground' : 'text-muted-foreground';
489
+ else if (family === 'negative') mapped = 'text-destructive';
490
+ else if (ON_COLOR_FAMILIES.has(family)) mapped = family === 'onnegative' ? 'text-destructive-foreground' : 'text-primary-foreground';
491
+ else mapped = 'text-primary';
492
+
493
+ return important + withVariantTone(prefix, mapped);
494
+ }
495
+
496
+ // Tailwind v4 / shadcn theme ties rounded-{sm,md,lg,xl} to the --radius token.
497
+ // Bare `rounded` (and bare directional variants) are fixed 0.25rem and ignore
498
+ // --radius, so map them onto the token-driven `rounded-md` scale.
499
+ function mapRadiusClass(base) {
500
+ if (base === 'rounded') return 'rounded-md';
501
+ const match = base.match(/^rounded-(t|b|l|r|tl|tr|bl|br|ss|se|es|ee|s|e)$/);
502
+ if (match) return `rounded-${match[1]}-md`;
503
+ return null;
504
+ }
505
+
506
+ function mapBorderClass(base, prefix) {
507
+ const match = base.match(/^!?border-([a-z]+)(?:-[a-z]+)*(?:-(hover|active|selected|disabled))?$/);
508
+ if (!match) return null;
509
+ const important = base.startsWith('!') ? '!' : '';
510
+ const family = match[1];
511
+ if (!COLOR_FAMILIES.has(family)) return null;
512
+
513
+ let mapped;
514
+ if (family === 'brand' || family === 'interactive') mapped = 'border-primary';
515
+ else if (family === 'negative') mapped = 'border-destructive';
516
+ else mapped = 'border-border';
517
+ return important + withVariantTone(prefix, mapped);
518
+ }
519
+
520
+ function neutralizeClassToken(token) {
521
+ if (!token) return token;
522
+ if (DIRECT_CLASS_MAP[token]) return DIRECT_CLASS_MAP[token];
523
+
524
+ const { prefix, base } = splitVariants(token);
525
+ const mappedBase = DIRECT_CLASS_MAP[base]
526
+ || mapBgClass(base, prefix)
527
+ || mapTextClass(base, prefix)
528
+ || mapBorderClass(base, prefix)
529
+ || mapRadiusClass(base)
530
+ || base;
531
+
532
+ return `${prefix}${mappedBase}`;
533
+ }
534
+
535
+ function neutralizeClassString(value) {
536
+ return value
537
+ .split(/\s+/)
538
+ .filter(Boolean)
539
+ .map(neutralizeClassToken)
540
+ .join(' ');
541
+ }
542
+
543
+ function neutralizeComponentsConfig(value) {
544
+ if (typeof value === 'string') return neutralizeClassString(value);
545
+ if (Array.isArray(value)) return value.map(neutralizeComponentsConfig);
546
+ if (isPlainObject(value)) {
547
+ return Object.fromEntries(
548
+ Object.entries(value).map(([key, nested]) => [key, neutralizeComponentsConfig(nested)])
549
+ );
550
+ }
551
+ return value;
552
+ }
553
+
554
+ function collectClassTokens(value, tokens = []) {
555
+ if (typeof value === 'string') {
556
+ tokens.push(...value.split(/\s+/).filter(Boolean));
557
+ } else if (Array.isArray(value)) {
558
+ value.forEach((item) => collectClassTokens(item, tokens));
559
+ } else if (isPlainObject(value)) {
560
+ Object.values(value).forEach((item) => collectClassTokens(item, tokens));
561
+ }
562
+ return tokens;
563
+ }
564
+
565
+ function findForbiddenDefaultClasses(config) {
566
+ return [...new Set(
567
+ collectClassTokens(config).filter((token) =>
568
+ FORBIDDEN_DEFAULT_CLASS_PATTERNS.some((pattern) => pattern.test(token))
569
+ )
570
+ )].sort();
571
+ }
572
+
573
+ function buildDefaultConfigData(rawMasterConfig) {
574
+ const masterConfig = neutralizeComponentsConfig(rawMasterConfig);
575
+ const generated = clone(masterConfig);
576
+
577
+ for (const [sourceKey, aliases] of Object.entries(REGISTRY_ALIASES)) {
578
+ if (!masterConfig[sourceKey]) continue;
579
+ for (const alias of aliases) {
580
+ generated[alias] = clone(masterConfig[sourceKey]);
581
+ }
582
+ }
583
+
584
+ for (const [targetKey, sourceKey] of Object.entries(DERIVED_FROM_MASTER)) {
585
+ if (masterConfig[sourceKey]) {
586
+ generated[targetKey] = clone(masterConfig[sourceKey]);
587
+ }
588
+ }
589
+
590
+ for (const [key, config] of Object.entries(MINIMAL_V4_FALLBACKS)) {
591
+ generated[key] = clone(config);
592
+ }
593
+
594
+ for (const [key, config] of Object.entries(SUPPLEMENTAL_CONFIG)) {
595
+ generated[key] = key in generated ? deepMerge(generated[key], config) : clone(config);
596
+ }
597
+
598
+ // Re-author shadcn-covered components from the shadcn sources. Deep-merge so
599
+ // shadcn leaves win (root/variant/size) while FrontFriend-only buckets persist.
600
+ for (const [key, override] of Object.entries(SHADCN_DEFAULTS)) {
601
+ generated[key] = key in generated ? deepMerge(generated[key], override) : clone(override);
602
+ }
603
+
604
+ const forbidden = findForbiddenDefaultClasses(generated);
605
+ if (forbidden.length > 0) {
606
+ throw new Error(`Default config still contains FrontFriend-only classes: ${forbidden.join(', ')}`);
607
+ }
608
+
609
+ return generated;
610
+ }
611
+
612
+ function generateDefaultConfigData() {
613
+ const masterConfig = JSON.parse(fs.readFileSync(sourcePath, 'utf8'));
614
+ return buildDefaultConfigData(masterConfig);
615
+ }
616
+
617
+ function formatModule(config) {
618
+ return `// Neutral bundled Master Design System fallback for Tailwind v4 registry components.\n` +
619
+ `// Structure + FrontFriend-only components are derived from\n` +
620
+ `// packages/frontfriend-tailwind/scripts/master-components-config.json (colors neutralized).\n` +
621
+ `// shadcn-covered components (button, badge, input) have their root/variant/size classes\n` +
622
+ `// re-authored from the shadcn new-york-v4 sources so the out-of-the-box fallback renders\n` +
623
+ `// like a stock shadcn install (token-driven --radius, shadcn size scale + treatments).\n` +
624
+ `// Run \`node packages/frontfriend-tailwind/scripts/update-default-config-data.js\` to regenerate.\n` +
625
+ `// Do not edit this file by hand.\n\n` +
626
+ `module.exports = ${JSON.stringify(config, null, 2)};\n`;
627
+ }
628
+
629
+ function writeDefaultConfigData() {
630
+ const config = generateDefaultConfigData();
631
+ fs.writeFileSync(outputPath, formatModule(config));
632
+ return config;
633
+ }
634
+
635
+ async function readMasterComponentsConfigFromDb() {
636
+ const { PrismaClient } = require('@prisma/client');
637
+ const prisma = new PrismaClient();
638
+ try {
639
+ const master = await prisma.designSystem.findFirst({
640
+ where: {
641
+ name: MASTER_DS_NAME,
642
+ workspace: { slug: SYSTEM_WORKSPACE_SLUG },
643
+ },
644
+ include: { latestVersion: true },
645
+ });
646
+
647
+ const componentsConfig = master?.latestVersion?.componentsConfig || master?.componentsConfig;
648
+ if (!componentsConfig) {
649
+ throw new Error('Master Design System componentsConfig not found. Run apps/saas/scripts/init-master-design-system.js first.');
650
+ }
651
+ return componentsConfig;
652
+ } finally {
653
+ await prisma.$disconnect();
654
+ }
655
+ }
656
+
657
+ async function writeDefaultConfigDataFromMasterDb() {
658
+ const masterConfig = await readMasterComponentsConfigFromDb();
659
+ // Keep the vendored snapshot in sync so the file-based generator stays reproducible.
660
+ fs.writeFileSync(sourcePath, `${JSON.stringify(masterConfig, null, 2)}\n`);
661
+ const config = buildDefaultConfigData(masterConfig);
662
+ fs.writeFileSync(outputPath, formatModule(config));
663
+ return config;
664
+ }
665
+
666
+ if (require.main === module) {
667
+ (async () => {
668
+ const fromMasterDb = process.argv.includes('--from-master-db');
669
+ const config = fromMasterDb ? await writeDefaultConfigDataFromMasterDb() : writeDefaultConfigData();
670
+ const source = fromMasterDb ? 'Master Design System DB' : path.relative(repoRoot, sourcePath);
671
+ console.log(`Wrote ${path.relative(repoRoot, outputPath)} (${Object.keys(config).length} component keys) from ${source}`);
672
+ })().catch((error) => {
673
+ console.error(error.message);
674
+ process.exit(1);
675
+ });
676
+ }
677
+
678
+ module.exports = {
679
+ FORBIDDEN_DEFAULT_CLASS_PATTERNS,
680
+ buildDefaultConfigData,
681
+ collectClassTokens,
682
+ findForbiddenDefaultClasses,
683
+ generateDefaultConfigData,
684
+ readMasterComponentsConfigFromDb,
685
+ neutralizeClassString,
686
+ neutralizeClassToken,
687
+ neutralizeComponentsConfig,
688
+ writeDefaultConfigData,
689
+ writeDefaultConfigDataFromMasterDb,
690
+ };
package/src/theme.css ADDED
@@ -0,0 +1,9 @@
1
+ /*
2
+ * Frontfriend Tailwind v4 bundled source.
3
+ *
4
+ * `frontfriend init` aliases this package entrypoint to the generated cache
5
+ * theme at node_modules/.cache/frontfriend/theme.css when available. Until
6
+ * then, this file still points Tailwind v4 at the bundled component config so
7
+ * config-driven component utilities are generated out-of-the-box.
8
+ */
9
+ @source "../dist/lib/core/default-config-data.js";