From eebb746866f8141290527a895e34f6268d5b0163 Mon Sep 17 00:00:00 2001 From: Montana Wong Date: Tue, 25 Aug 2026 09:01:32 -0400 Subject: [PATCH 1/2] feat(icons): morph copy/check icon toggles with morphicons Animates the clipboard-to-check transitions in CopyPromptButton, internal-explorer's CopyButton, and vibenet's CopyableValue instead of an instant swap. --- app/components/ui/icons.tsx | 7 +++ .../components/CopyButton.tsx | 30 +++------ app/vibenet/components/CopyableValue.tsx | 62 ++++++------------- .../demos/b20/components/CopyPromptButton.tsx | 14 +++-- package-lock.json | 31 ++++++++++ package.json | 1 + 6 files changed, 75 insertions(+), 70 deletions(-) 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", From 87797b6d6dc776dbeb2c2c2c88192b93286e7096 Mon Sep 17 00:00:00 2001 From: Montana Wong Date: Tue, 25 Aug 2026 10:30:48 -0400 Subject: [PATCH 2/2] docs(agents): note morphicons as the default for icon-state toggles --- AGENTS.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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