uifolio 0.6.0 → 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 +14 -4
- package/cli/uifolio.mjs +132 -25
- 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/README.md
CHANGED
|
@@ -14,6 +14,7 @@ and the alias is rewritten automatically.
|
|
|
14
14
|
# from any project, through the npm package
|
|
15
15
|
npx uifolio@latest add calendar --color "#10B981" --radius 6
|
|
16
16
|
pnpm dlx uifolio@latest add video-player otp
|
|
17
|
+
pnpm dlx uifolio@latest add horizontal-accordion storytelling
|
|
17
18
|
|
|
18
19
|
# from the root of the project that contains UIfolio
|
|
19
20
|
npm run uifolio -- list
|
|
@@ -31,11 +32,12 @@ node cli/uifolio.mjs add file-upload --alias "#lib" --dry-run # simulate
|
|
|
31
32
|
| `uifolio init` | writes `uifolio.json` (dir, lib, alias) in the target project |
|
|
32
33
|
| `uifolio add <names...>` | copies the component **+ transitive dependencies** (helpers `@/lib/*`) |
|
|
33
34
|
|
|
34
|
-
Options: `--cwd <dir>`, `--dir <path>` (default `src/components/ui`), `--lib <path>` (default `src/lib`), `--alias <alias>` (default `@`), `--page <path>` (created if missing), `--interactive` (force the questions), `--force` (overwrite), `--dry-run` (simulate), `--color <hex>` and `--radius <px>`.
|
|
35
|
+
Options: `--cwd <dir>`, `--dir <path>` (default `src/components/ui`), `--animations <path>` (default `src/components/animations`), `--lib <path>` (default `src/lib`), `--alias <alias>` (default `@`), `--page <path>` (created if missing), `--interactive` (force the questions), `--force` (overwrite), `--dry-run` (simulate), `--color <hex>` and `--radius <px>`.
|
|
35
36
|
|
|
36
|
-
`add` does not stop at the copy. The components
|
|
37
|
-
`src/components/ui` (override with `--dir`)
|
|
38
|
-
|
|
37
|
+
`add` does not stop at the copy. The components land in
|
|
38
|
+
`src/components/ui` (override with `--dir`), the animations in
|
|
39
|
+
`src/components/animations` (override with `--animations`), and the page imports
|
|
40
|
+
them through the `@/` alias. Without `--page`, the CLI asks where to use them:
|
|
39
41
|
|
|
40
42
|
1. *Do you already have a file ready to receive the component (…component…) (y/N) ?* `yes` → *In which file should the component (…component…) be copied (e.g. src/pages/calendar.tsx) ?*: the import and a ready-to-render example are added without duplication, and the file is created when missing;
|
|
41
43
|
2. `no` → *Would you like to create a new folder and an import file (Y/n) ?* `yes` → *Please specify the new path (e.g. src/pages/calendar.tsx) ?*: the folder and the file are created; `no` → install only, no file is touched.
|
|
@@ -114,6 +116,14 @@ only marks the installation as complete when that is relevant.
|
|
|
114
116
|
| OtpInput | `src/components/ui/otp-input.tsx` | 4 fields, paste, rotate + success check |
|
|
115
117
|
| ColorPicker | `src/components/ui/color-picker.tsx` | advanced panel, saturation/lightness, hue, alpha, eyedropper, RGB channels, presets, HEX/RGB/HSL/RGBA |
|
|
116
118
|
| 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 |
|
|
119
|
+
| HorizontalAccordion | `src/components/animations/horizontal-accordion.tsx` | theme-colored panels, hover, click, arrow/Home/End keyboard |
|
|
120
|
+
| Storytelling | `src/components/animations/horizontal-story.tsx` | vertical scroll driven horizontal story, palettes, mobile touch fallback |
|
|
121
|
+
| StackingCards | `src/components/animations/stacking-cards.tsx` | cards settle on scroll, customizable gradients or solid color |
|
|
122
|
+
| ScrollText | `src/components/animations/scroll-text.tsx` | letters light up with their viewport position, reversible |
|
|
123
|
+
| ImageCrossfade | `src/components/animations/image-crossfade.tsx` | pinned crossfading images, captions, dots and customizable overlay |
|
|
124
|
+
|
|
125
|
+
The animation helpers (`data.ts`, `palette.ts`, `animation-settings-*`, `palette-*`)
|
|
126
|
+
are copied automatically when the installed animation needs them.
|
|
117
127
|
|
|
118
128
|
Copyable helpers (the CLI follows them automatically): `src/lib/utils.ts`,
|
|
119
129
|
`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
|
@@ -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)
|
|
@@ -33,19 +34,30 @@
|
|
|
33
34
|
|
|
34
35
|
import { execFileSync } from 'node:child_process'
|
|
35
36
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
36
|
-
import { basename, dirname, join, relative, resolve } from 'node:path'
|
|
37
|
+
import { basename, dirname, join, relative, resolve, sep } from 'node:path'
|
|
37
38
|
import { createInterface } from 'node:readline/promises'
|
|
38
39
|
import { fileURLToPath } from 'node:url'
|
|
39
40
|
|
|
40
41
|
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
41
42
|
const REGISTRY_SRC = resolve(HERE, '..', 'src')
|
|
42
43
|
const REGISTRY_UI = join(REGISTRY_SRC, 'components', 'ui')
|
|
44
|
+
const REGISTRY_ANIMATIONS = join(REGISTRY_SRC, 'components', 'animations')
|
|
43
45
|
const CONFIG_NAME = 'uifolio.json'
|
|
44
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
|
+
|
|
45
56
|
const LIB_DEPS = ['clsx', 'tailwind-merge'] // cn() — always copied with the helpers
|
|
46
57
|
|
|
47
58
|
const DEFAULT_ACCENT = '#10b981'
|
|
48
59
|
const DEFAULT_COMPONENTS_DIR = 'src/components/ui'
|
|
60
|
+
const DEFAULT_ANIMATIONS_DIR = 'src/components/animations'
|
|
49
61
|
const THEME_FILE = 'uifolio-theme.css'
|
|
50
62
|
|
|
51
63
|
/** Global CSS candidates for wiring the Tailwind theme. */
|
|
@@ -146,7 +158,7 @@ function writeFailure(error, target) {
|
|
|
146
158
|
fail(`Cannot write ${target}: ${reasons[error?.code] ?? error?.message ?? error}.`)
|
|
147
159
|
}
|
|
148
160
|
|
|
149
|
-
function
|
|
161
|
+
function listUiComponents() {
|
|
150
162
|
if (!existsSync(REGISTRY_UI)) return []
|
|
151
163
|
return readdirSync(REGISTRY_UI)
|
|
152
164
|
.filter((name) => name.endsWith('.tsx'))
|
|
@@ -154,6 +166,24 @@ function listComponents() {
|
|
|
154
166
|
.sort()
|
|
155
167
|
}
|
|
156
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
|
+
|
|
157
187
|
function readConfig(cwd) {
|
|
158
188
|
const file = join(cwd, CONFIG_NAME)
|
|
159
189
|
if (!existsSync(file)) return null
|
|
@@ -164,14 +194,19 @@ function readConfig(cwd) {
|
|
|
164
194
|
}
|
|
165
195
|
}
|
|
166
196
|
|
|
167
|
-
/**
|
|
197
|
+
/** Local imports (alias or relative) of a source file. */
|
|
168
198
|
function localImports(source) {
|
|
169
199
|
const specifiers = new Set()
|
|
170
200
|
const importRe = /(?:from\s+|import\s*\(\s*|import\s+)['"]([^'"]+)['"]/g
|
|
171
201
|
let match
|
|
172
202
|
while ((match = importRe.exec(source)) !== null) {
|
|
173
203
|
const specifier = match[1]
|
|
174
|
-
if (
|
|
204
|
+
if (
|
|
205
|
+
specifier.startsWith('@/') ||
|
|
206
|
+
specifier.startsWith('~/') ||
|
|
207
|
+
specifier.startsWith('./') ||
|
|
208
|
+
specifier.startsWith('../')
|
|
209
|
+
) {
|
|
175
210
|
specifiers.add(specifier)
|
|
176
211
|
}
|
|
177
212
|
}
|
|
@@ -305,12 +340,14 @@ function collectFiles(entryPath, srcRoot) {
|
|
|
305
340
|
|
|
306
341
|
const source = readFileSync(current, 'utf8')
|
|
307
342
|
for (const specifier of localImports(source)) {
|
|
308
|
-
const base = specifier.
|
|
343
|
+
const base = specifier.startsWith('.')
|
|
344
|
+
? resolve(dirname(current), specifier)
|
|
345
|
+
: join(srcRoot, specifier.replace(/^[@~]\//, ''))
|
|
309
346
|
const candidates = [
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
join(
|
|
313
|
-
join(
|
|
347
|
+
`${base}.ts`,
|
|
348
|
+
`${base}.tsx`,
|
|
349
|
+
join(base, 'index.ts'),
|
|
350
|
+
join(base, 'index.tsx'),
|
|
314
351
|
]
|
|
315
352
|
const found = candidates.find((candidate) => existsSync(candidate))
|
|
316
353
|
if (found) queue.push(found)
|
|
@@ -877,6 +914,40 @@ const USAGE_EXAMPLES = {
|
|
|
877
914
|
<p className="text-sm">Fade in on mount.</p>
|
|
878
915
|
</FadeIn>
|
|
879
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
|
+
)
|
|
880
951
|
`,
|
|
881
952
|
},
|
|
882
953
|
table: {
|
|
@@ -1036,14 +1107,14 @@ function addJsxUsage(source, exportNames) {
|
|
|
1036
1107
|
* Writes (or completes) the chosen file: `@/` imports from the real folder of
|
|
1037
1108
|
* the components, a working example (`<CalendarExample />`) and its definition.
|
|
1038
1109
|
*/
|
|
1039
|
-
function writeUsage({ cwd, target,
|
|
1110
|
+
function writeUsage({ cwd, target, components, alias, dryRun }) {
|
|
1040
1111
|
if (existsSync(target) && statSync(target).isDirectory()) {
|
|
1041
1112
|
fail(`${relative(cwd, target)} is a folder: give a file (e.g. src/pages/${components[0].name}.tsx).`)
|
|
1042
1113
|
}
|
|
1043
1114
|
const created = !existsSync(target)
|
|
1044
1115
|
const entries = components.map((component) => ({
|
|
1045
1116
|
exportName: component.exportName,
|
|
1046
|
-
specifier: aliasImport(cwd,
|
|
1117
|
+
specifier: aliasImport(cwd, component.dir, component.name, alias),
|
|
1047
1118
|
example: exampleFor(component),
|
|
1048
1119
|
}))
|
|
1049
1120
|
const label = relative(cwd, target)
|
|
@@ -1122,15 +1193,26 @@ const ALIASES = {
|
|
|
1122
1193
|
shortcuts: 'command-menu',
|
|
1123
1194
|
upload: 'file-upload',
|
|
1124
1195
|
video: 'video-player',
|
|
1196
|
+
storytelling: 'horizontal-story',
|
|
1197
|
+
stacking: 'stacking-cards',
|
|
1125
1198
|
}
|
|
1126
1199
|
|
|
1127
1200
|
function commandList() {
|
|
1128
|
-
const components =
|
|
1129
|
-
|
|
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
|
+
}
|
|
1130
1206
|
console.log('\nAvailable components:\n')
|
|
1131
1207
|
for (const name of components) {
|
|
1132
1208
|
console.log(` \x1b[36m${name.padEnd(20)}\x1b[0m src/components/ui/${name}.tsx`)
|
|
1133
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
|
+
}
|
|
1134
1216
|
console.log('\nUsage: uifolio add <name> [--cwd <target project>]\n')
|
|
1135
1217
|
}
|
|
1136
1218
|
|
|
@@ -1142,12 +1224,14 @@ function commandInit({ flags }) {
|
|
|
1142
1224
|
const config = {
|
|
1143
1225
|
$schema: 'https://uifolio.dev/schema.json',
|
|
1144
1226
|
dir: typeof flags.dir === 'string' ? flags.dir : DEFAULT_COMPONENTS_DIR,
|
|
1227
|
+
animations:
|
|
1228
|
+
typeof flags.animations === 'string' ? flags.animations : DEFAULT_ANIMATIONS_DIR,
|
|
1145
1229
|
lib: typeof flags.lib === 'string' ? flags.lib : 'src/lib',
|
|
1146
1230
|
alias: typeof flags.alias === 'string' ? flags.alias : '@',
|
|
1147
1231
|
}
|
|
1148
1232
|
writeFileSync(file, `${JSON.stringify(config, null, 2)}\n`, 'utf8')
|
|
1149
1233
|
ok(`${CONFIG_NAME} written to ${cwd}`)
|
|
1150
|
-
info(`dir: ${config.dir} · lib: ${config.lib} · alias: ${config.alias}`)
|
|
1234
|
+
info(`dir: ${config.dir} · animations: ${config.animations} · lib: ${config.lib} · alias: ${config.alias}`)
|
|
1151
1235
|
info('Next: uifolio add video-player calendar …')
|
|
1152
1236
|
}
|
|
1153
1237
|
|
|
@@ -1227,13 +1311,21 @@ async function commandAdd({ positional, flags }) {
|
|
|
1227
1311
|
|
|
1228
1312
|
const prompter = createPrompter(flags)
|
|
1229
1313
|
try {
|
|
1230
|
-
// 1.
|
|
1314
|
+
// 1. Dossiers d'import : composants, animations et helpers.
|
|
1231
1315
|
const componentsDir = normalizeComponentsDir(
|
|
1232
1316
|
typeof flags.dir === 'string' ? flags.dir : config.dir ?? DEFAULT_COMPONENTS_DIR,
|
|
1233
1317
|
)
|
|
1234
1318
|
if (looksLikeFile(componentsDir)) {
|
|
1235
1319
|
fail(`"${componentsDir}" is a file: give a folder (e.g. ${DEFAULT_COMPONENTS_DIR}).`)
|
|
1236
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
|
+
}
|
|
1237
1329
|
|
|
1238
1330
|
// 2. Copie des fichiers + helpers.
|
|
1239
1331
|
const deps = new Set(LIB_DEPS)
|
|
@@ -1243,14 +1335,26 @@ async function commandAdd({ positional, flags }) {
|
|
|
1243
1335
|
const components = []
|
|
1244
1336
|
|
|
1245
1337
|
for (const name of names) {
|
|
1246
|
-
const entry =
|
|
1247
|
-
const files = collectFiles(entry, REGISTRY_SRC)
|
|
1248
|
-
|
|
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
|
+
})
|
|
1249
1346
|
|
|
1250
1347
|
for (const [absolute, relativePath] of files) {
|
|
1251
|
-
const
|
|
1252
|
-
const
|
|
1253
|
-
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)
|
|
1254
1358
|
if (destinations.has(destination)) continue
|
|
1255
1359
|
destinations.add(destination)
|
|
1256
1360
|
|
|
@@ -1301,7 +1405,6 @@ async function commandAdd({ positional, flags }) {
|
|
|
1301
1405
|
writeUsage({
|
|
1302
1406
|
cwd,
|
|
1303
1407
|
target: pageTarget,
|
|
1304
|
-
componentsDir,
|
|
1305
1408
|
components,
|
|
1306
1409
|
alias: targetAlias,
|
|
1307
1410
|
dryRun,
|
|
@@ -1336,6 +1439,7 @@ function usage() {
|
|
|
1336
1439
|
Options:
|
|
1337
1440
|
--cwd <dir> target project (default: current directory)
|
|
1338
1441
|
--dir <path> components folder (default: src/components/ui)
|
|
1442
|
+
--animations <path> animations folder (default: src/components/animations)
|
|
1339
1443
|
--lib <path> helpers folder (default: src/lib)
|
|
1340
1444
|
--alias <alias> target import alias (default: @)
|
|
1341
1445
|
--color <hex> primary color, written in the Tailwind tokens (@theme)
|
|
@@ -1348,9 +1452,11 @@ Options:
|
|
|
1348
1452
|
|
|
1349
1453
|
Without --page, the CLI asks whether a file is already ready to receive the
|
|
1350
1454
|
components: they are added to that file, to a new folder + file, or installed
|
|
1351
|
-
only.
|
|
1352
|
-
|
|
1353
|
-
|
|
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.
|
|
1354
1460
|
Colors go through a Tailwind CSS 4 theme (uifolio-theme.css, @theme), so classes
|
|
1355
1461
|
like bg-primary and text-muted-foreground stay in the code. When Tailwind is
|
|
1356
1462
|
missing, "add" installs it (with your lockfile's package manager), creates the
|
|
@@ -1361,6 +1467,7 @@ installed too, as runtime dependencies, when the manifest lacks them.
|
|
|
1361
1467
|
|
|
1362
1468
|
Examples:
|
|
1363
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
|
|
1364
1471
|
node cli/uifolio.mjs add otp --color "#10B981" --radius 8
|
|
1365
1472
|
node cli/uifolio.mjs add calendar --page src/pages/calendar.tsx
|
|
1366
1473
|
`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uifolio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"cli",
|
|
56
56
|
"src/components/ui",
|
|
57
57
|
"!src/components/ui/stories-tokens.ts",
|
|
58
|
+
"src/components/animations",
|
|
58
59
|
"src/lib/color.ts",
|
|
59
60
|
"src/lib/date.ts",
|
|
60
61
|
"src/lib/uploader.ts",
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface AnimationSettings {
|
|
4
|
+
delay: number
|
|
5
|
+
duration: number
|
|
6
|
+
easing: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface AnimationSettingsContextValue extends AnimationSettings {
|
|
10
|
+
setDelay: (value: number) => void
|
|
11
|
+
setDuration: (value: number) => void
|
|
12
|
+
setEasing: (value: string) => void
|
|
13
|
+
reset: () => void
|
|
14
|
+
isCustomized: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const EASING_OPTIONS = [
|
|
18
|
+
{ value: 'ease-out', label: 'Soft' },
|
|
19
|
+
{ value: 'ease-in-out', label: 'Smooth' },
|
|
20
|
+
{ value: 'cubic-bezier(0.65, 0, 0.35, 1)', label: 'Spring' },
|
|
21
|
+
{ value: 'cubic-bezier(0.34, 1.56, 0.64, 1)', label: 'Bounce' },
|
|
22
|
+
{ value: 'linear', label: 'Linear' },
|
|
23
|
+
] as const
|
|
24
|
+
|
|
25
|
+
const fallback: AnimationSettings = {
|
|
26
|
+
delay: 0,
|
|
27
|
+
duration: 500,
|
|
28
|
+
easing: 'ease-out',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const animationDefaults: Record<string, AnimationSettings> = {
|
|
32
|
+
'horizontal-accordion': {
|
|
33
|
+
delay: 0,
|
|
34
|
+
duration: 500,
|
|
35
|
+
easing: 'cubic-bezier(0.65, 0, 0.35, 1)',
|
|
36
|
+
},
|
|
37
|
+
storytelling: {
|
|
38
|
+
delay: 0,
|
|
39
|
+
duration: 500,
|
|
40
|
+
easing: 'cubic-bezier(0.65, 0, 0.35, 1)',
|
|
41
|
+
},
|
|
42
|
+
stacking: {
|
|
43
|
+
delay: 0,
|
|
44
|
+
duration: 300,
|
|
45
|
+
easing: 'ease-out',
|
|
46
|
+
},
|
|
47
|
+
'scroll-text': {
|
|
48
|
+
delay: 0,
|
|
49
|
+
duration: 300,
|
|
50
|
+
easing: 'ease-out',
|
|
51
|
+
},
|
|
52
|
+
'image-crossfade': {
|
|
53
|
+
delay: 0,
|
|
54
|
+
duration: 500,
|
|
55
|
+
easing: 'ease-out',
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function defaultAnimationSettings(slug?: string): AnimationSettings {
|
|
60
|
+
return (slug && animationDefaults[slug]) || fallback
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const AnimationSettingsContext = createContext<AnimationSettingsContextValue | null>(null)
|
|
64
|
+
|
|
65
|
+
export function useAnimationSettings() {
|
|
66
|
+
const context = useContext(AnimationSettingsContext)
|
|
67
|
+
if (!context) {
|
|
68
|
+
throw new Error('useAnimationSettings must be used within AnimationSettingsProvider')
|
|
69
|
+
}
|
|
70
|
+
return context
|
|
71
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from 'react'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import {
|
|
4
|
+
AnimationSettingsContext,
|
|
5
|
+
defaultAnimationSettings,
|
|
6
|
+
type AnimationSettings,
|
|
7
|
+
} from './animation-settings-context'
|
|
8
|
+
|
|
9
|
+
export function AnimationSettingsProvider({
|
|
10
|
+
current,
|
|
11
|
+
children,
|
|
12
|
+
}: {
|
|
13
|
+
current?: string
|
|
14
|
+
children: ReactNode
|
|
15
|
+
}) {
|
|
16
|
+
const key = current ?? '__default'
|
|
17
|
+
const [settingsBySlug, setSettingsBySlug] = useState<Record<string, AnimationSettings>>({})
|
|
18
|
+
|
|
19
|
+
const settings = settingsBySlug[key] ?? defaultAnimationSettings(current)
|
|
20
|
+
|
|
21
|
+
const update = useCallback(
|
|
22
|
+
(patch: Partial<AnimationSettings>) => {
|
|
23
|
+
setSettingsBySlug((previous) => ({
|
|
24
|
+
...previous,
|
|
25
|
+
[key]: {
|
|
26
|
+
...(previous[key] ?? defaultAnimationSettings(current)),
|
|
27
|
+
...patch,
|
|
28
|
+
},
|
|
29
|
+
}))
|
|
30
|
+
},
|
|
31
|
+
[current, key],
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
const setDelay = useCallback(
|
|
35
|
+
(delay: number) => update({ delay: Math.max(0, Math.min(2000, delay)) }),
|
|
36
|
+
[update],
|
|
37
|
+
)
|
|
38
|
+
const setDuration = useCallback(
|
|
39
|
+
(duration: number) => update({ duration: Math.max(100, Math.min(3000, duration)) }),
|
|
40
|
+
[update],
|
|
41
|
+
)
|
|
42
|
+
const setEasing = useCallback((easing: string) => update({ easing }), [update])
|
|
43
|
+
|
|
44
|
+
const reset = useCallback(() => {
|
|
45
|
+
setSettingsBySlug((previous) => {
|
|
46
|
+
const next = { ...previous }
|
|
47
|
+
delete next[key]
|
|
48
|
+
return next
|
|
49
|
+
})
|
|
50
|
+
}, [key])
|
|
51
|
+
|
|
52
|
+
const value = useMemo(
|
|
53
|
+
() => ({
|
|
54
|
+
...settings,
|
|
55
|
+
setDelay,
|
|
56
|
+
setDuration,
|
|
57
|
+
setEasing,
|
|
58
|
+
reset,
|
|
59
|
+
isCustomized: Boolean(settingsBySlug[key]),
|
|
60
|
+
}),
|
|
61
|
+
[reset, setDelay, setDuration, setEasing, settings, settingsBySlug, key],
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<AnimationSettingsContext.Provider value={value}>
|
|
66
|
+
{children}
|
|
67
|
+
</AnimationSettingsContext.Provider>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export interface MotionCardData {
|
|
2
|
+
id: string
|
|
3
|
+
number: string
|
|
4
|
+
title: string
|
|
5
|
+
kicker: string
|
|
6
|
+
description: string
|
|
7
|
+
gradient: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const seeds: Omit<MotionCardData, 'id' | 'number'>[] = [
|
|
11
|
+
{
|
|
12
|
+
title: 'Explore',
|
|
13
|
+
kicker: 'Field',
|
|
14
|
+
description: 'Spot the signals, collect the material and open up the range of possibilities.',
|
|
15
|
+
gradient: 'linear-gradient(135deg, color-mix(in oklab, var(--primary) 22%, #000) 0%, var(--primary) 58%, color-mix(in oklab, var(--primary) 55%, #fff) 100%)',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
title: 'Compose',
|
|
19
|
+
kicker: 'Workshop',
|
|
20
|
+
description: 'Assemble the pieces, test rhythms and let the structure breathe.',
|
|
21
|
+
gradient: 'linear-gradient(145deg, color-mix(in oklab, var(--primary) 38%, #000) 0%, color-mix(in oklab, var(--primary) 85%, #000) 52%, color-mix(in oklab, var(--primary) 62%, #fff) 100%)',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
title: 'Publish',
|
|
25
|
+
kicker: 'Release',
|
|
26
|
+
description: 'Shape it, tell it and share a clear version to read and to live with.',
|
|
27
|
+
gradient: 'linear-gradient(125deg, color-mix(in oklab, var(--primary) 15%, #000) 0%, color-mix(in oklab, var(--primary) 70%, #000) 55%, color-mix(in oklab, var(--primary) 35%, #fff) 100%)',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
title: 'Measure',
|
|
31
|
+
kicker: 'Reading',
|
|
32
|
+
description: 'Watch the usage, compare the signals and keep what truly makes sense.',
|
|
33
|
+
gradient: 'linear-gradient(155deg, color-mix(in oklab, var(--primary) 30%, #000) 0%, var(--primary) 60%, color-mix(in oklab, var(--primary) 75%, #fff) 100%)',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
title: 'Iterate',
|
|
37
|
+
kicker: 'Loop',
|
|
38
|
+
description: 'Fix, refine, start again: every pass tightens the gesture and the message.',
|
|
39
|
+
gradient: 'linear-gradient(120deg, color-mix(in oklab, var(--primary) 12%, #000) 0%, color-mix(in oklab, var(--primary) 60%, #000) 50%, color-mix(in oklab, var(--primary) 25%, #fff) 100%)',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
title: 'Spread',
|
|
43
|
+
kicker: 'Network',
|
|
44
|
+
description: 'Let the material circulate, build bridges and let the project spread.',
|
|
45
|
+
gradient: 'linear-gradient(135deg, color-mix(in oklab, var(--primary) 25%, #000) 0%, color-mix(in oklab, var(--primary) 90%, #000) 55%, color-mix(in oklab, var(--primary) 50%, #fff) 100%)',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
title: 'Archive',
|
|
49
|
+
kicker: 'Memory',
|
|
50
|
+
description: 'Keep the traces, document the choices and make the work pass on.',
|
|
51
|
+
gradient: 'linear-gradient(150deg, color-mix(in oklab, var(--primary) 45%, #000) 0%, color-mix(in oklab, var(--primary) 75%, #000) 50%, color-mix(in oklab, var(--primary) 40%, #fff) 100%)',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
title: 'Start over',
|
|
55
|
+
kicker: 'Cycle',
|
|
56
|
+
description: 'Set off again with what you learned, without losing the momentum or the precision of the first gesture.',
|
|
57
|
+
gradient: 'linear-gradient(130deg, color-mix(in oklab, var(--primary) 18%, #000) 0%, var(--primary) 55%, color-mix(in oklab, var(--primary) 70%, #fff) 100%)',
|
|
58
|
+
},
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
export function makeMotionCards(count: number): MotionCardData[] {
|
|
62
|
+
const total = Math.max(1, Math.min(count, seeds.length))
|
|
63
|
+
return seeds.slice(0, total).map((seed, index) => ({
|
|
64
|
+
...seed,
|
|
65
|
+
id: `${index}-${seed.title.toLowerCase()}`,
|
|
66
|
+
number: String(index + 1).padStart(2, '0'),
|
|
67
|
+
}))
|
|
68
|
+
}
|