- CanopyLogo: sway with mouse tracking, trunk/root thicken - RingLogo: counter-rotating rings, pulsing center dot - CollabLogo: bubbles drift apart on hover - All logos: translateY lift, brightness darken, .hover-row:hover support - Add loading.tsx for (tabs) route group to show skeleton during SSR fetch - Add grove.test to allowedDevOrigins to fix font 403 and HMR websocket
| @@ -1,28 +1,93 @@ | ||
| 1 | "use client"; | |
| 2 | ||
| 3 | import { useCallback, useId, useRef } from "react"; | |
| 4 | ||
| 1 | 5 | interface Props { |
| 2 | 6 | size?: number; |
| 3 | 7 | } |
| 4 | 8 | |
| 5 | 9 | export function CanopyLogo({ size = 24 }: Props) { |
| 10 | const svgRef = useRef<SVGSVGElement>(null); | |
| 11 | const id = useId(); | |
| 12 | const s = id.replace(/:/g, ""); | |
| 13 | ||
| 14 | const onMouseMove = useCallback((e: React.MouseEvent<SVGSVGElement>) => { | |
| 15 | const rect = e.currentTarget.getBoundingClientRect(); | |
| 16 | const relX = ((e.clientX - rect.left) / rect.width - 0.5) * 2; | |
| 17 | e.currentTarget.style.setProperty("--sway", `${relX * 5}deg`); | |
| 18 | }, []); | |
| 19 | ||
| 20 | const onMouseLeave = useCallback((e: React.MouseEvent<SVGSVGElement>) => { | |
| 21 | e.currentTarget.style.setProperty("--sway", "0deg"); | |
| 22 | }, []); | |
| 23 | ||
| 6 | 24 | return ( |
| 7 | 25 | <svg |
| 26 | ref={svgRef} | |
| 27 | className={`canopy-logo-${s}`} | |
| 8 | 28 | width={size} |
| 9 | 29 | height={size} |
| 10 | 30 | viewBox="0 0 64 64" |
| 11 | 31 | fill="none" |
| 32 | onMouseMove={onMouseMove} | |
| 33 | onMouseLeave={onMouseLeave} | |
| 12 | 34 | > |
| 13 | <circle cx="32" cy="32" r="32" fill="var(--accent)" /> | |
| 14 | <path d="M31 42 L30 53" stroke="white" strokeWidth="2.5" strokeLinecap="round"/> | |
| 15 | <path d="M30.5 46 L34 49.5" stroke="white" strokeWidth="1.5" strokeLinecap="round"/> | |
| 16 | <path d=" | |
| 17 | M31 42 | |
| 18 | C18 40, 11 32, 14 24 | |
| 19 | C16 17, 24 12, 31 14 | |
| 20 | C33 10, 40 12, 43 16 | |
| 21 | C48 14, 52 22, 49 28 | |
| 22 | C52 34, 46 40, 38 40 | |
| 23 | C36 42, 33 43, 31 42 | |
| 24 | Z | |
| 25 | " fill="white"/> | |
| 35 | <style>{` | |
| 36 | .canopy-logo-${s} { | |
| 37 | cursor: pointer; | |
| 38 | transition: transform 0.5s cubic-bezier(0.25, 1, 0.5, 1); | |
| 39 | } | |
| 40 | .canopy-logo-${s}:hover, | |
| 41 | .hover-row:hover .canopy-logo-${s} { | |
| 42 | transform: translateY(-2px); | |
| 43 | } | |
| 44 | .canopy-logo-${s} .cl-bg { | |
| 45 | transition: filter 0.6s ease; | |
| 46 | } | |
| 47 | .canopy-logo-${s}:hover .cl-bg, | |
| 48 | .hover-row:hover .canopy-logo-${s} .cl-bg { | |
| 49 | filter: brightness(0.9); | |
| 50 | } | |
| 51 | .canopy-logo-${s} .cl-canopy { | |
| 52 | transform-origin: 31px 42px; | |
| 53 | transform: rotate(var(--sway, 0deg)); | |
| 54 | transition: transform 0.8s cubic-bezier(0.34, 1.56, 0.64, 1); | |
| 55 | } | |
| 56 | .canopy-logo-${s}:hover .cl-canopy, | |
| 57 | .hover-row:hover .canopy-logo-${s} .cl-canopy { | |
| 58 | transition: transform 0.15s ease-out; | |
| 59 | } | |
| 60 | .canopy-logo-${s} .cl-trunk { | |
| 61 | transition: stroke-width 0.4s cubic-bezier(0.25, 1, 0.5, 1); | |
| 62 | } | |
| 63 | .canopy-logo-${s}:hover .cl-trunk, | |
| 64 | .hover-row:hover .canopy-logo-${s} .cl-trunk { | |
| 65 | stroke-width: 3.2; | |
| 66 | } | |
| 67 | .canopy-logo-${s} .cl-root { | |
| 68 | transition: stroke-width 0.4s cubic-bezier(0.25, 1, 0.5, 1) 0.05s; | |
| 69 | } | |
| 70 | .canopy-logo-${s}:hover .cl-root, | |
| 71 | .hover-row:hover .canopy-logo-${s} .cl-root { | |
| 72 | stroke-width: 2.2; | |
| 73 | } | |
| 74 | `}</style> | |
| 75 | ||
| 76 | <circle cx="32" cy="32" r="32" fill="var(--accent)" className="cl-bg" /> | |
| 77 | <g className="cl-canopy"> | |
| 78 | <path className="cl-trunk" d="M31 42 L30 53" stroke="white" strokeWidth="2.5" strokeLinecap="round"/> | |
| 79 | <path className="cl-root" d="M30.5 46 L34 49.5" stroke="white" strokeWidth="1.5" strokeLinecap="round"/> | |
| 80 | <path d=" | |
| 81 | M31 42 | |
| 82 | C18 40, 11 32, 14 24 | |
| 83 | C16 17, 24 12, 31 14 | |
| 84 | C33 10, 40 12, 43 16 | |
| 85 | C48 14, 52 22, 49 28 | |
| 86 | C52 34, 46 40, 38 40 | |
| 87 | C36 42, 33 43, 31 42 | |
| 88 | Z | |
| 89 | " fill="white"/> | |
| 90 | </g> | |
| 26 | 91 | </svg> |
| 27 | 92 | ); |
| 28 | 93 | } |
| 29 | 94 | |
| @@ -1,18 +1,65 @@ | ||
| 1 | "use client"; | |
| 2 | ||
| 3 | import { useId } from "react"; | |
| 4 | ||
| 1 | 5 | interface Props { |
| 2 | 6 | size?: number; |
| 3 | 7 | } |
| 4 | 8 | |
| 5 | 9 | export function CollabLogo({ size = 24 }: Props) { |
| 10 | const id = useId(); | |
| 11 | const s = id.replace(/:/g, ""); | |
| 12 | ||
| 6 | 13 | return ( |
| 7 | <svg width={size} height={size} viewBox="0 0 64 64" fill="none"> | |
| 8 | <circle cx="32" cy="32" r="32" fill="var(--accent)" /> | |
| 14 | <svg | |
| 15 | className={`collab-logo-${s}`} | |
| 16 | width={size} | |
| 17 | height={size} | |
| 18 | viewBox="0 0 64 64" | |
| 19 | fill="none" | |
| 20 | > | |
| 21 | <style>{` | |
| 22 | .collab-logo-${s} { | |
| 23 | cursor: pointer; | |
| 24 | transition: transform 0.5s cubic-bezier(0.25, 1, 0.5, 1); | |
| 25 | } | |
| 26 | .collab-logo-${s}:hover, | |
| 27 | .hover-row:hover .collab-logo-${s} { | |
| 28 | transform: translateY(-2px); | |
| 29 | } | |
| 30 | .collab-logo-${s} .cb-bg { | |
| 31 | transition: filter 0.6s ease; | |
| 32 | } | |
| 33 | .collab-logo-${s}:hover .cb-bg, | |
| 34 | .hover-row:hover .collab-logo-${s} .cb-bg { | |
| 35 | filter: brightness(0.9); | |
| 36 | } | |
| 37 | .collab-logo-${s} .cb-top { | |
| 38 | transition: transform 0.4s cubic-bezier(0.25, 1, 0.5, 1); | |
| 39 | } | |
| 40 | .collab-logo-${s}:hover .cb-top, | |
| 41 | .hover-row:hover .collab-logo-${s} .cb-top { | |
| 42 | transform: translate(-1.5px, -2px); | |
| 43 | } | |
| 44 | .collab-logo-${s} .cb-bottom { | |
| 45 | transition: transform 0.4s cubic-bezier(0.25, 1, 0.5, 1) 0.05s; | |
| 46 | } | |
| 47 | .collab-logo-${s}:hover .cb-bottom, | |
| 48 | .hover-row:hover .collab-logo-${s} .cb-bottom { | |
| 49 | transform: translate(1.5px, 2px); | |
| 50 | } | |
| 51 | `}</style> | |
| 52 | ||
| 53 | <circle cx="32" cy="32" r="32" fill="var(--accent)" className="cb-bg" /> | |
| 9 | 54 | {/* Top-left bubble — tail points left */} |
| 10 | 55 | <path |
| 56 | className="cb-top" | |
| 11 | 57 | d="M9 16 C9 11, 14 8, 21 8 L35 8 C42 8, 47 11, 47 16 L47 24 C47 29, 42 32, 35 32 L19 32 L13 37 L13 32 C10 31, 9 29, 9 24 Z" |
| 12 | 58 | fill="white" |
| 13 | 59 | /> |
| 14 | 60 | {/* Bottom-right bubble — mirrored, tail points right */} |
| 15 | 61 | <path |
| 62 | className="cb-bottom" | |
| 16 | 63 | d="M55 35 C55 30, 50 27, 43 27 L29 27 C22 27, 17 30, 17 35 L17 43 C17 48, 22 51, 29 51 L45 51 L51 56 L51 51 C54 50, 55 48, 55 43 Z" |
| 17 | 64 | fill="white" |
| 18 | 65 | opacity="0.65" |
| 19 | 66 | |
| @@ -1,12 +1,70 @@ | ||
| 1 | "use client"; | |
| 2 | ||
| 3 | import { useId } from "react"; | |
| 4 | ||
| 1 | 5 | interface Props { |
| 2 | 6 | size?: number; |
| 3 | 7 | } |
| 4 | 8 | |
| 5 | 9 | export function RingLogo({ size = 24 }: Props) { |
| 10 | const id = useId(); | |
| 11 | const s = id.replace(/:/g, ""); | |
| 12 | ||
| 6 | 13 | return ( |
| 7 | <svg width={size} height={size} viewBox="0 0 64 64" fill="none"> | |
| 8 | <circle cx="32" cy="32" r="32" fill="var(--accent)" /> | |
| 14 | <svg | |
| 15 | className={`ring-logo-${s}`} | |
| 16 | width={size} | |
| 17 | height={size} | |
| 18 | viewBox="0 0 64 64" | |
| 19 | fill="none" | |
| 20 | > | |
| 21 | <style>{` | |
| 22 | .ring-logo-${s} { | |
| 23 | cursor: pointer; | |
| 24 | transition: transform 0.5s cubic-bezier(0.25, 1, 0.5, 1); | |
| 25 | } | |
| 26 | .ring-logo-${s}:hover, | |
| 27 | .hover-row:hover .ring-logo-${s} { | |
| 28 | transform: translateY(-2px); | |
| 29 | } | |
| 30 | .ring-logo-${s} .rl-bg { | |
| 31 | transition: filter 0.6s ease; | |
| 32 | } | |
| 33 | .ring-logo-${s}:hover .rl-bg, | |
| 34 | .hover-row:hover .ring-logo-${s} .rl-bg { | |
| 35 | filter: brightness(0.9); | |
| 36 | } | |
| 37 | .ring-logo-${s} .rl-outer { | |
| 38 | transform-origin: 32px 32px; | |
| 39 | transition: transform 0.5s cubic-bezier(0.25, 1, 0.5, 1), stroke-width 0.4s ease; | |
| 40 | } | |
| 41 | .ring-logo-${s}:hover .rl-outer, | |
| 42 | .hover-row:hover .ring-logo-${s} .rl-outer { | |
| 43 | transform: rotate(15deg) scale(1.04); | |
| 44 | stroke-width: 3; | |
| 45 | } | |
| 46 | .ring-logo-${s} .rl-inner { | |
| 47 | transform-origin: 32px 34px; | |
| 48 | transition: transform 0.5s cubic-bezier(0.25, 1, 0.5, 1) 0.05s, stroke-width 0.4s ease 0.05s; | |
| 49 | } | |
| 50 | .ring-logo-${s}:hover .rl-inner, | |
| 51 | .hover-row:hover .ring-logo-${s} .rl-inner { | |
| 52 | transform: rotate(-20deg) scale(1.06); | |
| 53 | stroke-width: 2.6; | |
| 54 | } | |
| 55 | .ring-logo-${s} .rl-dot { | |
| 56 | transform-origin: 32px 34px; | |
| 57 | transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) 0.1s; | |
| 58 | } | |
| 59 | .ring-logo-${s}:hover .rl-dot, | |
| 60 | .hover-row:hover .ring-logo-${s} .rl-dot { | |
| 61 | transform: scale(1.35); | |
| 62 | } | |
| 63 | `}</style> | |
| 64 | ||
| 65 | <circle cx="32" cy="32" r="32" fill="var(--accent)" className="rl-bg" /> | |
| 9 | 66 | <path |
| 67 | className="rl-outer" | |
| 10 | 68 | d="M32 12C42 12 51 19 52 29C53 39 46 49 36 51C26 53 15 48 12 39C9 30 13 19 23 14C26 13 29 12 32 12Z" |
| 11 | 69 | stroke="white" |
| 12 | 70 | strokeWidth="2.6" |
| @@ -14,13 +72,14 @@ | ||
| 14 | 72 | strokeLinejoin="round" |
| 15 | 73 | /> |
| 16 | 74 | <path |
| 75 | className="rl-inner" | |
| 17 | 76 | d="M32 24C37 24 41 27 42 32C43 37 40 41 35 43C30 45 24 43 21 38C18 33 19 28 24 25C26 24 29 24 32 24Z" |
| 18 | 77 | stroke="white" |
| 19 | 78 | strokeWidth="2.2" |
| 20 | 79 | strokeLinecap="round" |
| 21 | 80 | strokeLinejoin="round" |
| 22 | 81 | /> |
| 23 | <ellipse cx="32" cy="34" rx="3.2" ry="2.6" fill="white" /> | |
| 82 | <ellipse className="rl-dot" cx="32" cy="34" rx="3.2" ry="2.6" fill="white" /> | |
| 24 | 83 | </svg> |
| 25 | 84 | ); |
| 26 | 85 | } |
| 27 | 86 | |
| @@ -4,7 +4,7 @@ | ||
| 4 | 4 | const GROVE_API_URL = process.env.GROVE_API_URL ?? "http://localhost:4000"; |
| 5 | 5 | const nextConfig: NextConfig = { |
| 6 | 6 | output: "standalone", |
| 7 | allowedDevOrigins: ["collab.grove.test", "canopy.grove.test", "ring.grove.test"], | |
| 7 | allowedDevOrigins: ["grove.test", "collab.grove.test", "canopy.grove.test", "ring.grove.test"], | |
| 8 | 8 | async rewrites() { |
| 9 | 9 | return [ |
| 10 | 10 | { |
| 11 | 11 | |