uifolio 0.5.4 → 0.6.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 +28 -5
- package/cli/uifolio.mjs +333 -26
- package/package.json +1 -1
- package/src/components/ui/table.tsx +49 -23
package/README.md
CHANGED
|
@@ -54,15 +54,38 @@ Colors go through **Tailwind CSS 4 tokens**: the CLI writes
|
|
|
54
54
|
into the project's global CSS (the one with `@import 'tailwindcss'`). The
|
|
55
55
|
components therefore keep `bg-primary`, `border-primary/50`, `ring-primary/40`:
|
|
56
56
|
no hard-coded colors. `--color` changes `--primary` in the theme and `--radius`
|
|
57
|
-
adds `--radius` to it.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
adds `--radius` to it. The CSS that imports Tailwind is detected wherever it
|
|
58
|
+
lives (`src/index.css`, `src/styles/globals.css`, `app/globals.css`…).
|
|
59
|
+
|
|
60
|
+
Tailwind CSS 4 is required: without it, no utility class exists and the
|
|
61
|
+
components render unstyled. When the target project does not have it (or no CSS
|
|
62
|
+
imports it yet), `add` sets it up before wiring the theme:
|
|
63
|
+
|
|
64
|
+
1. installs `tailwindcss` (+ `@tailwindcss/vite`) with the package manager of the
|
|
65
|
+
project's lockfile (`npm`, `pnpm`, `yarn` or `bun`);
|
|
66
|
+
2. creates the CSS entry (`src/index.css`, or `app/globals.css`) with
|
|
67
|
+
`@import 'tailwindcss';`, or prepends the import to the existing one;
|
|
68
|
+
3. imports that CSS in the app entry (`src/main.tsx`, `app/layout.tsx`…);
|
|
69
|
+
4. adds the `tailwindcss()` plugin to `vite.config.*`.
|
|
70
|
+
|
|
71
|
+
The installation is skipped by `--dry-run` (the steps are printed instead).
|
|
72
|
+
If the install fails (offline, read-only…), the theme is still written and the
|
|
73
|
+
exact steps to run manually are printed.
|
|
74
|
+
|
|
75
|
+
`add` also installs the npm packages the copied files import (`clsx`,
|
|
76
|
+
`tailwind-merge`, `lucide-react`, `hls.js`…), with the same package manager and
|
|
77
|
+
as runtime dependencies (`dependencies`, not `devDependencies`). Only the ones
|
|
78
|
+
missing from the manifest are installed, `--dry-run` prints the command without
|
|
79
|
+
running it, and a project without `package.json` simply gets the command to run
|
|
80
|
+
itself:
|
|
61
81
|
|
|
62
82
|
```bash
|
|
63
83
|
npm install clsx tailwind-merge lucide-react hls.js
|
|
64
84
|
```
|
|
65
85
|
|
|
86
|
+
Tailwind is handled the same way, as a dev dependency (see above), so it is not
|
|
87
|
+
part of that list.
|
|
88
|
+
|
|
66
89
|
### Preview parity
|
|
67
90
|
|
|
68
91
|
The docs preview and the installed component render identically because the page
|
|
@@ -90,7 +113,7 @@ only marks the installation as complete when that is relevant.
|
|
|
90
113
|
| ResizablePanels | `src/components/ui/resizable.tsx` | horizontal/vertical groups, handles, min-size, keyboard |
|
|
91
114
|
| OtpInput | `src/components/ui/otp-input.tsx` | 4 fields, paste, rotate + success check |
|
|
92
115
|
| ColorPicker | `src/components/ui/color-picker.tsx` | advanced panel, saturation/lightness, hue, alpha, eyedropper, RGB channels, presets, HEX/RGB/HSL/RGBA |
|
|
93
|
-
| Table | `src/components/ui/table.tsx` | typed
|
|
116
|
+
| Table | `src/components/ui/table.tsx` | shadcn-style typed table, uniform `px-4` cells and a wider gap before Actions, select-all checkbox, row actions |
|
|
94
117
|
|
|
95
118
|
Copyable helpers (the CLI follows them automatically): `src/lib/utils.ts`,
|
|
96
119
|
`src/lib/video.ts`, `src/lib/vtt.ts`, `src/lib/date.ts`, `src/lib/uploader.ts`, `src/lib/color.ts`,
|
package/cli/uifolio.mjs
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
* node cli/uifolio.mjs add video-player --alias "#lib" --dry-run
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
+
import { execFileSync } from 'node:child_process'
|
|
34
35
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
35
36
|
import { basename, dirname, join, relative, resolve } from 'node:path'
|
|
36
37
|
import { createInterface } from 'node:readline/promises'
|
|
@@ -52,8 +53,32 @@ const GLOBAL_CSS_CANDIDATES = [
|
|
|
52
53
|
'src/index.css',
|
|
53
54
|
'src/app.css',
|
|
54
55
|
'src/globals.css',
|
|
56
|
+
'src/styles/globals.css',
|
|
57
|
+
'src/styles/index.css',
|
|
58
|
+
'src/main.css',
|
|
59
|
+
'src/style.css',
|
|
60
|
+
'app/globals.css',
|
|
55
61
|
]
|
|
56
62
|
|
|
63
|
+
/** Folders never scanned when looking for the project's stylesheets. */
|
|
64
|
+
const IGNORED_DIRS = new Set([
|
|
65
|
+
'node_modules',
|
|
66
|
+
'dist',
|
|
67
|
+
'build',
|
|
68
|
+
'out',
|
|
69
|
+
'coverage',
|
|
70
|
+
'public',
|
|
71
|
+
'.git',
|
|
72
|
+
'.next',
|
|
73
|
+
'.vite',
|
|
74
|
+
'.turbo',
|
|
75
|
+
])
|
|
76
|
+
|
|
77
|
+
const MAX_SCAN_DEPTH = 6
|
|
78
|
+
|
|
79
|
+
/** A Tailwind CSS 4 entrypoint: `@import 'tailwindcss';` or `@tailwind base;`. */
|
|
80
|
+
const TAILWIND_ENTRY = /@import\s+['"]tailwindcss(?:[^'"]*)['"]|@tailwind\s+base/
|
|
81
|
+
|
|
57
82
|
/* -------------------------------------------------------------------------- */
|
|
58
83
|
/* Utilitaires */
|
|
59
84
|
/* -------------------------------------------------------------------------- */
|
|
@@ -67,6 +92,10 @@ function ok(message) {
|
|
|
67
92
|
console.log(`\x1b[32m✓\x1b[0m ${message}`)
|
|
68
93
|
}
|
|
69
94
|
|
|
95
|
+
function warn(message) {
|
|
96
|
+
console.log(`\x1b[33m!\x1b[0m ${message}`)
|
|
97
|
+
}
|
|
98
|
+
|
|
70
99
|
function info(message) {
|
|
71
100
|
console.log(` ${message}`)
|
|
72
101
|
}
|
|
@@ -389,19 +418,276 @@ ${darkTokens.join('\n')}
|
|
|
389
418
|
`
|
|
390
419
|
}
|
|
391
420
|
|
|
392
|
-
/**
|
|
421
|
+
/** Every stylesheet of the project, outside the build/ignored folders. */
|
|
422
|
+
function listCssFiles(cwd) {
|
|
423
|
+
const found = []
|
|
424
|
+
const walk = (dir, depth) => {
|
|
425
|
+
if (depth > MAX_SCAN_DEPTH) return
|
|
426
|
+
let entries
|
|
427
|
+
try {
|
|
428
|
+
entries = readdirSync(dir, { withFileTypes: true })
|
|
429
|
+
} catch {
|
|
430
|
+
return
|
|
431
|
+
}
|
|
432
|
+
for (const entry of entries) {
|
|
433
|
+
const full = join(dir, entry.name)
|
|
434
|
+
if (entry.isDirectory()) {
|
|
435
|
+
if (entry.name.startsWith('.') || IGNORED_DIRS.has(entry.name)) continue
|
|
436
|
+
walk(full, depth + 1)
|
|
437
|
+
} else if (/\.css$/i.test(entry.name)) {
|
|
438
|
+
found.push(full)
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
walk(resolve(cwd), 0)
|
|
443
|
+
return found.sort()
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* CSS global du projet: celui qui importe Tailwind en priorité, quel que soit
|
|
448
|
+
* son chemin (src/index.css, src/styles/globals.css, app/globals.css…).
|
|
449
|
+
*/
|
|
393
450
|
function findGlobalCss(cwd) {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
451
|
+
const known = GLOBAL_CSS_CANDIDATES.map((candidate) => join(resolve(cwd), candidate))
|
|
452
|
+
const candidates = [...new Set([...known, ...listCssFiles(cwd)])].filter((file) =>
|
|
453
|
+
existsSync(file),
|
|
454
|
+
)
|
|
455
|
+
const withTailwind = candidates.find((file) => TAILWIND_ENTRY.test(readFileSync(file, 'utf8')))
|
|
456
|
+
return withTailwind ?? candidates[0] ?? null
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** A dependency of the target project's manifest? */
|
|
460
|
+
function hasDependency(cwd, name) {
|
|
461
|
+
const file = join(cwd, 'package.json')
|
|
462
|
+
if (!existsSync(file)) return false
|
|
463
|
+
try {
|
|
464
|
+
const manifest = JSON.parse(readFileSync(file, 'utf8'))
|
|
465
|
+
return ['dependencies', 'devDependencies', 'peerDependencies'].some((key) =>
|
|
466
|
+
Boolean(manifest[key]?.[name]),
|
|
467
|
+
)
|
|
468
|
+
} catch {
|
|
469
|
+
return false
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Tailwind must be installed *and* imported by a CSS file: writing the tokens
|
|
475
|
+
* is not enough, without Tailwind the classes (bg-primary, fixed…) do not exist.
|
|
476
|
+
*/
|
|
477
|
+
function tailwindStatus(cwd, globalCss) {
|
|
478
|
+
const hasTailwind = hasDependency(cwd, 'tailwindcss')
|
|
479
|
+
const hasEntry =
|
|
480
|
+
Boolean(globalCss) &&
|
|
481
|
+
existsSync(globalCss) &&
|
|
482
|
+
TAILWIND_ENTRY.test(readFileSync(globalCss, 'utf8'))
|
|
483
|
+
return { hasDependency: hasTailwind, hasEntry }
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** Package manager of the target project, from its lockfile. */
|
|
487
|
+
function packageManager(cwd, dev = true) {
|
|
488
|
+
const flag = dev ? ['-D'] : []
|
|
489
|
+
if (existsSync(join(cwd, 'pnpm-lock.yaml'))) return { command: 'pnpm', args: ['add', ...flag] }
|
|
490
|
+
if (existsSync(join(cwd, 'yarn.lock'))) return { command: 'yarn', args: ['add', ...flag] }
|
|
491
|
+
if (existsSync(join(cwd, 'bun.lockb')) || existsSync(join(cwd, 'bun.lock'))) {
|
|
492
|
+
return { command: 'bun', args: ['add', ...(dev ? ['-d'] : [])] }
|
|
399
493
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
494
|
+
return { command: 'npm', args: ['install', ...flag] }
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* The components import npm packages (clsx, lucide-react, hls.js…): install the
|
|
499
|
+
* ones missing from the manifest so the copied files compile right away.
|
|
500
|
+
*/
|
|
501
|
+
function installDependencies({ cwd, deps, dryRun }) {
|
|
502
|
+
const missing = [...deps].filter((dep) => !hasDependency(cwd, dep)).sort()
|
|
503
|
+
const manager = packageManager(cwd, false)
|
|
504
|
+
const command = [manager.command, ...manager.args, ...missing].join(' ')
|
|
505
|
+
if (missing.length === 0) return { missing, command }
|
|
506
|
+
|
|
507
|
+
if (dryRun) {
|
|
508
|
+
info(`[dry-run] ${command}`)
|
|
509
|
+
return { missing, command }
|
|
403
510
|
}
|
|
404
|
-
|
|
511
|
+
// Not a JS project: nothing to install into, print the command instead.
|
|
512
|
+
if (!existsSync(join(cwd, 'package.json'))) return { missing, command }
|
|
513
|
+
|
|
514
|
+
info(`Installing the npm dependencies: ${command}`)
|
|
515
|
+
try {
|
|
516
|
+
execFileSync(manager.command, [...manager.args, ...missing], {
|
|
517
|
+
cwd,
|
|
518
|
+
stdio: 'inherit',
|
|
519
|
+
shell: process.platform === 'win32',
|
|
520
|
+
})
|
|
521
|
+
} catch {
|
|
522
|
+
warn('The automatic install failed: run the command above yourself.')
|
|
523
|
+
return { missing, command }
|
|
524
|
+
}
|
|
525
|
+
ok(`Installed: ${missing.join(', ')}`)
|
|
526
|
+
return { missing: [], command }
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const VITE_CONFIGS = [
|
|
530
|
+
'vite.config.ts',
|
|
531
|
+
'vite.config.js',
|
|
532
|
+
'vite.config.mjs',
|
|
533
|
+
'vite.config.mts',
|
|
534
|
+
'vite.config.cjs',
|
|
535
|
+
'vite.config.cts',
|
|
536
|
+
]
|
|
537
|
+
|
|
538
|
+
/** Modules that load the global CSS in a Vite/React project. */
|
|
539
|
+
const ENTRY_MODULES = [
|
|
540
|
+
'src/main.tsx',
|
|
541
|
+
'src/main.jsx',
|
|
542
|
+
'src/main.ts',
|
|
543
|
+
'src/main.js',
|
|
544
|
+
'src/index.tsx',
|
|
545
|
+
'src/index.jsx',
|
|
546
|
+
'app/layout.tsx',
|
|
547
|
+
'app/layout.jsx',
|
|
548
|
+
]
|
|
549
|
+
|
|
550
|
+
function findFirst(cwd, names) {
|
|
551
|
+
return names.map((name) => join(cwd, name)).find((file) => existsSync(file)) ?? null
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** Adds `import tailwindcss from '@tailwindcss/vite'` and `tailwindcss()`. */
|
|
555
|
+
function wireViteConfig(source) {
|
|
556
|
+
if (source.includes('@tailwindcss/vite')) return source
|
|
557
|
+
const plugins = /plugins\s*:\s*\[/.exec(source)
|
|
558
|
+
if (!plugins) return null
|
|
559
|
+
const at = plugins.index + plugins[0].length
|
|
560
|
+
const next = `${source.slice(0, at)}tailwindcss(), ${source.slice(at)}`
|
|
561
|
+
const lines = next.split('\n')
|
|
562
|
+
const lastImport = lines.reduce((last, line, index) => (/^\s*import\s/.test(line) ? index : last), -1)
|
|
563
|
+
const importLine = "import tailwindcss from '@tailwindcss/vite'"
|
|
564
|
+
if (lastImport === -1) return `${importLine}\n${next}`
|
|
565
|
+
lines.splice(lastImport + 1, 0, importLine)
|
|
566
|
+
return lines.join('\n')
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** Creates the Tailwind entrypoint when the project has no stylesheet at all. */
|
|
570
|
+
function defaultCssPath(cwd) {
|
|
571
|
+
if (existsSync(join(cwd, 'src'))) return join(cwd, 'src', 'index.css')
|
|
572
|
+
if (existsSync(join(cwd, 'app'))) return join(cwd, 'app', 'globals.css')
|
|
573
|
+
return join(cwd, 'index.css')
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Installs Tailwind CSS 4 and wires it (CSS entry, entry module, Vite plugin)
|
|
578
|
+
* when the target project does not have it. Without Tailwind no utility class
|
|
579
|
+
* exists, so the components would render unstyled.
|
|
580
|
+
*/
|
|
581
|
+
function setupTailwind({ cwd, globalCss, dryRun }) {
|
|
582
|
+
// Not a JS project (or nothing to install into): keep the manual instructions.
|
|
583
|
+
if (!existsSync(join(cwd, 'package.json'))) {
|
|
584
|
+
return { globalCss, reason: 'no-package' }
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const usesVite = Boolean(findFirst(cwd, VITE_CONFIGS)) || hasDependency(cwd, 'vite')
|
|
588
|
+
const manager = packageManager(cwd)
|
|
589
|
+
const packages = usesVite ? ['tailwindcss', '@tailwindcss/vite'] : ['tailwindcss']
|
|
590
|
+
const command = [manager.command, ...manager.args, ...packages].join(' ')
|
|
591
|
+
|
|
592
|
+
if (!hasDependency(cwd, 'tailwindcss')) {
|
|
593
|
+
if (dryRun) info(`[dry-run] ${command}`)
|
|
594
|
+
else info(`Tailwind CSS missing, installing it: ${command}`)
|
|
595
|
+
}
|
|
596
|
+
if (!dryRun && !hasDependency(cwd, 'tailwindcss')) {
|
|
597
|
+
try {
|
|
598
|
+
execFileSync(manager.command, [...manager.args, ...packages], {
|
|
599
|
+
cwd,
|
|
600
|
+
stdio: 'inherit',
|
|
601
|
+
shell: process.platform === 'win32',
|
|
602
|
+
})
|
|
603
|
+
ok('Tailwind CSS installed')
|
|
604
|
+
} catch {
|
|
605
|
+
warn('The automatic install failed.')
|
|
606
|
+
return { globalCss, reason: 'install-failed' }
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// 1. A stylesheet that imports Tailwind.
|
|
611
|
+
let cssFile = globalCss
|
|
612
|
+
if (!cssFile) {
|
|
613
|
+
cssFile = defaultCssPath(cwd)
|
|
614
|
+
if (dryRun) {
|
|
615
|
+
info(`[dry-run] create ${relative(cwd, cssFile)} with "@import 'tailwindcss';"`)
|
|
616
|
+
} else {
|
|
617
|
+
mkdirSync(dirname(cssFile), { recursive: true })
|
|
618
|
+
writeFileSync(cssFile, "@import 'tailwindcss';\n", 'utf8')
|
|
619
|
+
ok(`Tailwind entry created: ${relative(cwd, cssFile)}`)
|
|
620
|
+
}
|
|
621
|
+
} else if (!TAILWIND_ENTRY.test(readFileSync(cssFile, 'utf8'))) {
|
|
622
|
+
if (dryRun) {
|
|
623
|
+
info(`[dry-run] add "@import 'tailwindcss';" to ${relative(cwd, cssFile)}`)
|
|
624
|
+
} else {
|
|
625
|
+
const source = readFileSync(cssFile, 'utf8')
|
|
626
|
+
writeFileSync(cssFile, `@import 'tailwindcss';\n\n${source}`, 'utf8')
|
|
627
|
+
ok(`Tailwind imported in ${relative(cwd, cssFile)}`)
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// 2. That stylesheet must be loaded by the app entry.
|
|
632
|
+
const entryModule = findFirst(cwd, ENTRY_MODULES)
|
|
633
|
+
const loadsCss = Boolean(entryModule) && readFileSync(entryModule, 'utf8').includes(basename(cssFile))
|
|
634
|
+
if (entryModule && !loadsCss) {
|
|
635
|
+
if (dryRun) {
|
|
636
|
+
info(`[dry-run] import ${basename(cssFile)} in ${relative(cwd, entryModule)}`)
|
|
637
|
+
} else {
|
|
638
|
+
const specifier = relative(dirname(entryModule), cssFile).replace(/\\/g, '/')
|
|
639
|
+
const source = readFileSync(entryModule, 'utf8')
|
|
640
|
+
writeFileSync(
|
|
641
|
+
entryModule,
|
|
642
|
+
`import '${specifier.startsWith('.') ? '' : './'}${specifier}'\n${source}`,
|
|
643
|
+
'utf8',
|
|
644
|
+
)
|
|
645
|
+
ok(`CSS imported in ${relative(cwd, entryModule)}`)
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// 3. Vite only runs Tailwind through its plugin.
|
|
650
|
+
if (usesVite) {
|
|
651
|
+
const config = findFirst(cwd, VITE_CONFIGS)
|
|
652
|
+
if (!config) {
|
|
653
|
+
warn('No vite.config.* found: add tailwindcss() to your Vite plugins.')
|
|
654
|
+
} else {
|
|
655
|
+
const source = readFileSync(config, 'utf8')
|
|
656
|
+
const updated = wireViteConfig(source)
|
|
657
|
+
if (updated === null) {
|
|
658
|
+
warn(`Add the tailwindcss() plugin in ${relative(cwd, config)} (no "plugins: [" found).`)
|
|
659
|
+
} else if (updated !== source && dryRun) {
|
|
660
|
+
info(`[dry-run] add tailwindcss() to ${relative(cwd, config)}`)
|
|
661
|
+
} else if (updated !== source) {
|
|
662
|
+
writeFileSync(config, updated, 'utf8')
|
|
663
|
+
ok(`tailwindcss() plugin added to ${relative(cwd, config)}`)
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
} else {
|
|
667
|
+
info('Tailwind 4 also needs @tailwindcss/postcss in your PostCSS config.')
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return { globalCss: cssFile, reason: dryRun ? 'dry-run' : 'ready' }
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/** Actionable steps instead of a silent unstyled page. */
|
|
674
|
+
function reportMissingTailwind({ cwd, globalCss, themeFile, status }) {
|
|
675
|
+
const cssLabel = globalCss ? relative(cwd, globalCss) : 'your global CSS'
|
|
676
|
+
const specifier = globalCss
|
|
677
|
+
? `./${relative(dirname(globalCss), themeFile).replace(/\\/g, '/')}`
|
|
678
|
+
: `./${THEME_FILE}`
|
|
679
|
+
console.log('')
|
|
680
|
+
if (!status.hasDependency) {
|
|
681
|
+
const manager = packageManager(cwd)
|
|
682
|
+
warn('Tailwind CSS is not installed in this project: the components will render unstyled.')
|
|
683
|
+
info([manager.command, ...manager.args, 'tailwindcss', '@tailwindcss/vite'].join(' '))
|
|
684
|
+
info(`Then add "@import 'tailwindcss';" at the top of ${cssLabel}`)
|
|
685
|
+
info('and tailwindcss() to the Vite plugins (plugins: [react(), tailwindcss()]).')
|
|
686
|
+
} else {
|
|
687
|
+
warn(`Tailwind CSS is installed but ${cssLabel} does not import it yet.`)
|
|
688
|
+
info(`Add "@import 'tailwindcss';" at the top of ${cssLabel}.`)
|
|
689
|
+
}
|
|
690
|
+
info(`Then "@import '${specifier}';", or re-run "uifolio add" to wire it automatically.`)
|
|
405
691
|
}
|
|
406
692
|
|
|
407
693
|
/** Adds `@import './uifolio-theme.css';` right after the Tailwind import. */
|
|
@@ -430,7 +716,16 @@ function existingRadius(file) {
|
|
|
430
716
|
}
|
|
431
717
|
|
|
432
718
|
function writeTheme({ cwd, accent, radius, dryRun }) {
|
|
433
|
-
|
|
719
|
+
let globalCss = findGlobalCss(cwd)
|
|
720
|
+
|
|
721
|
+
// Install + wire Tailwind first: the tokens alone style nothing.
|
|
722
|
+
if (!tailwindStatus(cwd, globalCss).hasEntry) {
|
|
723
|
+
console.log('')
|
|
724
|
+
info('Tailwind CSS is not set up in this project: configuring it.')
|
|
725
|
+
globalCss = setupTailwind({ cwd, globalCss, dryRun }).globalCss
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const status = tailwindStatus(cwd, globalCss)
|
|
434
729
|
const themeFile = globalCss
|
|
435
730
|
? join(dirname(globalCss), THEME_FILE)
|
|
436
731
|
: join(cwd, THEME_FILE)
|
|
@@ -450,15 +745,19 @@ function writeTheme({ cwd, accent, radius, dryRun }) {
|
|
|
450
745
|
ok(`Tailwind theme written to ${label}`)
|
|
451
746
|
info(`--primary: ${finalAccent}${finalRadius === undefined ? '' : ` · --radius: ${finalRadius}px`}`)
|
|
452
747
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
} else {
|
|
460
|
-
info(`Add "@import './${THEME_FILE}';", at the top of your global CSS (the Tailwind one).`)
|
|
748
|
+
// The tokens only matter once Tailwind actually generates the utilities:
|
|
749
|
+
// without a `@import 'tailwindcss'`, wiring the theme would look installed
|
|
750
|
+
// and still render an unstyled page.
|
|
751
|
+
if (!status.hasEntry) {
|
|
752
|
+
reportMissingTailwind({ cwd, globalCss, themeFile, status })
|
|
753
|
+
return
|
|
461
754
|
}
|
|
755
|
+
|
|
756
|
+
const source = readFileSync(globalCss, 'utf8')
|
|
757
|
+
const specifier = `./${basename(themeFile)}`
|
|
758
|
+
const updated = wireThemeImport(source, specifier)
|
|
759
|
+
if (updated !== source) writeFileSync(globalCss, updated, 'utf8')
|
|
760
|
+
info(`imported in ${relative(cwd, globalCss)} — no hard-coded colors in the components`)
|
|
462
761
|
}
|
|
463
762
|
|
|
464
763
|
/* -------------------------------------------------------------------------- */
|
|
@@ -982,7 +1281,10 @@ async function commandAdd({ positional, flags }) {
|
|
|
982
1281
|
info(`${skipped.length} file(s) already present, skipped: ${skipped.join(', ')} (use --force to overwrite)`)
|
|
983
1282
|
}
|
|
984
1283
|
|
|
985
|
-
// 3.
|
|
1284
|
+
// 3. npm dependencies imported by the copied files.
|
|
1285
|
+
const dependencies = installDependencies({ cwd, deps, dryRun })
|
|
1286
|
+
|
|
1287
|
+
// 4. Tokens Tailwind : les classes bg-primary & co fonctionnent partout.
|
|
986
1288
|
writeTheme({ cwd, accent, radius, dryRun })
|
|
987
1289
|
|
|
988
1290
|
// 4. Component usage, after installation.
|
|
@@ -1006,12 +1308,12 @@ async function commandAdd({ positional, flags }) {
|
|
|
1006
1308
|
})
|
|
1007
1309
|
}
|
|
1008
1310
|
|
|
1009
|
-
if (
|
|
1311
|
+
if (targetAlias !== '@') {
|
|
1312
|
+
info(`Remember to align the ${targetAlias} alias in your tsconfig/vite (instead of @/).`)
|
|
1313
|
+
}
|
|
1314
|
+
if (dependencies.missing.length > 0 && !dryRun && written.size > 0) {
|
|
1010
1315
|
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
|
-
}
|
|
1316
|
+
info(dependencies.command)
|
|
1015
1317
|
console.log('')
|
|
1016
1318
|
}
|
|
1017
1319
|
} finally {
|
|
@@ -1050,7 +1352,12 @@ only. Files go to src/components/ui (override with --dir) and the page imports
|
|
|
1050
1352
|
them through the alias (@/components/ui/...). The page also gets a working
|
|
1051
1353
|
example, with the data and props each component needs, so it shows immediately.
|
|
1052
1354
|
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.
|
|
1355
|
+
like bg-primary and text-muted-foreground stay in the code. When Tailwind is
|
|
1356
|
+
missing, "add" installs it (with your lockfile's package manager), creates the
|
|
1357
|
+
CSS entry, imports it in the app entry and adds the tailwindcss() Vite plugin;
|
|
1358
|
+
without Tailwind no utility class exists and the components render unstyled.
|
|
1359
|
+
The npm packages the components import (clsx, lucide-react, hls.js…) are
|
|
1360
|
+
installed too, as runtime dependencies, when the manifest lacks them.
|
|
1054
1361
|
|
|
1055
1362
|
Examples:
|
|
1056
1363
|
node cli/uifolio.mjs add video-player calendar otp --cwd ../my-project
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uifolio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "CLI that copies React components (calendar, video/audio players, OTP, editor, upload) and their helpers into your project, with Tailwind CSS 4 tokens.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import type
|
|
3
|
+
import { useEffect, useRef, type ReactNode } from 'react'
|
|
4
4
|
import { cn } from '@/lib/utils'
|
|
5
5
|
import { useControllableValue } from '@/lib/use-controllable'
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ export interface TableColumn<T> {
|
|
|
8
8
|
/** Stable identifier, also used as the React key of the column. */
|
|
9
9
|
key: string
|
|
10
10
|
header: ReactNode
|
|
11
|
-
cell: (row: T) => ReactNode
|
|
11
|
+
cell: (row: T, index: number) => ReactNode
|
|
12
12
|
/** Applied to the header and body cells: width, nowrap… */
|
|
13
13
|
className?: string
|
|
14
14
|
align?: 'left' | 'center' | 'right'
|
|
@@ -36,9 +36,10 @@ function alignClass(align: TableColumn<unknown>['align']): string {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
|
-
* Data table with a shadcn-like API: typed columns, optional
|
|
40
|
-
* dedicated actions column. Every cell on the left shares the
|
|
41
|
-
* padding; the actions column keeps a wider `pl-10` gap with the
|
|
39
|
+
* Data table with a shadcn-like API: typed columns, an optional select-all
|
|
40
|
+
* checkbox, and a dedicated actions column. Every cell on the left shares the
|
|
41
|
+
* same `px-4` padding; the actions column keeps a wider `pl-10` gap with the
|
|
42
|
+
* last column.
|
|
42
43
|
*/
|
|
43
44
|
export function Table<T>({
|
|
44
45
|
columns,
|
|
@@ -59,6 +60,17 @@ export function Table<T>({
|
|
|
59
60
|
defaultSelectedKeys,
|
|
60
61
|
onSelectionChange,
|
|
61
62
|
)
|
|
63
|
+
const selectAllRef = useRef<HTMLInputElement>(null)
|
|
64
|
+
|
|
65
|
+
const keys = rows.map((row) => String(rowKey(row)))
|
|
66
|
+
const allSelected = keys.length > 0 && keys.every((key) => selection.includes(key))
|
|
67
|
+
const someSelected = keys.some((key) => selection.includes(key))
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (selectAllRef.current) {
|
|
71
|
+
selectAllRef.current.indeterminate = someSelected && !allSelected
|
|
72
|
+
}
|
|
73
|
+
}, [someSelected, allSelected])
|
|
62
74
|
|
|
63
75
|
const toggle = (key: string, checked: boolean) => {
|
|
64
76
|
setSelection((current) =>
|
|
@@ -70,13 +82,29 @@ export function Table<T>({
|
|
|
70
82
|
)
|
|
71
83
|
}
|
|
72
84
|
|
|
73
|
-
const
|
|
85
|
+
const toggleAll = (checked: boolean) => {
|
|
86
|
+
setSelection(checked ? keys : [])
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const columnsCount = columns.length + (selectable ? 1 : 0) + (actions ? 1 : 0)
|
|
74
90
|
|
|
75
91
|
return (
|
|
76
92
|
<div className={cn('w-full overflow-x-auto rounded-sm border bg-card', className)}>
|
|
77
93
|
<table className="w-full min-w-[860px] border-collapse text-left text-xs">
|
|
78
94
|
<thead>
|
|
79
95
|
<tr className="border-b bg-muted/50">
|
|
96
|
+
{selectable && (
|
|
97
|
+
<th scope="col" className="w-10 px-4 py-3.5">
|
|
98
|
+
<input
|
|
99
|
+
ref={selectAllRef}
|
|
100
|
+
type="checkbox"
|
|
101
|
+
checked={allSelected}
|
|
102
|
+
onChange={(event) => toggleAll(event.target.checked)}
|
|
103
|
+
aria-label="Select all rows"
|
|
104
|
+
className="size-4 cursor-pointer rounded-sm border-border accent-primary"
|
|
105
|
+
/>
|
|
106
|
+
</th>
|
|
107
|
+
)}
|
|
80
108
|
{columns.map((column) => (
|
|
81
109
|
<th
|
|
82
110
|
key={column.key}
|
|
@@ -108,7 +136,7 @@ export function Table<T>({
|
|
|
108
136
|
</td>
|
|
109
137
|
</tr>
|
|
110
138
|
) : (
|
|
111
|
-
rows.map((row) => {
|
|
139
|
+
rows.map((row, rowIndex) => {
|
|
112
140
|
const key = String(rowKey(row))
|
|
113
141
|
const selected = selection.includes(key)
|
|
114
142
|
|
|
@@ -122,7 +150,19 @@ export function Table<T>({
|
|
|
122
150
|
selected ? 'bg-primary/5' : 'hover:bg-muted/30',
|
|
123
151
|
)}
|
|
124
152
|
>
|
|
125
|
-
{
|
|
153
|
+
{selectable && (
|
|
154
|
+
<td className="w-10 px-4 py-3.5 align-middle">
|
|
155
|
+
<input
|
|
156
|
+
type="checkbox"
|
|
157
|
+
checked={selected}
|
|
158
|
+
onChange={(event) => toggle(key, event.target.checked)}
|
|
159
|
+
onClick={(event) => event.stopPropagation()}
|
|
160
|
+
aria-label={`Select row ${key}`}
|
|
161
|
+
className="size-4 cursor-pointer rounded-sm border-border accent-primary"
|
|
162
|
+
/>
|
|
163
|
+
</td>
|
|
164
|
+
)}
|
|
165
|
+
{columns.map((column) => (
|
|
126
166
|
<td
|
|
127
167
|
key={column.key}
|
|
128
168
|
className={cn(
|
|
@@ -131,21 +171,7 @@ export function Table<T>({
|
|
|
131
171
|
column.className,
|
|
132
172
|
)}
|
|
133
173
|
>
|
|
134
|
-
{
|
|
135
|
-
<div className="flex items-center gap-3">
|
|
136
|
-
<input
|
|
137
|
-
type="checkbox"
|
|
138
|
-
checked={selected}
|
|
139
|
-
onChange={(event) => toggle(key, event.target.checked)}
|
|
140
|
-
onClick={(event) => event.stopPropagation()}
|
|
141
|
-
aria-label={`Select row ${key}`}
|
|
142
|
-
className="size-4 shrink-0 cursor-pointer rounded-sm border-border accent-primary"
|
|
143
|
-
/>
|
|
144
|
-
{column.cell(row)}
|
|
145
|
-
</div>
|
|
146
|
-
) : (
|
|
147
|
-
column.cell(row)
|
|
148
|
-
)}
|
|
174
|
+
{column.cell(row, rowIndex)}
|
|
149
175
|
</td>
|
|
150
176
|
))}
|
|
151
177
|
{actions && (
|