@vinena/vectoys-cli 0.0.0-stage → 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 Mert Düzgün
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 CHANGED
@@ -1,3 +1,40 @@
1
- # Temporary Holding Version
1
+ # @vinena/vectoys-cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The vectoys command line: copy vectoys devices and toys into your project as source, and make, check and preview items of your own.
4
+
5
+ ## Install
6
+
7
+ Run it with npx, no install needed:
8
+
9
+ ```sh
10
+ npx @vinena/vectoys-cli add laptop
11
+ ```
12
+
13
+ Or install it to get the `vectoys` command:
14
+
15
+ ```sh
16
+ npm install --global @vinena/vectoys-cli
17
+ vectoys list
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```sh
23
+ npx @vinena/vectoys-cli list # show every item
24
+ npx @vinena/vectoys-cli add laptop phone # copy items in and install @vinena/vectoys and @vinena/vectoys-react
25
+ npx @vinena/vectoys-cli create spinning-top # start a new item
26
+ npx @vinena/vectoys-cli check spinning-top # check it (needs bun)
27
+ npx @vinena/vectoys-cli preview spinning-top # render it to preview.html (needs bun)
28
+ npx @vinena/vectoys-cli skill # print how a coding agent makes an item
29
+ ```
30
+
31
+ `npx @vinena/vectoys-cli --help` lists every command and option. Items need React 19 or later.
32
+
33
+ ## Links
34
+
35
+ - Site and items: [vectoys.com](https://vectoys.com)
36
+ - Source: [github.com/vinenastudio/vectoys](https://github.com/vinenastudio/vectoys)
37
+
38
+ ## License
39
+
40
+ MIT
package/SKILL.md ADDED
@@ -0,0 +1,171 @@
1
+ ---
2
+ name: vectoys
3
+ description: Use when making a 3D device or toy for a landing page with vectoys (a laptop, a phone, a gift box, a keycap, any small object drawn as SVG paths), when editing a vectoys item, or when checking and submitting one to the vectoys registry.
4
+ ---
5
+
6
+ # Make a vectoys item
7
+
8
+ vectoys draws 3D objects as plain SVG paths. An item is a small React component plus its geometry: it renders on the server, stays sharp at any size, and can carry live HTML on a face. `@vinena/vectoys` turns parts into paths. `@vinena/vectoys-react` draws them. Items copy into a project as source with `npx @vinena/vectoys-cli add <name>`.
9
+
10
+ ## Workflow
11
+
12
+ 1. `npx @vinena/vectoys-cli create <name>` writes `./<name>/` (or under `--dir <dir>`): `item.json`, `src/geometry.ts`, `src/<name>.tsx`, `src/index.ts`, a test, `package.json` and `tsconfig.json`. It renders a shaded, tilted slab. Under `registry/` of a vectoys checkout it uses the workspace's packages, which `bun install` links. Anywhere else it asks for `@vinena/vectoys` and `@vinena/vectoys-react` from npm by version: install them with `npm install` (or your package manager) in the item folder before you check it.
13
+ 2. Edit `src/geometry.ts` (parts, camera, poses) and `src/<name>.tsx` (looks, motion, interaction). Write a real `title` and `description` in `item.json`. List every source file in `files`.
14
+ 3. `npx @vinena/vectoys-cli check <dir>` runs 8 checks: item.json is valid, the files are under 64 KB together, item.json names no brand, the component renders on the server with no props, the markup has no `NaN` or `Infinity`, two renders give the same markup, the rendered text names no brand, and the root has an accessible name. It needs bun. Fix every FAIL line.
15
+ 4. `npx @vinena/vectoys-cli preview <dir>` writes `<dir>/preview.html` (`--open` opens it): the item rendered on the server in light and dark, and at a row of yaws when the component takes a `yaw` prop. Look at it. Faces drawn in the wrong order, gaps, and parts that float show here, not in the checks.
16
+ 5. Iterate on 2 to 4 until it looks right in both themes and at every yaw.
17
+ 6. `npx @vinena/vectoys-cli submit <dir>` runs the checks, then opens a pull request (it needs the GitHub CLI, `gh`) that adds the item to vinenastudio/vectoys. If you host your own registry, `npx @vinena/vectoys-cli submit --registry <url>` opens a pull request that lists it. Until submissions open at launch, submit prints what it would send and sends nothing. `--dry-run` does the same at any time.
18
+
19
+ ## The model
20
+
21
+ Units are scene units. The camera's `width` (100 by default) and `height` are the SVG viewBox. Pick a scale such as `const mm = (v: number) => v * 0.4` and write real sizes.
22
+
23
+ Axes. A part's own plane is x right, y down, z out of its top face. The model is x across, y towards the front, z up. A part lying flat faces up. Stand it upright towards the eye with `rotation: rotateX(-90)`.
24
+
25
+ Parts (`@vinena/vectoys`):
26
+
27
+ - `slab({ name, box, radii, thickness, edge, bevel, taper, pose, order, children, step, outline })`: a rounded rectangle `box: [left, top, right, bottom]` with corner `radii: [top, bottom]`, `thickness` deep. The top face is at z = 0 and the bottom at z = -thickness. `edge` rounds the side band (`bevel: "chamfer"` cuts it flat instead). `taper` leans the sides in, as a keycap's do.
28
+ - `plate({ name, box, radii, lift, role, pose, order, children })`: a flat face `lift` (0.02 by default) over its parent's z = 0: glass, a label, a trackpad, a ribbon band.
29
+ - `outline`: a convex, clockwise list of points that replaces the rounded box (an envelope's triangle flap). `box` still bounds it.
30
+ - `step`: the spacing of points round a corner (0.1 by default). Use 0.5 or more on a part that moves every frame, so fewer faces draw.
31
+
32
+ A scene is `Part[]`. Children sit in their parent's space, so a lid is a child posed at its hinge.
33
+
34
+ Poses and joints. A `Pose` is `{ position: [x, y, z], rotation }`. Build rotations with `rotateX`, `rotateY`, `rotateZ` (degrees, the same way round as CSS), `rotateAxis(axis, degrees)`, and join them with `combine(a, b, c)` (read like a CSS transform list: the last turns first). `IDENTITY` and `REST` are the no-turn rotation and pose. For a joint, compute the child's pose from a number (an open share, an angle) in a function such as `lidPose(open)`. Use `slerp(from, to, share)` to blend two rotations. To turn about a point that is not the part's origin, rotate the offset with `rotate(rotation, offset)` and add it to the position.
35
+
36
+ Camera. `camera({ width, height, perspective, yaw, pitch, roll, anchorX, anchorY, back, originX, originY })`. `perspective` about 280 to 400 for a scene 100 wide. `yaw` turns the model about its up axis and `pitch` tips it to or from the eye. A device at rest usually sits near `yaw: -24, pitch: -16`.
37
+
38
+ Drawing. `draw(scene, eye)` returns `{ faces, hulls }`: the faces that face the eye, far to near, and each part's silhouette. Parts sort by `order` first, then by depth. Leave `order` out unless two parts draw in the wrong order: parts that interlock, or one that swings past another's centre (a lid over its base). Then give the part in front a higher `order`.
39
+
40
+ `footprint(scene, eye, { gap, spread, ahead })` is the shadow on the ground under the model. Draw it first, blurred if you like. `poses(scene)` maps each part's name to its pose in the scene, for laying a screen on a child part.
41
+
42
+ Keep `seen(...)` in `geometry.ts`: one pure function from the item's state (open share, hover lift, yaw) to `{ camera, drawn, ... }`. The component calls it inside `useMemo`.
43
+
44
+ ## Looks and shading
45
+
46
+ `<Faces faces hulls styles shading />` draws what `draw` returned. `styles` maps a role to SVG path props: `top`, `side`, `bottom`, a plate's `role`, or `"part.role"` for one part only (`"body.top"`, `"knot.side"`). Write fills as `style: { fill }`.
47
+
48
+ Take colours from the page's CSS variables, each with a fallback so the item shows before the page sets them:
49
+
50
+ - `--vectoys-finish`: the main material. Expose a `finish` prop that sets it.
51
+ - `--vectoys-surface`, `--vectoys-hairline`, `--vectoys-shadow`, `--vectoys-accent`, `--vectoys-ink`, `--vectoys-bg`. Add an item variable such as `--vectoys-paper` only when none of these fits.
52
+ - `--vectoys-focus` and `--vectoys-focus-width` for focus rings.
53
+
54
+ Mix tones with `color-mix(in srgb, ...)` instead of new colours. `shading` darkens faces turned from the light and lifts the few turned right at it. The page tunes it with `--vectoys-shade-strength` (0.5), `--vectoys-shade-color`, `--vectoys-highlight-strength` (0.25) and `--vectoys-highlight`. Draw a hairline outline over the main body's hull with `vectorEffect: "non-scaling-stroke"`. Check the item on a dark page too.
55
+
56
+ ## Live HTML on a face
57
+
58
+ `<Stage camera overlay role label style>` is the box. It keeps the camera's aspect ratio and scales with its parent. Put a `<Screen camera pose rect width>` in `overlay` to lay live HTML on a face. `rect` is a `ScreenRect` `{ x, y, width, height, z }` in the part's own plane. `width` is the CSS pixels the content lays out at. The Screen hides its content while the face points away. Round its corners with `borderRadius` as a share of the rect, and set `overflow: "hidden"`.
59
+
60
+ ## Motion
61
+
62
+ - `useSpring(target, { stiffness, damping })` returns a number that springs to `target` each frame. Feed it into `seen`.
63
+ - `useFrames()` returns `run(step)`. It calls `step(elapsedMs)` every frame until `step` returns false. Use it for timed runs or several springs at once, with `spring` and `settled` from the engine.
64
+ - `useReducedMotion()` is true when the page asks for less motion. `useSpring` already jumps straight to its target then. In your own `useFrames` loop, set the end state at once instead.
65
+
66
+ ## Server rendering must be deterministic
67
+
68
+ The same props must give the same markup on the server and the client, every time:
69
+
70
+ - Write fixed numbers. Do not use `Math.random()`, `Date`, `performance.now()` or `window` during render. Start motion in effects.
71
+ - Round numbers you put in styles with `fixed()` from the engine.
72
+ - Make SVG ids unique per instance with `useId()` (strip characters other than `\w` and `-`), so two items share a page.
73
+ - The component must render with no props at all.
74
+
75
+ ## Accessibility
76
+
77
+ - A picture: the root is `role="img"` with an `aria-label` (Stage takes `role` and `label`). With live content inside, use `role="group"` so its links stay reachable.
78
+ - Interactive: use a real `<button>` or `<a href>` with a label, laid over the item's bounds. Give it a visible `:focus-visible` ring in `--vectoys-focus`. Enter and Space must do what a click does.
79
+ - Respect reduced motion: the state still changes, only without the motion.
80
+ - Start hover effects only when `event.pointerType === "mouse"`, so a tap does not stick in the hover state.
81
+
82
+ ## Rules
83
+
84
+ - No brand, maker or product names (no trademarked device names) in item.json, the code or the rendered text. Make a generic phone, not a named one.
85
+ - Keep the files under 64 KB together.
86
+ - React 19. Depend only on `@vinena/vectoys`, `@vinena/vectoys-react` and React. No other packages.
87
+ - TypeScript strict. One component per item, named after the item (`my-toy` exports `MyToy`).
88
+ - Props: `label`, `finish`, `yaw` (and `pitch`) for a device that turns, and the state as controlled plus default (`open` and `defaultOpen` with `onOpenChange`), as the registry items do.
89
+
90
+ ## Example
91
+
92
+ A badge that lifts towards the mouse and shows live HTML on its face. It passes all 8 checks.
93
+
94
+ `src/geometry.ts`:
95
+
96
+ ```ts
97
+ import { camera, draw, footprint, plate, rotateX, slab, type Pose, type Scene, type ScreenRect } from "@vinena/vectoys";
98
+
99
+ const WIDTH = 60;
100
+ const HEIGHT = 40;
101
+ const DEEP = 4;
102
+ export const FACE: ScreenRect = { x: -25, y: -15, width: 50, height: 30, z: 0.03 };
103
+
104
+ const scene = (pose: Pose): Scene => [
105
+ slab({
106
+ name: "body",
107
+ box: [-WIDTH / 2, -HEIGHT / 2, WIDTH / 2, HEIGHT / 2],
108
+ radii: [6, 6],
109
+ thickness: DEEP,
110
+ edge: 1.2,
111
+ pose,
112
+ children: [plate({ name: "face", box: [FACE.x, FACE.y, FACE.x + FACE.width, FACE.y + FACE.height], radii: [3, 3], role: "face" })],
113
+ }),
114
+ ];
115
+
116
+ /** The badge `lift` of the way up from rest (0 to 1): the camera, the faces, the face's pose and the shadow. */
117
+ export function seen(lift = 0) {
118
+ const eye = camera({ width: 100, height: 80, perspective: 280, yaw: -24 + lift * 12, pitch: -16 });
119
+ // Stood upright, face to the eye, and raised a little as it lifts.
120
+ const pose: Pose = { position: [0, DEEP / 2, lift * 3], rotation: rotateX(-90) };
121
+ const parts = scene(pose);
122
+ return { camera: eye, drawn: draw(parts, eye), face: pose, shadow: footprint(parts, eye, { gap: 2, spread: 1.5 }).d };
123
+ }
124
+ ```
125
+
126
+ `src/badge.tsx`:
127
+
128
+ ```tsx
129
+ import { Faces, Screen, Stage, useSpring, type Look } from "@vinena/vectoys-react";
130
+ import { useMemo, useState, type ReactNode } from "react";
131
+ import { FACE, seen } from "./geometry";
132
+
133
+ const FINISH = "var(--vectoys-finish, #5b7cfa)";
134
+ const SURFACE = "var(--vectoys-surface, #f5f5f5)";
135
+ const SHADOW = "var(--vectoys-shadow, rgba(0, 0, 0, 0.08))";
136
+ const fill = (color: string): Look => ({ style: { fill: color } });
137
+ const STYLES: Record<string, Look> = { top: fill(FINISH), side: fill(FINISH), bottom: fill(FINISH), face: fill(SURFACE) };
138
+
139
+ /** A badge that lifts and turns towards a mouse over it, with live HTML on its face. */
140
+ export function Badge({ children, label = "Badge" }: { children?: ReactNode; label?: string }) {
141
+ const [over, setOver] = useState(false);
142
+ // Springs to its target each frame; under reduced motion it jumps there.
143
+ const lift = useSpring(over ? 1 : 0, { stiffness: 260, damping: 24 });
144
+ const view = useMemo(() => seen(lift), [lift]);
145
+ const screen = (
146
+ <Screen camera={view.camera} pose={view.face} rect={FACE} width={250} style={{ overflow: "hidden", borderRadius: 14 }}>
147
+ {children}
148
+ </Screen>
149
+ );
150
+ return (
151
+ <div
152
+ role={children == null ? "img" : "group"}
153
+ aria-label={label}
154
+ onPointerEnter={(event) => event.pointerType === "mouse" && setOver(true)}
155
+ onPointerLeave={() => setOver(false)}
156
+ >
157
+ <Stage camera={view.camera} overlay={screen}>
158
+ <path d={view.shadow} style={{ fill: SHADOW }} />
159
+ <Faces faces={view.drawn.faces} hulls={view.drawn.hulls} styles={STYLES} shading />
160
+ </Stage>
161
+ </div>
162
+ );
163
+ }
164
+ ```
165
+
166
+ ## Before you submit
167
+
168
+ - `npx @vinena/vectoys-cli check <dir>` passes all 8 checks.
169
+ - The preview looks right in light and dark and at every yaw.
170
+ - Pointer, touch and keyboard all work, and reduced motion keeps the state changes.
171
+ - `item.json` has a plain title and a description that says what the item is and what it does.