@madebywild/wvk 0.0.2 → 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/AGENTS.md ADDED
@@ -0,0 +1,81 @@
1
+ # wvk — agent rules
2
+
3
+ These rules apply to every wvk (wild vibeframe kit) project. They are absolute. When a request conflicts with them, raise the conflict — do not silently work around them.
4
+
5
+ ## The kit is the source of truth
6
+
7
+ wvk projects are wireframes built from a fixed kit: a closed set of components, tokens, and typography styles. The kit is the vocabulary. If the kit does not have a word for it, the wireframe does not say it.
8
+
9
+ - **Components** live in `src/components/ui/*` and `src/components/icons/*`. Always import from there.
10
+ - **Tokens** are defined globally (CSS variables, Tailwind config). Color, spacing, radius, sizing, and icon size all come from kit tokens.
11
+ - **Typography** uses the kit's existing text styles only.
12
+
13
+ ## No new tokens. No new styles. Ever.
14
+
15
+ There is **no scenario** in a wireframe where you introduce a new color, spacing value, radius, font size, line height, or font weight.
16
+
17
+ - **Color**: only semantic tokens (`foreground`, `muted-foreground`, `border`, `--wvk-surface-*`, etc.). No one-off hex, no arbitrary Tailwind color scales (`bg-blue-500`, `text-red-600`), no `text-destructive` for emphasis. Signal destructive intent with an icon (e.g. trash), not a non-token accent.
18
+ - **Spacing**: only `var(--wvk-px-*)`, `var(--wvk-gap-*)`, `var(--wvk-size-*)` and the kit's Tailwind spacing scale. No arbitrary values like `p-[13px]` or `gap-[7px]`.
19
+ - **Radius**: only `var(--radius-*)` (`-md`, `-lg`, `-full`, etc.). No `rounded-[6px]`.
20
+ - **Typography**: only the existing text styles already used in the kit (the same Tailwind utilities the kit's components use — e.g. `text-sm`, `text-xs`, `font-bold`, `leading-5`). Do **not** invent a new size, weight, tracking, or leading. No `text-[15px]`, no `font-[450]`, no `leading-[1.35]`. If a reference implies a size that isn't in the kit, pick the closest existing style.
21
+ - **Shadows**: the kit is flat. No `shadow-*` on cards, panels, or chrome. Use `border border-border` for separation.
22
+
23
+ If a design genuinely cannot be expressed within the existing tokens, **stop and surface it**. Do not patch around it with arbitrary values.
24
+
25
+ ## Reference adaptation — strict translation, not mimicry
26
+
27
+ When the user provides a reference (screenshot, URL, prompt, Figma, competitor product), the job is to **translate the structure and information into the kit**, not to reproduce the reference's visual identity.
28
+
29
+ **Always use the kit's component as the base, even if it doesn't match the reference 1:1.** A more elaborate search field in the reference still maps to our `TextInput`. A custom-styled segmented control still maps to `SegmentedControl`. A bespoke card still uses our spacing, radius, and border.
30
+
31
+ **When the kit lacks a piece of the reference's UI:**
32
+
33
+ 1. Use the closest existing kit component as the base.
34
+ 2. Implement what the kit can express cleanly.
35
+ 3. **Surface the gap** in your reply — name the missing primitive, pattern, or interaction (e.g. "the reference's search field has an inline filter chip row inside the input — the kit's `TextInput` has no slot for that, so I omitted it; this is a candidate for a new component or an extension"). Be specific about what was dropped and why.
36
+
37
+ Do **not**:
38
+
39
+ - Build a one-off component to match the reference more closely.
40
+ - Add custom styling, icons, or chrome to fake a missing primitive.
41
+ - Adopt the reference's color palette, typography, spacing rhythm, or shadow language.
42
+ - Default to dark mode because the reference is dark — keep the kit's light theme unless the user explicitly asks for dark.
43
+
44
+ Do:
45
+
46
+ - **Keep the reference's information and actions.** Labels, copy, headings, helper text, buttons, links, tabs, table content — all should survive the kit pass. Visual translation is not a license to strip the UI for a "cleaner" wireframe. Drop content only when the user explicitly asks.
47
+ - **Preserve affordances**, not chrome. If the reference shows an action, keep an equivalent action with the kit's button/link variants.
48
+ - **Match dominant layout patterns** (sidebar shape, header placement, content hierarchy) using kit spacing.
49
+ - **Only reproduce UI that appears in the reference.** Don't add extra widgets to "use more components."
50
+
51
+ For multi-screen flows (onboarding, API explorer, checkout): build a real prototype driven by user actions and timed state transitions, not a stepper with "next/back" dev buttons. Mock data is fine; canonical strings belong in fixed slots.
52
+
53
+ ## Components — prefer kit, prefer semantic
54
+
55
+ Always reach for an existing kit component before writing markup. Two-option mode switch → `SegmentedControl`, not a button group. Status row → plain typography, not `Tag` unless the reference clearly shows a pill. Single primary action → `Button`, not `SplitButton`.
56
+
57
+ Pair controls with kit icons from `@/components/icons`. Dark mode chrome (`Tabs`, `SegmentedControl`, nav) must remain readable against the themed background — use semantic surface and border tokens (`--wvk-surface-*`, `--wvk-border-bold`, `--wvk-foreground-*`) so both light and `.dark` contrast correctly. Don't hard-code `--wvk-primitive-dark` for selected states when it can blend into a dark page fill.
58
+
59
+ ## Icons
60
+
61
+ Icons in `src/components/icons/*.svg` are imported as React components via `@svgr/webpack`. The SVGR config strips fixed `width`/`height` and adds `100%`/`100%` plus `preserveAspectRatio="xMidYMid meet"` so glyphs scale inside kit slots (`[&_svg]:size-full`, `--wvk-icon-sm`/`md`/`lg`). New icons must define a correct `viewBox` and must not rely on fixed `width`/`height` for layout.
62
+
63
+ Do **not** add `feDropShadow` or other shadow treatments to UI kit icons or other elements (unless asked). Drop shadows are reserved for `/public/cursors/*.svg` (cursor images can't use CSS shadow).
64
+
65
+ **Never use unicode arrow characters** (`→`, `←`, `↑`, `↓`) in UI text. Use the kit icon component (`<ArrowRight />`, `<ArrowLeft />`, etc.), sized with `className="size-[var(--wvk-icon-sm)]"` and `aria-hidden` (the surrounding text carries the meaning).
66
+
67
+ ## Motion
68
+
69
+ The kit ships with `framer-motion`. When something needs to animate (transitions, hover and tap feedback, list reveals, modal or drawer entrances, content swaps), reach for `motion` first. Don't roll a CSS keyframe or hand-spring a transform when the library already covers it cleanly.
70
+
71
+ Use small microinteractions to add a moment of delight: a subtle bounce on a successful action, a tilt on a hovered card, a soft fade-and-slide on content change, a gentle scale on press. Keep them quick (typically under ~300ms), purposeful, and tied to real user feedback.
72
+
73
+ Don't overdo it. Motion is seasoning, not the meal. If every element animates, nothing reads as special, and the page starts to feel busy instead of alive. Reserve animation for moments that benefit from it, and respect `prefers-reduced-motion` so users on that setting get a calm, near-static experience.
74
+
75
+ ## Responsiveness
76
+
77
+ Always make sure that pages are overall **responsive** (unless specified). This includes making sure that sections, components and type scales properly in small increments, but also when changing the viewport drastically (like going from Desktop to Mobile) that the page still works. Like moving nav items into a burger menu to make sure they're accessible.
78
+
79
+ ## When you're stuck
80
+
81
+ If a request can't be done within these rules — a reference needs a primitive the kit doesn't have, a layout needs a token that doesn't exist, a typographic hierarchy isn't expressible in the existing styles — **stop and ask**. Describe the gap, propose the closest kit-compliant alternative, and let the user decide whether to extend the kit or accept the wireframe-faithful version. Never resolve the gap by introducing new tokens, sizes, or one-off styling.
package/README.md CHANGED
@@ -73,6 +73,53 @@ export default function RootLayout({ children }) {
73
73
 
74
74
  `ThemeProvider` also installs a small cursor fallback stylesheet when the full package stylesheet is not present yet. For full component styling and design tokens, still import `@madebywild/wvk/styles.css` from your global stylesheet.
75
75
 
76
+ ### Whimsy cursor (on by default)
77
+
78
+ `ThemeProvider` auto-mounts `WhimsyCursor` — a DOM-rendered pointer that tilts on hover over interactive elements. It only activates on `(pointer: fine)` devices, so it's a no-op on touch.
79
+
80
+ ```tsx
81
+ // Disable
82
+ <ThemeProvider whimsyCursor={false}>{children}</ThemeProvider>
83
+
84
+ // Customize the hover tilt
85
+ <ThemeProvider whimsyCursor={{ hoverRotateDeg: -10 }}>{children}</ThemeProvider>
86
+ ```
87
+
88
+ You can still render `<WhimsyCursor />` yourself if you'd rather mount it outside `ThemeProvider`; in that case pass `whimsyCursor={false}` to avoid double-mounting.
89
+
90
+ ### Cursor style variants
91
+
92
+ The kit ships two default-cursor sprites:
93
+
94
+ - `"default"` (default) — the chunky kit signature pointer, paired with `WhimsyCursor`
95
+ - `"arrow"` — a slim classic arrow; `WhimsyCursor` auto-suppresses so the native cursor stays visible
96
+
97
+ Set the initial style on `ThemeProvider`, or flip it at runtime via the `useCursorStyle()` hook (persisted to `localStorage` under `wvk-cursor-style`). The kit doesn't ship a UI for this — wire your own toggle if you want to expose it to users.
98
+
99
+ ```tsx
100
+ import { ThemeProvider, useCursorStyle } from "@madebywild/wvk";
101
+
102
+ <ThemeProvider cursorStyle="arrow">{children}</ThemeProvider>;
103
+
104
+ // Anywhere inside the provider:
105
+ const { cursorStyle, setCursorStyle } = useCursorStyle();
106
+ ```
107
+
108
+ For SSR without a flash, mirror the value in the anti-flash inline script your app already uses for theme:
109
+
110
+ ```html
111
+ <script>
112
+ (function(){
113
+ try {
114
+ var c = localStorage.getItem('wvk-cursor-style');
115
+ if (c === 'arrow') document.documentElement.classList.add('wvk-cursor-style-arrow');
116
+ } catch (e) {}
117
+ })();
118
+ </script>
119
+ ```
120
+
121
+ Other cursor tokens — `--wvk-cursor-pointer`, `--wvk-cursor-move`, `--wvk-cursor-grabbing`, `--wvk-cursor-text` — are independent of the variant. Tailwind utilities `cursor-pointer`, `cursor-move`, `cursor-grabbing`, and `cursor-text` are wired to them. `cursor-grab` aliases to the move sprite (no open-hand variant ships yet).
122
+
76
123
  ## Design-token philosophy
77
124
 
78
125
  Tokens are organized in three tiers:
@@ -82,3 +129,23 @@ Tokens are organized in three tiers:
82
129
  - **Shadcn-compatible** (`--background`, `--foreground`, `--primary`, …) — driven by the semantic tier so any shadcn-style components keep working.
83
130
 
84
131
  Always prefer semantic tokens in product code; reach for primitives only for one-off accents.
132
+
133
+ ## Agent rules
134
+
135
+ The package ships an `AGENTS.md` describing the strict authoring contract for wvk wireframes (kit-only tokens, components, typography, icons). Drop it into your project so your agent harness picks it up:
136
+
137
+ ```bash
138
+ # write ./AGENTS.md (refuses to overwrite an existing file)
139
+ npx @madebywild/wvk copy-harness
140
+
141
+ # overwrite an existing AGENTS.md
142
+ npx @madebywild/wvk copy-harness --force
143
+
144
+ # merge into an existing AGENTS.md as an idempotent, delimited block
145
+ npx @madebywild/wvk copy-harness --append
146
+
147
+ # write to a custom path (parent dirs are created)
148
+ npx @madebywild/wvk copy-harness --out .claude/AGENTS.md
149
+ ```
150
+
151
+ `--append` wraps the rules in `<!-- BEGIN @madebywild/wvk AGENTS.md ... -->` / `<!-- END ... -->` markers. Re-running replaces the block in place, so the file never grows duplicate copies and stays safe to call from a postinstall or CI step.
package/bin/wvk.mjs ADDED
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from "node:crypto";
3
+ import {
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ statSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { dirname, relative, resolve } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ const PKG_NAME = "@madebywild/wvk";
14
+ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
15
+ const PKG_ROOT = resolve(SCRIPT_DIR, "..");
16
+ const SOURCE = resolve(PKG_ROOT, "AGENTS.md");
17
+ const PKG_JSON = JSON.parse(
18
+ readFileSync(resolve(PKG_ROOT, "package.json"), "utf8"),
19
+ );
20
+ const VERSION = PKG_JSON.version;
21
+
22
+ const BEGIN_RE = new RegExp(
23
+ `<!-- BEGIN ${escapeRegex(PKG_NAME)} AGENTS\\.md \\(v[^,]+, sha256:([a-f0-9]+)\\) -->`,
24
+ "g",
25
+ );
26
+ const END_MARK = `<!-- END ${PKG_NAME} AGENTS.md -->`;
27
+
28
+ function escapeRegex(s) {
29
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
30
+ }
31
+
32
+ function usage() {
33
+ return `wvk — ${PKG_NAME} CLI
34
+
35
+ Usage:
36
+ wvk copy-harness [--force] [--append] [--out <path>]
37
+ wvk --help
38
+ wvk --version
39
+
40
+ Commands:
41
+ copy-harness Copy this package's AGENTS.md into your project so your
42
+ agent harness picks up the wvk authoring rules.
43
+
44
+ Options:
45
+ --out <path> Destination path (default: ./AGENTS.md). Parent dirs are created.
46
+ --force Overwrite the destination if it exists. Mutually exclusive with --append.
47
+ --append Merge the rules into the destination as a delimited, idempotent block.
48
+ Re-running is safe: the block is replaced in place, never duplicated.
49
+ `;
50
+ }
51
+
52
+ function fail(msg, code = 1) {
53
+ process.stderr.write(`wvk: ${msg}\n`);
54
+ process.exit(code);
55
+ }
56
+
57
+ function parseArgs(argv) {
58
+ const args = { _: [], force: false, append: false, out: null };
59
+ for (let i = 0; i < argv.length; i++) {
60
+ const a = argv[i];
61
+ if (a === "--force") args.force = true;
62
+ else if (a === "--append") args.append = true;
63
+ else if (a === "--out") {
64
+ const v = argv[++i];
65
+ if (!v) fail("--out requires a path");
66
+ args.out = v;
67
+ } else if (a === "--help" || a === "-h") args.help = true;
68
+ else if (a === "--version" || a === "-v") args.version = true;
69
+ else if (a.startsWith("--")) fail(`unknown option: ${a}`);
70
+ else args._.push(a);
71
+ }
72
+ return args;
73
+ }
74
+
75
+ function shortHash(buf) {
76
+ return createHash("sha256").update(buf).digest("hex").slice(0, 12);
77
+ }
78
+
79
+ function buildBlock(body, hash) {
80
+ const begin = `<!-- BEGIN ${PKG_NAME} AGENTS.md (v${VERSION}, sha256:${hash}) -->`;
81
+ const trimmed = body.replace(/\s+$/, "");
82
+ return `${begin}\n${trimmed}\n${END_MARK}\n`;
83
+ }
84
+
85
+ function findBlocks(text) {
86
+ const blocks = [];
87
+ let m;
88
+ BEGIN_RE.lastIndex = 0;
89
+ while ((m = BEGIN_RE.exec(text)) !== null) {
90
+ const beginStart = m.index;
91
+ const beginEnd = m.index + m[0].length;
92
+ const endIdx = text.indexOf(END_MARK, beginEnd);
93
+ if (endIdx === -1) {
94
+ fail(
95
+ `corrupted AGENTS.md block: BEGIN marker without matching END at offset ${beginStart}`,
96
+ );
97
+ }
98
+ blocks.push({
99
+ hash: m[1],
100
+ start: beginStart,
101
+ end: endIdx + END_MARK.length,
102
+ });
103
+ }
104
+ return blocks;
105
+ }
106
+
107
+ function copyHarness(args) {
108
+ if (args.append && args.force) {
109
+ fail("--append and --force are mutually exclusive");
110
+ }
111
+ if (!existsSync(SOURCE)) {
112
+ fail(`source AGENTS.md missing in package at ${SOURCE}`);
113
+ }
114
+ const body = readFileSync(SOURCE, "utf8");
115
+ const hash = shortHash(body);
116
+ const dest = resolve(process.cwd(), args.out ?? "AGENTS.md");
117
+ const destRel = relative(process.cwd(), dest) || dest;
118
+ mkdirSync(dirname(dest), { recursive: true });
119
+
120
+ const exists = existsSync(dest) && statSync(dest).isFile();
121
+
122
+ if (args.append) {
123
+ const block = buildBlock(body, hash);
124
+ if (!exists) {
125
+ writeFileSync(dest, block);
126
+ report(destRel, "appended");
127
+ return;
128
+ }
129
+ const current = readFileSync(dest, "utf8");
130
+ const blocks = findBlocks(current);
131
+ if (blocks.length > 1) {
132
+ fail(
133
+ `${destRel} contains ${blocks.length} ${PKG_NAME} blocks; remove duplicates and retry`,
134
+ );
135
+ }
136
+ if (blocks.length === 1) {
137
+ const [b] = blocks;
138
+ if (b.hash === hash) {
139
+ report(destRel, "unchanged");
140
+ return;
141
+ }
142
+ const before = current.slice(0, b.start);
143
+ const after = current.slice(b.end);
144
+ const next =
145
+ before + block.replace(/\n$/, "") + (after.startsWith("\n") ? "" : "\n") + after;
146
+ writeFileSync(dest, next);
147
+ report(destRel, "updated");
148
+ return;
149
+ }
150
+ const sep = current.length === 0 || current.endsWith("\n") ? "" : "\n";
151
+ const gap = current.length === 0 ? "" : "\n";
152
+ writeFileSync(dest, current + sep + gap + block);
153
+ report(destRel, "appended");
154
+ return;
155
+ }
156
+
157
+ if (exists && !args.force) {
158
+ fail(
159
+ `${destRel} already exists. Re-run with --force to overwrite, or --append to merge.`,
160
+ );
161
+ }
162
+ writeFileSync(dest, body);
163
+ report(destRel, exists ? "overwritten" : "written");
164
+ }
165
+
166
+ function report(destRel, mode) {
167
+ process.stdout.write(`wvk: ${mode} ${destRel}\n`);
168
+ if (mode !== "unchanged") {
169
+ process.stdout.write(
170
+ `wvk: your agent harness should now pick up the ${PKG_NAME} rules.\n`,
171
+ );
172
+ }
173
+ }
174
+
175
+ function main() {
176
+ const args = parseArgs(process.argv.slice(2));
177
+ if (args.help) {
178
+ process.stdout.write(usage());
179
+ return;
180
+ }
181
+ if (args.version) {
182
+ process.stdout.write(`${VERSION}\n`);
183
+ return;
184
+ }
185
+ const cmd = args._[0];
186
+ if (!cmd) {
187
+ process.stdout.write(usage());
188
+ return;
189
+ }
190
+ if (cmd === "copy-harness") {
191
+ copyHarness(args);
192
+ return;
193
+ }
194
+ process.stderr.write(`wvk: unknown command: ${cmd}\n\n${usage()}`);
195
+ process.exit(1);
196
+ }
197
+
198
+ main();