diff --git a/AGENTS.md b/AGENTS.md index 500b188..85318e4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,6 +57,18 @@ of a single commit, use `SKIP_DOCS_HOOK=1` or put `[skip-docs]` in the message. - TypeScript strict mode - Follow the existing formatter config; do not reformat unrelated files +- **Icons**: there is no icon library; icons are hand-authored inline SVGs, + with the commonly reused ones centralized in `app/components/ui/icons.tsx`. + When adding or touching an icon that toggles between two states based on + props/state (e.g. copy → check, menu → close) — not just shown/hidden — + consider animating the transition with `morphicons` instead of an instant + swap: `` from `morphicons/react`, fed + path data on a shared 24x24 grid (`fitIcon()` first if the source art isn't + already on that grid). See `CLIPBOARD_MORPH_ICON`/`CHECK_MORPH_ICON` in + `app/components/ui/icons.tsx` and their use in + `app/demos/b20/components/CopyPromptButton.tsx`, + `app/internal-explorer/components/CopyButton.tsx`, and + `app/vibenet/components/CopyableValue.tsx`. ## Testing diff --git a/app/components/ui/icons.tsx b/app/components/ui/icons.tsx index 95b6370..2751fe2 100644 --- a/app/components/ui/icons.tsx +++ b/app/components/ui/icons.tsx @@ -93,6 +93,13 @@ export function ClipboardIcon({ size = 16, className }: { size?: number; classNa ); } +// Raw path data (24x24 grid, matching ClipboardIcon/CheckIcon below) for +// morphicons' , which morphs between two icons on state change +// instead of an instant swap. +export const CLIPBOARD_MORPH_ICON = + 'M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z'; +export const CHECK_MORPH_ICON = 'M5 13l4 4L19 7'; + export function CheckIcon({ size = 16, className }: { size?: number; className?: string }) { return ( - {copied ? ( - - Copied - - - ) : ( - - Copy - - - )} + ); } diff --git a/app/vibenet/components/CopyableValue.tsx b/app/vibenet/components/CopyableValue.tsx index 7a8754f..046e823 100644 --- a/app/vibenet/components/CopyableValue.tsx +++ b/app/vibenet/components/CopyableValue.tsx @@ -1,18 +1,25 @@ 'use client'; import { useCallback, useState } from 'react'; -import { AnimatePresence, motion } from 'motion/react'; +import { MorphIcon } from 'morphicons/react'; +import { fitIcon } from 'morphicons'; +import { CHECK_MORPH_ICON } from '../../components/ui/icons'; import { cn } from '../../components/ui/cn'; +// Original artwork is drawn on a 40x40 grid; regrid it onto morphicons' 24x24 +// baseline once, at module scope, so it can morph against CHECK_MORPH_ICON. +const COPY_MORPH_ICON = fitIcon( + 'M16.6667 23.3333V26.6667C16.6667 28.5076 18.1591 30 20 30H26.6667C28.5076 30 30 28.5076 30 26.6667V20C30 18.1591 28.5076 16.6667 26.6667 16.6667H23.3333M23.3333 16.6667V13.3333C23.3333 11.4924 21.8409 10 20 10H13.3333C11.4924 10 10 11.4924 10 13.3333V20C10 21.8409 11.4924 23.3333 13.3333 23.3333H20C21.8409 23.3333 23.3333 21.8409 23.3333 20V16.6667Z', + 40, +); + type CopyableValueProps = { value: string; display?: string; className?: string; }; -const ICON_TRANSITION = { duration: 0.15 }; - export function CopyableValue({ value, display, className }: CopyableValueProps) { const [copied, setCopied] = useState(false); @@ -44,46 +51,15 @@ export function CopyableValue({ value, display, className }: CopyableValueProps) )} > {shown} - - - {copied ? ( - - - - ) : ( - - - - )} - - + ); } diff --git a/app/vibenet/demos/b20/components/CopyPromptButton.tsx b/app/vibenet/demos/b20/components/CopyPromptButton.tsx index e3b14ec..b6fdebf 100644 --- a/app/vibenet/demos/b20/components/CopyPromptButton.tsx +++ b/app/vibenet/demos/b20/components/CopyPromptButton.tsx @@ -1,9 +1,10 @@ 'use client'; import { useCallback, useEffect, useRef, useState } from 'react'; +import { MorphIcon } from 'morphicons/react'; import { trackB20PromptCopy } from '../../../../analytics/events'; -import { CheckIcon, ClipboardIcon } from '../../../../components/ui/icons'; +import { CHECK_MORPH_ICON, CLIPBOARD_MORPH_ICON } from '../../../../components/ui/icons'; import { cn } from '../../../../components/ui/cn'; import type { B20Prompt } from '../lib/prompts'; @@ -50,11 +51,12 @@ export function CopyPromptButton({ prompt, module, className }: CopyPromptButton className, )} > - {copied ? ( - - ) : ( - - )} + {copied ? 'Copied' : 'Copy developer instructions'} ); diff --git a/package-lock.json b/package-lock.json index 1169f1a..7522a54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@vercel/analytics": "^2.0.1", "classnames": "^2.5.1", "d3": "^7.9.0", + "morphicons": "^1.7.0", "motion": "^12.0.0", "next": "^15.5.20", "react": "^18.3.1", @@ -8375,6 +8376,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/morphicons": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/morphicons/-/morphicons-1.7.0.tgz", + "integrity": "sha512-MOqSK+O5RdxynER5016vUvvqQaHkqqWYmNpocsk9TEszUZ9PB/K52Yqq8AGTRK1NUO5dj8znGSEVf9slqEIQaw==", + "license": "MIT", + "peerDependencies": { + "react": ">=18", + "react-native": ">=0.71", + "react-native-svg": ">=14", + "svelte": ">=5", + "vue": ">=3.3" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-native": { + "optional": true + }, + "react-native-svg": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, "node_modules/motion": { "version": "12.42.2", "resolved": "https://registry.npmjs.org/motion/-/motion-12.42.2.tgz", diff --git a/package.json b/package.json index 93a9eda..78017b8 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@vercel/analytics": "^2.0.1", "classnames": "^2.5.1", "d3": "^7.9.0", + "morphicons": "^1.7.0", "motion": "^12.0.0", "next": "^15.5.20", "react": "^18.3.1",