motion-presets-kit 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Orlando Lopez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,174 @@
1
+ # motion-presets-kit
2
+
3
+ Set tipado y validado en runtime de helpers para [`motion/react`](https://motion.dev) que facilita crear animaciones de entrada, salida y transiciones con valores por defecto sensatos.
4
+
5
+ - **API variant-returning**: funciones puras que retornan `Variants` de motion.
6
+ - **Validación con Zod** de todas las opciones (los tipos se derivan de los schemas).
7
+ - **Tree-shakeable**: 3 entry points (`.`, `./presets`, `./components`).
8
+ - **SSR-safe** y respeta `prefers-reduced-motion` por defecto.
9
+
10
+ ## Instalación
11
+
12
+ ```bash
13
+ pnpm add motion-presets-kit motion zod react react-dom
14
+ ```
15
+
16
+ `react`, `react-dom`, `motion` y `zod` son peer dependencies.
17
+
18
+ ## Skill para agentes de IA
19
+
20
+ El paquete incluye una skill (`skill/motion-presets-kit/SKILL.md`) para que
21
+ asistentes como opencode, Claude o agentes compatibles conozcan la API completa.
22
+ Al terminar la instalación se pregunta si deseas instalarla (solo en terminal
23
+ interactiva; en CI se omite). También puedes instalarla en cualquier momento:
24
+
25
+ ```bash
26
+ npx motion-presets-kit add-skill # auto-detecta .opencode, .claude, .agents
27
+ npx motion-presets-kit add-skill --global # instala en tu HOME
28
+ npx motion-presets-kit add-skill --tool opencode --project
29
+ ```
30
+
31
+ Para omitir la pregunta usa `MOTION_PRESETS_KIT_SKIP_SKILL=1`.
32
+
33
+ ## Uso rápido
34
+
35
+ ```tsx
36
+ import { motion } from "motion/react";
37
+ import { fade, parentVariants } from "motion-presets-kit/presets";
38
+
39
+ export function Hero() {
40
+ return (
41
+ <motion.div variants={parentVariants({ delayChildren: 0.12 })}>
42
+ <motion.h1 variants={fade({ direction: "up" })}>Hola</motion.h1>
43
+ <motion.p variants={fade({ direction: "up", excludeDelay: true })}>
44
+ Motion presets kit
45
+ </motion.p>
46
+ </motion.div>
47
+ );
48
+ }
49
+ ```
50
+
51
+ ```tsx
52
+ import { TextAnimate, Marquee, useCounter, Draggable } from "motion-presets-kit/components";
53
+ ```
54
+
55
+ ## Entry points
56
+
57
+ | Import | Contenido |
58
+ |---|---|
59
+ | `motion-presets-kit` | schemas + presets + componentes |
60
+ | `motion-presets-kit/presets` | `fade`, `slide`, `scale`, `clipReveal`, `parentVariants` |
61
+ | `motion-presets-kit/components` | `TextAnimate`, `Marquee`, `Draggable`, `useCounter` |
62
+
63
+ ## Presets
64
+
65
+ Todos los presets aceptan: `duration` (0.5), `delay` (0), `ease` (`[0.16, 1, 0.3, 1]`), `spring`, `excludeDelay` (false) y `reducedMotion` (true).
66
+
67
+ ### `fade(options?)`
68
+
69
+ `direction` (`"up" | "down" | "left" | "right" | "none"`, default `"up"`), `distance` (60), `blur` (0), `scale` (1).
70
+
71
+ ```tsx
72
+ <motion.div variants={fade({ direction: "left", distance: 40, blur: 8 })} />
73
+ ```
74
+
75
+ ### `slide(options?)`
76
+
77
+ Desplazamiento puro, **sin `opacity`**. `direction` (default `"right"`), `distance` (100).
78
+
79
+ ```tsx
80
+ <motion.div variants={slide({ direction: "right" })} />
81
+ // initial: { x: -100 } → animate: { x: 0 }
82
+ ```
83
+
84
+ ### `scale(options?)`
85
+
86
+ `from` (0.8) → `1` junto con `opacity: 0 → 1`.
87
+
88
+ ```tsx
89
+ <motion.div variants={scale({ from: 0.5 })} />
90
+ ```
91
+
92
+ ### `clipReveal(options?)`
93
+
94
+ Anima `clipPath` con insets por dirección y `scale` opcional.
95
+
96
+ ```tsx
97
+ <motion.div variants={clipReveal({ direction: "up", scale: 1.1 })} />
98
+ // initial: { clipPath: "inset(100% 0% 0% 0%)" } → animate: { clipPath: "inset(0% 0% 0% 0%)" }
99
+ ```
100
+
101
+ ### `parentVariants(options?)`
102
+
103
+ Contenedor para stagger: `delayChildren` (0) y `startDelay` (0).
104
+
105
+ ```tsx
106
+ <motion.div variants={parentVariants({ delayChildren: 0.1 })} initial="initial" whileInView="animate">
107
+ <motion.span variants={fade({ excludeDelay: true })} />
108
+ <motion.span variants={fade({ excludeDelay: true })} />
109
+ </motion.div>
110
+ ```
111
+
112
+ ## Componentes y hooks
113
+
114
+ ### `<TextAnimate>`
115
+
116
+ `text` (requerido), `as` (`"p"`), `by` (`"word" | "letter"`), `type` (`blurIn | slideUp | slideDown | slideLeft | slideRight | typeWriter`), `duration`, `startDelay`, `highlight: string[]`, `highlightClassName`.
117
+
118
+ ```tsx
119
+ <TextAnimate text="Hola <strong>mundo</strong>" by="letter" type="blurIn" highlight={["mundo"]} />
120
+ ```
121
+
122
+ El HTML inline se inyecta con `dangerouslySetInnerHTML`; sanitiza `text` si viene de input no confiable.
123
+
124
+ ### `<Marquee>`
125
+
126
+ CSS puro con `@keyframes` dinámicos. `children` (requerido), `speed` (20 s/vuelta), `direction` (`"left"`), `pauseOnHover` (true), `gap` (`"2rem"`).
127
+
128
+ ```tsx
129
+ <Marquee speed={30} direction="right" gap="1.5rem">
130
+ <span>Logo 1</span>
131
+ <span>Logo 2</span>
132
+ </Marquee>
133
+ ```
134
+
135
+ ### `<Draggable>`
136
+
137
+ Wrapper de `motion.div`. `axis` (`"both"`), `bounds` (`"parent" | "document" | {top,right,bottom,left}`), `snapToGrid: [number, number]`, `dragElastic`, `dragMomentum`, `dragControls`, `onDragStart`/`onDrag`/`onDragEnd`.
138
+
139
+ ```tsx
140
+ <Draggable axis="x" bounds="parent" snapToGrid={[20, 20]}>
141
+ <div>Arrastrame</div>
142
+ </Draggable>
143
+ ```
144
+
145
+ Con `bounds="parent"` el elemento queda restringido a su contenedor; el contenedor debería poder medirse (`position: relative`).
146
+
147
+ ### `useCounter(ref, options?)`
148
+
149
+ `from` (0), `to` (100), `decimals` (0), `prefix`, `suffix`, `separator` (","), `format`, `duration` (1). Escribe el valor formateado en `ref.current.textContent` y retorna `{ value, start, reset }`.
150
+
151
+ ```tsx
152
+ const ref = useRef<HTMLSpanElement>(null);
153
+ const { start, reset } = useCounter(ref, { to: 1250, separator: ",", suffix: " px" });
154
+ return <span ref={ref} />;
155
+ ```
156
+
157
+ ## Casos límite documentados
158
+
159
+ - **CL-001 — `excludeDelay` con stagger**: un hijo con `delay` propio sobrescribe el stagger del padre. Usa siempre `excludeDelay: true` en los hijos dentro de `parentVariants()`.
160
+ - **CL-003 — ClipReveal con `border-radius`**: el `clip-path` se superpone al radio del borde; es una limitación conocida del `clip-path`.
161
+ - **CL-005 — Counter con negativos**: el formateo preserva el signo (`-1,250`).
162
+ - **CL-007 — Draggable con bounds inválidos**: si `top > bottom` o `left > right`, se emite un `warn()` y se ignoran los límites.
163
+ - **CL-008 — SSR**: los presets retornan variants estáticas (sin `window`). Los componentes usan `whileInView`, que se activa solo en el cliente.
164
+ - **CL-009 — Reduced motion**: con `prefers-reduced-motion: reduce` (y `reducedMotion: true`, por defecto), los presets saltan al estado final, `TextAnimate` muestra el texto sin animar y `Marquee` no reproduce el loop.
165
+
166
+ ## Desarrollo
167
+
168
+ ```bash
169
+ pnpm playground # demo Vite de todos los presets/componentes
170
+ pnpm test # build + tests con node:test
171
+ pnpm typecheck
172
+ pnpm lint
173
+ pnpm build
174
+ ```
package/bin/cli.mjs ADDED
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ detectTargets,
4
+ installSkill,
5
+ readPackageVersion,
6
+ SKILL_NAME,
7
+ TOOLS,
8
+ } from "./skill.mjs";
9
+
10
+ const HELP = `
11
+ motion-presets-kit ${readPackageVersion()}
12
+
13
+ Uso:
14
+ motion-presets-kit <comando> [opciones]
15
+
16
+ Comandos:
17
+ add-skill Instala la skill de ${SKILL_NAME} para agentes de IA.
18
+ help Muestra esta ayuda.
19
+
20
+ Opciones:
21
+ --global Instala en el directorio de usuario (home).
22
+ --project Instala en el proyecto actual (por defecto).
23
+ --tool <nombre> ${Object.keys(TOOLS).join(" | ")} | all (por defecto: all).
24
+ --dir <ruta> Directorio de skills explícito.
25
+ --force Sobrescribe una skill ya instalada.
26
+
27
+ Ejemplos:
28
+ npx motion-presets-kit add-skill
29
+ npx motion-presets-kit add-skill --global
30
+ npx motion-presets-kit add-skill --tool opencode --project
31
+ `.trim();
32
+
33
+ function parseArgs(argv) {
34
+ const options = {
35
+ command: null,
36
+ global: false,
37
+ project: false,
38
+ tool: "all",
39
+ dir: undefined,
40
+ force: false,
41
+ };
42
+
43
+ for (let index = 0; index < argv.length; index += 1) {
44
+ const arg = argv[index];
45
+ if (arg === "add-skill" || arg === "skill" || arg === "init") {
46
+ options.command = "add-skill";
47
+ } else if (arg === "help" || arg === "--help" || arg === "-h") {
48
+ options.command = "help";
49
+ } else if (arg === "--version" || arg === "-v") {
50
+ options.command = "version";
51
+ } else if (arg === "--global" || arg === "-g") {
52
+ options.global = true;
53
+ } else if (arg === "--project") {
54
+ options.project = true;
55
+ } else if (arg === "--force" || arg === "-f") {
56
+ options.force = true;
57
+ } else if (arg === "--tool") {
58
+ options.tool = argv[index + 1] ?? "all";
59
+ index += 1;
60
+ } else if (arg === "--dir") {
61
+ options.dir = argv[index + 1];
62
+ index += 1;
63
+ }
64
+ }
65
+ return options;
66
+ }
67
+
68
+ function run() {
69
+ const options = parseArgs(process.argv.slice(2));
70
+
71
+ if (options.command === "version") {
72
+ console.log(readPackageVersion());
73
+ return;
74
+ }
75
+ if (options.command === "help" || options.command === null) {
76
+ console.log(HELP);
77
+ return;
78
+ }
79
+
80
+ if (options.tool !== "all" && !(options.tool in TOOLS)) {
81
+ console.error(
82
+ `[${SKILL_NAME}] Herramienta desconocida: ${options.tool}. Usa: ${Object.keys(
83
+ TOOLS,
84
+ ).join(", ")} o all.`,
85
+ );
86
+ process.exitCode = 1;
87
+ return;
88
+ }
89
+
90
+ const targets = detectTargets({
91
+ global: options.global && !options.project,
92
+ tool: options.tool,
93
+ dir: options.dir,
94
+ });
95
+ const results = installSkill(targets, { force: options.force });
96
+
97
+ for (const result of results) {
98
+ if (result.status === "installed") {
99
+ console.log(
100
+ `[${SKILL_NAME}] Skill instalada (${result.tool}): ${result.destination}`,
101
+ );
102
+ } else if (result.status === "exists") {
103
+ console.log(
104
+ `[${SKILL_NAME}] Ya existía (${result.tool}): ${result.destination}. Usa --force para sobrescribir.`,
105
+ );
106
+ } else {
107
+ console.error(
108
+ `[${SKILL_NAME}] Error al instalar (${result.tool}): ${result.error}`,
109
+ );
110
+ process.exitCode = 1;
111
+ }
112
+ }
113
+ }
114
+
115
+ run();
package/bin/skill.mjs ADDED
@@ -0,0 +1,94 @@
1
+ import {
2
+ copyFileSync,
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ } from "node:fs";
7
+ import { homedir } from "node:os";
8
+ import { dirname, join, resolve } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+
11
+ const HERE = dirname(fileURLToPath(import.meta.url));
12
+ export const PACKAGE_ROOT = resolve(HERE, "..");
13
+ export const SKILL_NAME = "motion-presets-kit";
14
+ export const SKILL_SOURCE = join(
15
+ PACKAGE_ROOT,
16
+ "skill",
17
+ SKILL_NAME,
18
+ "SKILL.md",
19
+ );
20
+
21
+ /** Raíces por herramienta: proyecto y global. */
22
+ export const TOOLS = {
23
+ opencode: { projectRoot: ".opencode", globalRoot: join(".config", "opencode") },
24
+ claude: { projectRoot: ".claude", globalRoot: ".claude" },
25
+ agents: { projectRoot: ".agents", globalRoot: ".agents" },
26
+ };
27
+
28
+ export function readPackageVersion() {
29
+ try {
30
+ const pkg = JSON.parse(
31
+ readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"),
32
+ );
33
+ return pkg.version ?? "0.0.0";
34
+ } catch {
35
+ return "0.0.0";
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Resuelve los directorios de skills donde instalar.
41
+ * Con `dir` se usa ese directorio; si no, auto-detecta las herramientas
42
+ * existentes (opencode, claude, agents) y cae en opencode si no hay ninguna.
43
+ */
44
+ export function detectTargets({ global = false, tool = "all", dir } = {}) {
45
+ if (dir) return [{ tool: "custom", dir: resolve(dir) }];
46
+
47
+ const names = tool === "all" ? Object.keys(TOOLS) : [tool];
48
+ const all = names.map((name) => {
49
+ const root = global
50
+ ? join(homedir(), TOOLS[name].globalRoot)
51
+ : resolve(process.cwd(), TOOLS[name].projectRoot);
52
+ return { tool: name, root, dir: join(root, "skills") };
53
+ });
54
+
55
+ const existing = all.filter((target) => existsSync(target.root));
56
+ if (existing.length > 0) {
57
+ return existing.map(({ tool: name, dir: skillsDir }) => ({
58
+ tool: name,
59
+ dir: skillsDir,
60
+ }));
61
+ }
62
+ if (tool !== "all") {
63
+ return all.map(({ tool: name, dir: skillsDir }) => ({
64
+ tool: name,
65
+ dir: skillsDir,
66
+ }));
67
+ }
68
+ return [{ tool: names[0], dir: all[0].dir }];
69
+ }
70
+
71
+ /**
72
+ * Copia la skill a cada directorio destino.
73
+ * Devuelve un resultado por destino con el estado de la operación.
74
+ */
75
+ export function installSkill(targets, { force = false } = {}) {
76
+ return targets.map((target) => {
77
+ const destination = join(target.dir, SKILL_NAME, "SKILL.md");
78
+ try {
79
+ if (existsSync(destination) && !force) {
80
+ return { ...target, destination, status: "exists" };
81
+ }
82
+ mkdirSync(join(target.dir, SKILL_NAME), { recursive: true });
83
+ copyFileSync(SKILL_SOURCE, destination);
84
+ return { ...target, destination, status: "installed" };
85
+ } catch (error) {
86
+ return {
87
+ ...target,
88
+ destination,
89
+ status: "error",
90
+ error: error instanceof Error ? error.message : String(error),
91
+ };
92
+ }
93
+ });
94
+ }