animate all logos on hover, add tabs loading skeleton, fix dev origins

- 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
Anton Kaminsky3/1/20269f470a7e7e8fparent 10621c5
4 files changed+190-19
web/app/components/canopy-logo.tsx
@@ -1,28 +1,93 @@
1"use client";
2
3import { useCallback, useId, useRef } from "react";
4
15interface Props {
26 size?: number;
37}
48
59export 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
624 return (
725 <svg
26 ref={svgRef}
27 className={`canopy-logo-${s}`}
828 width={size}
929 height={size}
1030 viewBox="0 0 64 64"
1131 fill="none"
32 onMouseMove={onMouseMove}
33 onMouseLeave={onMouseLeave}
1234 >
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>
2691 </svg>
2792 );
2893}
2994
web/app/components/collab-logo.tsx
@@ -1,18 +1,65 @@
1"use client";
2
3import { useId } from "react";
4
15interface Props {
26 size?: number;
37}
48
59export function CollabLogo({ size = 24 }: Props) {
10 const id = useId();
11 const s = id.replace(/:/g, "");
12
613 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" />
954 {/* Top-left bubble — tail points left */}
1055 <path
56 className="cb-top"
1157 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"
1258 fill="white"
1359 />
1460 {/* Bottom-right bubble — mirrored, tail points right */}
1561 <path
62 className="cb-bottom"
1663 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"
1764 fill="white"
1865 opacity="0.65"
1966
web/app/components/ring-logo.tsx
@@ -1,12 +1,70 @@
1"use client";
2
3import { useId } from "react";
4
15interface Props {
26 size?: number;
37}
48
59export function RingLogo({ size = 24 }: Props) {
10 const id = useId();
11 const s = id.replace(/:/g, "");
12
613 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" />
966 <path
67 className="rl-outer"
1068 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"
1169 stroke="white"
1270 strokeWidth="2.6"
@@ -14,13 +72,14 @@
1472 strokeLinejoin="round"
1573 />
1674 <path
75 className="rl-inner"
1776 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"
1877 stroke="white"
1978 strokeWidth="2.2"
2079 strokeLinecap="round"
2180 strokeLinejoin="round"
2281 />
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" />
2483 </svg>
2584 );
2685}
2786
web/next.config.ts
@@ -4,7 +4,7 @@
44const GROVE_API_URL = process.env.GROVE_API_URL ?? "http://localhost:4000";
55const nextConfig: NextConfig = {
66 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"],
88 async rewrites() {
99 return [
1010 {
1111