uifolio 0.5.5 → 0.7.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.
- package/README.md +41 -8
- package/cli/uifolio.mjs +465 -51
- package/package.json +2 -1
- package/src/components/animations/animation-settings-context.ts +71 -0
- package/src/components/animations/animation-settings-provider.tsx +69 -0
- package/src/components/animations/data.ts +68 -0
- package/src/components/animations/horizontal-accordion.tsx +142 -0
- package/src/components/animations/horizontal-story.tsx +204 -0
- package/src/components/animations/image-crossfade.tsx +276 -0
- package/src/components/animations/palette-context.ts +24 -0
- package/src/components/animations/palette-provider.tsx +28 -0
- package/src/components/animations/palette.ts +18 -0
- package/src/components/animations/scroll-text.tsx +161 -0
- package/src/components/animations/stacking-cards.tsx +162 -0
package/cli/uifolio.mjs
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* Options:
|
|
14
14
|
* --cwd <dir> target project (default: current directory)
|
|
15
15
|
* --dir <path> components folder (default: src/components/ui)
|
|
16
|
+
* --animations <path> animations folder (default: src/components/animations)
|
|
16
17
|
* --lib <path> helpers folder (default: src/lib)
|
|
17
18
|
* --alias <alias> target import alias (default: @)
|
|
18
19
|
* --color <hex> primary color, written in the Tailwind tokens (@theme)
|
|
@@ -31,20 +32,32 @@
|
|
|
31
32
|
* node cli/uifolio.mjs add video-player --alias "#lib" --dry-run
|
|
32
33
|
*/
|
|
33
34
|
|
|
35
|
+
import { execFileSync } from 'node:child_process'
|
|
34
36
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
35
|
-
import { basename, dirname, join, relative, resolve } from 'node:path'
|
|
37
|
+
import { basename, dirname, join, relative, resolve, sep } from 'node:path'
|
|
36
38
|
import { createInterface } from 'node:readline/promises'
|
|
37
39
|
import { fileURLToPath } from 'node:url'
|
|
38
40
|
|
|
39
41
|
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
40
42
|
const REGISTRY_SRC = resolve(HERE, '..', 'src')
|
|
41
43
|
const REGISTRY_UI = join(REGISTRY_SRC, 'components', 'ui')
|
|
44
|
+
const REGISTRY_ANIMATIONS = join(REGISTRY_SRC, 'components', 'animations')
|
|
42
45
|
const CONFIG_NAME = 'uifolio.json'
|
|
43
46
|
|
|
47
|
+
/** Stand-alone animations (the providers/contexts are helpers, not entries). */
|
|
48
|
+
const ANIMATION_NAMES = [
|
|
49
|
+
'horizontal-accordion',
|
|
50
|
+
'horizontal-story',
|
|
51
|
+
'image-crossfade',
|
|
52
|
+
'scroll-text',
|
|
53
|
+
'stacking-cards',
|
|
54
|
+
]
|
|
55
|
+
|
|
44
56
|
const LIB_DEPS = ['clsx', 'tailwind-merge'] // cn() — always copied with the helpers
|
|
45
57
|
|
|
46
58
|
const DEFAULT_ACCENT = '#10b981'
|
|
47
59
|
const DEFAULT_COMPONENTS_DIR = 'src/components/ui'
|
|
60
|
+
const DEFAULT_ANIMATIONS_DIR = 'src/components/animations'
|
|
48
61
|
const THEME_FILE = 'uifolio-theme.css'
|
|
49
62
|
|
|
50
63
|
/** Global CSS candidates for wiring the Tailwind theme. */
|
|
@@ -52,8 +65,32 @@ const GLOBAL_CSS_CANDIDATES = [
|
|
|
52
65
|
'src/index.css',
|
|
53
66
|
'src/app.css',
|
|
54
67
|
'src/globals.css',
|
|
68
|
+
'src/styles/globals.css',
|
|
69
|
+
'src/styles/index.css',
|
|
70
|
+
'src/main.css',
|
|
71
|
+
'src/style.css',
|
|
72
|
+
'app/globals.css',
|
|
55
73
|
]
|
|
56
74
|
|
|
75
|
+
/** Folders never scanned when looking for the project's stylesheets. */
|
|
76
|
+
const IGNORED_DIRS = new Set([
|
|
77
|
+
'node_modules',
|
|
78
|
+
'dist',
|
|
79
|
+
'build',
|
|
80
|
+
'out',
|
|
81
|
+
'coverage',
|
|
82
|
+
'public',
|
|
83
|
+
'.git',
|
|
84
|
+
'.next',
|
|
85
|
+
'.vite',
|
|
86
|
+
'.turbo',
|
|
87
|
+
])
|
|
88
|
+
|
|
89
|
+
const MAX_SCAN_DEPTH = 6
|
|
90
|
+
|
|
91
|
+
/** A Tailwind CSS 4 entrypoint: `@import 'tailwindcss';` or `@tailwind base;`. */
|
|
92
|
+
const TAILWIND_ENTRY = /@import\s+['"]tailwindcss(?:[^'"]*)['"]|@tailwind\s+base/
|
|
93
|
+
|
|
57
94
|
/* -------------------------------------------------------------------------- */
|
|
58
95
|
/* Utilitaires */
|
|
59
96
|
/* -------------------------------------------------------------------------- */
|
|
@@ -67,6 +104,10 @@ function ok(message) {
|
|
|
67
104
|
console.log(`\x1b[32m✓\x1b[0m ${message}`)
|
|
68
105
|
}
|
|
69
106
|
|
|
107
|
+
function warn(message) {
|
|
108
|
+
console.log(`\x1b[33m!\x1b[0m ${message}`)
|
|
109
|
+
}
|
|
110
|
+
|
|
70
111
|
function info(message) {
|
|
71
112
|
console.log(` ${message}`)
|
|
72
113
|
}
|
|
@@ -117,7 +158,7 @@ function writeFailure(error, target) {
|
|
|
117
158
|
fail(`Cannot write ${target}: ${reasons[error?.code] ?? error?.message ?? error}.`)
|
|
118
159
|
}
|
|
119
160
|
|
|
120
|
-
function
|
|
161
|
+
function listUiComponents() {
|
|
121
162
|
if (!existsSync(REGISTRY_UI)) return []
|
|
122
163
|
return readdirSync(REGISTRY_UI)
|
|
123
164
|
.filter((name) => name.endsWith('.tsx'))
|
|
@@ -125,6 +166,24 @@ function listComponents() {
|
|
|
125
166
|
.sort()
|
|
126
167
|
}
|
|
127
168
|
|
|
169
|
+
function listAnimationComponents() {
|
|
170
|
+
if (!existsSync(REGISTRY_ANIMATIONS)) return []
|
|
171
|
+
return ANIMATION_NAMES.filter((name) => existsSync(join(REGISTRY_ANIMATIONS, `${name}.tsx`)))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function listComponents() {
|
|
175
|
+
return [...listUiComponents(), ...listAnimationComponents()].sort()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Absolute entry + destination kind of a registry name. */
|
|
179
|
+
function registryEntry(name) {
|
|
180
|
+
const ui = join(REGISTRY_UI, `${name}.tsx`)
|
|
181
|
+
if (existsSync(ui)) return { path: ui, kind: 'ui' }
|
|
182
|
+
const animation = join(REGISTRY_ANIMATIONS, `${name}.tsx`)
|
|
183
|
+
if (existsSync(animation)) return { path: animation, kind: 'animations' }
|
|
184
|
+
return null
|
|
185
|
+
}
|
|
186
|
+
|
|
128
187
|
function readConfig(cwd) {
|
|
129
188
|
const file = join(cwd, CONFIG_NAME)
|
|
130
189
|
if (!existsSync(file)) return null
|
|
@@ -135,14 +194,19 @@ function readConfig(cwd) {
|
|
|
135
194
|
}
|
|
136
195
|
}
|
|
137
196
|
|
|
138
|
-
/**
|
|
197
|
+
/** Local imports (alias or relative) of a source file. */
|
|
139
198
|
function localImports(source) {
|
|
140
199
|
const specifiers = new Set()
|
|
141
200
|
const importRe = /(?:from\s+|import\s*\(\s*|import\s+)['"]([^'"]+)['"]/g
|
|
142
201
|
let match
|
|
143
202
|
while ((match = importRe.exec(source)) !== null) {
|
|
144
203
|
const specifier = match[1]
|
|
145
|
-
if (
|
|
204
|
+
if (
|
|
205
|
+
specifier.startsWith('@/') ||
|
|
206
|
+
specifier.startsWith('~/') ||
|
|
207
|
+
specifier.startsWith('./') ||
|
|
208
|
+
specifier.startsWith('../')
|
|
209
|
+
) {
|
|
146
210
|
specifiers.add(specifier)
|
|
147
211
|
}
|
|
148
212
|
}
|
|
@@ -276,12 +340,14 @@ function collectFiles(entryPath, srcRoot) {
|
|
|
276
340
|
|
|
277
341
|
const source = readFileSync(current, 'utf8')
|
|
278
342
|
for (const specifier of localImports(source)) {
|
|
279
|
-
const base = specifier.
|
|
343
|
+
const base = specifier.startsWith('.')
|
|
344
|
+
? resolve(dirname(current), specifier)
|
|
345
|
+
: join(srcRoot, specifier.replace(/^[@~]\//, ''))
|
|
280
346
|
const candidates = [
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
join(
|
|
284
|
-
join(
|
|
347
|
+
`${base}.ts`,
|
|
348
|
+
`${base}.tsx`,
|
|
349
|
+
join(base, 'index.ts'),
|
|
350
|
+
join(base, 'index.tsx'),
|
|
285
351
|
]
|
|
286
352
|
const found = candidates.find((candidate) => existsSync(candidate))
|
|
287
353
|
if (found) queue.push(found)
|
|
@@ -389,19 +455,276 @@ ${darkTokens.join('\n')}
|
|
|
389
455
|
`
|
|
390
456
|
}
|
|
391
457
|
|
|
392
|
-
/**
|
|
458
|
+
/** Every stylesheet of the project, outside the build/ignored folders. */
|
|
459
|
+
function listCssFiles(cwd) {
|
|
460
|
+
const found = []
|
|
461
|
+
const walk = (dir, depth) => {
|
|
462
|
+
if (depth > MAX_SCAN_DEPTH) return
|
|
463
|
+
let entries
|
|
464
|
+
try {
|
|
465
|
+
entries = readdirSync(dir, { withFileTypes: true })
|
|
466
|
+
} catch {
|
|
467
|
+
return
|
|
468
|
+
}
|
|
469
|
+
for (const entry of entries) {
|
|
470
|
+
const full = join(dir, entry.name)
|
|
471
|
+
if (entry.isDirectory()) {
|
|
472
|
+
if (entry.name.startsWith('.') || IGNORED_DIRS.has(entry.name)) continue
|
|
473
|
+
walk(full, depth + 1)
|
|
474
|
+
} else if (/\.css$/i.test(entry.name)) {
|
|
475
|
+
found.push(full)
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
walk(resolve(cwd), 0)
|
|
480
|
+
return found.sort()
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* CSS global du projet: celui qui importe Tailwind en priorité, quel que soit
|
|
485
|
+
* son chemin (src/index.css, src/styles/globals.css, app/globals.css…).
|
|
486
|
+
*/
|
|
393
487
|
function findGlobalCss(cwd) {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
488
|
+
const known = GLOBAL_CSS_CANDIDATES.map((candidate) => join(resolve(cwd), candidate))
|
|
489
|
+
const candidates = [...new Set([...known, ...listCssFiles(cwd)])].filter((file) =>
|
|
490
|
+
existsSync(file),
|
|
491
|
+
)
|
|
492
|
+
const withTailwind = candidates.find((file) => TAILWIND_ENTRY.test(readFileSync(file, 'utf8')))
|
|
493
|
+
return withTailwind ?? candidates[0] ?? null
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** A dependency of the target project's manifest? */
|
|
497
|
+
function hasDependency(cwd, name) {
|
|
498
|
+
const file = join(cwd, 'package.json')
|
|
499
|
+
if (!existsSync(file)) return false
|
|
500
|
+
try {
|
|
501
|
+
const manifest = JSON.parse(readFileSync(file, 'utf8'))
|
|
502
|
+
return ['dependencies', 'devDependencies', 'peerDependencies'].some((key) =>
|
|
503
|
+
Boolean(manifest[key]?.[name]),
|
|
504
|
+
)
|
|
505
|
+
} catch {
|
|
506
|
+
return false
|
|
399
507
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Tailwind must be installed *and* imported by a CSS file: writing the tokens
|
|
512
|
+
* is not enough, without Tailwind the classes (bg-primary, fixed…) do not exist.
|
|
513
|
+
*/
|
|
514
|
+
function tailwindStatus(cwd, globalCss) {
|
|
515
|
+
const hasTailwind = hasDependency(cwd, 'tailwindcss')
|
|
516
|
+
const hasEntry =
|
|
517
|
+
Boolean(globalCss) &&
|
|
518
|
+
existsSync(globalCss) &&
|
|
519
|
+
TAILWIND_ENTRY.test(readFileSync(globalCss, 'utf8'))
|
|
520
|
+
return { hasDependency: hasTailwind, hasEntry }
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** Package manager of the target project, from its lockfile. */
|
|
524
|
+
function packageManager(cwd, dev = true) {
|
|
525
|
+
const flag = dev ? ['-D'] : []
|
|
526
|
+
if (existsSync(join(cwd, 'pnpm-lock.yaml'))) return { command: 'pnpm', args: ['add', ...flag] }
|
|
527
|
+
if (existsSync(join(cwd, 'yarn.lock'))) return { command: 'yarn', args: ['add', ...flag] }
|
|
528
|
+
if (existsSync(join(cwd, 'bun.lockb')) || existsSync(join(cwd, 'bun.lock'))) {
|
|
529
|
+
return { command: 'bun', args: ['add', ...(dev ? ['-d'] : [])] }
|
|
403
530
|
}
|
|
404
|
-
return
|
|
531
|
+
return { command: 'npm', args: ['install', ...flag] }
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* The components import npm packages (clsx, lucide-react, hls.js…): install the
|
|
536
|
+
* ones missing from the manifest so the copied files compile right away.
|
|
537
|
+
*/
|
|
538
|
+
function installDependencies({ cwd, deps, dryRun }) {
|
|
539
|
+
const missing = [...deps].filter((dep) => !hasDependency(cwd, dep)).sort()
|
|
540
|
+
const manager = packageManager(cwd, false)
|
|
541
|
+
const command = [manager.command, ...manager.args, ...missing].join(' ')
|
|
542
|
+
if (missing.length === 0) return { missing, command }
|
|
543
|
+
|
|
544
|
+
if (dryRun) {
|
|
545
|
+
info(`[dry-run] ${command}`)
|
|
546
|
+
return { missing, command }
|
|
547
|
+
}
|
|
548
|
+
// Not a JS project: nothing to install into, print the command instead.
|
|
549
|
+
if (!existsSync(join(cwd, 'package.json'))) return { missing, command }
|
|
550
|
+
|
|
551
|
+
info(`Installing the npm dependencies: ${command}`)
|
|
552
|
+
try {
|
|
553
|
+
execFileSync(manager.command, [...manager.args, ...missing], {
|
|
554
|
+
cwd,
|
|
555
|
+
stdio: 'inherit',
|
|
556
|
+
shell: process.platform === 'win32',
|
|
557
|
+
})
|
|
558
|
+
} catch {
|
|
559
|
+
warn('The automatic install failed: run the command above yourself.')
|
|
560
|
+
return { missing, command }
|
|
561
|
+
}
|
|
562
|
+
ok(`Installed: ${missing.join(', ')}`)
|
|
563
|
+
return { missing: [], command }
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const VITE_CONFIGS = [
|
|
567
|
+
'vite.config.ts',
|
|
568
|
+
'vite.config.js',
|
|
569
|
+
'vite.config.mjs',
|
|
570
|
+
'vite.config.mts',
|
|
571
|
+
'vite.config.cjs',
|
|
572
|
+
'vite.config.cts',
|
|
573
|
+
]
|
|
574
|
+
|
|
575
|
+
/** Modules that load the global CSS in a Vite/React project. */
|
|
576
|
+
const ENTRY_MODULES = [
|
|
577
|
+
'src/main.tsx',
|
|
578
|
+
'src/main.jsx',
|
|
579
|
+
'src/main.ts',
|
|
580
|
+
'src/main.js',
|
|
581
|
+
'src/index.tsx',
|
|
582
|
+
'src/index.jsx',
|
|
583
|
+
'app/layout.tsx',
|
|
584
|
+
'app/layout.jsx',
|
|
585
|
+
]
|
|
586
|
+
|
|
587
|
+
function findFirst(cwd, names) {
|
|
588
|
+
return names.map((name) => join(cwd, name)).find((file) => existsSync(file)) ?? null
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** Adds `import tailwindcss from '@tailwindcss/vite'` and `tailwindcss()`. */
|
|
592
|
+
function wireViteConfig(source) {
|
|
593
|
+
if (source.includes('@tailwindcss/vite')) return source
|
|
594
|
+
const plugins = /plugins\s*:\s*\[/.exec(source)
|
|
595
|
+
if (!plugins) return null
|
|
596
|
+
const at = plugins.index + plugins[0].length
|
|
597
|
+
const next = `${source.slice(0, at)}tailwindcss(), ${source.slice(at)}`
|
|
598
|
+
const lines = next.split('\n')
|
|
599
|
+
const lastImport = lines.reduce((last, line, index) => (/^\s*import\s/.test(line) ? index : last), -1)
|
|
600
|
+
const importLine = "import tailwindcss from '@tailwindcss/vite'"
|
|
601
|
+
if (lastImport === -1) return `${importLine}\n${next}`
|
|
602
|
+
lines.splice(lastImport + 1, 0, importLine)
|
|
603
|
+
return lines.join('\n')
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** Creates the Tailwind entrypoint when the project has no stylesheet at all. */
|
|
607
|
+
function defaultCssPath(cwd) {
|
|
608
|
+
if (existsSync(join(cwd, 'src'))) return join(cwd, 'src', 'index.css')
|
|
609
|
+
if (existsSync(join(cwd, 'app'))) return join(cwd, 'app', 'globals.css')
|
|
610
|
+
return join(cwd, 'index.css')
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Installs Tailwind CSS 4 and wires it (CSS entry, entry module, Vite plugin)
|
|
615
|
+
* when the target project does not have it. Without Tailwind no utility class
|
|
616
|
+
* exists, so the components would render unstyled.
|
|
617
|
+
*/
|
|
618
|
+
function setupTailwind({ cwd, globalCss, dryRun }) {
|
|
619
|
+
// Not a JS project (or nothing to install into): keep the manual instructions.
|
|
620
|
+
if (!existsSync(join(cwd, 'package.json'))) {
|
|
621
|
+
return { globalCss, reason: 'no-package' }
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const usesVite = Boolean(findFirst(cwd, VITE_CONFIGS)) || hasDependency(cwd, 'vite')
|
|
625
|
+
const manager = packageManager(cwd)
|
|
626
|
+
const packages = usesVite ? ['tailwindcss', '@tailwindcss/vite'] : ['tailwindcss']
|
|
627
|
+
const command = [manager.command, ...manager.args, ...packages].join(' ')
|
|
628
|
+
|
|
629
|
+
if (!hasDependency(cwd, 'tailwindcss')) {
|
|
630
|
+
if (dryRun) info(`[dry-run] ${command}`)
|
|
631
|
+
else info(`Tailwind CSS missing, installing it: ${command}`)
|
|
632
|
+
}
|
|
633
|
+
if (!dryRun && !hasDependency(cwd, 'tailwindcss')) {
|
|
634
|
+
try {
|
|
635
|
+
execFileSync(manager.command, [...manager.args, ...packages], {
|
|
636
|
+
cwd,
|
|
637
|
+
stdio: 'inherit',
|
|
638
|
+
shell: process.platform === 'win32',
|
|
639
|
+
})
|
|
640
|
+
ok('Tailwind CSS installed')
|
|
641
|
+
} catch {
|
|
642
|
+
warn('The automatic install failed.')
|
|
643
|
+
return { globalCss, reason: 'install-failed' }
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// 1. A stylesheet that imports Tailwind.
|
|
648
|
+
let cssFile = globalCss
|
|
649
|
+
if (!cssFile) {
|
|
650
|
+
cssFile = defaultCssPath(cwd)
|
|
651
|
+
if (dryRun) {
|
|
652
|
+
info(`[dry-run] create ${relative(cwd, cssFile)} with "@import 'tailwindcss';"`)
|
|
653
|
+
} else {
|
|
654
|
+
mkdirSync(dirname(cssFile), { recursive: true })
|
|
655
|
+
writeFileSync(cssFile, "@import 'tailwindcss';\n", 'utf8')
|
|
656
|
+
ok(`Tailwind entry created: ${relative(cwd, cssFile)}`)
|
|
657
|
+
}
|
|
658
|
+
} else if (!TAILWIND_ENTRY.test(readFileSync(cssFile, 'utf8'))) {
|
|
659
|
+
if (dryRun) {
|
|
660
|
+
info(`[dry-run] add "@import 'tailwindcss';" to ${relative(cwd, cssFile)}`)
|
|
661
|
+
} else {
|
|
662
|
+
const source = readFileSync(cssFile, 'utf8')
|
|
663
|
+
writeFileSync(cssFile, `@import 'tailwindcss';\n\n${source}`, 'utf8')
|
|
664
|
+
ok(`Tailwind imported in ${relative(cwd, cssFile)}`)
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// 2. That stylesheet must be loaded by the app entry.
|
|
669
|
+
const entryModule = findFirst(cwd, ENTRY_MODULES)
|
|
670
|
+
const loadsCss = Boolean(entryModule) && readFileSync(entryModule, 'utf8').includes(basename(cssFile))
|
|
671
|
+
if (entryModule && !loadsCss) {
|
|
672
|
+
if (dryRun) {
|
|
673
|
+
info(`[dry-run] import ${basename(cssFile)} in ${relative(cwd, entryModule)}`)
|
|
674
|
+
} else {
|
|
675
|
+
const specifier = relative(dirname(entryModule), cssFile).replace(/\\/g, '/')
|
|
676
|
+
const source = readFileSync(entryModule, 'utf8')
|
|
677
|
+
writeFileSync(
|
|
678
|
+
entryModule,
|
|
679
|
+
`import '${specifier.startsWith('.') ? '' : './'}${specifier}'\n${source}`,
|
|
680
|
+
'utf8',
|
|
681
|
+
)
|
|
682
|
+
ok(`CSS imported in ${relative(cwd, entryModule)}`)
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// 3. Vite only runs Tailwind through its plugin.
|
|
687
|
+
if (usesVite) {
|
|
688
|
+
const config = findFirst(cwd, VITE_CONFIGS)
|
|
689
|
+
if (!config) {
|
|
690
|
+
warn('No vite.config.* found: add tailwindcss() to your Vite plugins.')
|
|
691
|
+
} else {
|
|
692
|
+
const source = readFileSync(config, 'utf8')
|
|
693
|
+
const updated = wireViteConfig(source)
|
|
694
|
+
if (updated === null) {
|
|
695
|
+
warn(`Add the tailwindcss() plugin in ${relative(cwd, config)} (no "plugins: [" found).`)
|
|
696
|
+
} else if (updated !== source && dryRun) {
|
|
697
|
+
info(`[dry-run] add tailwindcss() to ${relative(cwd, config)}`)
|
|
698
|
+
} else if (updated !== source) {
|
|
699
|
+
writeFileSync(config, updated, 'utf8')
|
|
700
|
+
ok(`tailwindcss() plugin added to ${relative(cwd, config)}`)
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
} else {
|
|
704
|
+
info('Tailwind 4 also needs @tailwindcss/postcss in your PostCSS config.')
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return { globalCss: cssFile, reason: dryRun ? 'dry-run' : 'ready' }
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/** Actionable steps instead of a silent unstyled page. */
|
|
711
|
+
function reportMissingTailwind({ cwd, globalCss, themeFile, status }) {
|
|
712
|
+
const cssLabel = globalCss ? relative(cwd, globalCss) : 'your global CSS'
|
|
713
|
+
const specifier = globalCss
|
|
714
|
+
? `./${relative(dirname(globalCss), themeFile).replace(/\\/g, '/')}`
|
|
715
|
+
: `./${THEME_FILE}`
|
|
716
|
+
console.log('')
|
|
717
|
+
if (!status.hasDependency) {
|
|
718
|
+
const manager = packageManager(cwd)
|
|
719
|
+
warn('Tailwind CSS is not installed in this project: the components will render unstyled.')
|
|
720
|
+
info([manager.command, ...manager.args, 'tailwindcss', '@tailwindcss/vite'].join(' '))
|
|
721
|
+
info(`Then add "@import 'tailwindcss';" at the top of ${cssLabel}`)
|
|
722
|
+
info('and tailwindcss() to the Vite plugins (plugins: [react(), tailwindcss()]).')
|
|
723
|
+
} else {
|
|
724
|
+
warn(`Tailwind CSS is installed but ${cssLabel} does not import it yet.`)
|
|
725
|
+
info(`Add "@import 'tailwindcss';" at the top of ${cssLabel}.`)
|
|
726
|
+
}
|
|
727
|
+
info(`Then "@import '${specifier}';", or re-run "uifolio add" to wire it automatically.`)
|
|
405
728
|
}
|
|
406
729
|
|
|
407
730
|
/** Adds `@import './uifolio-theme.css';` right after the Tailwind import. */
|
|
@@ -430,7 +753,16 @@ function existingRadius(file) {
|
|
|
430
753
|
}
|
|
431
754
|
|
|
432
755
|
function writeTheme({ cwd, accent, radius, dryRun }) {
|
|
433
|
-
|
|
756
|
+
let globalCss = findGlobalCss(cwd)
|
|
757
|
+
|
|
758
|
+
// Install + wire Tailwind first: the tokens alone style nothing.
|
|
759
|
+
if (!tailwindStatus(cwd, globalCss).hasEntry) {
|
|
760
|
+
console.log('')
|
|
761
|
+
info('Tailwind CSS is not set up in this project: configuring it.')
|
|
762
|
+
globalCss = setupTailwind({ cwd, globalCss, dryRun }).globalCss
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const status = tailwindStatus(cwd, globalCss)
|
|
434
766
|
const themeFile = globalCss
|
|
435
767
|
? join(dirname(globalCss), THEME_FILE)
|
|
436
768
|
: join(cwd, THEME_FILE)
|
|
@@ -450,15 +782,19 @@ function writeTheme({ cwd, accent, radius, dryRun }) {
|
|
|
450
782
|
ok(`Tailwind theme written to ${label}`)
|
|
451
783
|
info(`--primary: ${finalAccent}${finalRadius === undefined ? '' : ` · --radius: ${finalRadius}px`}`)
|
|
452
784
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
} else {
|
|
460
|
-
info(`Add "@import './${THEME_FILE}';", at the top of your global CSS (the Tailwind one).`)
|
|
785
|
+
// The tokens only matter once Tailwind actually generates the utilities:
|
|
786
|
+
// without a `@import 'tailwindcss'`, wiring the theme would look installed
|
|
787
|
+
// and still render an unstyled page.
|
|
788
|
+
if (!status.hasEntry) {
|
|
789
|
+
reportMissingTailwind({ cwd, globalCss, themeFile, status })
|
|
790
|
+
return
|
|
461
791
|
}
|
|
792
|
+
|
|
793
|
+
const source = readFileSync(globalCss, 'utf8')
|
|
794
|
+
const specifier = `./${basename(themeFile)}`
|
|
795
|
+
const updated = wireThemeImport(source, specifier)
|
|
796
|
+
if (updated !== source) writeFileSync(globalCss, updated, 'utf8')
|
|
797
|
+
info(`imported in ${relative(cwd, globalCss)} — no hard-coded colors in the components`)
|
|
462
798
|
}
|
|
463
799
|
|
|
464
800
|
/* -------------------------------------------------------------------------- */
|
|
@@ -578,6 +914,40 @@ const USAGE_EXAMPLES = {
|
|
|
578
914
|
<p className="text-sm">Fade in on mount.</p>
|
|
579
915
|
</FadeIn>
|
|
580
916
|
)
|
|
917
|
+
`,
|
|
918
|
+
},
|
|
919
|
+
'horizontal-accordion': {
|
|
920
|
+
body: ` return <HorizontalAccordion count={6} />
|
|
921
|
+
`,
|
|
922
|
+
},
|
|
923
|
+
'horizontal-story': {
|
|
924
|
+
body: ` return (
|
|
925
|
+
<HorizontalStory
|
|
926
|
+
count={6}
|
|
927
|
+
palette={{ mode: 'gradient', from: '#0EA5E9', to: '#8B5CF6' }}
|
|
928
|
+
/>
|
|
929
|
+
)
|
|
930
|
+
`,
|
|
931
|
+
},
|
|
932
|
+
'stacking-cards': {
|
|
933
|
+
body: ` return (
|
|
934
|
+
<StackingCards count={6} palette={{ mode: 'solid', color: '#10B981' }} />
|
|
935
|
+
)
|
|
936
|
+
`,
|
|
937
|
+
},
|
|
938
|
+
'scroll-text': {
|
|
939
|
+
body: ` return (
|
|
940
|
+
<ScrollText
|
|
941
|
+
prefix="C"
|
|
942
|
+
text="Colors light up letter by letter as you scroll."
|
|
943
|
+
/>
|
|
944
|
+
)
|
|
945
|
+
`,
|
|
946
|
+
},
|
|
947
|
+
'image-crossfade': {
|
|
948
|
+
body: ` return (
|
|
949
|
+
<ImageCrossfade overlay="linear-gradient(to top, rgba(0,0,0,0.7), transparent)" />
|
|
950
|
+
)
|
|
581
951
|
`,
|
|
582
952
|
},
|
|
583
953
|
table: {
|
|
@@ -737,14 +1107,14 @@ function addJsxUsage(source, exportNames) {
|
|
|
737
1107
|
* Writes (or completes) the chosen file: `@/` imports from the real folder of
|
|
738
1108
|
* the components, a working example (`<CalendarExample />`) and its definition.
|
|
739
1109
|
*/
|
|
740
|
-
function writeUsage({ cwd, target,
|
|
1110
|
+
function writeUsage({ cwd, target, components, alias, dryRun }) {
|
|
741
1111
|
if (existsSync(target) && statSync(target).isDirectory()) {
|
|
742
1112
|
fail(`${relative(cwd, target)} is a folder: give a file (e.g. src/pages/${components[0].name}.tsx).`)
|
|
743
1113
|
}
|
|
744
1114
|
const created = !existsSync(target)
|
|
745
1115
|
const entries = components.map((component) => ({
|
|
746
1116
|
exportName: component.exportName,
|
|
747
|
-
specifier: aliasImport(cwd,
|
|
1117
|
+
specifier: aliasImport(cwd, component.dir, component.name, alias),
|
|
748
1118
|
example: exampleFor(component),
|
|
749
1119
|
}))
|
|
750
1120
|
const label = relative(cwd, target)
|
|
@@ -823,15 +1193,26 @@ const ALIASES = {
|
|
|
823
1193
|
shortcuts: 'command-menu',
|
|
824
1194
|
upload: 'file-upload',
|
|
825
1195
|
video: 'video-player',
|
|
1196
|
+
storytelling: 'horizontal-story',
|
|
1197
|
+
stacking: 'stacking-cards',
|
|
826
1198
|
}
|
|
827
1199
|
|
|
828
1200
|
function commandList() {
|
|
829
|
-
const components =
|
|
830
|
-
|
|
1201
|
+
const components = listUiComponents()
|
|
1202
|
+
const animations = listAnimationComponents()
|
|
1203
|
+
if (components.length === 0 && animations.length === 0) {
|
|
1204
|
+
fail('No component found in the registry.')
|
|
1205
|
+
}
|
|
831
1206
|
console.log('\nAvailable components:\n')
|
|
832
1207
|
for (const name of components) {
|
|
833
1208
|
console.log(` \x1b[36m${name.padEnd(20)}\x1b[0m src/components/ui/${name}.tsx`)
|
|
834
1209
|
}
|
|
1210
|
+
if (animations.length > 0) {
|
|
1211
|
+
console.log('\nAvailable animations:\n')
|
|
1212
|
+
for (const name of animations) {
|
|
1213
|
+
console.log(` \x1b[36m${name.padEnd(20)}\x1b[0m src/components/animations/${name}.tsx`)
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
835
1216
|
console.log('\nUsage: uifolio add <name> [--cwd <target project>]\n')
|
|
836
1217
|
}
|
|
837
1218
|
|
|
@@ -843,12 +1224,14 @@ function commandInit({ flags }) {
|
|
|
843
1224
|
const config = {
|
|
844
1225
|
$schema: 'https://uifolio.dev/schema.json',
|
|
845
1226
|
dir: typeof flags.dir === 'string' ? flags.dir : DEFAULT_COMPONENTS_DIR,
|
|
1227
|
+
animations:
|
|
1228
|
+
typeof flags.animations === 'string' ? flags.animations : DEFAULT_ANIMATIONS_DIR,
|
|
846
1229
|
lib: typeof flags.lib === 'string' ? flags.lib : 'src/lib',
|
|
847
1230
|
alias: typeof flags.alias === 'string' ? flags.alias : '@',
|
|
848
1231
|
}
|
|
849
1232
|
writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`, 'utf8')
|
|
850
1233
|
ok(`${CONFIG_NAME} written to ${cwd}`)
|
|
851
|
-
info(`dir: ${config.dir} · lib: ${config.lib} · alias: ${config.alias}`)
|
|
1234
|
+
info(`dir: ${config.dir} · animations: ${config.animations} · lib: ${config.lib} · alias: ${config.alias}`)
|
|
852
1235
|
info('Next: uifolio add video-player calendar …')
|
|
853
1236
|
}
|
|
854
1237
|
|
|
@@ -928,13 +1311,21 @@ async function commandAdd({ positional, flags }) {
|
|
|
928
1311
|
|
|
929
1312
|
const prompter = createPrompter(flags)
|
|
930
1313
|
try {
|
|
931
|
-
// 1.
|
|
1314
|
+
// 1. Dossiers d'import : composants, animations et helpers.
|
|
932
1315
|
const componentsDir = normalizeComponentsDir(
|
|
933
1316
|
typeof flags.dir === 'string' ? flags.dir : config.dir ?? DEFAULT_COMPONENTS_DIR,
|
|
934
1317
|
)
|
|
935
1318
|
if (looksLikeFile(componentsDir)) {
|
|
936
1319
|
fail(`"${componentsDir}" is a file: give a folder (e.g. ${DEFAULT_COMPONENTS_DIR}).`)
|
|
937
1320
|
}
|
|
1321
|
+
const animationsDir = normalizeComponentsDir(
|
|
1322
|
+
typeof flags.animations === 'string'
|
|
1323
|
+
? flags.animations
|
|
1324
|
+
: config.animations ?? DEFAULT_ANIMATIONS_DIR,
|
|
1325
|
+
)
|
|
1326
|
+
if (looksLikeFile(animationsDir)) {
|
|
1327
|
+
fail(`"${animationsDir}" is a file: give a folder (e.g. ${DEFAULT_ANIMATIONS_DIR}).`)
|
|
1328
|
+
}
|
|
938
1329
|
|
|
939
1330
|
// 2. Copie des fichiers + helpers.
|
|
940
1331
|
const deps = new Set(LIB_DEPS)
|
|
@@ -944,14 +1335,26 @@ async function commandAdd({ positional, flags }) {
|
|
|
944
1335
|
const components = []
|
|
945
1336
|
|
|
946
1337
|
for (const name of names) {
|
|
947
|
-
const entry =
|
|
948
|
-
const files = collectFiles(entry, REGISTRY_SRC)
|
|
949
|
-
|
|
1338
|
+
const entry = registryEntry(name)
|
|
1339
|
+
const files = collectFiles(entry.path, REGISTRY_SRC)
|
|
1340
|
+
const dir = entry.kind === 'animations' ? animationsDir : componentsDir
|
|
1341
|
+
components.push({
|
|
1342
|
+
name,
|
|
1343
|
+
exportName: componentExportName(readFileSync(entry.path, 'utf8'), name),
|
|
1344
|
+
dir,
|
|
1345
|
+
})
|
|
950
1346
|
|
|
951
1347
|
for (const [absolute, relativePath] of files) {
|
|
952
|
-
const
|
|
953
|
-
const
|
|
954
|
-
const
|
|
1348
|
+
const segments = relativePath.split(sep)
|
|
1349
|
+
const kind = segments[0] === 'components' ? segments[1] : segments[0]
|
|
1350
|
+
const destinationRoot =
|
|
1351
|
+
kind === 'ui'
|
|
1352
|
+
? resolve(cwd, componentsDir)
|
|
1353
|
+
: kind === 'animations'
|
|
1354
|
+
? resolve(cwd, animationsDir)
|
|
1355
|
+
: resolve(cwd, libDir)
|
|
1356
|
+
const inner = kind === 'lib' ? relativePath.slice('lib'.length + 1) : segments.slice(2).join(sep)
|
|
1357
|
+
const destination = join(destinationRoot, inner)
|
|
955
1358
|
if (destinations.has(destination)) continue
|
|
956
1359
|
destinations.add(destination)
|
|
957
1360
|
|
|
@@ -982,7 +1385,10 @@ async function commandAdd({ positional, flags }) {
|
|
|
982
1385
|
info(`${skipped.length} file(s) already present, skipped: ${skipped.join(', ')} (use --force to overwrite)`)
|
|
983
1386
|
}
|
|
984
1387
|
|
|
985
|
-
// 3.
|
|
1388
|
+
// 3. npm dependencies imported by the copied files.
|
|
1389
|
+
const dependencies = installDependencies({ cwd, deps, dryRun })
|
|
1390
|
+
|
|
1391
|
+
// 4. Tokens Tailwind : les classes bg-primary & co fonctionnent partout.
|
|
986
1392
|
writeTheme({ cwd, accent, radius, dryRun })
|
|
987
1393
|
|
|
988
1394
|
// 4. Component usage, after installation.
|
|
@@ -999,19 +1405,18 @@ async function commandAdd({ positional, flags }) {
|
|
|
999
1405
|
writeUsage({
|
|
1000
1406
|
cwd,
|
|
1001
1407
|
target: pageTarget,
|
|
1002
|
-
componentsDir,
|
|
1003
1408
|
components,
|
|
1004
1409
|
alias: targetAlias,
|
|
1005
1410
|
dryRun,
|
|
1006
1411
|
})
|
|
1007
1412
|
}
|
|
1008
1413
|
|
|
1009
|
-
if (
|
|
1414
|
+
if (targetAlias !== '@') {
|
|
1415
|
+
info(`Remember to align the ${targetAlias} alias in your tsconfig/vite (instead of @/).`)
|
|
1416
|
+
}
|
|
1417
|
+
if (dependencies.missing.length > 0 && !dryRun && written.size > 0) {
|
|
1010
1418
|
console.log('\n Detected npm dependencies:')
|
|
1011
|
-
info(
|
|
1012
|
-
if (targetAlias !== '@') {
|
|
1013
|
-
info(`Remember to align the ${targetAlias} alias in your tsconfig/vite (instead of @/).`)
|
|
1014
|
-
}
|
|
1419
|
+
info(dependencies.command)
|
|
1015
1420
|
console.log('')
|
|
1016
1421
|
}
|
|
1017
1422
|
} finally {
|
|
@@ -1034,6 +1439,7 @@ function usage() {
|
|
|
1034
1439
|
Options:
|
|
1035
1440
|
--cwd <dir> target project (default: current directory)
|
|
1036
1441
|
--dir <path> components folder (default: src/components/ui)
|
|
1442
|
+
--animations <path> animations folder (default: src/components/animations)
|
|
1037
1443
|
--lib <path> helpers folder (default: src/lib)
|
|
1038
1444
|
--alias <alias> target import alias (default: @)
|
|
1039
1445
|
--color <hex> primary color, written in the Tailwind tokens (@theme)
|
|
@@ -1046,14 +1452,22 @@ Options:
|
|
|
1046
1452
|
|
|
1047
1453
|
Without --page, the CLI asks whether a file is already ready to receive the
|
|
1048
1454
|
components: they are added to that file, to a new folder + file, or installed
|
|
1049
|
-
only.
|
|
1050
|
-
|
|
1051
|
-
|
|
1455
|
+
only. Components go to src/components/ui (override with --dir), animations to
|
|
1456
|
+
src/components/animations (override with --animations), and the page imports
|
|
1457
|
+
them through the alias (@/components/ui/..., @/components/animations/...). The
|
|
1458
|
+
page also gets a working example, with the data and props each component needs,
|
|
1459
|
+
so it shows immediately.
|
|
1052
1460
|
Colors go through a Tailwind CSS 4 theme (uifolio-theme.css, @theme), so classes
|
|
1053
|
-
like bg-primary and text-muted-foreground stay in the code.
|
|
1461
|
+
like bg-primary and text-muted-foreground stay in the code. When Tailwind is
|
|
1462
|
+
missing, "add" installs it (with your lockfile's package manager), creates the
|
|
1463
|
+
CSS entry, imports it in the app entry and adds the tailwindcss() Vite plugin;
|
|
1464
|
+
without Tailwind no utility class exists and the components render unstyled.
|
|
1465
|
+
The npm packages the components import (clsx, lucide-react, hls.js…) are
|
|
1466
|
+
installed too, as runtime dependencies, when the manifest lacks them.
|
|
1054
1467
|
|
|
1055
1468
|
Examples:
|
|
1056
1469
|
node cli/uifolio.mjs add video-player calendar otp --cwd ../my-project
|
|
1470
|
+
node cli/uifolio.mjs add horizontal-accordion storytelling --cwd ../my-project
|
|
1057
1471
|
node cli/uifolio.mjs add otp --color "#10B981" --radius 8
|
|
1058
1472
|
node cli/uifolio.mjs add calendar --page src/pages/calendar.tsx
|
|
1059
1473
|
`)
|