branding v1
@@ -2,9 +2,9 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/attune-favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>web</title>
|
<title>Attune</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
44
web/public/attune-favicon.svg
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="120 190 272 260" width="64" height="64">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad-primary" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#4F46E5" />
|
||||||
|
<stop offset="50%" stop-color="#6366F1" />
|
||||||
|
<stop offset="100%" stop-color="#818CF8" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="grad-accent" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#14B8A6" />
|
||||||
|
<stop offset="100%" stop-color="#22D3EE" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Single U-shaped resonator -->
|
||||||
|
<path d="M 220 412 L 220 278 Q 220 248 250 228 Q 256 224 262 228 Q 292 248 292 278 L 292 412"
|
||||||
|
fill="none" stroke="url(#grad-primary)" stroke-width="32" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
|
||||||
|
<!-- Crossbar -->
|
||||||
|
<path d="M 218 330 L 294 330"
|
||||||
|
fill="none" stroke="url(#grad-primary)" stroke-width="20" stroke-linecap="round" />
|
||||||
|
|
||||||
|
<!-- Event nodes on the crossbar -->
|
||||||
|
<circle cx="242" cy="330" r="11" fill="#14B8A6" opacity="0.9" />
|
||||||
|
<circle cx="270" cy="330" r="11" fill="#22D3EE" opacity="0.9" />
|
||||||
|
|
||||||
|
<!-- Outer branching event dots -->
|
||||||
|
<circle cx="318" cy="330" r="9" fill="url(#grad-accent)" opacity="0.7" />
|
||||||
|
<line x1="296" y1="330" x2="309" y2="330" stroke="url(#grad-accent)" stroke-width="5" stroke-linecap="round" opacity="0.6" />
|
||||||
|
|
||||||
|
<circle cx="194" cy="330" r="9" fill="url(#grad-accent)" opacity="0.7" />
|
||||||
|
<line x1="216" y1="330" x2="203" y2="330" stroke="url(#grad-accent)" stroke-width="5" stroke-linecap="round" opacity="0.6" />
|
||||||
|
|
||||||
|
<!-- Resonance arcs — left -->
|
||||||
|
<path d="M 168 274 Q 138 330 168 386"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="16" stroke-linecap="round" opacity="0.7" />
|
||||||
|
<path d="M 148 254 Q 110 330 148 406"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="12" stroke-linecap="round" opacity="0.5" />
|
||||||
|
|
||||||
|
<!-- Resonance arcs — right -->
|
||||||
|
<path d="M 344 274 Q 374 330 344 386"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="16" stroke-linecap="round" opacity="0.7" />
|
||||||
|
<path d="M 364 254 Q 402 330 364 406"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="12" stroke-linecap="round" opacity="0.5" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
152
web/public/attune-logo-full.svg
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 512" width="800" height="512">
|
||||||
|
<defs>
|
||||||
|
<!-- Primary gradient: deep indigo to vibrant blue -->
|
||||||
|
<linearGradient id="grad-primary" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#4F46E5" />
|
||||||
|
<stop offset="50%" stop-color="#6366F1" />
|
||||||
|
<stop offset="100%" stop-color="#818CF8" />
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<!-- Accent gradient: teal to cyan for flow lines -->
|
||||||
|
<linearGradient id="grad-accent" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#14B8A6" />
|
||||||
|
<stop offset="100%" stop-color="#22D3EE" />
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<!-- Glow gradient for the resonance ring -->
|
||||||
|
<linearGradient id="grad-glow" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#818CF8" />
|
||||||
|
<stop offset="50%" stop-color="#6366F1" />
|
||||||
|
<stop offset="100%" stop-color="#14B8A6" />
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<!-- Text gradient (left to right for horizontal text) -->
|
||||||
|
<linearGradient id="grad-text" x1="0%" y1="0%" x2="100%" y2="20%">
|
||||||
|
<stop offset="0%" stop-color="#4F46E5" />
|
||||||
|
<stop offset="50%" stop-color="#6366F1" />
|
||||||
|
<stop offset="100%" stop-color="#818CF8" />
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<!-- Subtle shadow for depth -->
|
||||||
|
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
|
||||||
|
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#4F46E5" flood-opacity="0.2" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Resonance arcs — left side (centered on crossbar y=330) -->
|
||||||
|
<g transform="translate(0,0)">
|
||||||
|
<animateTransform attributeName="transform" type="translate" values="0 0; -4 0; 0 0" dur="3s" repeatCount="indefinite" />
|
||||||
|
<path d="M 118 274 Q 88 330 118 386"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="6" stroke-linecap="round" opacity="0.5">
|
||||||
|
<animate attributeName="opacity" values="0.3;0.6;0.3" dur="3s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="stroke-width" values="4;8;4" dur="3s" repeatCount="indefinite" />
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(0,0)">
|
||||||
|
<animateTransform attributeName="transform" type="translate" values="0 0; -5 0; 0 0" dur="3s" begin="0.3s" repeatCount="indefinite" />
|
||||||
|
<path d="M 98 254 Q 58 330 98 406"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="4.5" stroke-linecap="round" opacity="0.35">
|
||||||
|
<animate attributeName="opacity" values="0.2;0.45;0.2" dur="3s" begin="0.3s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="stroke-width" values="3;6;3" dur="3s" begin="0.3s" repeatCount="indefinite" />
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(0,0)">
|
||||||
|
<animateTransform attributeName="transform" type="translate" values="0 0; -6 0; 0 0" dur="3s" begin="0.6s" repeatCount="indefinite" />
|
||||||
|
<path d="M 80 234 Q 30 330 80 426"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="3" stroke-linecap="round" opacity="0.2">
|
||||||
|
<animate attributeName="opacity" values="0.1;0.3;0.1" dur="3s" begin="0.6s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="stroke-width" values="2;4;2" dur="3s" begin="0.6s" repeatCount="indefinite" />
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Stylized 'A' — inverted tuning fork -->
|
||||||
|
<g filter="url(#shadow)">
|
||||||
|
<!-- Single U-shaped resonator: left foot → up left leg → curve through apex → down right leg → right foot -->
|
||||||
|
<path d="M 170 412 L 170 278 Q 170 248 200 228 Q 206 224 212 228 Q 242 248 242 278 L 242 412"
|
||||||
|
fill="none" stroke="url(#grad-primary)" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
|
||||||
|
<!-- Handle / stem above the apex -->
|
||||||
|
<path d="M 206 70 L 206 224"
|
||||||
|
fill="none" stroke="url(#grad-primary)" stroke-width="22" stroke-linecap="round" />
|
||||||
|
|
||||||
|
<!-- Crossbar of the A -->
|
||||||
|
<path d="M 174 330 L 238 330"
|
||||||
|
fill="none" stroke="url(#grad-primary)" stroke-width="14" stroke-linecap="round" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Stem dots (orchestration flow along the handle) -->
|
||||||
|
<circle cx="206" cy="120" r="5" fill="#22D3EE" opacity="0.6">
|
||||||
|
<animate attributeName="opacity" values="0.3;0.7;0.3" dur="2.5s" begin="0.2s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="r" values="3;6;3" dur="2.5s" begin="0.2s" repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
<circle cx="206" cy="150" r="6" fill="#14B8A6" opacity="0.7">
|
||||||
|
<animate attributeName="opacity" values="0.4;0.8;0.4" dur="2.5s" begin="0.6s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="r" values="4;7;4" dur="2.5s" begin="0.6s" repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
<circle cx="206" cy="182" r="5" fill="#22D3EE" opacity="0.6">
|
||||||
|
<animate attributeName="opacity" values="0.3;0.7;0.3" dur="2.5s" begin="1.0s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="r" values="3;6;3" dur="2.5s" begin="1.0s" repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
|
||||||
|
<!-- Crossbar event node dots on the A -->
|
||||||
|
<circle cx="194" cy="330" r="7" fill="#14B8A6" opacity="0.8">
|
||||||
|
<animate attributeName="r" values="5;8;5" dur="2s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="opacity" values="0.6;1;0.6" dur="2s" repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
<circle cx="218" cy="330" r="7" fill="#22D3EE" opacity="0.8">
|
||||||
|
<animate attributeName="r" values="5;8;5" dur="2s" begin="0.5s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="opacity" values="0.6;1;0.6" dur="2s" begin="0.5s" repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
|
||||||
|
<!-- Left branching dot from the A crossbar -->
|
||||||
|
<circle cx="154" cy="330" r="5" fill="url(#grad-accent)" opacity="0.6">
|
||||||
|
<animate attributeName="opacity" values="0.3;0.7;0.3" dur="2.5s" begin="0.5s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="r" values="3;6;3" dur="2.5s" begin="0.5s" repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
<line x1="169" y1="330" x2="159" y2="330" stroke="url(#grad-accent)" stroke-width="2" stroke-linecap="round" opacity="0.5" />
|
||||||
|
|
||||||
|
<!-- Apex dot at the top of the A -->
|
||||||
|
<circle cx="206" cy="66" r="10" fill="url(#grad-primary)" opacity="0.9" />
|
||||||
|
|
||||||
|
<!-- "ttune" text — vertically centered on crossbar y=330 -->
|
||||||
|
<text x="260" y="372"
|
||||||
|
font-family="'Inter', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif"
|
||||||
|
font-size="130"
|
||||||
|
font-weight="600"
|
||||||
|
letter-spacing="-2"
|
||||||
|
fill="url(#grad-text)"
|
||||||
|
filter="url(#shadow)">ttune</text>
|
||||||
|
|
||||||
|
<!-- Right-side branching dot (connects text to resonance arcs) -->
|
||||||
|
<circle cx="583" cy="330" r="5" fill="url(#grad-accent)" opacity="0.6">
|
||||||
|
<animate attributeName="opacity" values="0.3;0.7;0.3" dur="2.5s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="r" values="3;6;3" dur="2.5s" repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
<line x1="570" y1="330" x2="578" y2="330" stroke="url(#grad-accent)" stroke-width="2" stroke-linecap="round" opacity="0.5" />
|
||||||
|
|
||||||
|
<!-- Resonance arcs — right side (close to the 'e', centered on y=330) -->
|
||||||
|
<g transform="translate(0,0)">
|
||||||
|
<animateTransform attributeName="transform" type="translate" values="0 0; 4 0; 0 0" dur="3s" repeatCount="indefinite" />
|
||||||
|
<path d="M 609 274 Q 639 330 609 386"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="6" stroke-linecap="round" opacity="0.5">
|
||||||
|
<animate attributeName="opacity" values="0.3;0.6;0.3" dur="3s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="stroke-width" values="4;8;4" dur="3s" repeatCount="indefinite" />
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(0,0)">
|
||||||
|
<animateTransform attributeName="transform" type="translate" values="0 0; 5 0; 0 0" dur="3s" begin="0.3s" repeatCount="indefinite" />
|
||||||
|
<path d="M 629 254 Q 669 330 629 406"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="4.5" stroke-linecap="round" opacity="0.35">
|
||||||
|
<animate attributeName="opacity" values="0.2;0.45;0.2" dur="3s" begin="0.3s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="stroke-width" values="3;6;3" dur="3s" begin="0.3s" repeatCount="indefinite" />
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(0,0)">
|
||||||
|
<animateTransform attributeName="transform" type="translate" values="0 0; 6 0; 0 0" dur="3s" begin="0.6s" repeatCount="indefinite" />
|
||||||
|
<path d="M 647 234 Q 697 330 647 426"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="3" stroke-linecap="round" opacity="0.2">
|
||||||
|
<animate attributeName="opacity" values="0.1;0.3;0.1" dur="3s" begin="0.6s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="stroke-width" values="2;4;2" dur="3s" begin="0.6s" repeatCount="indefinite" />
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 8.3 KiB |
71
web/public/attune-logo-icon.svg
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="60 55 392 385" width="392" height="385">
|
||||||
|
<defs>
|
||||||
|
<!-- Primary gradient: deep indigo to vibrant blue -->
|
||||||
|
<linearGradient id="grad-primary" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#4F46E5" />
|
||||||
|
<stop offset="50%" stop-color="#6366F1" />
|
||||||
|
<stop offset="100%" stop-color="#818CF8" />
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<!-- Accent gradient: teal to cyan for flow lines -->
|
||||||
|
<linearGradient id="grad-accent" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#14B8A6" />
|
||||||
|
<stop offset="100%" stop-color="#22D3EE" />
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<!-- Subtle shadow for depth -->
|
||||||
|
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
|
||||||
|
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#4F46E5" flood-opacity="0.2" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Resonance arcs — left side (centered on crossbar y=330) -->
|
||||||
|
<path d="M 168 274 Q 138 330 168 386"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="6" stroke-linecap="round" opacity="0.5" />
|
||||||
|
<path d="M 148 254 Q 108 330 148 406"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="4.5" stroke-linecap="round" opacity="0.35" />
|
||||||
|
<path d="M 130 234 Q 80 330 130 426"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="3" stroke-linecap="round" opacity="0.2" />
|
||||||
|
|
||||||
|
<!-- Resonance arcs — right side (centered on crossbar y=330) -->
|
||||||
|
<path d="M 344 274 Q 374 330 344 386"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="6" stroke-linecap="round" opacity="0.5" />
|
||||||
|
<path d="M 364 254 Q 404 330 364 406"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="4.5" stroke-linecap="round" opacity="0.35" />
|
||||||
|
<path d="M 382 234 Q 432 330 382 426"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="3" stroke-linecap="round" opacity="0.2" />
|
||||||
|
|
||||||
|
<!-- Inverted tuning fork forming an 'A' shape -->
|
||||||
|
<g filter="url(#shadow)">
|
||||||
|
<!-- Single U-shaped resonator: left foot → up left leg → curve through apex → down right leg → right foot -->
|
||||||
|
<path d="M 220 412 L 220 278 Q 220 248 250 228 Q 256 224 262 228 Q 292 248 292 278 L 292 412"
|
||||||
|
fill="none" stroke="url(#grad-primary)" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
|
||||||
|
<!-- Handle / stem above the apex -->
|
||||||
|
<path d="M 256 70 L 256 224"
|
||||||
|
fill="none" stroke="url(#grad-primary)" stroke-width="22" stroke-linecap="round" />
|
||||||
|
|
||||||
|
<!-- Crossbar of the A -->
|
||||||
|
<path d="M 224 330 L 288 330"
|
||||||
|
fill="none" stroke="url(#grad-primary)" stroke-width="14" stroke-linecap="round" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Stem dots (orchestration flow along the handle) -->
|
||||||
|
<circle cx="256" cy="120" r="5" fill="#22D3EE" opacity="0.6" />
|
||||||
|
<circle cx="256" cy="150" r="6" fill="#14B8A6" opacity="0.7" />
|
||||||
|
<circle cx="256" cy="182" r="5" fill="#22D3EE" opacity="0.6" />
|
||||||
|
|
||||||
|
<!-- Node dots on the crossbar representing events/connections -->
|
||||||
|
<circle cx="244" cy="330" r="7" fill="#14B8A6" opacity="0.8" />
|
||||||
|
<circle cx="268" cy="330" r="7" fill="#22D3EE" opacity="0.8" />
|
||||||
|
|
||||||
|
<!-- Small orchestration flow dots branching from crossbar -->
|
||||||
|
<circle cx="308" cy="330" r="5" fill="url(#grad-accent)" opacity="0.6" />
|
||||||
|
<line x1="293" y1="330" x2="303" y2="330" stroke="url(#grad-accent)" stroke-width="2" stroke-linecap="round" opacity="0.5" />
|
||||||
|
|
||||||
|
<circle cx="204" cy="330" r="5" fill="url(#grad-accent)" opacity="0.6" />
|
||||||
|
<line x1="219" y1="330" x2="209" y2="330" stroke="url(#grad-accent)" stroke-width="2" stroke-linecap="round" opacity="0.5" />
|
||||||
|
|
||||||
|
<!-- Apex dot at the top of the A -->
|
||||||
|
<circle cx="256" cy="66" r="10" fill="url(#grad-primary)" opacity="0.9" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.6 KiB |
124
web/public/attune-logo-navbar.svg
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="20 215 690 210" width="690" height="210">
|
||||||
|
<defs>
|
||||||
|
<!-- Primary gradient: deep indigo to vibrant blue -->
|
||||||
|
<linearGradient id="grad-primary" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#4F46E5" />
|
||||||
|
<stop offset="50%" stop-color="#6366F1" />
|
||||||
|
<stop offset="100%" stop-color="#818CF8" />
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<!-- Accent gradient: teal to cyan for flow lines -->
|
||||||
|
<linearGradient id="grad-accent" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#14B8A6" />
|
||||||
|
<stop offset="100%" stop-color="#22D3EE" />
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<!-- Text gradient (left to right for horizontal text) -->
|
||||||
|
<linearGradient id="grad-text" x1="0%" y1="0%" x2="100%" y2="20%">
|
||||||
|
<stop offset="0%" stop-color="#4F46E5" />
|
||||||
|
<stop offset="50%" stop-color="#6366F1" />
|
||||||
|
<stop offset="100%" stop-color="#818CF8" />
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<!-- Subtle shadow for depth -->
|
||||||
|
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
|
||||||
|
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#4F46E5" flood-opacity="0.2" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Resonance arcs — left side (centered on crossbar y=330) -->
|
||||||
|
<g transform="translate(0,0)">
|
||||||
|
<animateTransform attributeName="transform" type="translate" values="0 0; -4 0; 0 0" dur="3s" repeatCount="indefinite" />
|
||||||
|
<path d="M 118 274 Q 88 330 118 386"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="6" stroke-linecap="round" opacity="0.5">
|
||||||
|
<animate attributeName="opacity" values="0.3;0.6;0.3" dur="3s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="stroke-width" values="4;8;4" dur="3s" repeatCount="indefinite" />
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(0,0)">
|
||||||
|
<animateTransform attributeName="transform" type="translate" values="0 0; -5 0; 0 0" dur="3s" begin="0.3s" repeatCount="indefinite" />
|
||||||
|
<path d="M 98 254 Q 58 330 98 406"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="4.5" stroke-linecap="round" opacity="0.35">
|
||||||
|
<animate attributeName="opacity" values="0.2;0.45;0.2" dur="3s" begin="0.3s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="stroke-width" values="3;6;3" dur="3s" begin="0.3s" repeatCount="indefinite" />
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(0,0)">
|
||||||
|
<animateTransform attributeName="transform" type="translate" values="0 0; -6 0; 0 0" dur="3s" begin="0.6s" repeatCount="indefinite" />
|
||||||
|
<path d="M 80 234 Q 30 330 80 426"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="3" stroke-linecap="round" opacity="0.2">
|
||||||
|
<animate attributeName="opacity" values="0.1;0.3;0.1" dur="3s" begin="0.6s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="stroke-width" values="2;4;2" dur="3s" begin="0.6s" repeatCount="indefinite" />
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Stylized 'A' — inverted tuning fork (no handle/stem) -->
|
||||||
|
<g filter="url(#shadow)">
|
||||||
|
<!-- Single U-shaped resonator: left foot → up left leg → curve through apex → down right leg → right foot -->
|
||||||
|
<path d="M 170 412 L 170 278 Q 170 248 200 228 Q 206 224 212 228 Q 242 248 242 278 L 242 412"
|
||||||
|
fill="none" stroke="url(#grad-primary)" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
|
||||||
|
<!-- Crossbar of the A -->
|
||||||
|
<path d="M 174 330 L 238 330"
|
||||||
|
fill="none" stroke="url(#grad-primary)" stroke-width="14" stroke-linecap="round" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Crossbar event node dots on the A -->
|
||||||
|
<circle cx="194" cy="330" r="10" fill="#14B8A6" opacity="0.8">
|
||||||
|
<animate attributeName="r" values="8;11;8" dur="2s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="opacity" values="0.6;1;0.6" dur="2s" repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
<circle cx="218" cy="330" r="10" fill="#22D3EE" opacity="0.8">
|
||||||
|
<animate attributeName="r" values="8;11;8" dur="2s" begin="0.5s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="opacity" values="0.6;1;0.6" dur="2s" begin="0.5s" repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
|
||||||
|
<!-- Left branching dot from the A crossbar -->
|
||||||
|
<circle cx="154" cy="330" r="5" fill="url(#grad-accent)" opacity="0.6">
|
||||||
|
<animate attributeName="opacity" values="0.3;0.7;0.3" dur="2.5s" begin="0.5s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="r" values="3;6;3" dur="2.5s" begin="0.5s" repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
<line x1="169" y1="330" x2="159" y2="330" stroke="url(#grad-accent)" stroke-width="2" stroke-linecap="round" opacity="0.5" />
|
||||||
|
|
||||||
|
<!-- "ttune" text — vertically centered on crossbar y=330 -->
|
||||||
|
<text x="260" y="372"
|
||||||
|
font-family="'Inter', 'Segoe UI', 'Helvetica Neue', Arial, sans-serif"
|
||||||
|
font-size="130"
|
||||||
|
font-weight="600"
|
||||||
|
letter-spacing="-2"
|
||||||
|
fill="url(#grad-text)"
|
||||||
|
filter="url(#shadow)">ttune</text>
|
||||||
|
|
||||||
|
<!-- Right-side branching dot (connects text to resonance arcs) -->
|
||||||
|
<circle cx="583" cy="330" r="5" fill="url(#grad-accent)" opacity="0.6">
|
||||||
|
<animate attributeName="opacity" values="0.3;0.7;0.3" dur="2.5s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="r" values="3;6;3" dur="2.5s" repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
<line x1="570" y1="330" x2="578" y2="330" stroke="url(#grad-accent)" stroke-width="2" stroke-linecap="round" opacity="0.5" />
|
||||||
|
|
||||||
|
<!-- Resonance arcs — right side (close to the 'e', centered on y=330) -->
|
||||||
|
<g transform="translate(0,0)">
|
||||||
|
<animateTransform attributeName="transform" type="translate" values="0 0; 4 0; 0 0" dur="3s" repeatCount="indefinite" />
|
||||||
|
<path d="M 609 274 Q 639 330 609 386"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="6" stroke-linecap="round" opacity="0.5">
|
||||||
|
<animate attributeName="opacity" values="0.3;0.6;0.3" dur="3s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="stroke-width" values="4;8;4" dur="3s" repeatCount="indefinite" />
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(0,0)">
|
||||||
|
<animateTransform attributeName="transform" type="translate" values="0 0; 5 0; 0 0" dur="3s" begin="0.3s" repeatCount="indefinite" />
|
||||||
|
<path d="M 629 254 Q 669 330 629 406"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="4.5" stroke-linecap="round" opacity="0.35">
|
||||||
|
<animate attributeName="opacity" values="0.2;0.45;0.2" dur="3s" begin="0.3s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="stroke-width" values="3;6;3" dur="3s" begin="0.3s" repeatCount="indefinite" />
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(0,0)">
|
||||||
|
<animateTransform attributeName="transform" type="translate" values="0 0; 6 0; 0 0" dur="3s" begin="0.6s" repeatCount="indefinite" />
|
||||||
|
<path d="M 647 234 Q 697 330 647 426"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="3" stroke-linecap="round" opacity="0.2">
|
||||||
|
<animate attributeName="opacity" values="0.1;0.3;0.1" dur="3s" begin="0.6s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="stroke-width" values="2;4;2" dur="3s" begin="0.6s" repeatCount="indefinite" />
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 6.9 KiB |
83
web/public/attune-logo-static.svg
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
||||||
|
<defs>
|
||||||
|
<!-- Primary gradient: deep indigo to vibrant blue -->
|
||||||
|
<linearGradient id="grad-primary" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#4F46E5" />
|
||||||
|
<stop offset="50%" stop-color="#6366F1" />
|
||||||
|
<stop offset="100%" stop-color="#818CF8" />
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<!-- Accent gradient: teal to cyan for flow lines -->
|
||||||
|
<linearGradient id="grad-accent" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#14B8A6" />
|
||||||
|
<stop offset="100%" stop-color="#22D3EE" />
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<!-- Glow gradient for the resonance ring -->
|
||||||
|
<linearGradient id="grad-glow" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#818CF8" />
|
||||||
|
<stop offset="50%" stop-color="#6366F1" />
|
||||||
|
<stop offset="100%" stop-color="#14B8A6" />
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<!-- Subtle shadow for depth -->
|
||||||
|
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
|
||||||
|
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#4F46E5" flood-opacity="0.2" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Background circle -->
|
||||||
|
<circle cx="256" cy="256" r="248" fill="none" stroke="url(#grad-glow)" stroke-width="3" opacity="0.3" />
|
||||||
|
|
||||||
|
<!-- Resonance arcs — left side (centered on crossbar y=330) -->
|
||||||
|
<path d="M 168 274 Q 138 330 168 386"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="6" stroke-linecap="round" opacity="0.5" />
|
||||||
|
<path d="M 148 254 Q 108 330 148 406"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="4.5" stroke-linecap="round" opacity="0.35" />
|
||||||
|
<path d="M 130 234 Q 80 330 130 426"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="3" stroke-linecap="round" opacity="0.2" />
|
||||||
|
|
||||||
|
<!-- Resonance arcs — right side (centered on crossbar y=330) -->
|
||||||
|
<path d="M 344 274 Q 374 330 344 386"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="6" stroke-linecap="round" opacity="0.5" />
|
||||||
|
<path d="M 364 254 Q 404 330 364 406"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="4.5" stroke-linecap="round" opacity="0.35" />
|
||||||
|
<path d="M 382 234 Q 432 330 382 426"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="3" stroke-linecap="round" opacity="0.2" />
|
||||||
|
|
||||||
|
<!-- Inverted tuning fork forming an 'A' shape -->
|
||||||
|
<g filter="url(#shadow)">
|
||||||
|
<!-- Single U-shaped resonator: left foot → up left leg → curve through apex → down right leg → right foot -->
|
||||||
|
<path d="M 220 412 L 220 278 Q 220 248 250 228 Q 256 224 262 228 Q 292 248 292 278 L 292 412"
|
||||||
|
fill="none" stroke="url(#grad-primary)" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
|
||||||
|
<!-- Handle / stem above the apex -->
|
||||||
|
<path d="M 256 70 L 256 224"
|
||||||
|
fill="none" stroke="url(#grad-primary)" stroke-width="22" stroke-linecap="round" />
|
||||||
|
|
||||||
|
<!-- Crossbar of the A -->
|
||||||
|
<path d="M 224 330 L 288 330"
|
||||||
|
fill="none" stroke="url(#grad-primary)" stroke-width="14" stroke-linecap="round" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Stem dots (orchestration flow along the handle) -->
|
||||||
|
<circle cx="256" cy="120" r="5" fill="#22D3EE" opacity="0.6" />
|
||||||
|
<circle cx="256" cy="150" r="6" fill="#14B8A6" opacity="0.7" />
|
||||||
|
<circle cx="256" cy="182" r="5" fill="#22D3EE" opacity="0.6" />
|
||||||
|
|
||||||
|
<!-- Node dots on the crossbar representing events/connections -->
|
||||||
|
<circle cx="244" cy="330" r="7" fill="#14B8A6" opacity="0.8" />
|
||||||
|
<circle cx="268" cy="330" r="7" fill="#22D3EE" opacity="0.8" />
|
||||||
|
|
||||||
|
<!-- Small orchestration flow dots branching from crossbar -->
|
||||||
|
<circle cx="308" cy="330" r="5" fill="url(#grad-accent)" opacity="0.6" />
|
||||||
|
<line x1="293" y1="330" x2="303" y2="330" stroke="url(#grad-accent)" stroke-width="2" stroke-linecap="round" opacity="0.5" />
|
||||||
|
|
||||||
|
<circle cx="204" cy="330" r="5" fill="url(#grad-accent)" opacity="0.6" />
|
||||||
|
<line x1="219" y1="330" x2="209" y2="330" stroke="url(#grad-accent)" stroke-width="2" stroke-linecap="round" opacity="0.5" />
|
||||||
|
|
||||||
|
<!-- Apex dot at the top of the A -->
|
||||||
|
<circle cx="256" cy="66" r="10" fill="url(#grad-primary)" opacity="0.9" />
|
||||||
|
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
65
web/public/attune-logo-watermark-tile.svg
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3000 2100" width="3000" height="2100">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad-primary" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#4F46E5" />
|
||||||
|
<stop offset="50%" stop-color="#6366F1" />
|
||||||
|
<stop offset="100%" stop-color="#818CF8" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="grad-accent" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#14B8A6" />
|
||||||
|
<stop offset="100%" stop-color="#22D3EE" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="grad-glow" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#818CF8" />
|
||||||
|
<stop offset="50%" stop-color="#6366F1" />
|
||||||
|
<stop offset="100%" stop-color="#14B8A6" />
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
|
||||||
|
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#4F46E5" flood-opacity="0.2" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- All content offset to center of tile: translate(1244, 794) -->
|
||||||
|
<g transform="translate(1244, 794)">
|
||||||
|
<!-- Background circle -->
|
||||||
|
<circle cx="256" cy="256" r="248" fill="none" stroke="url(#grad-glow)" stroke-width="3" opacity="0.3" />
|
||||||
|
|
||||||
|
<!-- Resonance arcs — left side -->
|
||||||
|
<path d="M 168 274 Q 138 330 168 386" fill="none" stroke="url(#grad-accent)" stroke-width="6" stroke-linecap="round" opacity="0.5" />
|
||||||
|
<path d="M 148 254 Q 108 330 148 406" fill="none" stroke="url(#grad-accent)" stroke-width="4.5" stroke-linecap="round" opacity="0.35" />
|
||||||
|
<path d="M 130 234 Q 80 330 130 426" fill="none" stroke="url(#grad-accent)" stroke-width="3" stroke-linecap="round" opacity="0.2" />
|
||||||
|
|
||||||
|
<!-- Resonance arcs — right side -->
|
||||||
|
<path d="M 344 274 Q 374 330 344 386" fill="none" stroke="url(#grad-accent)" stroke-width="6" stroke-linecap="round" opacity="0.5" />
|
||||||
|
<path d="M 364 254 Q 404 330 364 406" fill="none" stroke="url(#grad-accent)" stroke-width="4.5" stroke-linecap="round" opacity="0.35" />
|
||||||
|
<path d="M 382 234 Q 432 330 382 426" fill="none" stroke="url(#grad-accent)" stroke-width="3" stroke-linecap="round" opacity="0.2" />
|
||||||
|
|
||||||
|
<!-- Inverted tuning fork forming an 'A' shape -->
|
||||||
|
<g filter="url(#shadow)">
|
||||||
|
<path d="M 220 412 L 220 278 Q 220 248 250 228 Q 256 224 262 228 Q 292 248 292 278 L 292 412"
|
||||||
|
fill="none" stroke="url(#grad-primary)" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<path d="M 256 70 L 256 224"
|
||||||
|
fill="none" stroke="url(#grad-primary)" stroke-width="22" stroke-linecap="round" />
|
||||||
|
<path d="M 224 330 L 288 330"
|
||||||
|
fill="none" stroke="url(#grad-primary)" stroke-width="14" stroke-linecap="round" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Stem dots -->
|
||||||
|
<circle cx="256" cy="120" r="5" fill="#22D3EE" opacity="0.6" />
|
||||||
|
<circle cx="256" cy="150" r="6" fill="#14B8A6" opacity="0.7" />
|
||||||
|
<circle cx="256" cy="182" r="5" fill="#22D3EE" opacity="0.6" />
|
||||||
|
|
||||||
|
<!-- Crossbar event nodes -->
|
||||||
|
<circle cx="244" cy="330" r="7" fill="#14B8A6" opacity="0.8" />
|
||||||
|
<circle cx="268" cy="330" r="7" fill="#22D3EE" opacity="0.8" />
|
||||||
|
|
||||||
|
<!-- Branching dots -->
|
||||||
|
<circle cx="308" cy="330" r="5" fill="url(#grad-accent)" opacity="0.6" />
|
||||||
|
<line x1="293" y1="330" x2="303" y2="330" stroke="url(#grad-accent)" stroke-width="2" stroke-linecap="round" opacity="0.5" />
|
||||||
|
<circle cx="204" cy="330" r="5" fill="url(#grad-accent)" opacity="0.6" />
|
||||||
|
<line x1="219" y1="330" x2="209" y2="330" stroke="url(#grad-accent)" stroke-width="2" stroke-linecap="round" opacity="0.5" />
|
||||||
|
|
||||||
|
<!-- Apex dot -->
|
||||||
|
<circle cx="256" cy="66" r="10" fill="url(#grad-primary)" opacity="0.9" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.6 KiB |
139
web/public/attune-logo.svg
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
||||||
|
<defs>
|
||||||
|
<!-- Primary gradient: deep indigo to vibrant blue -->
|
||||||
|
<linearGradient id="grad-primary" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#4F46E5" />
|
||||||
|
<stop offset="50%" stop-color="#6366F1" />
|
||||||
|
<stop offset="100%" stop-color="#818CF8" />
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<!-- Accent gradient: teal to cyan for flow lines -->
|
||||||
|
<linearGradient id="grad-accent" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#14B8A6" />
|
||||||
|
<stop offset="100%" stop-color="#22D3EE" />
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<!-- Glow gradient for the resonance ring -->
|
||||||
|
<linearGradient id="grad-glow" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#818CF8" />
|
||||||
|
<stop offset="50%" stop-color="#6366F1" />
|
||||||
|
<stop offset="100%" stop-color="#14B8A6" />
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<!-- Subtle shadow for depth -->
|
||||||
|
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
|
||||||
|
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-color="#4F46E5" flood-opacity="0.2" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Background circle -->
|
||||||
|
<circle cx="256" cy="256" r="248" fill="none" stroke="url(#grad-glow)" stroke-width="3" opacity="0.3" />
|
||||||
|
|
||||||
|
<!-- Resonance arcs — left side (centered on crossbar y=330) -->
|
||||||
|
<g transform="translate(0,0)">
|
||||||
|
<animateTransform attributeName="transform" type="translate" values="0 0; -4 0; 0 0" dur="3s" repeatCount="indefinite" />
|
||||||
|
<path d="M 168 274 Q 138 330 168 386"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="6" stroke-linecap="round" opacity="0.5">
|
||||||
|
<animate attributeName="opacity" values="0.3;0.6;0.3" dur="3s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="stroke-width" values="4;8;4" dur="3s" repeatCount="indefinite" />
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(0,0)">
|
||||||
|
<animateTransform attributeName="transform" type="translate" values="0 0; -5 0; 0 0" dur="3s" begin="0.3s" repeatCount="indefinite" />
|
||||||
|
<path d="M 148 254 Q 108 330 148 406"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="4.5" stroke-linecap="round" opacity="0.35">
|
||||||
|
<animate attributeName="opacity" values="0.2;0.45;0.2" dur="3s" begin="0.3s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="stroke-width" values="3;6;3" dur="3s" begin="0.3s" repeatCount="indefinite" />
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(0,0)">
|
||||||
|
<animateTransform attributeName="transform" type="translate" values="0 0; -6 0; 0 0" dur="3s" begin="0.6s" repeatCount="indefinite" />
|
||||||
|
<path d="M 130 234 Q 80 330 130 426"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="3" stroke-linecap="round" opacity="0.2">
|
||||||
|
<animate attributeName="opacity" values="0.1;0.3;0.1" dur="3s" begin="0.6s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="stroke-width" values="2;4;2" dur="3s" begin="0.6s" repeatCount="indefinite" />
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Resonance arcs — right side (centered on crossbar y=330) -->
|
||||||
|
<g transform="translate(0,0)">
|
||||||
|
<animateTransform attributeName="transform" type="translate" values="0 0; 4 0; 0 0" dur="3s" repeatCount="indefinite" />
|
||||||
|
<path d="M 344 274 Q 374 330 344 386"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="6" stroke-linecap="round" opacity="0.5">
|
||||||
|
<animate attributeName="opacity" values="0.3;0.6;0.3" dur="3s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="stroke-width" values="4;8;4" dur="3s" repeatCount="indefinite" />
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(0,0)">
|
||||||
|
<animateTransform attributeName="transform" type="translate" values="0 0; 5 0; 0 0" dur="3s" begin="0.3s" repeatCount="indefinite" />
|
||||||
|
<path d="M 364 254 Q 404 330 364 406"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="4.5" stroke-linecap="round" opacity="0.35">
|
||||||
|
<animate attributeName="opacity" values="0.2;0.45;0.2" dur="3s" begin="0.3s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="stroke-width" values="3;6;3" dur="3s" begin="0.3s" repeatCount="indefinite" />
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(0,0)">
|
||||||
|
<animateTransform attributeName="transform" type="translate" values="0 0; 6 0; 0 0" dur="3s" begin="0.6s" repeatCount="indefinite" />
|
||||||
|
<path d="M 382 234 Q 432 330 382 426"
|
||||||
|
fill="none" stroke="url(#grad-accent)" stroke-width="3" stroke-linecap="round" opacity="0.2">
|
||||||
|
<animate attributeName="opacity" values="0.1;0.3;0.1" dur="3s" begin="0.6s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="stroke-width" values="2;4;2" dur="3s" begin="0.6s" repeatCount="indefinite" />
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Inverted tuning fork forming an 'A' shape -->
|
||||||
|
<g filter="url(#shadow)">
|
||||||
|
<!-- Single U-shaped resonator: left foot → up left leg → curve through apex → down right leg → right foot -->
|
||||||
|
<path d="M 220 412 L 220 278 Q 220 248 250 228 Q 256 224 262 228 Q 292 248 292 278 L 292 412"
|
||||||
|
fill="none" stroke="url(#grad-primary)" stroke-width="22" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
|
||||||
|
<!-- Handle / stem above the apex -->
|
||||||
|
<path d="M 256 70 L 256 224"
|
||||||
|
fill="none" stroke="url(#grad-primary)" stroke-width="22" stroke-linecap="round" />
|
||||||
|
|
||||||
|
<!-- Crossbar of the A -->
|
||||||
|
<path d="M 224 330 L 288 330"
|
||||||
|
fill="none" stroke="url(#grad-primary)" stroke-width="14" stroke-linecap="round" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Stem dots (orchestration flow along the handle) -->
|
||||||
|
<circle cx="256" cy="120" r="5" fill="#22D3EE" opacity="0.6">
|
||||||
|
<animate attributeName="opacity" values="0.3;0.7;0.3" dur="2.5s" begin="0.2s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="r" values="3;6;3" dur="2.5s" begin="0.2s" repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
<circle cx="256" cy="150" r="6" fill="#14B8A6" opacity="0.7">
|
||||||
|
<animate attributeName="opacity" values="0.4;0.8;0.4" dur="2.5s" begin="0.6s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="r" values="4;7;4" dur="2.5s" begin="0.6s" repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
<circle cx="256" cy="182" r="5" fill="#22D3EE" opacity="0.6">
|
||||||
|
<animate attributeName="opacity" values="0.3;0.7;0.3" dur="2.5s" begin="1.0s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="r" values="3;6;3" dur="2.5s" begin="1.0s" repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
|
||||||
|
<!-- Node dots on the crossbar representing events/connections -->
|
||||||
|
<circle cx="244" cy="330" r="7" fill="#14B8A6" opacity="0.8">
|
||||||
|
<animate attributeName="r" values="5;8;5" dur="2s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="opacity" values="0.6;1;0.6" dur="2s" repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
<circle cx="268" cy="330" r="7" fill="#22D3EE" opacity="0.8">
|
||||||
|
<animate attributeName="r" values="5;8;5" dur="2s" begin="0.5s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="opacity" values="0.6;1;0.6" dur="2s" begin="0.5s" repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
|
||||||
|
<!-- Small orchestration flow dots branching from crossbar -->
|
||||||
|
<circle cx="308" cy="330" r="5" fill="url(#grad-accent)" opacity="0.6">
|
||||||
|
<animate attributeName="opacity" values="0.3;0.7;0.3" dur="2.5s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="r" values="3;6;3" dur="2.5s" repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
<line x1="293" y1="330" x2="303" y2="330" stroke="url(#grad-accent)" stroke-width="2" stroke-linecap="round" opacity="0.5" />
|
||||||
|
|
||||||
|
<circle cx="204" cy="330" r="5" fill="url(#grad-accent)" opacity="0.6">
|
||||||
|
<animate attributeName="opacity" values="0.3;0.7;0.3" dur="2.5s" begin="0.5s" repeatCount="indefinite" />
|
||||||
|
<animate attributeName="r" values="3;6;3" dur="2.5s" begin="0.5s" repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
<line x1="219" y1="330" x2="209" y2="330" stroke="url(#grad-accent)" stroke-width="2" stroke-linecap="round" opacity="0.5" />
|
||||||
|
|
||||||
|
<!-- Apex dot at the top of the A -->
|
||||||
|
<circle cx="256" cy="66" r="10" fill="url(#grad-primary)" opacity="0.9" />
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 7.8 KiB |
@@ -18,29 +18,53 @@ import {
|
|||||||
Home,
|
Home,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
export default function MainLayout() {
|
// Color mappings for navigation items — defined outside component for stable reference
|
||||||
const { user, logout } = useAuth();
|
const colorClasses = {
|
||||||
const navigate = useNavigate();
|
gray: {
|
||||||
const location = useLocation();
|
inactive: "text-gray-300 hover:text-white hover:bg-gray-800",
|
||||||
const [isCollapsed, setIsCollapsed] = useState(() => {
|
active: "bg-gray-800 text-white",
|
||||||
// Initialize from localStorage
|
icon: "text-gray-400",
|
||||||
const saved = localStorage.getItem("sidebar-collapsed");
|
},
|
||||||
return saved === "true";
|
cyan: {
|
||||||
});
|
inactive: "text-cyan-300 hover:text-cyan-100 hover:bg-cyan-950/30",
|
||||||
const [showUserMenu, setShowUserMenu] = useState(false);
|
active: "bg-cyan-950/50 text-cyan-100 shadow-lg shadow-cyan-900/50",
|
||||||
|
icon: "text-cyan-400",
|
||||||
|
},
|
||||||
|
blue: {
|
||||||
|
inactive: "text-blue-300 hover:text-blue-100 hover:bg-blue-950/30",
|
||||||
|
active: "bg-blue-950/50 text-blue-100 shadow-lg shadow-blue-900/50",
|
||||||
|
icon: "text-blue-400",
|
||||||
|
},
|
||||||
|
violet: {
|
||||||
|
inactive: "text-violet-300 hover:text-violet-100 hover:bg-violet-950/30",
|
||||||
|
active: "bg-violet-950/50 text-violet-100 shadow-lg shadow-violet-900/50",
|
||||||
|
icon: "text-violet-400",
|
||||||
|
},
|
||||||
|
purple: {
|
||||||
|
inactive: "text-purple-300 hover:text-purple-100 hover:bg-purple-950/30",
|
||||||
|
active: "bg-purple-950/50 text-purple-100 shadow-lg shadow-purple-900/50",
|
||||||
|
icon: "text-purple-400",
|
||||||
|
},
|
||||||
|
fuchsia: {
|
||||||
|
inactive: "text-fuchsia-300 hover:text-fuchsia-100 hover:bg-fuchsia-950/30",
|
||||||
|
active:
|
||||||
|
"bg-fuchsia-950/50 text-fuchsia-100 shadow-lg shadow-fuchsia-900/50",
|
||||||
|
icon: "text-fuchsia-400",
|
||||||
|
},
|
||||||
|
rose: {
|
||||||
|
inactive: "text-rose-300 hover:text-rose-100 hover:bg-rose-950/30",
|
||||||
|
active: "bg-rose-950/50 text-rose-100 shadow-lg shadow-rose-900/50",
|
||||||
|
icon: "text-rose-400",
|
||||||
|
},
|
||||||
|
orange: {
|
||||||
|
inactive: "text-orange-300 hover:text-orange-100 hover:bg-orange-950/30",
|
||||||
|
active: "bg-orange-950/50 text-orange-100 shadow-lg shadow-orange-900/50",
|
||||||
|
icon: "text-orange-400",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// Persist collapsed state to localStorage
|
// Navigation sections with dividers and colors
|
||||||
useEffect(() => {
|
const navSections = [
|
||||||
localStorage.setItem("sidebar-collapsed", isCollapsed.toString());
|
|
||||||
}, [isCollapsed]);
|
|
||||||
|
|
||||||
const handleLogout = () => {
|
|
||||||
logout();
|
|
||||||
navigate("/login");
|
|
||||||
};
|
|
||||||
|
|
||||||
// Navigation sections with dividers and colors
|
|
||||||
const navSections = [
|
|
||||||
{
|
{
|
||||||
items: [{ to: "/", label: "Dashboard", icon: Home, color: "gray" }],
|
items: [{ to: "/", label: "Dashboard", icon: Home, color: "gray" }],
|
||||||
},
|
},
|
||||||
@@ -97,121 +121,122 @@ export default function MainLayout() {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Color mappings for navigation items
|
// NavLink extracted outside MainLayout so React preserves DOM identity across
|
||||||
const colorClasses = {
|
// re-renders, which is required for CSS transitions to work on collapse/expand.
|
||||||
gray: {
|
function NavLink({
|
||||||
inactive: "text-gray-300 hover:text-white hover:bg-gray-800",
|
|
||||||
active: "bg-gray-800 text-white",
|
|
||||||
icon: "text-gray-400",
|
|
||||||
},
|
|
||||||
cyan: {
|
|
||||||
inactive: "text-cyan-300 hover:text-cyan-100 hover:bg-cyan-950/30",
|
|
||||||
active: "bg-cyan-950/50 text-cyan-100 shadow-lg shadow-cyan-900/50",
|
|
||||||
icon: "text-cyan-400",
|
|
||||||
},
|
|
||||||
blue: {
|
|
||||||
inactive: "text-blue-300 hover:text-blue-100 hover:bg-blue-950/30",
|
|
||||||
active: "bg-blue-950/50 text-blue-100 shadow-lg shadow-blue-900/50",
|
|
||||||
icon: "text-blue-400",
|
|
||||||
},
|
|
||||||
violet: {
|
|
||||||
inactive: "text-violet-300 hover:text-violet-100 hover:bg-violet-950/30",
|
|
||||||
active: "bg-violet-950/50 text-violet-100 shadow-lg shadow-violet-900/50",
|
|
||||||
icon: "text-violet-400",
|
|
||||||
},
|
|
||||||
purple: {
|
|
||||||
inactive: "text-purple-300 hover:text-purple-100 hover:bg-purple-950/30",
|
|
||||||
active: "bg-purple-950/50 text-purple-100 shadow-lg shadow-purple-900/50",
|
|
||||||
icon: "text-purple-400",
|
|
||||||
},
|
|
||||||
fuchsia: {
|
|
||||||
inactive:
|
|
||||||
"text-fuchsia-300 hover:text-fuchsia-100 hover:bg-fuchsia-950/30",
|
|
||||||
active:
|
|
||||||
"bg-fuchsia-950/50 text-fuchsia-100 shadow-lg shadow-fuchsia-900/50",
|
|
||||||
icon: "text-fuchsia-400",
|
|
||||||
},
|
|
||||||
rose: {
|
|
||||||
inactive: "text-rose-300 hover:text-rose-100 hover:bg-rose-950/30",
|
|
||||||
active: "bg-rose-950/50 text-rose-100 shadow-lg shadow-rose-900/50",
|
|
||||||
icon: "text-rose-400",
|
|
||||||
},
|
|
||||||
orange: {
|
|
||||||
inactive: "text-orange-300 hover:text-orange-100 hover:bg-orange-950/30",
|
|
||||||
active: "bg-orange-950/50 text-orange-100 shadow-lg shadow-orange-900/50",
|
|
||||||
icon: "text-orange-400",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const NavLink = ({
|
|
||||||
to,
|
to,
|
||||||
label,
|
label,
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
color = "gray",
|
color = "gray",
|
||||||
}: {
|
isCollapsed,
|
||||||
|
isActive,
|
||||||
|
}: {
|
||||||
to: string;
|
to: string;
|
||||||
label: string;
|
label: string;
|
||||||
icon: React.ElementType;
|
icon: React.ElementType;
|
||||||
color?: string;
|
color?: string;
|
||||||
}) => {
|
isCollapsed: boolean;
|
||||||
const isActive =
|
isActive: boolean;
|
||||||
location.pathname === to ||
|
}) {
|
||||||
(to !== "/" && location.pathname.startsWith(to));
|
|
||||||
|
|
||||||
const colors =
|
const colors =
|
||||||
colorClasses[color as keyof typeof colorClasses] || colorClasses.gray;
|
colorClasses[color as keyof typeof colorClasses] || colorClasses.gray;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to={to}
|
to={to}
|
||||||
className={`flex items-center gap-3 px-4 py-2 rounded-md transition-all duration-200 ${
|
className={`flex items-center px-4 py-2 rounded-md transition-colors duration-200 whitespace-nowrap ${
|
||||||
isActive ? colors.active : colors.inactive
|
isActive ? colors.active : colors.inactive
|
||||||
} ${isCollapsed ? "justify-center" : ""}`}
|
}`}
|
||||||
title={isCollapsed ? label : undefined}
|
title={isCollapsed ? label : undefined}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
className={`w-5 h-5 flex-shrink-0 ${isActive ? "" : colors.icon}`}
|
className={`w-5 h-5 flex-shrink-0 ${isActive ? "" : colors.icon}`}
|
||||||
/>
|
/>
|
||||||
{!isCollapsed && <span>{label}</span>}
|
<span
|
||||||
|
className="ml-3 inline-block overflow-hidden transition-all duration-300"
|
||||||
|
style={{ maxWidth: isCollapsed ? 0 : "10rem" }}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MainLayout() {
|
||||||
|
const { user, logout } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const [isCollapsed, setIsCollapsed] = useState(() => {
|
||||||
|
// Initialize from localStorage
|
||||||
|
const saved = localStorage.getItem("sidebar-collapsed");
|
||||||
|
return saved === "true";
|
||||||
|
});
|
||||||
|
const [showUserMenu, setShowUserMenu] = useState(false);
|
||||||
|
|
||||||
|
// Persist collapsed state to localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem("sidebar-collapsed", isCollapsed.toString());
|
||||||
|
}, [isCollapsed]);
|
||||||
|
|
||||||
|
// Close user menu when expanding sidebar
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isCollapsed) {
|
||||||
|
setShowUserMenu(false);
|
||||||
|
}
|
||||||
|
}, [isCollapsed]);
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
logout();
|
||||||
|
navigate("/login");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen bg-gray-100 flex overflow-hidden">
|
<div className="h-full bg-gray-100 flex overflow-hidden">
|
||||||
|
{/* Sidebar */}
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
isCollapsed ? "w-20" : "w-64"
|
isCollapsed ? "w-20" : "w-64"
|
||||||
} bg-gray-900 text-white flex flex-col transition-all duration-300 relative flex-shrink-0`}
|
} bg-gray-900 text-white flex flex-col transition-all duration-300 relative flex-shrink-0 overflow-hidden`}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-center h-16 bg-gray-800">
|
<div className="flex items-center justify-center h-20 bg-gray-800 whitespace-nowrap">
|
||||||
<Link
|
<Link to="/" className="flex items-center justify-center">
|
||||||
to="/"
|
<img
|
||||||
className={`font-bold transition-all ${
|
src="/attune-logo-icon.svg"
|
||||||
isCollapsed ? "text-lg" : "text-xl"
|
alt="Attune"
|
||||||
}`}
|
className={`h-14 transition-opacity duration-300 ${isCollapsed ? "opacity-100" : "opacity-0 w-0"}`}
|
||||||
>
|
/>
|
||||||
{isCollapsed ? "A" : "Attune"}
|
<img
|
||||||
|
src="/attune-logo-navbar.svg"
|
||||||
|
alt="Attune"
|
||||||
|
className={`h-14 transition-opacity duration-300 ${isCollapsed ? "opacity-0 w-0" : "opacity-100"}`}
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
<nav className="flex-1 px-4 py-6 overflow-y-auto">
|
<nav className="flex-1 px-4 py-6 overflow-y-auto overflow-x-hidden">
|
||||||
{navSections.map((section, sectionIndex) => (
|
{navSections.map((section, sectionIndex) => (
|
||||||
<div key={sectionIndex}>
|
<div key={sectionIndex}>
|
||||||
<div className="space-y-1 mb-3">
|
<div className="space-y-1 mb-3">
|
||||||
{section.items.map((item) => (
|
{section.items.map((item) => {
|
||||||
|
const isActive =
|
||||||
|
location.pathname === item.to ||
|
||||||
|
(item.to !== "/" && location.pathname.startsWith(item.to));
|
||||||
|
return (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={item.to}
|
key={item.to}
|
||||||
to={item.to}
|
to={item.to}
|
||||||
label={item.label}
|
label={item.label}
|
||||||
icon={item.icon}
|
icon={item.icon}
|
||||||
color={item.color}
|
color={item.color}
|
||||||
|
isCollapsed={isCollapsed}
|
||||||
|
isActive={isActive}
|
||||||
/>
|
/>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
{sectionIndex < navSections.length - 1 && (
|
{sectionIndex < navSections.length - 1 && (
|
||||||
<div className="my-3 mx-2 border-t border-gray-700" />
|
<div className="my-3 mx-2 border-t border-gray-700" />
|
||||||
@@ -221,39 +246,68 @@ export default function MainLayout() {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Toggle Button */}
|
{/* Toggle Button */}
|
||||||
<div
|
<div className="px-4 py-3">
|
||||||
className={`px-4 py-3 ${isCollapsed ? "flex justify-center" : ""}`}
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||||
className="flex items-center gap-2 w-full px-3 py-2 text-gray-400 hover:text-white hover:bg-gray-800 rounded-md transition-colors"
|
className="flex items-center w-full px-3 py-2 text-gray-400 hover:text-white hover:bg-gray-800 rounded-md transition-colors whitespace-nowrap"
|
||||||
title={isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
title={isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
||||||
>
|
>
|
||||||
{isCollapsed ? (
|
<div className="w-5 h-5 flex-shrink-0 relative">
|
||||||
<ChevronRight className="w-5 h-5" />
|
<ChevronLeft
|
||||||
) : (
|
className={`w-5 h-5 absolute inset-0 transition-opacity duration-300 ${
|
||||||
<>
|
isCollapsed ? "opacity-0" : "opacity-100"
|
||||||
<ChevronLeft className="w-5 h-5" />
|
}`}
|
||||||
<span className="text-sm">Collapse</span>
|
/>
|
||||||
</>
|
<ChevronRight
|
||||||
)}
|
className={`w-5 h-5 absolute inset-0 transition-opacity duration-300 ${
|
||||||
|
isCollapsed ? "opacity-100" : "opacity-0"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className="ml-2 inline-block overflow-hidden text-sm transition-all duration-300"
|
||||||
|
style={{ maxWidth: isCollapsed ? 0 : "10rem" }}
|
||||||
|
>
|
||||||
|
Collapse
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* User Section */}
|
{/* User Section */}
|
||||||
<div className="p-4 bg-gray-800 border-t border-gray-700">
|
<div className="p-4 bg-gray-800 border-t border-gray-700 overflow-hidden whitespace-nowrap">
|
||||||
{isCollapsed ? (
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center min-w-0">
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowUserMenu(!showUserMenu)}
|
onClick={() => isCollapsed && setShowUserMenu(!showUserMenu)}
|
||||||
className="w-full flex items-center justify-center p-2 rounded-md hover:bg-gray-700 transition-colors"
|
className={`flex-shrink-0 ${isCollapsed ? "cursor-pointer" : "cursor-default"}`}
|
||||||
title={user?.login}
|
title={user?.login}
|
||||||
>
|
>
|
||||||
<User className="w-6 h-6 text-gray-400" />
|
<User className="w-5 h-5 text-gray-400" />
|
||||||
</button>
|
</button>
|
||||||
|
<span
|
||||||
|
className="ml-2 inline-block overflow-hidden transition-all duration-300 min-w-0"
|
||||||
|
style={{ maxWidth: isCollapsed ? 0 : "8rem" }}
|
||||||
|
>
|
||||||
|
<p className="font-medium text-sm truncate">{user?.login}</p>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className="ml-2 inline-block overflow-hidden transition-all duration-300"
|
||||||
|
style={{ maxWidth: isCollapsed ? 0 : "2rem" }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="text-gray-400 hover:text-white p-1 flex-shrink-0"
|
||||||
|
title="Logout"
|
||||||
|
>
|
||||||
|
<LogOut className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* User Menu Popup */}
|
{/* User Menu Popup (collapsed mode only) */}
|
||||||
{showUserMenu && (
|
{isCollapsed && showUserMenu && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-10"
|
className="fixed inset-0 z-10"
|
||||||
@@ -276,23 +330,10 @@ export default function MainLayout() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
|
||||||
<User className="w-5 h-5 text-gray-400 flex-shrink-0" />
|
|
||||||
<p className="font-medium text-sm truncate">{user?.login}</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={handleLogout}
|
|
||||||
className="text-gray-400 hover:text-white p-1 flex-shrink-0"
|
|
||||||
title="Logout"
|
|
||||||
>
|
|
||||||
<LogOut className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -200,8 +200,8 @@ function TaskNodeInner({
|
|||||||
const handleMouseMove = (moveEvent: MouseEvent) => {
|
const handleMouseMove = (moveEvent: MouseEvent) => {
|
||||||
const cur = screenToCanvas(moveEvent.clientX, moveEvent.clientY);
|
const cur = screenToCanvas(moveEvent.clientX, moveEvent.clientY);
|
||||||
onPositionChange(task.id, {
|
onPositionChange(task.id, {
|
||||||
x: Math.max(0, cur.x - dragOffset.current.x),
|
x: cur.x - dragOffset.current.x,
|
||||||
y: Math.max(0, cur.y - dragOffset.current.y),
|
y: cur.y - dragOffset.current.y,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,25 @@ function gridBackground(pan: { x: number; y: number }, zoom: number) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a brick-lay tiled watermark using two CSS background layers.
|
||||||
|
* Both layers repeat the same logo at the tile period, but the second
|
||||||
|
* layer is offset by half the period in both axes for a staggered look.
|
||||||
|
* Using background-size equal to the tile period causes the SVG to scale
|
||||||
|
* to fill the tile — the logo's own viewBox whitespace provides the
|
||||||
|
* visual padding around the mark.
|
||||||
|
*/
|
||||||
|
function watermarkBackground(pan: { x: number; y: number }, zoom: number) {
|
||||||
|
const tileW = 1000 * zoom;
|
||||||
|
const tileH = 700 * zoom;
|
||||||
|
const logo = `url("/attune-logo-watermark-tile.svg")`;
|
||||||
|
return {
|
||||||
|
backgroundImage: `${logo}, ${logo}`,
|
||||||
|
backgroundSize: `${tileW}px ${tileH}px, ${tileW}px ${tileH}px`,
|
||||||
|
backgroundPosition: `${pan.x}px ${pan.y}px, ${pan.x + tileW / 2}px ${pan.y + tileH / 2}px`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export type ScreenToCanvas = (
|
export type ScreenToCanvas = (
|
||||||
clientX: number,
|
clientX: number,
|
||||||
clientY: number,
|
clientY: number,
|
||||||
@@ -79,6 +98,7 @@ export default function WorkflowCanvas({
|
|||||||
}: WorkflowCanvasProps) {
|
}: WorkflowCanvasProps) {
|
||||||
const canvasRef = useRef<HTMLDivElement>(null);
|
const canvasRef = useRef<HTMLDivElement>(null);
|
||||||
const innerRef = useRef<HTMLDivElement>(null);
|
const innerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const watermarkRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// ---- Camera state ----
|
// ---- Camera state ----
|
||||||
// We keep refs for high-frequency updates (panning/zooming) and sync to
|
// We keep refs for high-frequency updates (panning/zooming) and sync to
|
||||||
@@ -141,6 +161,12 @@ export default function WorkflowCanvas({
|
|||||||
canvasRef.current.style.backgroundSize = bg.backgroundSize;
|
canvasRef.current.style.backgroundSize = bg.backgroundSize;
|
||||||
canvasRef.current.style.backgroundPosition = bg.backgroundPosition;
|
canvasRef.current.style.backgroundPosition = bg.backgroundPosition;
|
||||||
}
|
}
|
||||||
|
if (watermarkRef.current) {
|
||||||
|
const wm = watermarkBackground(panRef.current, zoomRef.current);
|
||||||
|
watermarkRef.current.style.backgroundImage = wm.backgroundImage;
|
||||||
|
watermarkRef.current.style.backgroundSize = wm.backgroundSize;
|
||||||
|
watermarkRef.current.style.backgroundPosition = wm.backgroundPosition;
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ---- Canvas click (deselect / cancel connection) ----
|
// ---- Canvas click (deselect / cancel connection) ----
|
||||||
@@ -461,17 +487,22 @@ export default function WorkflowCanvas({
|
|||||||
|
|
||||||
// ---- Inner div dimensions (large enough to contain all content) ----
|
// ---- Inner div dimensions (large enough to contain all content) ----
|
||||||
const innerSize = useMemo(() => {
|
const innerSize = useMemo(() => {
|
||||||
|
let minX = 0;
|
||||||
|
let minY = 0;
|
||||||
let maxX = 4000;
|
let maxX = 4000;
|
||||||
let maxY = 4000;
|
let maxY = 4000;
|
||||||
for (const task of tasks) {
|
for (const task of tasks) {
|
||||||
|
minX = Math.min(minX, task.position.x - 100);
|
||||||
|
minY = Math.min(minY, task.position.y - 100);
|
||||||
maxX = Math.max(maxX, task.position.x + 500);
|
maxX = Math.max(maxX, task.position.x + 500);
|
||||||
maxY = Math.max(maxY, task.position.y + 500);
|
maxY = Math.max(maxY, task.position.y + 500);
|
||||||
}
|
}
|
||||||
return { width: maxX, height: maxY };
|
return { width: maxX - minX, height: maxY - minY };
|
||||||
}, [tasks]);
|
}, [tasks]);
|
||||||
|
|
||||||
// ---- Grid background (recomputed from React state for the render) ----
|
// ---- Grid background (recomputed from React state for the render) ----
|
||||||
const gridBg = useMemo(() => gridBackground(pan, zoom), [pan, zoom]);
|
const gridBg = useMemo(() => gridBackground(pan, zoom), [pan, zoom]);
|
||||||
|
const wmBg = useMemo(() => watermarkBackground(pan, zoom), [pan, zoom]);
|
||||||
|
|
||||||
// Zoom percentage for display
|
// Zoom percentage for display
|
||||||
const zoomPercent = Math.round(zoom * 100);
|
const zoomPercent = Math.round(zoom * 100);
|
||||||
@@ -491,6 +522,17 @@ export default function WorkflowCanvas({
|
|||||||
onMouseUp={handleCanvasMouseUp}
|
onMouseUp={handleCanvasMouseUp}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
>
|
>
|
||||||
|
{/* Tiled watermark layer — moves with grid, transparent */}
|
||||||
|
<div
|
||||||
|
ref={watermarkRef}
|
||||||
|
className="absolute inset-0 pointer-events-none opacity-[0.15]"
|
||||||
|
style={{
|
||||||
|
backgroundImage: wmBg.backgroundImage,
|
||||||
|
backgroundSize: wmBg.backgroundSize,
|
||||||
|
backgroundPosition: wmBg.backgroundPosition,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Transformed canvas content */}
|
{/* Transformed canvas content */}
|
||||||
<div
|
<div
|
||||||
ref={innerRef}
|
ref={innerRef}
|
||||||
|
|||||||
@@ -525,15 +525,19 @@ function WorkflowEdgesInner({
|
|||||||
|
|
||||||
const svgBounds = useMemo(() => {
|
const svgBounds = useMemo(() => {
|
||||||
if (tasks.length === 0) return { width: 2000, height: 2000 };
|
if (tasks.length === 0) return { width: 2000, height: 2000 };
|
||||||
|
let minX = 0;
|
||||||
|
let minY = 0;
|
||||||
let maxX = 0;
|
let maxX = 0;
|
||||||
let maxY = 0;
|
let maxY = 0;
|
||||||
for (const task of tasks) {
|
for (const task of tasks) {
|
||||||
|
minX = Math.min(minX, task.position.x - 100);
|
||||||
|
minY = Math.min(minY, task.position.y - 100);
|
||||||
maxX = Math.max(maxX, task.position.x + nodeWidth + 100);
|
maxX = Math.max(maxX, task.position.x + nodeWidth + 100);
|
||||||
maxY = Math.max(maxY, task.position.y + nodeHeight + 100);
|
maxY = Math.max(maxY, task.position.y + nodeHeight + 100);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
width: Math.max(maxX, 2000),
|
width: Math.max(maxX - minX, 2000),
|
||||||
height: Math.max(maxY, 2000),
|
height: Math.max(maxY - minY, 2000),
|
||||||
};
|
};
|
||||||
}, [tasks, nodeWidth, nodeHeight]);
|
}, [tasks, nodeWidth, nodeHeight]);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,14 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
@apply h-full;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes flash-highlight {
|
@keyframes flash-highlight {
|
||||||
0% {
|
0% {
|
||||||
background-color: rgb(191 219 254); /* blue-200 */
|
background-color: rgb(191 219 254); /* blue-200 */
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export default function ActionsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-[calc(100vh-4rem)]">
|
<div className="flex h-full">
|
||||||
{/* Left sidebar - Actions List */}
|
{/* Left sidebar - Actions List */}
|
||||||
<div className="w-96 border-r border-gray-200 overflow-y-auto bg-gray-50">
|
<div className="w-96 border-r border-gray-200 overflow-y-auto bg-gray-50">
|
||||||
<div className="p-4 border-b border-gray-200 bg-white sticky top-0 z-10">
|
<div className="p-4 border-b border-gray-200 bg-white sticky top-0 z-10">
|
||||||
|
|||||||
@@ -608,14 +608,14 @@ export default function WorkflowBuilderPage() {
|
|||||||
|
|
||||||
if (isEditing && workflowLoading) {
|
if (isEditing && workflowLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-screen">
|
<div className="flex items-center justify-center h-full">
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600" />
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-[calc(100vh-4rem)] flex flex-col overflow-hidden">
|
<div className="h-full flex flex-col overflow-hidden">
|
||||||
{/* Top toolbar */}
|
{/* Top toolbar */}
|
||||||
<div className="flex-shrink-0 bg-white border-b border-gray-200 px-4 py-2.5">
|
<div className="flex-shrink-0 bg-white border-b border-gray-200 px-4 py-2.5">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|||||||
@@ -534,7 +534,7 @@ export default function ExecutionsPage() {
|
|||||||
}, [filteredExecutions]);
|
}, [filteredExecutions]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-[calc(100vh-4rem)]">
|
<div className="flex h-full">
|
||||||
{/* Main content area */}
|
{/* Main content area */}
|
||||||
<div
|
<div
|
||||||
className={`flex-1 min-w-0 overflow-y-auto p-6 ${selectedExecutionId ? "mr-0" : ""}`}
|
className={`flex-1 min-w-0 overflow-y-auto p-6 ${selectedExecutionId ? "mr-0" : ""}`}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export default function PacksPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-[calc(100vh-4rem)]">
|
<div className="flex h-full">
|
||||||
{/* Left sidebar - Packs List */}
|
{/* Left sidebar - Packs List */}
|
||||||
<div className="w-96 border-r border-gray-200 overflow-y-auto bg-gray-50">
|
<div className="w-96 border-r border-gray-200 overflow-y-auto bg-gray-50">
|
||||||
<div className="p-4 border-b border-gray-200 bg-white sticky top-0 z-10">
|
<div className="p-4 border-b border-gray-200 bg-white sticky top-0 z-10">
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export default function RulesPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-[calc(100vh-4rem)]">
|
<div className="flex h-full">
|
||||||
{/* Left sidebar - Rules List */}
|
{/* Left sidebar - Rules List */}
|
||||||
<div className="w-96 border-r border-gray-200 overflow-y-auto bg-gray-50">
|
<div className="w-96 border-r border-gray-200 overflow-y-auto bg-gray-50">
|
||||||
<div className="p-4 border-b border-gray-200 bg-white sticky top-0 z-10">
|
<div className="p-4 border-b border-gray-200 bg-white sticky top-0 z-10">
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export default function SensorsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-[calc(100vh-4rem)]">
|
<div className="flex h-full">
|
||||||
{/* Left sidebar - Sensors List */}
|
{/* Left sidebar - Sensors List */}
|
||||||
<div className="w-96 border-r border-gray-200 overflow-y-auto bg-gray-50">
|
<div className="w-96 border-r border-gray-200 overflow-y-auto bg-gray-50">
|
||||||
<div className="p-4 border-b border-gray-200 bg-white sticky top-0 z-10">
|
<div className="p-4 border-b border-gray-200 bg-white sticky top-0 z-10">
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export default function TriggersPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-[calc(100vh-4rem)]">
|
<div className="flex h-full">
|
||||||
{/* Left sidebar - Triggers List */}
|
{/* Left sidebar - Triggers List */}
|
||||||
<div className="w-96 border-r border-gray-200 overflow-y-auto bg-gray-50">
|
<div className="w-96 border-r border-gray-200 overflow-y-auto bg-gray-50">
|
||||||
<div className="p-4 border-b border-gray-200 bg-white sticky top-0 z-10">
|
<div className="p-4 border-b border-gray-200 bg-white sticky top-0 z-10">
|
||||||
|
|||||||