banhaten 0.1.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 (201) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +361 -0
  3. package/banhaten.config.example.json +13 -0
  4. package/package.json +59 -0
  5. package/registry/assets/activity-feed-avatar.png +0 -0
  6. package/registry/assets/avatars/avatar-01.jpg +0 -0
  7. package/registry/assets/avatars/avatar-02.jpg +0 -0
  8. package/registry/assets/avatars/avatar-03.jpg +0 -0
  9. package/registry/assets/avatars/avatar-04.jpg +0 -0
  10. package/registry/assets/avatars/avatar-05.jpg +0 -0
  11. package/registry/assets/avatars/avatar-06.jpg +0 -0
  12. package/registry/assets/avatars/avatar-07.jpg +0 -0
  13. package/registry/assets/avatars/avatar-08.jpg +0 -0
  14. package/registry/assets/avatars/avatar-09.jpg +0 -0
  15. package/registry/assets/avatars/avatar-10.jpg +0 -0
  16. package/registry/assets/avatars/avatar-11.jpg +0 -0
  17. package/registry/assets/avatars/avatar-12.jpg +0 -0
  18. package/registry/assets/avatars/avatar-13.jpg +0 -0
  19. package/registry/assets/avatars/avatar-14.jpg +0 -0
  20. package/registry/assets/avatars/avatar-15.jpg +0 -0
  21. package/registry/assets/avatars/avatar-16.jpg +0 -0
  22. package/registry/assets/avatars/avatar-17.jpg +0 -0
  23. package/registry/assets/avatars/avatar-18.jpg +0 -0
  24. package/registry/assets/avatars/avatar-19.jpg +0 -0
  25. package/registry/assets/avatars/avatar-20.jpg +0 -0
  26. package/registry/assets/avatars/avatar-21.jpg +0 -0
  27. package/registry/assets/avatars/avatar-22.jpg +0 -0
  28. package/registry/assets/avatars/avatar-23.jpg +0 -0
  29. package/registry/assets/avatars/avatar-24.jpg +0 -0
  30. package/registry/assets/avatars/avatar-25.jpg +0 -0
  31. package/registry/assets/avatars/avatar-26.jpg +0 -0
  32. package/registry/assets/avatars/avatar-27.jpg +0 -0
  33. package/registry/assets/avatars/avatar-28.jpg +0 -0
  34. package/registry/assets/avatars/avatar-29.jpg +0 -0
  35. package/registry/assets/avatars/avatar-30.jpg +0 -0
  36. package/registry/assets/avatars/avatar-31.jpg +0 -0
  37. package/registry/assets/avatars/avatar-32.jpg +0 -0
  38. package/registry/assets/avatars/avatar-33.jpg +0 -0
  39. package/registry/assets/avatars/avatar-34.jpg +0 -0
  40. package/registry/assets/avatars/avatar-35.jpg +0 -0
  41. package/registry/assets/image-assets.json +744 -0
  42. package/registry/assets/images/art-01.jpg +0 -0
  43. package/registry/assets/images/art-02.jpg +0 -0
  44. package/registry/assets/images/art-03.jpg +0 -0
  45. package/registry/assets/images/art-04.jpg +0 -0
  46. package/registry/assets/images/art-05.jpg +0 -0
  47. package/registry/assets/images/art-06.jpg +0 -0
  48. package/registry/assets/images/art-07.jpg +0 -0
  49. package/registry/assets/images/art-08.jpg +0 -0
  50. package/registry/assets/images/art-09.jpg +0 -0
  51. package/registry/assets/images/art-10.jpg +0 -0
  52. package/registry/assets/images/art-11.jpg +0 -0
  53. package/registry/assets/images/art-12.jpg +0 -0
  54. package/registry/assets/images/art-13.jpg +0 -0
  55. package/registry/assets/images/art-14.jpg +0 -0
  56. package/registry/assets/images/art-15.jpg +0 -0
  57. package/registry/assets/images/art-16.jpg +0 -0
  58. package/registry/assets/images/art-17.jpg +0 -0
  59. package/registry/assets/images/art-18.jpg +0 -0
  60. package/registry/assets/images/art-19.jpg +0 -0
  61. package/registry/assets/images/art-20.jpg +0 -0
  62. package/registry/assets/images/art-21.jpg +0 -0
  63. package/registry/assets/images/art-22.jpg +0 -0
  64. package/registry/assets/images/art-23.jpg +0 -0
  65. package/registry/assets/images/art-24.jpg +0 -0
  66. package/registry/assets/images/art-25.jpg +0 -0
  67. package/registry/assets/images/art-26.jpg +0 -0
  68. package/registry/assets/images/art-27.jpg +0 -0
  69. package/registry/assets/images/nature-01.jpg +0 -0
  70. package/registry/assets/images/nature-02.jpg +0 -0
  71. package/registry/assets/images/nature-03.jpg +0 -0
  72. package/registry/assets/images/nature-04.jpg +0 -0
  73. package/registry/assets/images/nature-05.jpg +0 -0
  74. package/registry/assets/images/nature-06.jpg +0 -0
  75. package/registry/assets/images/nature-07.jpg +0 -0
  76. package/registry/assets/images/nature-08.jpg +0 -0
  77. package/registry/assets/images/nature-09.jpg +0 -0
  78. package/registry/assets/images/nature-10.jpg +0 -0
  79. package/registry/assets/images/nature-11.jpg +0 -0
  80. package/registry/assets/images/nature-12.jpg +0 -0
  81. package/registry/assets/images/nature-13.jpg +0 -0
  82. package/registry/assets/images/nature-14.jpg +0 -0
  83. package/registry/assets/images/nature-15.jpg +0 -0
  84. package/registry/assets/images/nature-16.jpg +0 -0
  85. package/registry/assets/images/nature-17.jpg +0 -0
  86. package/registry/assets/images/nature-18.jpg +0 -0
  87. package/registry/assets/images/nature-19.jpg +0 -0
  88. package/registry/assets/images/nature-20.jpg +0 -0
  89. package/registry/components/accordion.tsx +119 -0
  90. package/registry/components/alert.tsx +282 -0
  91. package/registry/components/attribute.tsx +452 -0
  92. package/registry/components/avatar.tsx +142 -0
  93. package/registry/components/badge.tsx +567 -0
  94. package/registry/components/button-group.tsx +246 -0
  95. package/registry/components/button.tsx +102 -0
  96. package/registry/components/card.tsx +613 -0
  97. package/registry/components/checkbox.tsx +244 -0
  98. package/registry/components/date-picker.tsx +1143 -0
  99. package/registry/components/divider.tsx +82 -0
  100. package/registry/components/expanded/ActivityFeed.tsx +226 -0
  101. package/registry/components/expanded/Banner.tsx +145 -0
  102. package/registry/components/expanded/BannerBoard.tsx +225 -0
  103. package/registry/components/expanded/Breadcrumbs.tsx +156 -0
  104. package/registry/components/expanded/CatalogComponentsShowcase.tsx +211 -0
  105. package/registry/components/expanded/CatalogDivider.tsx +48 -0
  106. package/registry/components/expanded/CatalogTag.tsx +92 -0
  107. package/registry/components/expanded/CommandBar.tsx +406 -0
  108. package/registry/components/expanded/FileUpload.tsx +231 -0
  109. package/registry/components/expanded/IconExplorer.tsx +612 -0
  110. package/registry/components/expanded/OnboardingStepListItem.tsx +67 -0
  111. package/registry/components/expanded/PageHeader.tsx +184 -0
  112. package/registry/components/expanded/Slideout.tsx +514 -0
  113. package/registry/components/expanded/Steps.tsx +266 -0
  114. package/registry/components/expanded/Table.tsx +1014 -0
  115. package/registry/components/expanded/Tabs.tsx +86 -0
  116. package/registry/components/expanded/Timeline.tsx +235 -0
  117. package/registry/components/expanded/TimelineShowcase.tsx +158 -0
  118. package/registry/components/expanded/activityFeed.css +292 -0
  119. package/registry/components/expanded/banner.css +312 -0
  120. package/registry/components/expanded/breadcrumbs.css +140 -0
  121. package/registry/components/expanded/catalogComponentsShowcase.css +87 -0
  122. package/registry/components/expanded/commandBar.css +473 -0
  123. package/registry/components/expanded/divider.css +75 -0
  124. package/registry/components/expanded/fileUpload.css +228 -0
  125. package/registry/components/expanded/iconExplorer.css +764 -0
  126. package/registry/components/expanded/iconPacks.ts +866 -0
  127. package/registry/components/expanded/onboardingStepListItem.css +126 -0
  128. package/registry/components/expanded/pageHeader.css +287 -0
  129. package/registry/components/expanded/slideout.css +955 -0
  130. package/registry/components/expanded/steps.css +329 -0
  131. package/registry/components/expanded/table.css +607 -0
  132. package/registry/components/expanded/tabs.css +197 -0
  133. package/registry/components/expanded/tag.css +148 -0
  134. package/registry/components/expanded/timeline.css +282 -0
  135. package/registry/components/input-content.ts +106 -0
  136. package/registry/components/input.tsx +866 -0
  137. package/registry/components/menu.tsx +758 -0
  138. package/registry/components/modal.tsx +799 -0
  139. package/registry/components/pagination.tsx +543 -0
  140. package/registry/components/progress-slider.tsx +216 -0
  141. package/registry/components/progress.tsx +367 -0
  142. package/registry/components/radio-card.tsx +654 -0
  143. package/registry/components/radio-group.tsx +570 -0
  144. package/registry/components/select-content.tsx +313 -0
  145. package/registry/components/select.tsx +871 -0
  146. package/registry/components/slider.tsx +380 -0
  147. package/registry/components/social-button.tsx +360 -0
  148. package/registry/components/spinner.tsx +31 -0
  149. package/registry/components/tag.tsx +423 -0
  150. package/registry/components/textarea.tsx +625 -0
  151. package/registry/components/toggle.tsx +272 -0
  152. package/registry/components/toolbar.tsx +467 -0
  153. package/registry/components/tooltip.tsx +427 -0
  154. package/registry/examples/accordion-demo.tsx +34 -0
  155. package/registry/examples/alert-demo.tsx +14 -0
  156. package/registry/examples/attribute-demo.tsx +65 -0
  157. package/registry/examples/avatar-demo.tsx +74 -0
  158. package/registry/examples/badge-demo.tsx +53 -0
  159. package/registry/examples/button-demo.tsx +83 -0
  160. package/registry/examples/button-group-demo.tsx +42 -0
  161. package/registry/examples/card-demo.tsx +48 -0
  162. package/registry/examples/checkbox-demo.tsx +67 -0
  163. package/registry/examples/date-picker-demo.tsx +74 -0
  164. package/registry/examples/divider-demo.tsx +17 -0
  165. package/registry/examples/expanded/activity-feed-demo.tsx +22 -0
  166. package/registry/examples/expanded/banner-demo.tsx +23 -0
  167. package/registry/examples/expanded/catalog-components-demo.tsx +5 -0
  168. package/registry/examples/expanded/command-bar-demo.tsx +10 -0
  169. package/registry/examples/expanded/icons-demo.tsx +5 -0
  170. package/registry/examples/expanded/onboarding-step-demo.tsx +11 -0
  171. package/registry/examples/expanded/page-header-demo.tsx +19 -0
  172. package/registry/examples/expanded/slideout-demo.tsx +15 -0
  173. package/registry/examples/expanded/steps-demo.tsx +18 -0
  174. package/registry/examples/expanded/tabs-demo.tsx +13 -0
  175. package/registry/examples/expanded/timeline-demo.tsx +18 -0
  176. package/registry/examples/input-demo.tsx +87 -0
  177. package/registry/examples/menu-demo.tsx +109 -0
  178. package/registry/examples/modal-demo.tsx +16 -0
  179. package/registry/examples/pagination-demo.tsx +17 -0
  180. package/registry/examples/progress-demo.tsx +37 -0
  181. package/registry/examples/progress-slider-demo.tsx +29 -0
  182. package/registry/examples/radio-card-demo.tsx +51 -0
  183. package/registry/examples/radio-group-demo.tsx +62 -0
  184. package/registry/examples/select-demo.tsx +73 -0
  185. package/registry/examples/slider-demo.tsx +31 -0
  186. package/registry/examples/social-button-demo.tsx +51 -0
  187. package/registry/examples/tag-demo.tsx +29 -0
  188. package/registry/examples/textarea-demo.tsx +79 -0
  189. package/registry/examples/toggle-demo.tsx +59 -0
  190. package/registry/examples/toolbar-demo.tsx +80 -0
  191. package/registry/examples/tooltip-demo.tsx +115 -0
  192. package/registry/hooks/use-direction.ts +27 -0
  193. package/registry/index.json +1213 -0
  194. package/registry/styles/globals.css +4600 -0
  195. package/registry/utils/cn.ts +6 -0
  196. package/src/cli/index.js +826 -0
  197. package/tokens/Color mode.zip +0 -0
  198. package/tokens/Numbers.zip +0 -0
  199. package/tokens/Radius.zip +0 -0
  200. package/tokens/Theme.zip +0 -0
  201. package/tokens/banhaten.tokens.json +5525 -0
@@ -0,0 +1,826 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs/promises"
4
+ import path from "node:path"
5
+ import process from "node:process"
6
+ import { fileURLToPath } from "node:url"
7
+
8
+ const packageRoot = path.resolve(
9
+ path.dirname(fileURLToPath(import.meta.url)),
10
+ "..",
11
+ ".."
12
+ )
13
+ const registryRoot = path.join(packageRoot, "registry")
14
+ const registryIndexPath = path.join(registryRoot, "index.json")
15
+
16
+ const CONFIG_FILE = "banhaten.config.json"
17
+ const FONT_IMPORTS = [
18
+ '@import "@fontsource/inter/400.css";',
19
+ '@import "@fontsource/inter/500.css";',
20
+ '@import "@fontsource/inter/600.css";',
21
+ '@import "@fontsource/inter/700.css";',
22
+ ]
23
+ const TAILWIND_IMPORT = '@import "tailwindcss";'
24
+ const SETUP_DEV_DEPENDENCIES = {
25
+ tailwindcss: "^4.0.0",
26
+ }
27
+ const VITE_DEV_DEPENDENCIES = {
28
+ "@tailwindcss/vite": "^4.0.0",
29
+ }
30
+ const CSS_START = "/* Banhaten design system tokens:"
31
+ const CSS_END = "/* End Banhaten design system tokens. */"
32
+ const syncExistsCache = new Map()
33
+
34
+ const DEFAULT_CONFIG = {
35
+ $schema: "https://banhaten.dev/schema.json",
36
+ style: "default",
37
+ tsx: true,
38
+ tailwind: {
39
+ css: "app/globals.css",
40
+ },
41
+ aliases: {
42
+ ui: "@/components/ui",
43
+ utils: "@/lib/utils",
44
+ hooks: "@/hooks",
45
+ },
46
+ }
47
+
48
+ const registry = await readJson(registryIndexPath)
49
+
50
+ main().catch((error) => {
51
+ console.error(`banhaten: ${error.message}`)
52
+ process.exitCode = 1
53
+ })
54
+
55
+ async function main() {
56
+ const { command, positionals, flags } = parseArgs(process.argv.slice(2))
57
+
58
+ if (!command || command === "help" || flags.help || flags.h) {
59
+ printHelp()
60
+ return
61
+ }
62
+
63
+ if (command === "list") {
64
+ listComponents()
65
+ return
66
+ }
67
+
68
+ if (command === "init") {
69
+ await initProject(flags)
70
+ return
71
+ }
72
+
73
+ if (command === "add") {
74
+ await addComponents(positionals, flags)
75
+ return
76
+ }
77
+
78
+ if (command === "diff") {
79
+ await diffComponents(positionals, flags)
80
+ return
81
+ }
82
+
83
+ if (command === "update") {
84
+ await updateComponents(positionals, flags)
85
+ return
86
+ }
87
+
88
+ throw new Error(`unknown command "${command}"`)
89
+ }
90
+
91
+ function parseArgs(argv) {
92
+ const flags = {}
93
+ const positionals = []
94
+
95
+ for (let index = 0; index < argv.length; index += 1) {
96
+ const arg = argv[index]
97
+
98
+ if (arg === "-h") {
99
+ flags.h = true
100
+ continue
101
+ }
102
+
103
+ if (!arg.startsWith("--")) {
104
+ positionals.push(arg)
105
+ continue
106
+ }
107
+
108
+ const [rawKey, inlineValue] = arg.slice(2).split("=")
109
+ const key = rawKey.trim()
110
+
111
+ if (inlineValue !== undefined) {
112
+ flags[key] = inlineValue
113
+ continue
114
+ }
115
+
116
+ const next = argv[index + 1]
117
+ if ((key === "cwd" || key === "out") && next && !next.startsWith("-")) {
118
+ flags[key] = next
119
+ index += 1
120
+ continue
121
+ }
122
+
123
+ flags[key] = true
124
+ }
125
+
126
+ const [command, ...rest] = positionals
127
+ return { command, positionals: rest, flags }
128
+ }
129
+
130
+ function printHelp() {
131
+ console.log(`Banhaten design system
132
+
133
+ Usage:
134
+ banhaten init [--cwd <path>] [--force] [--dry-run]
135
+ banhaten add <component...> [--cwd <path>] [--force] [--dry-run]
136
+ banhaten diff [component...] [--cwd <path>] [--force]
137
+ banhaten update [component...] [--cwd <path>] [--force] [--dry-run]
138
+ banhaten list
139
+
140
+ Examples:
141
+ npx banhaten init
142
+ npx banhaten add button
143
+ npx banhaten diff
144
+ npx banhaten update button
145
+ `)
146
+ }
147
+
148
+ function listComponents() {
149
+ const names = Object.keys(registry.components)
150
+
151
+ console.log("Available Banhaten design system components:")
152
+ for (const name of names) {
153
+ const component = registry.components[name]
154
+ console.log(`- ${name}: ${component.description}`)
155
+ }
156
+ }
157
+
158
+ async function initProject(flags) {
159
+ const cwd = path.resolve(String(flags.cwd || process.cwd()))
160
+ const dryRun = Boolean(flags["dry-run"])
161
+ const force = Boolean(flags.force)
162
+ await warmPathHints(cwd)
163
+ const config = await loadConfig(cwd)
164
+ const writes = []
165
+
166
+ writes.push(
167
+ ...(await writeConfig(cwd, config, { dryRun, force })),
168
+ ...(await writeBaseFiles(cwd, config, { dryRun, force })),
169
+ ...(await writeGlobalCss(cwd, config, { dryRun, force })),
170
+ ...(await writeAliasConfig(cwd, config, { dryRun, force }))
171
+ )
172
+
173
+ const dependencyWrites = await updatePackageJson(
174
+ cwd,
175
+ await getPackageChanges(cwd, registry.base.dependencies || {}),
176
+ { dryRun }
177
+ )
178
+ writes.push(...dependencyWrites)
179
+
180
+ printResult("Initialized Banhaten design system", writes, dryRun)
181
+ printInstallHint(writes, dryRun)
182
+ }
183
+
184
+ async function addComponents(names, flags) {
185
+ if (names.length === 0) {
186
+ throw new Error("missing component name. Try `banhaten add button`.")
187
+ }
188
+
189
+ await writeComponents(names, flags, {
190
+ inferInstalled: false,
191
+ title: `Added ${names.join(", ")}`,
192
+ })
193
+ }
194
+
195
+ async function diffComponents(names, flags) {
196
+ await writeComponents(names, { ...flags, "dry-run": true }, {
197
+ inferInstalled: true,
198
+ title: names.length
199
+ ? `Checked ${names.join(", ")} for updates`
200
+ : "Checked installed components for updates",
201
+ })
202
+ }
203
+
204
+ async function updateComponents(names, flags) {
205
+ await writeComponents(names, flags, {
206
+ inferInstalled: true,
207
+ title: names.length
208
+ ? `Updated ${names.join(", ")}`
209
+ : "Updated installed components",
210
+ })
211
+ }
212
+
213
+ async function writeComponents(requestedNames, flags, options) {
214
+ const cwd = path.resolve(String(flags.cwd || process.cwd()))
215
+ const dryRun = Boolean(flags["dry-run"])
216
+ const force = Boolean(flags.force)
217
+ await warmPathHints(cwd)
218
+ const config = await loadConfig(cwd)
219
+ const selectedNames =
220
+ requestedNames.length > 0
221
+ ? requestedNames
222
+ : options.inferInstalled
223
+ ? await findInstalledComponents(cwd, config)
224
+ : requestedNames
225
+
226
+ if (selectedNames.length === 0) {
227
+ throw new Error(
228
+ "no installed components found. Run `banhaten add button` first, or pass component names."
229
+ )
230
+ }
231
+
232
+ const names = resolveRegistryDependencies(selectedNames)
233
+
234
+ const writes = []
235
+ const deps = { ...(registry.base.dependencies || {}) }
236
+
237
+ writes.push(
238
+ ...(await writeConfig(cwd, config, { dryRun, force })),
239
+ ...(await writeBaseFiles(cwd, config, { dryRun, force })),
240
+ ...(await writeAliasConfig(cwd, config, { dryRun, force }))
241
+ )
242
+
243
+ for (const name of names) {
244
+ const component = registry.components[name]
245
+ if (!component) {
246
+ throw new Error(`unknown component "${name}". Run \`banhaten list\`.`)
247
+ }
248
+
249
+ Object.assign(deps, component.dependencies || {})
250
+ writes.push(
251
+ ...(await writeRegistryFiles(cwd, config, component.files || [], {
252
+ dryRun,
253
+ force,
254
+ }))
255
+ )
256
+ }
257
+
258
+ writes.push(...(await updatePackageJson(
259
+ cwd,
260
+ await getPackageChanges(cwd, deps),
261
+ { dryRun }
262
+ )))
263
+
264
+ printResult(options.title, writes, dryRun)
265
+ printInstallHint(writes, dryRun)
266
+ }
267
+
268
+ async function findInstalledComponents(cwd, config) {
269
+ const installed = []
270
+
271
+ for (const [name, component] of Object.entries(registry.components)) {
272
+ for (const file of component.files || []) {
273
+ const targetPath = path.join(cwd, resolveTarget(cwd, config, file.target))
274
+ if (await exists(targetPath)) {
275
+ installed.push(name)
276
+ break
277
+ }
278
+ }
279
+ }
280
+
281
+ return installed
282
+ }
283
+
284
+ function resolveRegistryDependencies(names) {
285
+ const resolved = []
286
+ const seen = new Set()
287
+ const visiting = new Set()
288
+
289
+ for (const name of names) {
290
+ visit(name)
291
+ }
292
+
293
+ return resolved
294
+
295
+ function visit(name) {
296
+ const component = registry.components[name]
297
+ if (!component) {
298
+ throw new Error(`unknown component "${name}". Run \`banhaten list\`.`)
299
+ }
300
+
301
+ if (seen.has(name)) return
302
+ if (visiting.has(name)) {
303
+ throw new Error(`circular registry dependency detected at "${name}".`)
304
+ }
305
+
306
+ visiting.add(name)
307
+ for (const dependency of component.registryDependencies || []) {
308
+ visit(dependency)
309
+ }
310
+ visiting.delete(name)
311
+
312
+ seen.add(name)
313
+ resolved.push(name)
314
+ }
315
+ }
316
+
317
+ async function loadConfig(cwd) {
318
+ const configPath = path.join(cwd, CONFIG_FILE)
319
+ const componentsPath = path.join(cwd, "components.json")
320
+ const config = structuredClone(DEFAULT_CONFIG)
321
+
322
+ const componentsConfig = await readJsonIfExists(componentsPath)
323
+ let hasExplicitCssPath = false
324
+ if (componentsConfig) {
325
+ if (componentsConfig.tailwind?.css) {
326
+ config.tailwind.css = componentsConfig.tailwind.css
327
+ hasExplicitCssPath = true
328
+ }
329
+ config.aliases.utils = componentsConfig.aliases?.utils || config.aliases.utils
330
+ config.aliases.hooks = componentsConfig.aliases?.hooks || config.aliases.hooks
331
+ config.aliases.ui =
332
+ componentsConfig.aliases?.ui ||
333
+ appendAliasSegment(componentsConfig.aliases?.components, "ui") ||
334
+ config.aliases.ui
335
+ }
336
+
337
+ const localConfig = await readJsonIfExists(configPath)
338
+ if (localConfig) {
339
+ config.style = localConfig.style || config.style
340
+ config.tsx = localConfig.tsx ?? config.tsx
341
+ if (localConfig.tailwind?.css) hasExplicitCssPath = true
342
+ config.tailwind = { ...config.tailwind, ...(localConfig.tailwind || {}) }
343
+ config.aliases = { ...config.aliases, ...(localConfig.aliases || {}) }
344
+ }
345
+
346
+ if (!hasExplicitCssPath) {
347
+ config.tailwind.css = await detectCssPath(cwd, config.tailwind.css)
348
+ }
349
+
350
+ return config
351
+ }
352
+
353
+ async function detectCssPath(cwd, fallback) {
354
+ const candidates = [
355
+ "app/globals.css",
356
+ "src/app/globals.css",
357
+ "src/index.css",
358
+ "src/globals.css",
359
+ "styles/globals.css",
360
+ ]
361
+
362
+ for (const candidate of candidates) {
363
+ if (await exists(path.join(cwd, candidate))) return candidate
364
+ }
365
+
366
+ if (await exists(path.join(cwd, "src", "app"))) return "src/app/globals.css"
367
+
368
+ return fallback
369
+ }
370
+
371
+ async function writeConfig(cwd, config, options) {
372
+ const configPath = path.join(cwd, CONFIG_FILE)
373
+ if ((await exists(configPath)) && !options.force) {
374
+ return [`kept ${relative(cwd, configPath)}`]
375
+ }
376
+
377
+ const status = await writeFile(
378
+ configPath,
379
+ `${JSON.stringify(config, null, 2)}\n`,
380
+ options
381
+ )
382
+ return [`${status} ${relative(cwd, configPath)}`]
383
+ }
384
+
385
+ async function writeBaseFiles(cwd, config, options) {
386
+ return writeRegistryFiles(cwd, config, registry.base.files || [], options)
387
+ }
388
+
389
+ async function writeRegistryFiles(cwd, config, files, options) {
390
+ const writes = []
391
+
392
+ for (const file of files) {
393
+ const sourcePath = path.join(registryRoot, file.source)
394
+ const targetPath = path.join(cwd, resolveTarget(cwd, config, file.target))
395
+ const source = await fs.readFile(sourcePath)
396
+ const content = isTextRegistryFile(file.source)
397
+ ? applyTemplate(source.toString("utf8"), config)
398
+ : source
399
+
400
+ const status = await writeFile(targetPath, content, options)
401
+ writes.push(`${status} ${relative(cwd, targetPath)}`)
402
+ }
403
+
404
+ return writes
405
+ }
406
+
407
+ async function writeGlobalCss(cwd, config, options) {
408
+ const cssPath = path.join(cwd, config.tailwind.css)
409
+ const sourcePath = path.join(registryRoot, "styles", "globals.css")
410
+ const tokenCss = await fs.readFile(sourcePath, "utf8")
411
+ const current = await readTextIfExists(cssPath)
412
+
413
+ if (current?.includes(CSS_START) && !options.force) {
414
+ return [`kept ${relative(cwd, cssPath)}`]
415
+ }
416
+
417
+ const nextCss = current
418
+ ? `${removeExistingTokenBlock(current).trimEnd()}\n\n${tokenCss}\n`
419
+ : `${tokenCss}\n`
420
+ const next = ensureGlobalCssImports(nextCss)
421
+
422
+ const status = await writeFile(cssPath, next, { ...options, force: true })
423
+ return [`${status} ${relative(cwd, cssPath)}`]
424
+ }
425
+
426
+ async function writeAliasConfig(cwd, config, options) {
427
+ if (!usesRootAlias(config, "@")) return []
428
+
429
+ const writes = []
430
+ writes.push(...(await writeTypescriptAliasConfig(cwd, "@", options)))
431
+ writes.push(...(await writeViteAliasConfig(cwd, "@", options)))
432
+ return writes
433
+ }
434
+
435
+ function usesRootAlias(config, aliasRoot) {
436
+ return Object.values(config.aliases || {}).some((alias) =>
437
+ alias === aliasRoot || alias?.startsWith(`${aliasRoot}/`)
438
+ )
439
+ }
440
+
441
+ async function writeTypescriptAliasConfig(cwd, aliasRoot, options) {
442
+ const tsconfigPath = await findTypescriptConfigPath(cwd)
443
+ if (!tsconfigPath) return []
444
+
445
+ const config = await readJsonc(tsconfigPath)
446
+ const compilerOptions = config.compilerOptions || {}
447
+ const paths = compilerOptions.paths || {}
448
+ const sourceRoot = shouldUseSrcDirectory(cwd) ? "./src" : "."
449
+ const aliasPattern = `${aliasRoot}/*`
450
+ const targetPattern = `${sourceRoot}/*`
451
+
452
+ if (Array.isArray(paths[aliasPattern]) && paths[aliasPattern].includes(targetPattern)) {
453
+ return [`kept ${relative(cwd, tsconfigPath)}`]
454
+ }
455
+
456
+ config.compilerOptions = {
457
+ ...compilerOptions,
458
+ paths: {
459
+ ...paths,
460
+ [aliasPattern]: [targetPattern],
461
+ },
462
+ }
463
+
464
+ const status = await writeFile(tsconfigPath, `${JSON.stringify(config, null, 2)}\n`, {
465
+ dryRun: options.dryRun,
466
+ force: true,
467
+ })
468
+ return [`${status} ${relative(cwd, tsconfigPath)}`]
469
+ }
470
+
471
+ async function findTypescriptConfigPath(cwd) {
472
+ const candidates = [
473
+ "tsconfig.app.json",
474
+ "tsconfig.json",
475
+ "jsconfig.json",
476
+ ]
477
+
478
+ for (const candidate of candidates) {
479
+ const target = path.join(cwd, candidate)
480
+ if (await exists(target)) return target
481
+ }
482
+
483
+ return null
484
+ }
485
+
486
+ async function writeViteAliasConfig(cwd, aliasRoot, options) {
487
+ const viteConfigPath = await findViteConfigPath(cwd)
488
+ if (!viteConfigPath) return []
489
+
490
+ const current = await fs.readFile(viteConfigPath, "utf8")
491
+ const sourceRoot = shouldUseSrcDirectory(cwd) ? "./src" : "."
492
+ const aliasLine = `"${aliasRoot}": fileURLToPath(new URL("${sourceRoot}", import.meta.url))`
493
+
494
+ if (
495
+ current.includes(aliasLine) &&
496
+ current.includes("@tailwindcss/vite") &&
497
+ current.includes("tailwindcss()")
498
+ ) {
499
+ return [`kept ${relative(cwd, viteConfigPath)}`]
500
+ }
501
+
502
+ let next = ensureNodeUrlImport(current)
503
+ next = ensureTailwindViteImport(next)
504
+ next = addViteResolveAlias(next, aliasRoot, sourceRoot)
505
+ next = addVitePluginCall(next, "tailwindcss()")
506
+
507
+ const status = await writeFile(viteConfigPath, next, {
508
+ dryRun: options.dryRun,
509
+ force: true,
510
+ })
511
+ return [`${status} ${relative(cwd, viteConfigPath)}`]
512
+ }
513
+
514
+ async function findViteConfigPath(cwd) {
515
+ const candidates = [
516
+ "vite.config.ts",
517
+ "vite.config.mts",
518
+ "vite.config.js",
519
+ "vite.config.mjs",
520
+ ]
521
+
522
+ for (const candidate of candidates) {
523
+ const target = path.join(cwd, candidate)
524
+ if (await exists(target)) return target
525
+ }
526
+
527
+ return null
528
+ }
529
+
530
+ function ensureNodeUrlImport(source) {
531
+ if (source.includes("fileURLToPath") && source.includes("node:url")) return source
532
+
533
+ return `import { fileURLToPath, URL } from "node:url"\n${source}`
534
+ }
535
+
536
+ function ensureTailwindViteImport(source) {
537
+ if (source.includes("@tailwindcss/vite")) return source
538
+
539
+ return `import tailwindcss from "@tailwindcss/vite"\n${source}`
540
+ }
541
+
542
+ function addViteResolveAlias(source, aliasRoot, sourceRoot) {
543
+ const aliasLine = `"${aliasRoot}": fileURLToPath(new URL("${sourceRoot}", import.meta.url))`
544
+ if (source.includes(aliasLine)) return source
545
+ if (!source.includes("defineConfig({")) return source
546
+
547
+ const resolveBlock = [
548
+ " resolve: {",
549
+ " alias: {",
550
+ ` "${aliasRoot}": fileURLToPath(new URL("${sourceRoot}", import.meta.url)),`,
551
+ " },",
552
+ " },",
553
+ ].join("\n")
554
+
555
+ return source.replace("defineConfig({", `defineConfig({\n${resolveBlock}`)
556
+ }
557
+
558
+ function addVitePluginCall(source, pluginCall) {
559
+ if (source.includes(pluginCall)) return source
560
+
561
+ const pluginsPattern = /plugins:\s*\[([\s\S]*?)\]/
562
+ if (pluginsPattern.test(source)) {
563
+ return source.replace(pluginsPattern, (_match, plugins) => {
564
+ const currentPlugins = plugins.trim().replace(/,\s*$/, "")
565
+ const nextPlugins = currentPlugins
566
+ ? `${currentPlugins}, ${pluginCall}`
567
+ : pluginCall
568
+ return `plugins: [${nextPlugins}]`
569
+ })
570
+ }
571
+
572
+ if (!source.includes("defineConfig({")) return source
573
+
574
+ return source.replace("defineConfig({", `defineConfig({\n plugins: [${pluginCall}],`)
575
+ }
576
+
577
+ async function getPackageChanges(cwd, dependencies) {
578
+ const devDependencies = { ...SETUP_DEV_DEPENDENCIES }
579
+ if (await findViteConfigPath(cwd)) {
580
+ Object.assign(devDependencies, VITE_DEV_DEPENDENCIES)
581
+ }
582
+
583
+ return {
584
+ dependencies,
585
+ devDependencies,
586
+ }
587
+ }
588
+
589
+ async function updatePackageJson(cwd, changes, options) {
590
+ const packagePath = path.join(cwd, "package.json")
591
+ const dependencies = changes.dependencies || {}
592
+ const devDependencies = changes.devDependencies || {}
593
+ const packageJson = (await readJsonIfExists(packagePath)) || {
594
+ name: path.basename(cwd).toLowerCase(),
595
+ version: "0.0.0",
596
+ private: true,
597
+ }
598
+ const added = [
599
+ ...addPackageSection(packageJson, "dependencies", dependencies),
600
+ ...addPackageSection(packageJson, "devDependencies", devDependencies),
601
+ ]
602
+
603
+ if (added.length === 0 && (await exists(packagePath))) {
604
+ return [`kept ${relative(cwd, packagePath)}`]
605
+ }
606
+
607
+ const status = await writeFile(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`, {
608
+ dryRun: options.dryRun,
609
+ force: true,
610
+ })
611
+
612
+ return [
613
+ `${status} ${relative(cwd, packagePath)}`,
614
+ ]
615
+ }
616
+
617
+ function addPackageSection(packageJson, section, dependencies) {
618
+ const added = []
619
+ const existing = packageJson[section] || {}
620
+
621
+ for (const [name, version] of Object.entries(dependencies)) {
622
+ if (hasPackageDependency(packageJson, name)) continue
623
+
624
+ existing[name] = version
625
+ added.push(name)
626
+ }
627
+
628
+ if (added.length > 0 || packageJson[section]) {
629
+ packageJson[section] = sortObject(existing)
630
+ }
631
+
632
+ return added
633
+ }
634
+
635
+ function hasPackageDependency(packageJson, name) {
636
+ return Boolean(
637
+ packageJson.dependencies?.[name] ||
638
+ packageJson.devDependencies?.[name] ||
639
+ packageJson.peerDependencies?.[name] ||
640
+ packageJson.optionalDependencies?.[name]
641
+ )
642
+ }
643
+
644
+ function applyTemplate(source, config) {
645
+ return source
646
+ .replaceAll("@/lib/utils", config.aliases.utils)
647
+ .replaceAll("@/hooks", config.aliases.hooks)
648
+ }
649
+
650
+ function isTextRegistryFile(source) {
651
+ return !/\.(avif|eot|gif|ico|jpe?g|otf|png|ttf|webp|woff2?)$/i.test(source)
652
+ }
653
+
654
+ function resolveTarget(cwd, config, target) {
655
+ const replacements = {
656
+ "{{ui}}": aliasToPath(cwd, config.aliases.ui),
657
+ "{{utils}}": stripExtension(aliasToPath(cwd, config.aliases.utils)),
658
+ "{{hooks}}": aliasToPath(cwd, config.aliases.hooks),
659
+ }
660
+
661
+ let resolved = target
662
+ for (const [placeholder, value] of Object.entries(replacements)) {
663
+ resolved = resolved.replaceAll(placeholder, value)
664
+ }
665
+
666
+ return resolved
667
+ }
668
+
669
+ function aliasToPath(cwd, alias) {
670
+ if (!alias) return ""
671
+ if (path.isAbsolute(alias)) return alias
672
+ if (alias.startsWith("./")) return alias.slice(2)
673
+ if (alias.startsWith("../")) return alias
674
+
675
+ if (alias.startsWith("@/") || alias.startsWith("~/")) {
676
+ const rest = alias.slice(2)
677
+ return path.join(shouldUseSrcDirectory(cwd) ? "src" : "", rest)
678
+ }
679
+
680
+ return alias
681
+ }
682
+
683
+ function shouldUseSrcDirectory(cwd) {
684
+ return pathExistsSyncHint(cwd, "src/app") || pathExistsSyncHint(cwd, "src")
685
+ }
686
+
687
+ function pathExistsSyncHint(cwd, segment) {
688
+ return Boolean(syncExistsCache.get(path.join(cwd, segment)))
689
+ }
690
+
691
+ async function warmPathHints(cwd) {
692
+ for (const segment of ["src", "src/app"]) {
693
+ const target = path.join(cwd, segment)
694
+ syncExistsCache.set(target, await exists(target))
695
+ }
696
+ }
697
+
698
+ async function writeFile(targetPath, content, options) {
699
+ const alreadyExists = await exists(targetPath)
700
+ const isBinary = Buffer.isBuffer(content)
701
+
702
+ if (options.dryRun) {
703
+ if (!alreadyExists) return "would write"
704
+
705
+ const current = await fs.readFile(targetPath)
706
+ const isEqual = isBinary
707
+ ? current.equals(content)
708
+ : current.toString("utf8") === content
709
+ if (isEqual) return "would keep"
710
+
711
+ return "would update"
712
+ }
713
+
714
+ if (alreadyExists && !options.force) {
715
+ const current = await fs.readFile(targetPath)
716
+ const isEqual = isBinary
717
+ ? current.equals(content)
718
+ : current.toString("utf8") === content
719
+ if (isEqual) return "kept"
720
+ }
721
+
722
+ await fs.mkdir(path.dirname(targetPath), { recursive: true })
723
+ await fs.writeFile(targetPath, content, isBinary ? undefined : "utf8")
724
+ return alreadyExists ? "updated" : "wrote"
725
+ }
726
+
727
+ async function readJson(filePath) {
728
+ return JSON.parse(await fs.readFile(filePath, "utf8"))
729
+ }
730
+
731
+ async function readJsonc(filePath) {
732
+ const content = await fs.readFile(filePath, "utf8")
733
+ return JSON.parse(stripJsonComments(content))
734
+ }
735
+
736
+ function stripJsonComments(content) {
737
+ return content
738
+ .replace(/\/\*[\s\S]*?\*\//g, "")
739
+ .replace(/(^|[^:])\/\/.*$/gm, "$1")
740
+ }
741
+
742
+ async function readJsonIfExists(filePath) {
743
+ try {
744
+ return await readJson(filePath)
745
+ } catch (error) {
746
+ if (error.code === "ENOENT") return null
747
+ throw error
748
+ }
749
+ }
750
+
751
+ async function readTextIfExists(filePath) {
752
+ try {
753
+ return await fs.readFile(filePath, "utf8")
754
+ } catch (error) {
755
+ if (error.code === "ENOENT") return null
756
+ throw error
757
+ }
758
+ }
759
+
760
+ async function exists(filePath) {
761
+ try {
762
+ await fs.access(filePath)
763
+ return true
764
+ } catch {
765
+ return false
766
+ }
767
+ }
768
+
769
+ function removeExistingTokenBlock(content) {
770
+ const start = content.indexOf(CSS_START)
771
+ if (start === -1) return content
772
+
773
+ const end = content.indexOf(CSS_END, start)
774
+ if (end === -1) return content
775
+
776
+ return `${content.slice(0, start)}${content.slice(end + CSS_END.length)}`
777
+ }
778
+
779
+ function ensureGlobalCssImports(content) {
780
+ const fontImportPattern =
781
+ /^\s*@import\s+["']@fontsource(?:-variable)?\/inter(?:\/(?:400|500|600|700)\.css)?["'];\s*/gm
782
+ const tailwindImportPattern = /^\s*@import\s+["']tailwindcss["'];\s*/gm
783
+ const cleaned = content
784
+ .replace(fontImportPattern, "")
785
+ .replace(tailwindImportPattern, "")
786
+ .trimStart()
787
+ return `${TAILWIND_IMPORT}\n${FONT_IMPORTS.join("\n")}\n${cleaned}`
788
+ }
789
+
790
+ function appendAliasSegment(alias, segment) {
791
+ if (!alias) return null
792
+ return `${alias.replace(/\/$/, "")}/${segment}`
793
+ }
794
+
795
+ function stripExtension(filePath) {
796
+ return filePath.replace(/\.(t|j)sx?$/, "")
797
+ }
798
+
799
+ function sortObject(value) {
800
+ return Object.fromEntries(
801
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right))
802
+ )
803
+ }
804
+
805
+ function relative(cwd, targetPath) {
806
+ return path.relative(cwd, targetPath).replaceAll("\\", "/")
807
+ }
808
+
809
+ function printResult(title, writes, dryRun) {
810
+ console.log(`${dryRun ? "Dry run: " : ""}${title}`)
811
+ for (const write of writes) {
812
+ console.log(`- ${write}`)
813
+ }
814
+ }
815
+
816
+ function printInstallHint(writes, dryRun) {
817
+ if (dryRun) return
818
+
819
+ const packageJsonChanged = writes.some((write) =>
820
+ /^(wrote|updated) package\.json$/.test(write)
821
+ )
822
+
823
+ if (packageJsonChanged) {
824
+ console.log("\nNext: run npm install to install the added dependencies.")
825
+ }
826
+ }