-
Notifications
You must be signed in to change notification settings - Fork 0
Rewrite landing around keep → share → see #142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| import { AnimatePresence, motion } from "motion/react"; | ||
| import { useState } from "react"; | ||
|
|
||
| const SAMPLE_CLIPS = [ | ||
| { | ||
| id: "facade", | ||
| name: "ParametricFacade", | ||
| description: "Panel divisions with custom depth offsets", | ||
| tags: ["architecture", "facade"], | ||
| }, | ||
| { | ||
| id: "curve", | ||
| name: "DisplayCurveLength", | ||
| description: "Curve length labels with unit formatting", | ||
| tags: ["analysis"], | ||
| }, | ||
| { | ||
| id: "voronoi", | ||
| name: "VoronoiPattern", | ||
| description: "Surface subdivision with attractor points", | ||
| tags: ["geometry"], | ||
| }, | ||
| ] as const; | ||
|
|
||
| type DemoMode = "preview" | "copy" | "share"; | ||
|
|
||
| /** | ||
| * Tiny in-page product mock: preview, or copy/share one sample clip. | ||
| * Uses Hopper Card accents (green-300 Shared, tag chips) — not a full tour. | ||
| */ | ||
| export function ClipLibraryDemo({ | ||
| mode = "preview", | ||
| featuredId = "facade", | ||
| }: { | ||
| mode?: DemoMode; | ||
| featuredId?: (typeof SAMPLE_CLIPS)[number]["id"]; | ||
| }) { | ||
| const [copiedId, setCopiedId] = useState<string | null>(null); | ||
| const [sharedId, setSharedId] = useState<string | null>(null); | ||
|
|
||
| const handleCopy = (id: string) => { | ||
| setSharedId(null); | ||
| setCopiedId(id); | ||
| window.setTimeout(() => setCopiedId((cur) => (cur === id ? null : cur)), 1800); | ||
| }; | ||
|
|
||
| const handleShare = (id: string) => { | ||
| setCopiedId(null); | ||
| setSharedId(id); | ||
| window.setTimeout(() => setSharedId((cur) => (cur === id ? null : cur)), 2800); | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="relative"> | ||
| <ul className="flex flex-col gap-2.5"> | ||
| {SAMPLE_CLIPS.map((clip) => { | ||
| const featured = clip.id === featuredId; | ||
| const justCopied = copiedId === clip.id; | ||
| const justShared = sharedId === clip.id; | ||
| const showSharedBadge = | ||
| (mode === "preview" && clip.id === "facade") || justShared; | ||
|
|
||
| return ( | ||
| <li | ||
| key={clip.id} | ||
| className={`relative rounded-md p-3 ring-1 transition-colors ${ | ||
| featured | ||
| ? "bg-neutral-900 ring-neutral-500" | ||
| : "bg-neutral-950 ring-neutral-800 opacity-70" | ||
| }`} | ||
| > | ||
| {showSharedBadge && ( | ||
| <span className="absolute top-2.5 right-2.5 rounded-md bg-green-300 px-2 text-xs font-bold text-neutral-800"> | ||
| Shared | ||
| </span> | ||
| )} | ||
| <div className="flex items-start justify-between gap-3"> | ||
| <div | ||
| className={`min-w-0 ${showSharedBadge ? "pr-16" : ""}`} | ||
| > | ||
| <p className="truncate text-sm font-semibold text-white"> | ||
| {clip.name} | ||
| </p> | ||
| <p className="mt-0.5 line-clamp-1 text-xs text-neutral-400"> | ||
| {clip.description} | ||
| </p> | ||
| <div className="mt-2 flex flex-wrap gap-1.5"> | ||
| {clip.tags.map((tag) => ( | ||
| <span | ||
| key={tag} | ||
| className="rounded-sm bg-neutral-600 px-2 text-xs font-semibold text-neutral-100" | ||
| > | ||
| {tag} | ||
| </span> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| {featured && mode === "copy" && ( | ||
| <button | ||
| type="button" | ||
| onClick={() => handleCopy(clip.id)} | ||
| className={`shrink-0 px-2 text-sm font-bold transition-colors ${ | ||
| justCopied | ||
| ? "rounded-md bg-green-300 text-neutral-800" | ||
| : "text-neutral-400 hover:text-neutral-50" | ||
| }`} | ||
| > | ||
| {justCopied ? "copied!" : "copy"} | ||
| </button> | ||
| )} | ||
| {featured && mode === "share" && !justShared && ( | ||
| <button | ||
| type="button" | ||
| onClick={() => handleShare(clip.id)} | ||
| className="shrink-0 px-2 text-sm font-bold text-neutral-400 transition-colors hover:text-neutral-50" | ||
| > | ||
| share | ||
| </button> | ||
| )} | ||
| </div> | ||
| </li> | ||
| ); | ||
| })} | ||
| </ul> | ||
|
|
||
| <AnimatePresence> | ||
| {sharedId && ( | ||
| <motion.div | ||
| initial={{ opacity: 0, y: 8 }} | ||
| animate={{ opacity: 1, y: 0 }} | ||
| exit={{ opacity: 0, y: 4 }} | ||
| transition={{ duration: 0.25, ease: [0.16, 1, 0.3, 1] }} | ||
| className="absolute inset-x-0 -bottom-14 flex justify-center" | ||
| > | ||
| <div className="rounded-md border border-green-300/30 bg-neutral-950 px-3 py-2 font-mono text-xs text-green-300"> | ||
| hopperclip.com/share?… | ||
| </div> | ||
| </motion.div> | ||
| )} | ||
| </AnimatePresence> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,232 @@ | ||
| import { motion } from "motion/react"; | ||
|
|
||
| /** | ||
| * Quiet product frame for the LP: graph + soft diff highlight. | ||
| * Dark palette to match the landing page — not the real GH canvas look. | ||
| */ | ||
| export function InspectDiffVisual() { | ||
| return ( | ||
| <div | ||
| className="relative overflow-hidden rounded-xl border border-neutral-800 bg-neutral-950" | ||
| aria-hidden | ||
| > | ||
| <svg | ||
| viewBox="0 0 480 220" | ||
| className="h-auto w-full" | ||
| xmlns="http://www.w3.org/2000/svg" | ||
| > | ||
| <defs> | ||
| <pattern | ||
| id="lp-gh-grid" | ||
| width="40" | ||
| height="40" | ||
| patternUnits="userSpaceOnUse" | ||
| > | ||
| <path | ||
| d="M 40 0 L 0 0 0 40" | ||
| fill="none" | ||
| stroke="#262626" | ||
| strokeWidth="1" | ||
| /> | ||
| </pattern> | ||
| </defs> | ||
| <rect width="480" height="220" fill="#0a0a0a" /> | ||
| <rect width="480" height="220" fill="url(#lp-gh-grid)" /> | ||
|
|
||
| {/* wires */} | ||
| <motion.path | ||
| d="M 118 70 C 160 70, 160 70, 190 70" | ||
| fill="none" | ||
| stroke="#525252" | ||
| strokeWidth="1.5" | ||
| initial={{ pathLength: 0 }} | ||
| whileInView={{ pathLength: 1 }} | ||
| viewport={{ once: true }} | ||
| transition={{ duration: 0.8, delay: 0.2 }} | ||
| /> | ||
| <motion.path | ||
| d="M 118 150 C 160 150, 160 110, 190 110" | ||
| fill="none" | ||
| stroke="#86efac" | ||
| strokeWidth="2" | ||
| initial={{ pathLength: 0, opacity: 0 }} | ||
| whileInView={{ pathLength: 1, opacity: 1 }} | ||
| viewport={{ once: true }} | ||
| transition={{ duration: 0.9, delay: 0.55 }} | ||
| /> | ||
| <motion.path | ||
| d="M 300 90 C 340 90, 340 150, 362 150" | ||
| fill="none" | ||
| stroke="#f87171" | ||
| strokeWidth="1.5" | ||
| strokeDasharray="4 3" | ||
| initial={{ opacity: 0 }} | ||
| whileInView={{ opacity: 0.85 }} | ||
| viewport={{ once: true }} | ||
| transition={{ duration: 0.5, delay: 0.7 }} | ||
| /> | ||
|
|
||
| {/* value nodes */} | ||
| <g transform="translate(48, 48)"> | ||
| <rect | ||
| width="70" | ||
| height="44" | ||
| rx="4" | ||
| fill="#262626" | ||
| stroke="#525252" | ||
| /> | ||
| <text | ||
| x="10" | ||
| y="18" | ||
| fontSize="9" | ||
| fill="#737373" | ||
| fontFamily="ui-monospace, monospace" | ||
| > | ||
| Number | ||
| </text> | ||
| <text | ||
| x="10" | ||
| y="34" | ||
| fontSize="11" | ||
| fill="#e5e5e5" | ||
| fontFamily="ui-sans-serif, system-ui" | ||
| > | ||
| 12.0 | ||
| </text> | ||
| </g> | ||
|
|
||
| <g transform="translate(48, 128)"> | ||
| <rect | ||
| width="70" | ||
| height="44" | ||
| rx="4" | ||
| fill="#262626" | ||
| stroke="#525252" | ||
| /> | ||
| <text | ||
| x="10" | ||
| y="18" | ||
| fontSize="9" | ||
| fill="#737373" | ||
| fontFamily="ui-monospace, monospace" | ||
| > | ||
| Number | ||
| </text> | ||
| <text | ||
| x="10" | ||
| y="34" | ||
| fontSize="11" | ||
| fill="#e5e5e5" | ||
| fontFamily="ui-sans-serif, system-ui" | ||
| > | ||
| 4.0 | ||
| </text> | ||
| </g> | ||
|
|
||
| {/* component — unmodified */} | ||
| <g transform="translate(190, 48)"> | ||
| <rect | ||
| width="110" | ||
| height="64" | ||
| rx="4" | ||
| fill="#171717" | ||
| stroke="#404040" | ||
| /> | ||
| <rect width="110" height="18" rx="4" fill="#262626" /> | ||
| <rect y="14" width="110" height="8" fill="#262626" /> | ||
| <text | ||
| x="8" | ||
| y="13" | ||
| fontSize="9" | ||
| fill="#d4d4d4" | ||
| fontFamily="ui-sans-serif, system-ui" | ||
| > | ||
| Divide Curve | ||
| </text> | ||
| <circle cx="0" cy="22" r="4" fill="#525252" /> | ||
| <circle cx="0" cy="42" r="4" fill="#525252" /> | ||
| <circle cx="110" cy="32" r="4" fill="#525252" /> | ||
| </g> | ||
|
|
||
| {/* component — modified */} | ||
| <motion.g | ||
| transform="translate(190, 128)" | ||
| initial={{ opacity: 0.35 }} | ||
| whileInView={{ opacity: 1 }} | ||
| viewport={{ once: true }} | ||
| transition={{ duration: 0.6, delay: 0.45 }} | ||
| > | ||
| <rect | ||
| width="110" | ||
| height="52" | ||
| rx="4" | ||
| fill="#171717" | ||
| stroke="#fbbf24" | ||
| strokeWidth="2" | ||
| /> | ||
| <rect width="110" height="18" rx="4" fill="#262626" /> | ||
| <rect y="14" width="110" height="8" fill="#262626" /> | ||
| <text | ||
| x="8" | ||
| y="13" | ||
| fontSize="9" | ||
| fill="#d4d4d4" | ||
| fontFamily="ui-sans-serif, system-ui" | ||
| > | ||
| Extrude | ||
| </text> | ||
| <circle cx="0" cy="28" r="4" fill="#525252" /> | ||
| <circle cx="110" cy="28" r="4" fill="#525252" /> | ||
| </motion.g> | ||
|
|
||
| {/* panel — added */} | ||
| <motion.g | ||
| transform="translate(362, 128)" | ||
| initial={{ opacity: 0, y: 6 }} | ||
| whileInView={{ opacity: 1, y: 0 }} | ||
| viewport={{ once: true }} | ||
| transition={{ duration: 0.5, delay: 0.75 }} | ||
|
Comment on lines
+183
to
+188
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Locate SVG elements that combine a transform attribute with Motion x/y transforms.
rg -n -U 'transform=.*\n(?:.*\n){0,8}(?:initial|whileInView)=\{\{[^}]*\b[xy]:' \
src/app/components/landing/inspect-diff-visual.tsxRepository: tsoumdoa/hopperclip Length of output: 157 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect the target component and relevant package metadata without running repo code.
echo "== candidate file excerpt =="
sed -n '160,225p' src/app/components/landing/inspect-diff-visual.tsx
echo
echo "== motion/framer package versions from package manifests =="
for f in package.json pnpm-lock.yaml package-lock.json yarn.lock bun.lockb; do
if [ -f "$f" ]; then
echo "-- $f --"
if [ "$f" = "package.json" ]; then
rg -n '"(framer-motion|motion|react-dom)"' "$f" -C 1 || true
else
rg -n '(framer-motion|motion)\W|/framer-motion@|/motion@' "$f" -C 1 | head -80 || true
fi
fi
done
echo
echo "== all transform+motion svg groups near each other =="
python3 - <<'PY'
from pathlib import Path
p=Path('src/app/components/landing/inspect-diff-visual.tsx')
text=p.read_text().splitlines()
for i,line in enumerate(text,1):
if '<motion.g' in line.lower() or '<motion.path' in line.lower() or 'transform=' in line:
if any('initial' in l or 'whileInView' in l or 'transform=' in l for l in text[max(1,i-12):i+12]):
print(f"-- around line {i} --")
for j in range(max(1,i-8), min(len(text),i+14)+1):
print(f"{j:4}: {text[j-1]}")
print()
PYRepository: tsoumdoa/hopperclip Length of output: 10863 🌐 Web query:
💡 Result: In Motion (formerly Framer Motion), SVG transforms are handled specifically to overcome limitations of standard SVG behavior [1][2]. Key behaviors regarding SVG transforms, styles, and attributes in version 0.27.2 and the broader Motion 12+ ecosystem include: Transform Origins: By default, SVG elements apply transforms relative to the top-left corner of the viewBox [1][2]. Motion automatically changes this behavior for motion components to match CSS, making transforms relative to the element's center [1][2]. This is achieved by defaulting to transform-box: fill-box [1][3]. You can restore the default SVG behavior or customize it by explicitly setting the transformBox style (e.g., transformBox: "view-box") [1][2]. Styles vs. Attributes: - Motion generally prefers animating values via CSS styles (transform property) rather than the SVG transform attribute, as this provides better performance and consistent behavior across modern browsers [4][3]. - When animating properties like x, y, or scale on SVG elements, Motion components will default to using CSS transforms (translateX/translateY) [5]. - If you specifically need to animate positional attributes (like the x or y attributes of a or ), you should use the attr-prefixed props: attrX, attrY, and attrScale [1][6][5]. Recent Pipeline Unification: As of recent updates (part of the unified Motion architecture), SVG values that exist as valid CSS properties (such as fill, opacity, and others) are rendered as inline styles rather than presentation attributes [3]. This approach ensures deterministic specificity (inline styles take priority) and improves performance by avoiding attribute-mutation overhead [3]. For scenarios where you must animate the SVG transform attribute directly (e.g., complex strings or legacy compatibility), Motion does not provide a native shorthand for animating the raw string. You may need to use external parsing logic or utilize Motion's mix function to interpolate transform matrices manually before updating the attribute via a ref [7]. Top results: [1][2][3][5] Citations:
Preserve the panel’s SVG translation while animating it. Motion renders SVG Proposed fix- <motion.g
- transform="translate(362, 128)"
+ <g transform="translate(362, 128)">
+ <motion.g
initial={{ opacity: 0, y: 4 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.75 }}
- >
+ >
...
- </motion.g>
+ </motion.g>
+ </g>🤖 Prompt for AI Agents |
||
| > | ||
| <rect | ||
| width="70" | ||
| height="44" | ||
| rx="4" | ||
| fill="#171717" | ||
| stroke="#86efac" | ||
| strokeWidth="2" | ||
| /> | ||
| <text | ||
| x="10" | ||
| y="18" | ||
| fontSize="9" | ||
| fill="#737373" | ||
| fontFamily="ui-monospace, monospace" | ||
| > | ||
| Panel | ||
| </text> | ||
| <text | ||
| x="10" | ||
| y="34" | ||
| fontSize="10" | ||
| fill="#e5e5e5" | ||
| fontFamily="ui-sans-serif, system-ui" | ||
| > | ||
| ok | ||
| </text> | ||
| </motion.g> | ||
| </svg> | ||
|
|
||
| <div className="absolute right-3 bottom-3 flex gap-2 font-mono text-[10px] tracking-wide uppercase"> | ||
| <span className="rounded bg-green-300 px-1.5 py-0.5 font-bold text-neutral-800"> | ||
| + added | ||
| </span> | ||
| <span className="rounded border border-neutral-700 bg-neutral-950/90 px-1.5 py-0.5 text-amber-300"> | ||
| ~ modified | ||
| </span> | ||
| <span className="rounded border border-neutral-700 bg-neutral-950/90 px-1.5 py-0.5 text-red-400"> | ||
| − removed | ||
| </span> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Prevent stale feedback timers from clearing a later action.
Clicking the same button again leaves its earlier timeout active, so it can clear the newer “copied”/“shared” feedback at the original deadline. Cancel and replace each mode’s existing timeout before scheduling a new one; also clear pending timers on unmount.
🤖 Prompt for AI Agents