diff --git a/package-lock.json b/package-lock.json index 74e088b..e6c6691 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,6 +59,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1527,6 +1528,7 @@ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -1616,6 +1618,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2300,6 +2303,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -2312,6 +2316,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -2544,6 +2549,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 28a6279..5aa9780 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { Link, Outlet } from "react-router-dom"; import ConnectWalletModal from "./ConnectWalletModal"; import Footer from "./Footer"; -import "./layout.css"; +import "./Layout.css"; type NavItem = { to: string; label: string; shortLabel: string }; diff --git a/src/components/TreasuryOnboarding.css b/src/components/TreasuryOnboarding.css new file mode 100644 index 0000000..1945d16 --- /dev/null +++ b/src/components/TreasuryOnboarding.css @@ -0,0 +1,522 @@ +/* ── Treasury Onboarding ─────────────────────────────────────────────────── */ + +.treasury-onboarding { + display: flex; + flex-direction: column; + align-items: center; + gap: 1.5rem; + padding: 2rem 1rem 3rem; + max-width: 640px; + margin: 0 auto; + width: 100%; +} + +/* ── Stepper ─────────────────────────────────────────────────────────────── */ + +.onboarding-stepper { + display: flex; + align-items: flex-start; + gap: 0; + width: 100%; + max-width: 360px; + justify-content: center; +} + +.onboarding-step-dot-wrap { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.375rem; + position: relative; + flex: 1; +} + +/* connector line between dots */ +.onboarding-connector { + position: absolute; + top: 14px; + left: calc(50% + 14px); + right: calc(-50% + 14px); + height: 2px; + background: var(--border, #1e2d42); + transition: background 0.3s; + z-index: 0; +} + +.onboarding-connector.done { + background: var(--accent, #00d4aa); +} + +.onboarding-step-dot { + width: 28px; + height: 28px; + border-radius: 50%; + border: 2px solid var(--border, #1e2d42); + background: var(--surface, #121a2a); + color: var(--muted, #6b7a94); + font-size: 0.75rem; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; + cursor: default; + transition: all 0.2s; + position: relative; + z-index: 1; + outline-offset: 3px; +} + +.onboarding-step-dot-wrap.current .onboarding-step-dot { + border-color: var(--accent, #00d4aa); + color: var(--accent, #00d4aa); + background: rgba(0, 212, 170, 0.1); +} + +.onboarding-step-dot-wrap.done .onboarding-step-dot { + border-color: var(--accent, #00d4aa); + background: var(--accent, #00d4aa); + color: #000; + cursor: pointer; +} + +.onboarding-step-dot-wrap.done .onboarding-step-dot:focus-visible { + outline: 2px solid var(--accent, #00d4aa); +} + +.onboarding-step-label { + font-size: 0.6875rem; + color: var(--muted, #6b7a94); + text-align: center; + white-space: nowrap; +} + +.onboarding-step-dot-wrap.current .onboarding-step-label { + color: var(--text, #e8ecf4); + font-weight: 500; +} + +.onboarding-step-dot-wrap.done .onboarding-step-label { + color: var(--accent, #00d4aa); +} + +/* ── Card ────────────────────────────────────────────────────────────────── */ + +.onboarding-card { + width: 100%; + background: var(--surface, #121a2a); + border: 1px solid var(--border, #1e2d42); + border-radius: 16px; + overflow: hidden; +} + +.onboarding-step-content { + padding: 2rem; + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +/* ── Icon ring ───────────────────────────────────────────────────────────── */ + +.onboarding-icon-ring { + width: 72px; + height: 72px; + border-radius: 50%; + border: 2px solid var(--accent, #00d4aa); + background: rgba(0, 212, 170, 0.08); + display: flex; + align-items: center; + justify-content: center; + color: var(--accent, #00d4aa); + margin: 0 auto 0.5rem; +} + +.onboarding-icon-ring.connected { + border-color: #22d3ee; + background: rgba(34, 211, 238, 0.08); + color: #22d3ee; +} + +.onboarding-icon-ring.disconnected { + border-color: #f59e0b; + background: rgba(245, 158, 11, 0.08); + color: #f59e0b; +} + +/* ── Heading & body ──────────────────────────────────────────────────────── */ + +.onboarding-heading { + font-size: 1.375rem; + font-weight: 700; + color: var(--text, #e8ecf4); + text-align: center; + margin: 0; + outline: none; /* focus managed programmatically */ +} + +.onboarding-body { + font-size: 0.9375rem; + color: var(--muted, #6b7a94); + line-height: 1.6; + text-align: center; + margin: 0; +} + +/* ── Concept grid (step 0) ───────────────────────────────────────────────── */ + +.onboarding-concept-grid { + display: flex; + flex-direction: column; + gap: 0.75rem; + margin-top: 0.25rem; +} + +.onboarding-concept-card { + display: flex; + align-items: flex-start; + gap: 0.875rem; + padding: 0.875rem 1rem; + background: var(--bg, #0a0e17); + border: 1px solid var(--border, #1e2d42); + border-radius: 10px; +} + +.concept-icon { + flex-shrink: 0; + width: 36px; + height: 36px; + border-radius: 8px; + background: rgba(0, 212, 170, 0.08); + border: 1px solid rgba(0, 212, 170, 0.2); + display: flex; + align-items: center; + justify-content: center; + color: var(--accent, #00d4aa); +} + +.concept-title { + font-size: 0.875rem; + font-weight: 600; + color: var(--text, #e8ecf4); + margin-bottom: 0.2rem; +} + +.concept-desc { + font-size: 0.8125rem; + color: var(--muted, #6b7a94); + line-height: 1.45; +} + +/* ── Steps list (step 1) ─────────────────────────────────────────────────── */ + +.onboarding-steps-list { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.onboarding-steps-item { + display: flex; + gap: 1rem; + align-items: flex-start; + padding: 0.875rem 1rem; + background: var(--bg, #0a0e17); + border: 1px solid var(--border, #1e2d42); + border-radius: 10px; +} + +.step-num { + flex-shrink: 0; + width: 28px; + height: 28px; + border-radius: 50%; + background: rgba(0, 212, 170, 0.12); + border: 1px solid var(--accent, #00d4aa); + color: var(--accent, #00d4aa); + font-size: 0.8125rem; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; +} + +.step-title { + font-size: 0.875rem; + font-weight: 600; + color: var(--text, #e8ecf4); + margin-bottom: 0.25rem; +} + +.step-desc { + font-size: 0.8125rem; + color: var(--muted, #6b7a94); + line-height: 1.5; +} + +.step-desc code { + font-family: "SF Mono", "Fira Code", "Fira Mono", monospace; + font-size: 0.8em; + background: rgba(0, 212, 170, 0.1); + color: var(--accent, #00d4aa); + padding: 0.1em 0.35em; + border-radius: 4px; +} + +/* ── Info box ────────────────────────────────────────────────────────────── */ + +.onboarding-info-box { + display: flex; + align-items: flex-start; + gap: 0.625rem; + background: rgba(99, 179, 237, 0.06); + border: 1px solid rgba(99, 179, 237, 0.2); + border-radius: 10px; + padding: 0.875rem 1rem; + color: #63b3ed; +} + +.onboarding-info-box svg { + flex-shrink: 0; + margin-top: 1px; +} + +.onboarding-info-box p { + font-size: 0.8125rem; + line-height: 1.5; + margin: 0; + color: #93c5fd; +} + +/* ── Checklist (step 2 connected) ────────────────────────────────────────── */ + +.onboarding-checklist { + display: flex; + flex-direction: column; + gap: 0.625rem; +} + +.checklist-item { + display: flex; + align-items: center; + gap: 0.625rem; + padding: 0.75rem 1rem; + background: var(--bg, #0a0e17); + border: 1px solid var(--border, #1e2d42); + border-radius: 8px; + font-size: 0.875rem; + color: var(--muted, #6b7a94); +} + +.checklist-item.done { + border-color: rgba(0, 212, 170, 0.3); + color: var(--text, #e8ecf4); +} + +.checklist-item.done svg { + color: var(--accent, #00d4aa); +} + +.checklist-item svg { + flex-shrink: 0; + color: var(--border, #1e2d42); +} + +/* ── Wallet address pill ─────────────────────────────────────────────────── */ + +.wallet-address-pill { + display: inline-block; + font-family: "SF Mono", "Fira Code", monospace; + font-size: 0.8125em; + background: rgba(0, 212, 170, 0.1); + color: var(--accent, #00d4aa); + padding: 0.1em 0.5em; + border-radius: 4px; + border: 1px solid rgba(0, 212, 170, 0.25); +} + +/* ── Wallet options (step 2 anonymous) ───────────────────────────────────── */ + +.onboarding-wallet-options { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.wallet-option { + display: flex; + align-items: center; + gap: 0.875rem; + padding: 0.875rem 1rem; + background: var(--bg, #0a0e17); + border: 1px solid var(--border, #1e2d42); + border-radius: 10px; + transition: border-color 0.2s; +} + +.wallet-option.featured { + border-color: rgba(123, 47, 247, 0.4); + background: rgba(123, 47, 247, 0.05); +} + +.wallet-option-icon { + flex-shrink: 0; + width: 40px; + height: 40px; + border-radius: 10px; + overflow: hidden; +} + +.wallet-option-name { + font-size: 0.9rem; + font-weight: 600; + color: var(--text, #e8ecf4); +} + +.wallet-option-desc { + font-size: 0.8rem; + color: var(--muted, #6b7a94); +} + +.wallet-badge { + margin-left: auto; + font-size: 0.6875rem; + font-weight: 600; + padding: 0.2em 0.55em; + border-radius: 20px; + background: rgba(123, 47, 247, 0.2); + color: #c4b5fd; + border: 1px solid rgba(123, 47, 247, 0.3); + white-space: nowrap; +} + +/* ── Footer ──────────────────────────────────────────────────────────────── */ + +.onboarding-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 2rem; + border-top: 1px solid var(--border, #1e2d42); + background: var(--bg, #0a0e17); + gap: 0.75rem; +} + +.onboarding-nav-btns { + display: flex; + gap: 0.625rem; +} + +.onboarding-btn-ghost { + background: none; + border: none; + color: var(--muted, #6b7a94); + font-size: 0.875rem; + cursor: pointer; + padding: 0.5rem 0.75rem; + border-radius: 6px; + transition: color 0.2s; +} + +.onboarding-btn-ghost:hover, +.onboarding-btn-ghost:focus-visible { + color: var(--text, #e8ecf4); + outline: 2px solid var(--border, #1e2d42); + outline-offset: 2px; +} + +.onboarding-btn-secondary { + background: var(--surface, #121a2a); + border: 1px solid var(--border, #1e2d42); + color: var(--text, #e8ecf4); + font-size: 0.875rem; + font-weight: 500; + padding: 0.5rem 1.125rem; + border-radius: 8px; + cursor: pointer; + transition: border-color 0.2s, background 0.2s; +} + +.onboarding-btn-secondary:hover, +.onboarding-btn-secondary:focus-visible { + border-color: var(--accent, #00d4aa); + outline: 2px solid var(--accent, #00d4aa); + outline-offset: 2px; +} + +.onboarding-btn-primary { + background: var(--accent, #00d4aa); + border: none; + color: #000; + font-size: 0.9375rem; + font-weight: 700; + padding: 0.5625rem 1.375rem; + border-radius: 8px; + cursor: pointer; + transition: background 0.2s, box-shadow 0.2s; + box-shadow: 0 0 16px rgba(0, 212, 170, 0.3); +} + +.onboarding-btn-primary:hover { + background: var(--accent-dim, #00a884); + box-shadow: 0 0 24px rgba(0, 212, 170, 0.45); +} + +.onboarding-btn-primary:focus-visible { + outline: 2px solid #fff; + outline-offset: 2px; +} + +/* ── Hint ────────────────────────────────────────────────────────────────── */ + +.onboarding-hint { + font-size: 0.75rem; + color: var(--muted, #6b7a94); + margin: 0; +} + +/* ── Light theme overrides ───────────────────────────────────────────────── */ + +[data-theme="light"] .onboarding-concept-card, +[data-theme="light"] .onboarding-steps-item, +[data-theme="light"] .checklist-item, +[data-theme="light"] .wallet-option { + background: #ffffff; +} + +[data-theme="light"] .onboarding-footer { + background: #f5f7fa; +} + +[data-theme="light"] .onboarding-btn-secondary { + background: #ffffff; +} + +[data-theme="light"] .step-desc code { + background: rgba(0, 180, 140, 0.08); +} + +/* ── Responsive ──────────────────────────────────────────────────────────── */ + +@media (max-width: 480px) { + .onboarding-step-content { + padding: 1.5rem 1.125rem; + } + + .onboarding-footer { + padding: 0.875rem 1.125rem; + flex-direction: column-reverse; + align-items: stretch; + } + + .onboarding-nav-btns { + justify-content: flex-end; + } + + .onboarding-btn-primary { + flex: 1; + text-align: center; + } +} diff --git a/src/components/TreasuryOnboarding.tsx b/src/components/TreasuryOnboarding.tsx new file mode 100644 index 0000000..c9c3428 --- /dev/null +++ b/src/components/TreasuryOnboarding.tsx @@ -0,0 +1,372 @@ +import { useState, useEffect, useRef } from "react"; +import "./TreasuryOnboarding.css"; + +interface TreasuryOnboardingProps { + walletConnected: boolean; + walletAddress?: string | null; + onConnectWallet: () => void; + onCreateStream: () => void; + onDismiss: () => void; +} + +const STEPS = [ + { + id: "welcome", + label: "Welcome", + }, + { + id: "how-it-works", + label: "How it works", + }, + { + id: "get-started", + label: "Get started", + }, +]; + +export default function TreasuryOnboarding({ + walletConnected, + walletAddress, + onConnectWallet, + onCreateStream, + onDismiss, +}: TreasuryOnboardingProps) { + const [step, setStep] = useState(0); + const headingRef = useRef(null); + const regionRef = useRef(null); + + // Move focus to heading when step changes so screen readers announce new content + useEffect(() => { + headingRef.current?.focus(); + }, [step]); + + const isLastStep = step === STEPS.length - 1; + + const handleNext = () => { + if (isLastStep) { + if (!walletConnected) { + onConnectWallet(); + } else { + onCreateStream(); + } + } else { + setStep((s) => s + 1); + } + }; + + const handleBack = () => setStep((s) => Math.max(0, s - 1)); + + return ( +
+ {/* Progress stepper */} +
+ {STEPS.map((s, i) => ( +
+ + + {i < STEPS.length - 1 && ( + + ))} +
+ + {/* Step content */} +
+ {/* Step 0: Welcome */} + {step === 0 && ( +
+ + +

+ Welcome to Fluxora Treasury +

+

+ Fluxora lets you stream USDC to recipients in real time — second by + second — using Soroban smart contracts on Stellar. No batch payroll. + No manual transfers. Just continuous, programmable capital flow. +

+ +
+
+ +
+
Real-time streams
+
USDC accrues per second. Recipients withdraw whenever they want.
+
+
+ +
+ +
+
Smart-contract lock
+
Your deposit is held in a Soroban contract — trustless and auditable.
+
+
+ +
+ +
+
USDC on Stellar
+
Low fees, fast finality. Stellar's USDC is the stream currency.
+
+
+
+
+ )} + + {/* Step 1: How it works */} + {step === 1 && ( +
+ + +

+ How a stream works +

+

+ Creating a stream takes three steps. Once live, it runs autonomously + on-chain — you don't need to stay online. +

+ +
    +
  1. + +
    +
    Set recipient & deposit
    +
    + Paste a valid Stellar address (starts with G, 56 + chars). Choose how much USDC to lock — must cover the full + stream duration at your chosen rate. +
    +
    +
  2. +
  3. + +
    +
    Configure rate & schedule
    +
    + Pick how fast USDC accrues, when to start (now or a future + date), and optionally add a cliff — a period during which the + stream accumulates but withdrawals are blocked. +
    +
    +
  4. +
  5. + +
    +
    Review & sign
    +
    + Confirm the summary and approve the Soroban transaction in + Freighter. Your USDC is locked; the recipient can start + withdrawing their accrued share immediately. +
    +
    +
  6. +
+ +
+ +

+ You can cancel an active stream at any time. Unaccrued USDC is + returned to your wallet; accrued amounts remain available to the + recipient. +

+
+
+ )} + + {/* Step 2: Get started */} + {step === 2 && ( +
+ + +

+ {walletConnected ? "You're ready to stream" : "Connect your wallet first"} +

+ + {walletConnected ? ( + <> +

+ Your Stellar wallet is connected + {walletAddress && ( + <> + {" "}as{" "} + + {walletAddress.slice(0, 6)} … {walletAddress.slice(-6)} + + + )} + . Create your first stream to begin the empty-to-active + journey. +

+ +
+
+ + Wallet connected +
+
+ + Recipient Stellar address ready +
+
+ + USDC balance to cover stream duration +
+
+ + ) : ( + <> +

+ Fluxora requires a Stellar wallet to sign on-chain transactions. + Connect Freighter — the recommended browser + extension — to continue. +

+ +
+
+ +
+
Freighter
+
Recommended · Stellar browser extension
+
+ Recommended +
+
+ +
+ +

+ Fluxora connects to Stellar Testnet by default. No real funds + are used during exploration. +

+
+ + )} +
+ )} + + {/* Navigation footer */} +
+ + +
+ {step > 0 && ( + + )} + +
+
+
+ + {/* Keyboard hint */} + +
+ ); +} diff --git a/src/components/layout.css b/src/components/layout.css new file mode 100644 index 0000000..e934631 --- /dev/null +++ b/src/components/layout.css @@ -0,0 +1,296 @@ +.app-layout { + display: flex; + flex-direction: column; /* ← change row to column */ + min-height: 100vh; + position: relative; + background: var(--bg); +} + +.app-layout__body { + display: flex; + flex: 1; + overflow: hidden; +} + +.app-layout__sidebar { + width: 220px; +} +.app-sidebar { + width: 244px; + background: var(--surface); + border-right: 1px solid var(--border); + padding: 1.2rem 0.9rem; + display: flex; + flex-direction: column; + gap: 1rem; + transition: + width 0.22s ease, + transform 0.22s ease; + z-index: 40; +} + +.app-sidebar-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; +} + +.app-logo { + font-size: 1.2rem; + font-weight: 700; + color: var(--accent); + letter-spacing: 0.01em; + white-space: nowrap; +} + +.app-sidebar-toggle { + width: 2rem; + height: 2rem; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid var(--border); + border-radius: 8px; + background: rgba(255, 255, 255, 0.02); + color: var(--text); + cursor: pointer; +} + +.app-toggle-chevron { + width: 1rem; + height: 1rem; + display: inline-flex; + transition: transform 0.2s ease; +} + +.app-toggle-chevron svg { + width: 100%; + height: 100%; +} + +.app-toggle-chevron.is-rotated { + transform: rotate(180deg); +} + +.app-nav { + display: flex; + flex-direction: column; + gap: 0.35rem; + flex: 1; + min-height: 0; +} + +.app-nav-link { + display: flex; + align-items: center; + gap: 0.7rem; + min-height: 2.6rem; + padding: 0.5rem 0.6rem; + border-radius: 10px; + color: var(--text); + text-decoration: none; +} + +.app-nav-link:hover { + background: rgba(255, 255, 255, 0.04); + color: #ecf4ff; +} + +.app-nav-badge { + width: 1.6rem; + height: 1.6rem; + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 0.76rem; + font-weight: 600; + background: rgba(107, 145, 209, 0.18); + color: #b8d4ff; + flex-shrink: 0; +} + +.app-nav-label { + white-space: nowrap; +} + +.app-connect-button { + margin-top: auto; + min-height: 2.7rem; + border-radius: 10px; + border: none; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + cursor: pointer; + background: var(--accent); + color: #0a0e17; + font-weight: 600; + padding: 0.75rem 0.9rem; +} + +.app-connect-icon { + width: 1rem; + height: 1rem; + display: inline-flex; +} + +.app-connect-icon svg { + width: 100%; + height: 100%; +} + +.app-connect-label { + white-space: nowrap; +} + +.app-content-area { + display: flex; + flex-direction: column; + flex: 1; + min-width: 0; +} + +.app-mobile-topbar { + display: none; +} + +.app-main { + flex: 1; + padding: 2rem; + overflow: auto; +} + +.app-sidebar-backdrop { + display: none; +} + +.app-layout.is-collapsed .app-sidebar { + width: 84px; + padding-left: 0.65rem; + padding-right: 0.65rem; +} + +.app-layout.is-collapsed .app-logo { + font-size: 1.04rem; +} + +.app-layout.is-collapsed .app-nav-link { + justify-content: center; + padding-left: 0.35rem; + padding-right: 0.35rem; +} + +.app-layout.is-collapsed .app-nav-label, +.app-layout.is-collapsed .app-connect-label { + display: none; +} + +.app-layout.is-collapsed .app-connect-button { + padding-left: 0; + padding-right: 0; +} + +@media (max-width: 1024px) { + .app-main { + padding: 1.5rem; + } +} + +@media (max-width: 860px) { + .app-sidebar { + position: fixed; + left: 0; + top: 0; + height: 100vh; + transform: translateX(-105%); + width: 244px; + } + + .app-mobile-topbar { + display: flex; + align-items: center; + gap: 0.8rem; + padding: 0.9rem 1rem; + border-bottom: 1px solid var(--border); + background: var(--surface-elevated); + position: sticky; + top: 0; + z-index: 35; + } + + .app-mobile-menu-btn { + width: 2.1rem; + height: 2rem; + border: 1px solid var(--border); + border-radius: 8px; + background: transparent; + color: var(--text); + display: inline-flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.18rem; + padding: 0; + cursor: pointer; + } + + .app-mobile-menu-btn span { + width: 1rem; + height: 2px; + background: currentColor; + border-radius: 3px; + } + + .app-mobile-title { + font-size: 1rem; + font-weight: 600; + color: var(--text); + } + + .app-main { + padding: 1rem; + } + + .app-layout.is-mobile-open .app-sidebar { + transform: translateX(0); + } + + .app-sidebar-backdrop { + display: block; + position: fixed; + inset: 0; + border: none; + padding: 0; + margin: 0; + background: rgba(0, 0, 0, 0.46); + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease; + z-index: 30; + } + + .app-layout.is-mobile-open .app-sidebar-backdrop { + opacity: 1; + pointer-events: auto; + } + + .app-layout.is-collapsed .app-sidebar { + width: 244px; + padding-left: 0.9rem; + padding-right: 0.9rem; + } + + .app-layout.is-collapsed .app-nav-link { + justify-content: flex-start; + padding-left: 0.6rem; + padding-right: 0.6rem; + } + + .app-layout.is-collapsed .app-nav-label, + .app-layout.is-collapsed .app-connect-label { + display: inline; + } +} diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 56b5531..3b7e1b4 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -3,40 +3,79 @@ import RecentStreams, { Stream } from '../components/RecentStreams'; import CreateStreamModal from '../components/CreateStreamModal'; import TreasuryOverviewLoading from '../components/TreasuryOverviewLoading'; import TreasuryEmptyState from '../components/TreasuryEmptyState'; +import TreasuryOnboarding from '../components/TreasuryOnboarding'; +import ConnectWalletModal from '../components/ConnectWalletModal'; + +const ONBOARDING_KEY = 'fluxora_onboarding_dismissed'; + +function hasSeenOnboarding(): boolean { + try { + return localStorage.getItem(ONBOARDING_KEY) === 'true'; + } catch { + return false; + } +} + +function markOnboardingSeen(): void { + try { + localStorage.setItem(ONBOARDING_KEY, 'true'); + } catch { + // storage unavailable — treat as transient + } +} export default function Dashboard() { const [loading, setLoading] = useState(true); const [isModalOpen, setIsModalOpen] = useState(false); + const [isWalletModalOpen, setIsWalletModalOpen] = useState(false); const [streams] = useState([]); + const [showOnboarding, setShowOnboarding] = useState(false); + + // Resolve wallet connection state from Freighter (best-effort, no popup) + const [walletConnected, setWalletConnected] = useState(false); + const [walletAddress, setWalletAddress] = useState(null); + + useEffect(() => { + (async () => { + try { + const { isConnected, getAddress } = await import('@stellar/freighter-api'); + const conn = await isConnected(); + if (!conn.isConnected) return; + const addr = await getAddress(); + if (!addr.error && addr.address) { + setWalletConnected(true); + setWalletAddress(addr.address); + } + } catch { + // Freighter not installed or not approved — silent + } + })(); + }, []); useEffect(() => { // Demo: simulate async fetch — remove when wiring real fetch. - const t = setTimeout(() => { - // For testing empty state, we keep it empty. - // To see the data state, uncomment the following: - /* - setStreams([ - { - id: "STR-001", - name: "Dev Grant - Alice", - recipient: "GABC...xyz1", - rate: "5,000 USDC/mo", - status: "Active", - }, - { - id: "STR-002", - name: "Marketing Budget", - recipient: "GDEF...abc2", - rate: "3,200 USDC/mo", - status: "Active", - }, - ]); - */ - setLoading(false); - }, 2000); + const t = setTimeout(() => setLoading(false), 1200); return () => clearTimeout(t); }, []); + // Show onboarding on first-ever visit to an empty treasury + useEffect(() => { + if (!loading && streams.length === 0 && !hasSeenOnboarding()) { + setShowOnboarding(true); + } + }, [loading, streams.length]); + + const handleDismissOnboarding = () => { + markOnboardingSeen(); + setShowOnboarding(false); + }; + + const handleOnboardingCreateStream = () => { + markOnboardingSeen(); + setShowOnboarding(false); + setIsModalOpen(true); + }; + if (loading) return ; const hasStreams = streams.length > 0; @@ -48,6 +87,29 @@ export default function Dashboard() { Treasury overview and active stream summary. Connect your wallet to see real-time capital flow.

+ + {/* Wallet connection banner — shown only when not connected and past onboarding */} + {!walletConnected && !showOnboarding && ( +
+
+ + + Connect your Stellar wallet to see real balances and create streams. + +
+ +
+ )} +
Active Streams
@@ -75,6 +137,14 @@ export default function Dashboard() { Create stream + ) : showOnboarding ? ( + setIsWalletModalOpen(true)} + onCreateStream={handleOnboardingCreateStream} + onDismiss={handleDismissOnboarding} + /> ) : ( setIsModalOpen(true)} /> )} @@ -83,10 +153,41 @@ export default function Dashboard() { isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} /> + + setIsWalletModalOpen(false)} + />
); } +const walletBannerStyle: React.CSSProperties = { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + flexWrap: 'wrap', + gap: '0.75rem', + background: 'rgba(245, 158, 11, 0.06)', + border: '1px solid rgba(245, 158, 11, 0.25)', + borderRadius: '10px', + padding: '0.75rem 1rem', + marginTop: '0.75rem', + marginBottom: '0.25rem', +}; + +const connectBannerBtnStyle: React.CSSProperties = { + background: 'var(--accent)', + color: '#000', + border: 'none', + borderRadius: '6px', + padding: '0.375rem 0.875rem', + fontSize: '0.8125rem', + fontWeight: 600, + cursor: 'pointer', + whiteSpace: 'nowrap', +}; + const createBtnStyle: React.CSSProperties = { display: "flex", alignItems: "center",