@frontfriend/tailwind 4.0.18 → 4.0.21

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 (46) hide show
  1. package/dist/browser.mjs +1 -1
  2. package/dist/browser.mjs.map +3 -3
  3. package/dist/cli.js +37 -36
  4. package/dist/component-recipes.d.ts +14 -0
  5. package/dist/components-config-schema.js +1 -1
  6. package/dist/components-config-schema.js.map +3 -3
  7. package/dist/ffdc.d.ts +11 -1
  8. package/dist/ffdc.js +1 -1
  9. package/dist/ffdc.js.map +4 -4
  10. package/dist/index.js +3 -3
  11. package/dist/index.js.map +4 -4
  12. package/dist/index.mjs +1 -1
  13. package/dist/index.mjs.map +3 -3
  14. package/dist/lib/core/api-client.js +2 -2
  15. package/dist/lib/core/api-client.js.map +3 -3
  16. package/dist/lib/core/cache-manager.js +6 -4
  17. package/dist/lib/core/cached-components-config.js +2 -0
  18. package/dist/lib/core/cached-components-config.js.map +7 -0
  19. package/dist/lib/core/component-downloader.js +3 -3
  20. package/dist/lib/core/component-downloader.js.map +3 -3
  21. package/dist/lib/core/component-recipes.js +8 -0
  22. package/dist/lib/core/component-recipes.js.map +7 -0
  23. package/dist/lib/core/components-config-schema.js +1 -1
  24. package/dist/lib/core/components-config-schema.js.map +3 -3
  25. package/dist/lib/core/constants.js +1 -1
  26. package/dist/lib/core/constants.js.map +3 -3
  27. package/dist/lib/core/credentials.js +2 -2
  28. package/dist/lib/core/credentials.js.map +3 -3
  29. package/dist/next.js +1 -1
  30. package/dist/next.js.map +4 -4
  31. package/dist/runtime.js +1 -1
  32. package/dist/runtime.js.map +3 -3
  33. package/dist/types/index.d.ts +15 -1
  34. package/dist/vite.js +6 -19
  35. package/dist/vite.js.map +4 -4
  36. package/dist/vite.mjs +4 -4
  37. package/dist/vite.mjs.map +3 -3
  38. package/package.json +7 -1
  39. package/scripts/build.js +32 -9
  40. package/src/theme.css +3 -9
  41. package/dist/lib/core/default-config-data.js +0 -2
  42. package/dist/lib/core/default-config-data.js.map +0 -7
  43. package/dist/lib/core/default-config.js +0 -2
  44. package/dist/lib/core/default-config.js.map +0 -7
  45. package/scripts/master-components-config.json +0 -1678
  46. package/scripts/update-default-config-data.js +0 -891
@@ -1,891 +0,0 @@
1
- #!/usr/bin/env node
2
- const fs = require('fs');
3
- const path = require('path');
4
- const { migrateConfig } = require('./migrate-config-to-slots');
5
-
6
- const packageRoot = path.join(__dirname, '..');
7
- const repoRoot = path.join(packageRoot, '..', '..');
8
- // Vendored snapshot of the Master Design System componentsConfig. Lives inside
9
- // the package so the bundled fallback can be regenerated without depending on
10
- // apps/saas (which no longer ships the code-editor setup config). Refresh it
11
- // from the canonical source with `--from-master-db`.
12
- const sourcePath = path.join(packageRoot, 'scripts', 'master-components-config.json');
13
- const outputPath = path.join(packageRoot, 'lib', 'core', 'default-config-data.js');
14
- const SYSTEM_WORKSPACE_SLUG = 'system-templates';
15
- const MASTER_DS_NAME = 'Master Design System';
16
-
17
- const REGISTRY_ALIASES = {
18
- alertDialog: ['alertdialog'],
19
- avatarGroup: ['avatargroup'],
20
- checkboxField: ['checkboxfield'],
21
- datePicker: ['datepicker'],
22
- dateInput: ['dateinput'],
23
- dropdown: ['dropdownmenu'],
24
- fileuploader: ['fileupload'],
25
- multiSelect: ['multiselect'],
26
- pdfViewer: ['pdfviewer'],
27
- radiogroupeField: ['radiogroupfield'],
28
- timePicker: ['timepicker'],
29
- inputOTP: ['inputotp'],
30
- };
31
-
32
- const DERIVED_FROM_MASTER = {
33
- phoneInput: 'input',
34
- phoneinput: 'input',
35
- inputNumber: 'input',
36
- inputnumber: 'input',
37
- dateSlider: 'calendar',
38
- dateslider: 'calendar',
39
- tabBar: 'actionbar',
40
- tabbar: 'actionbar',
41
- };
42
-
43
- const SUPPLEMENTAL_CONFIG = {
44
- multiSelect: {
45
- createNew: 'flex flex-col gap-2 items-center',
46
- createNewButton: 'w-fit',
47
- error: 'px-3 py-1.5 text-destructive text-xs',
48
- empty: 'px-2 py-1.5 text-xs text-muted-foreground',
49
- },
50
- multiselect: {
51
- createNew: 'flex flex-col gap-2 items-center',
52
- createNewButton: 'w-fit',
53
- error: 'px-3 py-1.5 text-destructive text-xs',
54
- empty: 'px-2 py-1.5 text-xs text-muted-foreground',
55
- },
56
- phoneInput: {
57
- wrapper: 'flex gap-4',
58
- selectorWrapper: 'flex items-center',
59
- nativeSelect: 'base-select',
60
- trigger: { layout: 'flex items-center' },
61
- dialCode: 'ml-2',
62
- triggerIcon: 'ml-2 h-4 w-4',
63
- flagIcon: '-mr-2 h-5 w-5',
64
- content: 'w-[300px] p-0',
65
- item: 'gap-2',
66
- itemLabel: 'flex-1 text-sm',
67
- input: 'w-full',
68
- flag: 'bg-muted flex h-4 w-6 overflow-hidden rounded-sm',
69
- },
70
- phoneinput: {
71
- wrapper: 'flex gap-4',
72
- selectorWrapper: 'flex items-center',
73
- nativeSelect: 'base-select',
74
- trigger: { layout: 'flex items-center' },
75
- dialCode: 'ml-2',
76
- triggerIcon: 'ml-2 h-4 w-4',
77
- flagIcon: '-mr-2 h-5 w-5',
78
- content: 'w-[300px] p-0',
79
- item: 'gap-2',
80
- itemLabel: 'flex-1 text-sm',
81
- input: 'w-full',
82
- flag: 'bg-muted flex h-4 w-6 overflow-hidden rounded-sm',
83
- },
84
- spinner: {
85
- root: 'animate-spin',
86
- track: 'opacity-20',
87
- },
88
- tabs: {
89
- list: { solidRoot: 'relative' },
90
- },
91
- table: {
92
- container: 'w-full',
93
- },
94
- sheet: {
95
- srOnly: 'sr-only',
96
- },
97
- themepicker: {
98
- icon: {
99
- light: 'h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90',
100
- dark: 'absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0',
101
- },
102
- srOnly: 'sr-only',
103
- },
104
- themeprovider: {
105
- themePicker: { srOnly: 'sr-only' },
106
- },
107
- toast: {
108
- toaster: 'toaster group',
109
- },
110
- textarea: {
111
- noResize: 'resize-none',
112
- },
113
- input: {
114
- textCenter: 'text-center',
115
- },
116
- inputNumber: {
117
- textCenter: 'text-center',
118
- },
119
- inputnumber: {
120
- textCenter: 'text-center',
121
- },
122
- datePicker: {
123
- selectContent: 'z-50',
124
- fullWidth: 'w-full',
125
- },
126
- datepicker: {
127
- selectContent: 'z-50',
128
- fullWidth: 'w-full',
129
- },
130
- fileuploader: {
131
- cursor: 'cursor-pointer',
132
- borderless: 'border-0',
133
- },
134
- fileupload: {
135
- cursor: 'cursor-pointer',
136
- borderless: 'border-0',
137
- },
138
- sidebar: {
139
- srOnly: 'sr-only',
140
- root: { foreground: 'text-sidebar-foreground' },
141
- },
142
- segmented: {
143
- trigger: 'tab-trigger',
144
- activeTrigger: 'tab-trigger-active',
145
- disabled: 'pointer-events-none opacity-50',
146
- },
147
- calendar: {
148
- customRoot: 'p-3 rounded-md font-primary',
149
- header: 'flex justify-center pt-1 relative items-center',
150
- content: 'grid grid-cols-3 gap-2',
151
- options: 'cursor-pointer',
152
- selectGroup: 'flex items-center gap-2',
153
- nativeSelect: 'h-8 rounded-md border border-input bg-background px-2 text-sm',
154
- range: {
155
- root: 'p-3 rounded-md font-primary',
156
- container: 'flex flex-col gap-4 sm:flex-row',
157
- row: 'flex w-full mt-2',
158
- navSpacer: 'h-7 w-7',
159
- },
160
- },
161
- dateSlider: {
162
- customRootSlider: 'w-full',
163
- header: 'flex justify-center pt-1 relative items-center',
164
- cell: 'text-center text-sm p-0 relative',
165
- selectContent: 'z-50',
166
- buttons: { props: { variant: 'ghost' } },
167
- dialog: {
168
- false: 'hidden',
169
- content: 'grid gap-4',
170
- body: 'grid gap-4',
171
- range: 'grid gap-4',
172
- },
173
- },
174
- dateslider: {
175
- customRootSlider: 'w-full',
176
- header: 'flex justify-center pt-1 relative items-center',
177
- cell: 'text-center text-sm p-0 relative',
178
- selectContent: 'z-50',
179
- buttons: { props: { variant: 'ghost' } },
180
- dialog: {
181
- false: 'hidden',
182
- content: 'grid gap-4',
183
- body: 'grid gap-4',
184
- range: 'grid gap-4',
185
- },
186
- },
187
- };
188
-
189
- const MINIMAL_V4_FALLBACKS = {
190
- avatarGroup: {
191
- root: 'flex flex-row relative h-10 w-full font-primary',
192
- avatar: 'relative -ml-2 first:ml-0',
193
- },
194
- avatargroup: {
195
- root: 'flex flex-row relative h-10 w-full font-primary',
196
- avatar: 'relative -ml-2 first:ml-0',
197
- },
198
- timePicker: {
199
- trigger: 'w-full',
200
- input: 'w-full',
201
- wrapper: 'grid gap-4',
202
- popover: {
203
- content: 'w-auto p-4',
204
- },
205
- select: {
206
- root: 'grid gap-2',
207
- label: 'text-sm font-medium text-foreground',
208
- container: 'flex flex-col gap-2',
209
- content: 'max-h-48 overflow-y-auto',
210
- },
211
- },
212
- timepicker: {
213
- trigger: 'w-full',
214
- input: 'w-full',
215
- wrapper: 'grid gap-4',
216
- popover: {
217
- content: 'w-auto p-4',
218
- },
219
- select: {
220
- root: 'grid gap-2',
221
- label: 'text-sm font-medium text-foreground',
222
- container: 'flex flex-col gap-2',
223
- content: 'max-h-48 overflow-y-auto',
224
- },
225
- },
226
- collapsible: {
227
- root: 'w-full',
228
- trigger: 'inline-flex items-center justify-between gap-2',
229
- content: 'overflow-hidden',
230
- },
231
- icon: { root: 'shrink-0' },
232
- resizable: {
233
- root: 'flex',
234
- panel: 'relative',
235
- handle: 'bg-border focus-visible:ring-ring relative flex w-px items-center justify-center',
236
- },
237
- stepper: {
238
- root: 'block w-full',
239
- nav: 'group/stepper-nav inline-flex data-[orientation=horizontal]:w-full data-[orientation=horizontal]:flex-row data-[orientation=vertical]:flex-col',
240
- item: 'group/step flex items-center justify-center not-last:flex-1 data-[disabled]:pointer-events-none data-[disabled]:opacity-60 group-data-[orientation=horizontal]/stepper-nav:flex-row group-data-[orientation=vertical]/stepper-nav:flex-col',
241
- trigger: 'focus-visible:border-ring focus-visible:ring-ring/50 inline-flex cursor-pointer items-center gap-2.5 rounded-full outline-none focus-visible:z-10 focus-visible:ring-3 disabled:pointer-events-none disabled:opacity-60',
242
- indicator: 'border-background bg-muted text-foreground data-[state=completed]:bg-primary data-[state=completed]:text-primary-foreground data-[state=active]:bg-primary data-[state=active]:text-primary-foreground relative flex size-6 shrink-0 items-center justify-center overflow-hidden rounded-full text-xs',
243
- indicatorContent: 'absolute',
244
- separator: 'bg-muted group-data-[state=completed]/step:bg-primary data-[state=completed]:bg-primary m-0.5 rounded-sm group-data-[orientation=horizontal]/stepper-nav:h-0.5 group-data-[orientation=horizontal]/stepper-nav:flex-1 group-data-[orientation=vertical]/stepper-nav:h-12 group-data-[orientation=vertical]/stepper-nav:w-0.5',
245
- title: 'text-sm leading-none font-medium text-foreground',
246
- description: 'text-sm text-muted-foreground',
247
- panel: 'w-full',
248
- content: 'w-full',
249
- contentHidden: 'hidden',
250
- },
251
- themeprovider: { root: 'contents' },
252
- themepicker: {
253
- root: 'inline-flex items-center gap-2 rounded-md border border-border bg-background p-1',
254
- },
255
- toggle: {
256
- root: 'inline-flex items-center justify-center rounded-md text-sm font-medium hover:bg-accent hover:text-accent-foreground',
257
- variant: {
258
- default: 'bg-transparent',
259
- outline: 'border border-input bg-transparent',
260
- },
261
- size: {
262
- default: 'h-9 px-3',
263
- sm: 'h-8 px-2',
264
- lg: 'h-10 px-4',
265
- },
266
- },
267
- };
268
-
269
- // Class values re-authored from the shadcn/ui new-york-v4 sources
270
- // (clone: apps/v4/registry/new-york-v4/ui/{button,badge,input}.tsx). These are
271
- // deep-merged on top of the neutralized Master DS so the out-of-the-box fallback
272
- // renders like a stock shadcn install: token-driven radius (rounded-md), the
273
- // shadcn size scale (default h-9 / sm h-8 / lg h-10 / icon size-9), and shadcn
274
- // variant treatments. shadcn's cva variant/size buckets are mapped into the
275
- // FrontFriend component envelope keys (e.g. shadcn `default` -> FF `main`,
276
- // shadcn `outline` -> FF `tertiary`). FrontFriend-only buckets (ghostmain,
277
- // linkdestructive, badge success/warning/subtle, ...) are left to the
278
- // neutralized Master DS values and survive the merge untouched.
279
- const SHADCN_DEFAULTS = {
280
- button: {
281
- root:
282
- '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',
283
- variant: {
284
- main: 'bg-primary text-primary-foreground hover:bg-primary/90',
285
- destructive:
286
- 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20',
287
- tertiary:
288
- 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground',
289
- secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
290
- ghost: 'hover:bg-accent hover:text-accent-foreground',
291
- ghostmain: 'text-primary hover:bg-accent hover:text-primary/90',
292
- link: 'text-primary underline-offset-4 hover:underline',
293
- linkdestructive: 'text-destructive underline-offset-4 hover:underline',
294
- },
295
- size: {
296
- xs: 'h-6 gap-1 px-2 text-xs',
297
- sm: 'h-8 gap-1.5 px-3',
298
- default: 'h-9 px-4 py-2',
299
- lg: 'h-10 px-6',
300
- icon: 'size-9',
301
- },
302
- icon: {
303
- size: { sm: '4', default: '4', lg: '4' },
304
- color: {
305
- main: 'text-primary-foreground',
306
- secondary: 'text-secondary-foreground',
307
- tertiary: 'text-foreground group-hover:text-accent-foreground',
308
- destructive: 'text-white',
309
- ghost: 'text-foreground group-hover:text-accent-foreground',
310
- ghostmain: 'text-primary',
311
- link: 'text-primary',
312
- linkdestructive: 'text-destructive',
313
- },
314
- },
315
- iconPosition: {
316
- default: {
317
- small: 'w-8 px-0',
318
- default: 'w-9 px-0',
319
- large: 'w-10 px-0',
320
- },
321
- },
322
- },
323
- badge: {
324
- root:
325
- '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',
326
- variant: {
327
- main: 'bg-primary border-transparent text-primary-foreground [&>svg]:text-primary-foreground',
328
- secondary:
329
- 'bg-secondary border-transparent text-secondary-foreground [&>svg]:text-secondary-foreground',
330
- destructive:
331
- 'bg-destructive border-transparent text-white [&>svg]:text-white',
332
- tertiary: 'bg-transparent border-border text-foreground [&>svg]:text-foreground',
333
- },
334
- },
335
- input: {
336
- root:
337
- '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',
338
- numberField: {
339
- root: 'grid gap-1.5',
340
- content:
341
- 'relative [&>[data-slot=input]]:has-[[data-slot=increment]]:pr-5 [&>[data-slot=input]]:has-[[data-slot=decrement]]:pl-5',
342
- input:
343
- 'flex h-9 w-full rounded-md border border-input bg-transparent py-1 text-sm text-center shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
344
- increment:
345
- 'absolute top-1/2 -translate-y-1/2 right-0 disabled:cursor-not-allowed disabled:opacity-20 p-3',
346
- decrement:
347
- 'absolute top-1/2 -translate-y-1/2 left-0 p-3 disabled:cursor-not-allowed disabled:opacity-20',
348
- icon: 'h-4 w-4',
349
- },
350
- },
351
- // Fidelity pass (#468 follow-up): align components whose neutralized treatment
352
- // visibly diverged from shadcn new-york-v4 (size scale / padding / shape).
353
- // Each value stays config-driven (token utilities only, no brand, no bare rounded).
354
- textarea: {
355
- root:
356
- 'flex field-sizing-content min-h-16 w-full font-primary rounded-md border border-border bg-transparent px-3 py-2 text-base md:text-sm shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50',
357
- },
358
- label: {
359
- root:
360
- 'flex items-center gap-2 text-sm leading-none font-medium select-none font-primary group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
361
- },
362
- checkbox: {
363
- root:
364
- 'peer size-4 shrink-0 rounded-[4px] border border-border bg-background shadow-xs transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
365
- icon: 'text-primary-foreground flex items-center justify-center',
366
- },
367
- switch: {
368
- root:
369
- 'peer inline-flex h-[1.15rem] w-8 shrink-0 cursor-pointer items-center rounded-full border border-transparent shadow-xs transition-all focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
370
- trigger:
371
- 'pointer-events-none block size-4 rounded-full bg-background shadow ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0',
372
- },
373
- radiogroup: {
374
- item:
375
- 'aspect-square group size-4 shrink-0 bg-background text-primary border-border rounded-full border shadow-xs focus:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50',
376
- icon: 'size-2 rounded-full',
377
- },
378
- avatar: {
379
- root: 'font-primary group/avatar relative flex size-8 shrink-0 overflow-hidden rounded-full select-none',
380
- fallback:
381
- 'bg-muted flex size-full items-center justify-center rounded-full text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs',
382
- badge:
383
- 'absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground ring-2 ring-background select-none group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2 group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2',
384
- group:
385
- 'group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background',
386
- groupCount:
387
- 'relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3',
388
- props: {
389
- size: {
390
- default: '',
391
- sm: 'size-6',
392
- lg: 'size-10',
393
- },
394
- },
395
- },
396
- card: {
397
- root:
398
- 'flex flex-col gap-6 rounded-xl font-primary border bg-card text-foreground border-border relative py-6 shadow-sm',
399
- header: 'flex flex-col gap-1.5 px-6',
400
- content: 'px-6',
401
- footer: 'flex gap-4 items-center px-6',
402
- },
403
- navigationmenu: {
404
- item: 'relative',
405
- },
406
- sheet: {
407
- srOnly: 'sr-only',
408
- },
409
- sidebar: {
410
- srOnly: 'sr-only',
411
- },
412
- resizable: {
413
- handleIcon: 'z-10 flex h-4 w-3 items-center justify-center rounded-xs border bg-border',
414
- iconSize: '2.5',
415
- icons: {
416
- handle: 'GripVertical',
417
- },
418
- },
419
- chart: {
420
- slots: {
421
- tooltip: {
422
- root: 'grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl',
423
- label: {
424
- root: 'flex flex-1 justify-between leading-none',
425
- wrapper: 'font-medium',
426
- },
427
- payload: {
428
- root: 'flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground',
429
- wrapper: 'grid gap-1.5',
430
- },
431
- indicator: 'shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]',
432
- value: 'font-mono font-medium tabular-nums text-foreground',
433
- },
434
- legend: {
435
- content: {
436
- root: 'flex items-center justify-center gap-4',
437
- wrapper: 'flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground',
438
- },
439
- align: {
440
- true: 'pb-3',
441
- false: 'pt-3',
442
- },
443
- item: 'h-2 w-2 shrink-0 rounded-[2px]',
444
- },
445
- dot: {
446
- indicator: {
447
- root: 'items-center',
448
- size: 'h-2.5 w-2.5',
449
- },
450
- },
451
- line: {
452
- indicator: {
453
- size: 'w-1',
454
- },
455
- },
456
- dashed: {
457
- indicator: {
458
- root: 'w-0 border-[1.5px] border-dashed bg-transparent',
459
- label: 'my-0.5',
460
- },
461
- },
462
- nest: {
463
- label: {
464
- true: 'items-end',
465
- false: 'items-center',
466
- container: 'grid gap-1.5',
467
- text: 'text-muted-foreground',
468
- },
469
- },
470
- },
471
- },
472
- drawer: {
473
- handle:
474
- 'bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block',
475
- },
476
- tooltip: {
477
- content:
478
- 'z-30 w-fit overflow-hidden rounded-md font-normal font-primary bg-foreground px-3 py-1.5 text-xs text-balance text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
479
- arrow:
480
- 'z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground',
481
- },
482
- tabs: {
483
- root: 'group/tabs flex gap-2 data-[orientation=horizontal]:flex-col',
484
- list: {
485
- root:
486
- 'inline-flex h-9 w-fit gap-1 items-center justify-center rounded-lg p-[3px] font-primary bg-muted text-muted-foreground',
487
- },
488
- },
489
- select: {
490
- trigger:
491
- 'flex w-full items-center justify-between gap-2 font-primary rounded-md px-3 py-2 border border-border bg-transparent text-sm text-muted-foreground shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:truncate h-9',
492
- content: {
493
- root:
494
- 'font-primary p-1 max-h-96 min-w-32 relative bg-popover text-popover-foreground border-border gap-y-1 border rounded-md z-30 overflow-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 shadow-md',
495
- },
496
- separator: '-mx-1 my-1 h-px bg-border',
497
- },
498
- skeleton: {
499
- root: 'animate-pulse rounded-md bg-accent',
500
- },
501
- progress: {
502
- root: 'relative h-2 w-full overflow-hidden rounded-full bg-primary/20',
503
- },
504
- };
505
-
506
- const DIRECT_CLASS_MAP = {
507
- 'border-transperent': 'border-transparent',
508
- 'hover:interactive-brand-mid-hover': 'hover:text-primary/90',
509
-
510
- 'bg-white/10': 'bg-background',
511
- 'bg-transparent': 'bg-transparent',
512
- 'border-transparent': 'border-transparent',
513
-
514
- 'bg-overlay': 'bg-black/80',
515
- 'bg-overlay-mid': 'bg-black/80',
516
- 'bg-overlay-strong': 'bg-black/80',
517
-
518
- 'bg-layer-control': 'bg-background',
519
- 'bg-layer-below': 'bg-background',
520
- 'bg-layer-raised': 'bg-card',
521
- 'bg-layer-surface': 'bg-card',
522
- 'bg-layer-dialog': 'bg-background',
523
- 'bg-layer-popover': 'bg-popover',
524
-
525
- 'bg-disabled': 'opacity-50',
526
- 'text-disabled': 'opacity-50',
527
- 'fill-disabled': 'opacity-50',
528
- 'border-disabled': 'opacity-50',
529
- };
530
-
531
- const COLOR_FAMILIES = new Set([
532
- 'brand',
533
- 'interactive',
534
- 'neutral',
535
- 'negative',
536
- 'positive',
537
- 'warning',
538
- 'info',
539
- 'highlight',
540
- 'inverse',
541
- ]);
542
-
543
- const ON_COLOR_FAMILIES = new Set([
544
- 'onbrand',
545
- 'oninteractive',
546
- 'onnegative',
547
- 'onpositive',
548
- 'onwarning',
549
- 'oninfo',
550
- 'onhighlight',
551
- ]);
552
-
553
- const FORBIDDEN_DEFAULT_CLASS_PATTERNS = [
554
- /(?:^|:)bg-brand(?:-|$)/,
555
- /(?:^|:)text-brand(?:-|$)/,
556
- /(?:^|:)text-onbrand(?:-|$)/,
557
- /(?:^|:)(?:text|fill)-icon-(?:brand|onbrand)(?:-|$)/,
558
- /(?:^|:)border-brand(?:-|$)/,
559
- /(?:^|:)ring-brand(?:-|$)/,
560
- /(?:^|:)bg-layer(?:-|$)/,
561
- /(?:^|:)bg-disabled$/,
562
- /(?:^|:)text-disabled$/,
563
- /(?:^|:)fill-disabled$/,
564
- /(?:^|:)border-disabled$/,
565
- /transperent/,
566
- // Fixed radius that ignores --radius; must use the rounded-md/lg/xl scale.
567
- /(?:^|:)rounded(?:-(?:t|b|l|r|tl|tr|bl|br|ss|se|es|ee|s|e))?$/,
568
- ];
569
-
570
- function clone(value) {
571
- return JSON.parse(JSON.stringify(value));
572
- }
573
-
574
- function isPlainObject(value) {
575
- return value !== null && typeof value === 'object' && !Array.isArray(value);
576
- }
577
-
578
- function deepMerge(base, override) {
579
- if (!isPlainObject(base) || !isPlainObject(override)) {
580
- return clone(override);
581
- }
582
-
583
- const merged = clone(base);
584
- for (const [key, value] of Object.entries(override)) {
585
- merged[key] = key in merged ? deepMerge(merged[key], value) : clone(value);
586
- }
587
- return merged;
588
- }
589
-
590
- function splitVariants(token) {
591
- let depth = 0;
592
- let lastColon = -1;
593
- for (let index = 0; index < token.length; index += 1) {
594
- const char = token[index];
595
- if (char === '[') depth += 1;
596
- if (char === ']') depth = Math.max(0, depth - 1);
597
- if (char === ':' && depth === 0) lastColon = index;
598
- }
599
-
600
- if (lastColon === -1) {
601
- return { prefix: '', base: token };
602
- }
603
- return {
604
- prefix: token.slice(0, lastColon + 1),
605
- base: token.slice(lastColon + 1),
606
- };
607
- }
608
-
609
- function withVariantTone(prefix, baseClass) {
610
- const variants = prefix.split(':').filter(Boolean);
611
- const isActiveLike = variants.some((variant) =>
612
- variant === 'hover' ||
613
- variant === 'active' ||
614
- variant.includes('hover') ||
615
- variant.includes('active') ||
616
- variant.includes('selected') ||
617
- variant.includes('checked') ||
618
- variant.includes('highlighted') ||
619
- variant.includes('expanded')
620
- );
621
- const isDisabled = variants.some((variant) => variant.includes('disabled'));
622
-
623
- if (isDisabled) return baseClass;
624
- if (!isActiveLike) return baseClass;
625
- if (baseClass === 'bg-primary') return 'bg-primary/90';
626
- if (baseClass === 'bg-secondary') return 'bg-secondary/80';
627
- if (baseClass === 'bg-muted') return 'bg-muted/80';
628
- if (baseClass === 'bg-accent') return 'bg-accent/80';
629
- if (baseClass === 'bg-destructive') return 'bg-destructive/90';
630
- if (baseClass === 'text-primary') return 'text-primary/90';
631
- if (baseClass === 'border-primary') return 'border-primary/80';
632
- return baseClass;
633
- }
634
-
635
- function mapBgClass(base, prefix) {
636
- const match = base.match(/^!?bg-([a-z]+)(?:-[a-z]+)*(?:-(hover|active|selected|disabled))?$/);
637
- if (!match) return null;
638
- const important = base.startsWith('!') ? '!' : '';
639
- const family = match[1];
640
- if (!COLOR_FAMILIES.has(family)) return null;
641
-
642
- let mapped;
643
- if (family === 'brand' || family === 'interactive') mapped = 'bg-primary';
644
- else if (family === 'neutral') mapped = base.includes('-low') || base.includes('-subtle') ? 'bg-muted' : 'bg-accent';
645
- else if (family === 'negative') mapped = 'bg-destructive';
646
- else mapped = 'bg-primary';
647
-
648
- return important + withVariantTone(prefix, mapped);
649
- }
650
-
651
- function mapTextClass(base, prefix) {
652
- const match = base.match(/^!?(?:text|fill)-(?:(icon)-)?([a-z]+)(?:-[a-z]+)*(?:-(hover|active|selected|disabled))?$/);
653
- if (!match) return null;
654
- const important = base.startsWith('!') ? '!' : '';
655
- const family = match[2];
656
- if (!COLOR_FAMILIES.has(family) && !ON_COLOR_FAMILIES.has(family)) return null;
657
-
658
- let mapped;
659
- if (family === 'brand' || family === 'interactive') mapped = 'text-primary';
660
- else if (family === 'neutral' || family === 'inverse') mapped = base.includes('-mid') || base.includes('-strong') ? 'text-foreground' : 'text-muted-foreground';
661
- else if (family === 'negative') mapped = 'text-destructive';
662
- else if (ON_COLOR_FAMILIES.has(family)) mapped = family === 'onnegative' ? 'text-destructive-foreground' : 'text-primary-foreground';
663
- else mapped = 'text-primary';
664
-
665
- return important + withVariantTone(prefix, mapped);
666
- }
667
-
668
- // Tailwind v4 / shadcn theme ties rounded-{sm,md,lg,xl} to the --radius token.
669
- // Bare `rounded` (and bare directional variants) are fixed 0.25rem and ignore
670
- // --radius, so map them onto the token-driven `rounded-md` scale.
671
- function mapRadiusClass(base) {
672
- if (base === 'rounded') return 'rounded-md';
673
- const match = base.match(/^rounded-(t|b|l|r|tl|tr|bl|br|ss|se|es|ee|s|e)$/);
674
- if (match) return `rounded-${match[1]}-md`;
675
- return null;
676
- }
677
-
678
- function mapBorderClass(base, prefix) {
679
- const match = base.match(/^!?border-([a-z]+)(?:-[a-z]+)*(?:-(hover|active|selected|disabled))?$/);
680
- if (!match) return null;
681
- const important = base.startsWith('!') ? '!' : '';
682
- const family = match[1];
683
- if (!COLOR_FAMILIES.has(family)) return null;
684
-
685
- let mapped;
686
- if (family === 'brand' || family === 'interactive') mapped = 'border-primary';
687
- else if (family === 'negative') mapped = 'border-destructive';
688
- else mapped = 'border-border';
689
- return important + withVariantTone(prefix, mapped);
690
- }
691
-
692
- function mapRingClass(base, prefix) {
693
- const match = base.match(/^!?ring-([a-z]+)(?:-[a-z]+)*(?:-(hover|active|selected|disabled))?$/);
694
- if (!match) return null;
695
- const important = base.startsWith('!') ? '!' : '';
696
- const family = match[1];
697
- // `ring-ring`, `ring-border`, `ring-background`, … carry no FF color family and
698
- // are already neutral shadcn tokens, so they fall through untouched here.
699
- if (!COLOR_FAMILIES.has(family)) return null;
700
-
701
- let mapped;
702
- if (family === 'negative') mapped = 'ring-destructive';
703
- else mapped = 'ring-ring';
704
- return important + withVariantTone(prefix, mapped);
705
- }
706
-
707
- function neutralizeClassToken(token) {
708
- if (!token) return token;
709
- if (DIRECT_CLASS_MAP[token]) return DIRECT_CLASS_MAP[token];
710
-
711
- const { prefix, base } = splitVariants(token);
712
- // Peel off an opacity suffix (e.g. `/50`)
713
- const opacityMatch = base.match(/^(.*?)(\/\d{1,3})$/);
714
- const core = opacityMatch ? opacityMatch[1] : base;
715
- const opacity = opacityMatch ? opacityMatch[2] : '';
716
-
717
- let mappedCore = DIRECT_CLASS_MAP[core]
718
- || mapBgClass(core, prefix)
719
- || mapTextClass(core, prefix)
720
- || mapBorderClass(core, prefix)
721
- || mapRingClass(core, prefix)
722
- || mapRadiusClass(core)
723
- || core;
724
-
725
- // An explicit opacity on the source token wins over any tone-derived opacity
726
- // that withVariantTone may have appended (e.g. bg-primary/90).
727
- if (opacity) mappedCore = mappedCore.replace(/\/\d{1,3}$/, '') + opacity;
728
-
729
- return `${prefix}${mappedCore}`;
730
- }
731
-
732
- function neutralizeClassString(value) {
733
- return value
734
- .split(/\s+/)
735
- .filter(Boolean)
736
- .map(neutralizeClassToken)
737
- .join(' ');
738
- }
739
-
740
- function neutralizeComponentsConfig(value) {
741
- if (typeof value === 'string') return neutralizeClassString(value);
742
- if (Array.isArray(value)) return value.map(neutralizeComponentsConfig);
743
- if (isPlainObject(value)) {
744
- return Object.fromEntries(
745
- Object.entries(value).map(([key, nested]) => [key, neutralizeComponentsConfig(nested)])
746
- );
747
- }
748
- return value;
749
- }
750
-
751
- function collectClassTokens(value, tokens = []) {
752
- if (typeof value === 'string') {
753
- tokens.push(...value.split(/\s+/).filter(Boolean));
754
- } else if (Array.isArray(value)) {
755
- value.forEach((item) => collectClassTokens(item, tokens));
756
- } else if (isPlainObject(value)) {
757
- Object.values(value).forEach((item) => collectClassTokens(item, tokens));
758
- }
759
- return tokens;
760
- }
761
-
762
- function findForbiddenDefaultClasses(config) {
763
- return [...new Set(
764
- collectClassTokens(config).filter((token) =>
765
- FORBIDDEN_DEFAULT_CLASS_PATTERNS.some((pattern) => pattern.test(token))
766
- )
767
- )].sort();
768
- }
769
-
770
- function buildDefaultConfigData(rawMasterConfig) {
771
- const masterConfig = neutralizeComponentsConfig(rawMasterConfig);
772
- const generated = clone(masterConfig);
773
-
774
- for (const [sourceKey, aliases] of Object.entries(REGISTRY_ALIASES)) {
775
- if (!masterConfig[sourceKey]) continue;
776
- for (const alias of aliases) {
777
- generated[alias] = clone(masterConfig[sourceKey]);
778
- }
779
- }
780
-
781
- for (const [targetKey, sourceKey] of Object.entries(DERIVED_FROM_MASTER)) {
782
- if (masterConfig[sourceKey]) {
783
- generated[targetKey] = clone(masterConfig[sourceKey]);
784
- }
785
- }
786
-
787
- for (const [key, config] of Object.entries(MINIMAL_V4_FALLBACKS)) {
788
- generated[key] = clone(config);
789
- }
790
-
791
- for (const [key, config] of Object.entries(SUPPLEMENTAL_CONFIG)) {
792
- generated[key] = key in generated ? deepMerge(generated[key], config) : clone(config);
793
- }
794
-
795
- // Re-author shadcn-covered components from the shadcn sources. Deep-merge so
796
- // shadcn leaves win (root/variant/size) while FrontFriend-only buckets persist.
797
- for (const [key, override] of Object.entries(SHADCN_DEFAULTS)) {
798
- generated[key] = key in generated ? deepMerge(generated[key], override) : clone(override);
799
- }
800
-
801
- const forbidden = findForbiddenDefaultClasses(generated);
802
- if (forbidden.length > 0) {
803
- throw new Error(`Default config still contains FrontFriend-only classes: ${forbidden.join(', ')}`);
804
- }
805
-
806
- // The master source and the shadcn override maps are authored in the legacy
807
- // "direct named slots" shape; the path-based neutralize/merge above relies on
808
- // that. Migrate the finished config to the canonical top-level `slots:{}`
809
- // layout so the bundled fallback matches the component library contract.
810
- return migrateConfig(generated);
811
- }
812
-
813
- function generateDefaultConfigData() {
814
- const masterConfig = JSON.parse(fs.readFileSync(sourcePath, 'utf8'));
815
- return buildDefaultConfigData(masterConfig);
816
- }
817
-
818
- function formatModule(config) {
819
- return `// Neutral bundled Master Design System fallback for Tailwind v4 registry components.\n` +
820
- `// Structure + FrontFriend-only components are derived from\n` +
821
- `// packages/frontfriend-tailwind/scripts/master-components-config.json (colors neutralized).\n` +
822
- `// shadcn-covered components (button, badge, input) have their root/variant/size classes\n` +
823
- `// re-authored from the shadcn new-york-v4 sources so the out-of-the-box fallback renders\n` +
824
- `// like a stock shadcn install (token-driven --radius, shadcn size scale + treatments).\n` +
825
- `// Run \`node packages/frontfriend-tailwind/scripts/update-default-config-data.js\` to regenerate.\n` +
826
- `// Do not edit this file by hand.\n\n` +
827
- `module.exports = ${JSON.stringify(config, null, 2)};\n`;
828
- }
829
-
830
- function writeDefaultConfigData() {
831
- const config = generateDefaultConfigData();
832
- fs.writeFileSync(outputPath, formatModule(config));
833
- return config;
834
- }
835
-
836
- async function readMasterComponentsConfigFromDb() {
837
- const { PrismaClient } = require('@prisma/client');
838
- const prisma = new PrismaClient();
839
- try {
840
- const master = await prisma.designSystem.findFirst({
841
- where: {
842
- name: MASTER_DS_NAME,
843
- workspace: { slug: SYSTEM_WORKSPACE_SLUG },
844
- },
845
- include: { latestVersion: true },
846
- });
847
-
848
- const componentsConfig = master?.latestVersion?.componentsConfig || master?.componentsConfig;
849
- if (!componentsConfig) {
850
- throw new Error('Master Design System componentsConfig not found. Run apps/saas/scripts/init-master-design-system.js first.');
851
- }
852
- return componentsConfig;
853
- } finally {
854
- await prisma.$disconnect();
855
- }
856
- }
857
-
858
- async function writeDefaultConfigDataFromMasterDb() {
859
- const masterConfig = await readMasterComponentsConfigFromDb();
860
- // Keep the vendored snapshot in sync so the file-based generator stays reproducible.
861
- fs.writeFileSync(sourcePath, `${JSON.stringify(masterConfig, null, 2)}\n`);
862
- const config = buildDefaultConfigData(masterConfig);
863
- fs.writeFileSync(outputPath, formatModule(config));
864
- return config;
865
- }
866
-
867
- if (require.main === module) {
868
- (async () => {
869
- const fromMasterDb = process.argv.includes('--from-master-db');
870
- const config = fromMasterDb ? await writeDefaultConfigDataFromMasterDb() : writeDefaultConfigData();
871
- const source = fromMasterDb ? 'Master Design System DB' : path.relative(repoRoot, sourcePath);
872
- console.log(`Wrote ${path.relative(repoRoot, outputPath)} (${Object.keys(config).length} component keys) from ${source}`);
873
- })().catch((error) => {
874
- console.error(error.message);
875
- process.exit(1);
876
- });
877
- }
878
-
879
- module.exports = {
880
- FORBIDDEN_DEFAULT_CLASS_PATTERNS,
881
- buildDefaultConfigData,
882
- collectClassTokens,
883
- findForbiddenDefaultClasses,
884
- generateDefaultConfigData,
885
- readMasterComponentsConfigFromDb,
886
- neutralizeClassString,
887
- neutralizeClassToken,
888
- neutralizeComponentsConfig,
889
- writeDefaultConfigData,
890
- writeDefaultConfigDataFromMasterDb,
891
- };