From ef2bb4edce48060fc537a1b84b42227093d5e7fa Mon Sep 17 00:00:00 2001 From: ciregenz Date: Sun, 19 Jul 2026 12:07:33 -0700 Subject: [PATCH] [eric] onboarding: arc visuals (swarm card leaf + tinted logo, brand-lit app tiles, theme device, grain) --- frontend/public/grain-texture.png | Bin 0 -> 32478 bytes .../OnboardingV3/OnboardingLogo.tsx | 16 + .../app/components/OnboardingV3/Starburst.tsx | 21 -- .../OnboardingV3/beats/BeatApps.tsx | 30 +- .../OnboardingV3/beats/BeatCard.tsx | 281 ++++++++++++++---- .../OnboardingV3/beats/BeatShell.tsx | 214 +++++++++---- .../OnboardingV3/beats/BeatTheme.tsx | 6 +- .../components/OnboardingV3/providerLogos.tsx | 36 +++ .../app/components/theme/AccentColorPad.tsx | 54 +++- .../src/app/components/theme/WashDials.tsx | 62 ++++ frontend/src/shared/styles/grainTexture.ts | 4 + 11 files changed, 558 insertions(+), 166 deletions(-) create mode 100644 frontend/public/grain-texture.png create mode 100644 frontend/src/app/components/OnboardingV3/OnboardingLogo.tsx delete mode 100644 frontend/src/app/components/OnboardingV3/Starburst.tsx create mode 100644 frontend/src/app/components/OnboardingV3/providerLogos.tsx create mode 100644 frontend/src/app/components/theme/WashDials.tsx create mode 100644 frontend/src/shared/styles/grainTexture.ts diff --git a/frontend/public/grain-texture.png b/frontend/public/grain-texture.png new file mode 100644 index 0000000000000000000000000000000000000000..e4777a5ad6c6d4fc7241aa7766e8b937eec0c9bf GIT binary patch literal 32478 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 = ({ size, style }) => ( + OpenSwarm +); + +export default OnboardingLogo; diff --git a/frontend/src/app/components/OnboardingV3/Starburst.tsx b/frontend/src/app/components/OnboardingV3/Starburst.tsx deleted file mode 100644 index 9acd5187..00000000 --- a/frontend/src/app/components/OnboardingV3/Starburst.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; - -// Claude's signature asterisk mark, drawn as 8 rounded rays around a hollow center; gradient runs hover->pressed so it wears whatever accent the user picked. -const Starburst: React.FC<{ size: number; from: string; to: string }> = ({ size, from, to }) => { - const id = React.useId(); - return ( - - - - - - - - {Array.from({ length: 8 }, (unused, i) => ( - - ))} - - ); -}; - -export default Starburst; diff --git a/frontend/src/app/components/OnboardingV3/beats/BeatApps.tsx b/frontend/src/app/components/OnboardingV3/beats/BeatApps.tsx index 408f085f..8ee41e18 100644 --- a/frontend/src/app/components/OnboardingV3/beats/BeatApps.tsx +++ b/frontend/src/app/components/OnboardingV3/beats/BeatApps.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { motion } from 'framer-motion'; -import { Check } from 'lucide-react'; import { INTEGRATIONS } from '@/app/pages/Tools/integrations'; import type { ClaudeTokens } from '@/shared/styles/claudeTokens'; import BeatShell from './BeatShell'; @@ -29,9 +28,11 @@ const BeatApps: React.FC<{ c={c} title="Choose the apps you live in." body="I'll shape your starting canvas around them." - nextLabel={picks.length > 0 ? 'Next' : 'Skip for now'} + nextLabel="Next" onNext={onNext} onBack={onBack} + secondaryLabel="Skip for now" + onSecondary={onNext} > OpenSwarm -
+
{entries.map((entry, i) => { const picked = picks.includes(entry.id); + // Arc's picked tile lights up in the APP's own brand color, not one shared accent. return ( - {picked && ( - - - - )} - {entry.icon} - {entry.name} + {entry.icon} + {entry.name} ); })} diff --git a/frontend/src/app/components/OnboardingV3/beats/BeatCard.tsx b/frontend/src/app/components/OnboardingV3/beats/BeatCard.tsx index df405342..c8ce1aa2 100644 --- a/frontend/src/app/components/OnboardingV3/beats/BeatCard.tsx +++ b/frontend/src/app/components/OnboardingV3/beats/BeatCard.tsx @@ -1,11 +1,13 @@ -import React, { useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { motion } from 'framer-motion'; -import { useThemeAccent } from '@/shared/styles/ThemeContext'; -import { Dices } from 'lucide-react'; +import { Check, Copy, Dices, Download, Share } from 'lucide-react'; import type { ClaudeTokens } from '@/shared/styles/claudeTokens'; +import { hexToHsl, hslToHex } from '@/shared/styles/claudeTokens'; +import { useThemeAccent } from '@/shared/styles/ThemeContext'; import type { ProviderIdentity } from '../onboardingV3Api'; -import Starburst from '../Starburst'; -import BeatShell from './BeatShell'; +import BeatShell, { ONBOARDING_SANS } from './BeatShell'; + +const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace'; const EPITHETS = [ 'METHODICAL PERFECTIONIST', 'SAVORY ARCHIVIST', 'MIDNIGHT ORCHESTRATOR', 'GENTLE MAXIMALIST', @@ -21,80 +23,255 @@ function nameFromIdentity(identity: ProviderIdentity[]): string { return letters.charAt(0).toUpperCase() + letters.slice(1, 12); } -// The Arc Card moment: onboarding ends with an identity artifact, not a settings screen. Name is editable in place, the epithet re-rolls, and the leaf wears the accent they just picked. +// The card's leaf takes the two ends of the user's picked theme gradient (or a dark->light pair +// derived from the single accent), so the artifact literally wears the theme they just chose. +function leafStops(gradient: string[] | null, base: string, c: ClaudeTokens): [string, string] { + if (gradient && gradient.length >= 2) return [gradient[0], gradient[gradient.length - 1]]; + const hsl = hexToHsl(base); + if (!hsl) return [c.accent.pressed, c.accent.primary]; + const dark = hslToHex({ h: hsl.h, s: Math.min(1, hsl.s * 1.02), l: Math.max(0.34, hsl.l - 0.1) }); + const light = hslToHex({ h: (hsl.h + 0.015) % 1, s: Math.max(0.55, hsl.s * 0.92), l: Math.min(0.74, hsl.l + 0.18) }); + return [dark, light]; +} + +// One accent-hued ink dark enough to read on the cream card, for every bit of card type. +function readableInk(base: string, c: ClaudeTokens): string { + const hsl = hexToHsl(base); + if (!hsl) return c.accent.pressed; + return hslToHex({ h: hsl.h, s: Math.max(0.5, hsl.s), l: Math.min(0.42, hsl.l) }); +} + +// The Arc Card moment: onboarding ends with an identity artifact, not a settings screen. A gradient +// leaf wearing the picked theme, the name + a re-rollable epithet, and a real PNG you can save, +// copy, or share, an artifact you show off has to be takeable. const BeatCard: React.FC<{ c: ClaudeTokens; identity: ProviderIdentity[]; onFinish: (name: string | null) => void; onBack: () => void; }> = ({ c, identity, onFinish, onBack }) => { - const { gradient } = useThemeAccent(); + const { accent, gradient } = useThemeAccent(); const [name, setName] = useState(() => nameFromIdentity(identity)); const seed = useMemo(() => Math.floor(Math.random() * EPITHETS.length), []); const [roll, setRoll] = useState(0); const epithet = EPITHETS[(seed + roll) % EPITHETS.length]; const today = useMemo(() => new Date().toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }), []); + const [tilt, setTilt] = useState<{ rx: number; ry: number; mx: number; my: number } | null>(null); + const [copied, setCopied] = useState(false); + + const baseHex = (gradient && gradient[0]) || accent || c.accent.primary; + const [dark, light] = leafStops(gradient, baseHex, c); + const ink = readableInk(baseHex, c); + + const drawCard = useCallback(async (): Promise => { + const W = 580; + const H = 800; + const cv = document.createElement('canvas'); + cv.width = W; + cv.height = H; + const ctx = cv.getContext('2d'); + if (!ctx) return cv; + ctx.fillStyle = '#FCFBF5'; + ctx.beginPath(); + ctx.roundRect(0, 0, W, H, 36); + ctx.fill(); + // The leaf: three round corners + one soft point at bottom-right, wearing the theme gradient. + const grad = ctx.createLinearGradient(48, 44, 532, 400); + grad.addColorStop(0, dark); + grad.addColorStop(1, light); + ctx.fillStyle = grad; + ctx.beginPath(); + ctx.roundRect(48, 44, W - 96, 340, [150, 150, 12, 150]); + ctx.fill(); + // Small brand mark in the corner, tinted with the same gradient (source-in keeps the octopus alpha). + const logo = new Image(); + logo.src = './logo.png'; + await new Promise((res) => { logo.onload = () => res(); logo.onerror = () => res(); }); + if (logo.naturalWidth > 0) { + const lc = document.createElement('canvas'); + lc.width = 72; + lc.height = 72; + const lctx = lc.getContext('2d'); + if (lctx) { + lctx.drawImage(logo, 0, 0, 72, 72); + lctx.globalCompositeOperation = 'source-in'; + const lg = lctx.createLinearGradient(0, 0, 72, 72); + lg.addColorStop(0, dark); + lg.addColorStop(1, light); + lctx.fillStyle = lg; + lctx.fillRect(0, 0, 72, 72); + ctx.drawImage(lc, 34, 30, 42, 42); + } + } + ctx.fillStyle = ink; + ctx.font = `800 58px ${ONBOARDING_SANS}`; + ctx.fillText(name.trim() || 'Your name', 52, 476); + ctx.font = `600 21px ${MONO}`; + ctx.fillText(epithet.split('').join(' '), 54, 520); + // Bottom-left stamp: OPENSWARM | hatch | date, outlined; bottom-right OPEN / SWARM lockup. + const stampText = `OPENSWARM ${today.toUpperCase()}`; + ctx.font = `600 18px ${MONO}`; + const stampW = ctx.measureText(stampText).width + 30; + ctx.strokeStyle = `${ink}88`; + ctx.lineWidth = 1.5; + ctx.beginPath(); + ctx.roundRect(48, H - 96, stampW, 42, 8); + ctx.stroke(); + ctx.fillStyle = ink; + ctx.fillText(stampText, 63, H - 68); + ctx.textAlign = 'right'; + ctx.font = `800 19px ${ONBOARDING_SANS}`; + ctx.fillText('OPEN', W - 48, H - 84); + ctx.fillText('SWARM', W - 48, H - 62); + ctx.textAlign = 'left'; + return cv; + }, [name, epithet, today, dark, light, ink]); + + const saveCard = useCallback(() => { + void drawCard().then((cv) => { + const a = document.createElement('a'); + a.download = 'swarm-card.png'; + a.href = cv.toDataURL('image/png'); + a.click(); + }); + }, [drawCard]); + + const copyCard = useCallback(() => { + void drawCard().then((cv) => { + cv.toBlob((blob) => { + if (!blob || !navigator.clipboard || typeof ClipboardItem === 'undefined') return; + void navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]).then(() => { + setCopied(true); + window.setTimeout(() => setCopied(false), 1600); + }); + }); + }); + }, [drawCard]); + + // Native share sheet when the platform offers one (Arc's first card action); quietly absent otherwise. + const canShare = typeof navigator.canShare === 'function' && typeof File !== 'undefined' + && navigator.canShare({ files: [new File([''], 'swarm-card.png', { type: 'image/png' })] }); + const shareCard = useCallback(() => { + void drawCard().then((cv) => { + cv.toBlob((blob) => { + if (!blob) return; + const file = new File([blob], 'swarm-card.png', { type: 'image/png' }); + void navigator.share({ files: [file], title: 'My Swarm Card' }).catch(() => {}); + }); + }); + }, [drawCard]); + + // Arc's card actions: quiet icon-only buttons under the card on the dark stage. + const chip = (label: string, Icon: typeof Dices, onClick: () => void): React.ReactElement => ( + + ); return ( onFinish(name.trim() || null)} onBack={onBack} + stageDark > -
+
-
- -
- setName(e.target.value.slice(0, 18))} - placeholder="Your name" - style={{ - marginTop: 26, border: 'none', outline: 'none', background: 'transparent', - fontSize: '1.7rem', fontWeight: 800, color: c.accent.pressed, fontFamily: 'inherit', - width: '100%', padding: 0, +
{ + const r = e.currentTarget.getBoundingClientRect(); + const px = (e.clientX - r.left) / r.width; + const py = (e.clientY - r.top) / r.height; + setTilt({ rx: -(py - 0.5) * 13, ry: (px - 0.5) * 13, mx: px * 100, my: py * 100 }); }} - /> -
- {epithet} -
-
- - OPENSWARM ยท {today.toUpperCase()} - - - OPEN
SWARM -
+ onMouseLeave={() => setTilt(null)} + style={{ + width: 300, height: 414, borderRadius: 18, background: '#FCFBF5', + border: '1px solid rgba(0,0,0,0.05)', + boxShadow: tilt ? '0 30px 70px rgba(0,0,0,0.34)' : '0 24px 60px rgba(0,0,0,0.28)', + padding: '22px 22px 20px', boxSizing: 'border-box', + display: 'flex', flexDirection: 'column', position: 'relative', overflow: 'hidden', + transform: tilt ? `rotateX(${tilt.rx}deg) rotateY(${tilt.ry}deg)` : 'rotateX(0deg) rotateY(0deg)', + transition: tilt ? 'box-shadow 200ms ease' : 'transform 320ms ease, box-shadow 200ms ease', + willChange: 'transform', + }} + > + {/* Cursor-following shine, the ProfileCard glare. */} +
+ {/* Small brand mark in the corner, wearing the same theme gradient (masked octopus). */} +
+ {/* The leaf: three round corners + one soft point, bottom-right, wearing the theme gradient. */} +
+ setName(e.target.value.slice(0, 18))} + placeholder="Your name" + style={{ + marginTop: 20, border: 'none', outline: 'none', background: 'transparent', + fontSize: '1.7rem', fontWeight: 800, color: ink, fontFamily: 'inherit', + width: '100%', padding: 0, + }} + /> +
+ {epithet} +
+
+ + OPENSWARM + + {today.toUpperCase()} + + + OPEN
SWARM +
+
- setRoll((r) => r + 1)} - style={{ - display: 'flex', alignItems: 'center', gap: 7, border: 'none', background: 'transparent', - color: c.text.tertiary, fontSize: '0.82rem', cursor: 'pointer', fontFamily: 'inherit', padding: 4, - }} - > - Re-roll the title - + + {chip('Re-roll the title', Dices, () => setRoll((r) => r + 1))} + {canShare && chip('Share', Share, shareCard)} + {chip('Save as image', Download, saveCard)} + {chip(copied ? 'Copied' : 'Copy to clipboard', copied ? Check : Copy, copyCard)} +
); diff --git a/frontend/src/app/components/OnboardingV3/beats/BeatShell.tsx b/frontend/src/app/components/OnboardingV3/beats/BeatShell.tsx index 84e047f9..b6263170 100644 --- a/frontend/src/app/components/OnboardingV3/beats/BeatShell.tsx +++ b/frontend/src/app/components/OnboardingV3/beats/BeatShell.tsx @@ -1,18 +1,40 @@ import React, { useEffect, useState } from 'react'; import { motion } from 'framer-motion'; import { ArrowLeft } from 'lucide-react'; -import { useThemeAccent } from '@/shared/styles/ThemeContext'; +import { useThemeAccent, useThemeWash } from '@/shared/styles/ThemeContext'; import type { ClaudeTokens } from '@/shared/styles/claudeTokens'; +import { GRAIN_URL } from '@/shared/styles/grainTexture'; -// Film grain as a data URI so the CSP never phones out; opacity keeps it a texture, not noise. -export const GRAIN_URL = "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E\")"; +// Arc's torn seam as a SMOOTH wave: a sine sampled densely enough that the polygon reads as a +// soft ripple (no sharp points), ~18px wavelength, 6px swell. +const WAVE_PERIODS = 52; +const WAVE_SAMPLES = WAVE_PERIODS * 8; +const ZIGZAG_CLIP = `polygon(0 0, ${Array.from({ length: WAVE_SAMPLES + 1 }, (unused, i) => { + const inset = 3 + 3 * Math.sin((i / WAVE_SAMPLES) * WAVE_PERIODS * 2 * Math.PI); + return `calc(100% - ${inset.toFixed(2)}px) ${((i / WAVE_SAMPLES) * 100).toFixed(3)}%`; +}).join(', ')}, 0 100%)`; -// Arc's torn seam: the dark copy panel ends in a zigzag bite instead of a straight border. -const ZIGZAG_CLIP = `polygon(0 0, 100% 0, ${Array.from({ length: 30 }, (unused, i) => `calc(100% - 12px) ${i * 3.33 + 1.66}%, 100% ${i * 3.33 + 3.33}%`).join(', ')}, 0 100%)`; +// Arc's electric indigo CTA: onboarding buttons are brand-colored, not user-accent (the accent doesn't exist until the theme beat). +export const CTA_BLUE = '#4b48f8'; +// Arc sets its onboarding in a heavy grotesque; the app's token "sans" actually falls back to a +// serif (Anthropic Sans isn't bundled), so the flow pins a real sans stack. +export const ONBOARDING_SANS = '-apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Helvetica, Arial, sans-serif'; +// Arc's onboarding-window backdrop: cold azure light from the top-left, deep indigo mid, a WARM +// violet-magenta glow rising from the bottom-right corner, always grained. +export const ARC_BLUE_BG = [ + 'radial-gradient(110% 95% at 92% 100%, rgba(186, 70, 235, 0.55) 0%, rgba(146, 60, 244, 0.28) 34%, transparent 62%)', + 'radial-gradient(120% 100% at 6% 4%, rgba(148, 178, 255, 0.85) 0%, rgba(110, 135, 255, 0.35) 38%, transparent 66%)', + 'linear-gradient(152deg, #5f7bff 0%, #4a4ff6 44%, #4338ef 68%, #6f3af3 100%)', +].join(', '); +// Arc's neutral stage: warm mauve-gray under heavy grain (their import beat), until the user picks a color. +const STAGE_MAUVE = '#a8a5b3'; const SPRING = { type: 'spring' as const, stiffness: 260, damping: 26 }; -// One idea per room: dark copy panel left (torn edge, grain, staggered spring+blur copy), live artifact on a grained stage right. Copy never moves after it lands; only the artifact is alive. +// One idea per room, staged EXACTLY like Arc: the whole window is grained electric blue, a rounded +// split card floats centered in it, dark copy panel (torn right edge, grain, staggered spring+blur +// copy, bottom-pinned CTA) beside a heavily grained stage. `wide` = Arc's account layout instead: +// edge-to-edge 50/50 split, centered copy, no floating card. const BeatShell: React.FC<{ c: ClaudeTokens; title: string; @@ -22,11 +44,31 @@ const BeatShell: React.FC<{ onNext: () => void; onBack?: () => void; children: React.ReactNode; -}> = ({ c, title, body, nextLabel, nextDisabled, onNext, onBack, children }) => { - // Arc's post-theme screens wear the user's color everywhere; the stage drinks the picked stops (or accent) instead of staying flat paper. + wide?: boolean; + logo?: React.ReactNode; + stageDark?: boolean; + secondaryLabel?: string; + onSecondary?: () => void; +}> = ({ c, title, body, nextLabel, nextDisabled, onNext, onBack, children, wide, logo, stageDark, secondaryLabel, onSecondary }) => { + // Once the user has picked stops the stage wears them (our theme beat repaints live); before that it stays Arc-mauve. const { accent, gradient } = useThemeAccent(); - const stops = gradient ?? (accent ? [accent] : [c.accent.primary]); - const stageWash = `linear-gradient(115deg, ${stops.map((hex, i) => `${hex}2e ${stops.length > 1 ? (i / (stops.length - 1)) * 100 : 100}%`).join(', ')}), ${c.bg.secondary}`; + const { washOpacity, grain } = useThemeWash(); + const stops = gradient ?? (accent ? [accent] : null); + // Picked color reads VIVID on the stage (alpha floor over near-white), not muddied into the mauve. + const washAlpha = Math.round(Math.max(0.5, washOpacity) * 255).toString(16).padStart(2, '0'); + const stageBg = stageDark + ? '#262320' + : stops + ? `linear-gradient(115deg, ${stops.map((hex, i) => `${hex}${washAlpha} ${stops.length > 1 ? (i / (stops.length - 1)) * 100 : 100}%`).join(', ')}), #edebe7` + : STAGE_MAUVE; + // Arc post-theme: the CTA flips to cream (dark label) and the window backdrop wears the user's + // stops under a soft white veil; before any pick both stay brand blue. + const themed = !!stops; + const ctaBg = themed ? '#F5EFDF' : CTA_BLUE; + const ctaFg = themed ? '#232320' : '#fff'; + const backdrop = stops + ? `linear-gradient(rgba(255,255,255,0.16), rgba(255,255,255,0.16)), linear-gradient(160deg, ${stops.map((hex, i) => `${hex} ${stops.length > 1 ? (i / (stops.length - 1)) * 100 : 100}%`).join(', ')})` + : ARC_BLUE_BG; // Zen steal: controls stay inert until the entrance lands so a double-click from the prior beat can't fire them. const [armed, setArmed] = useState(false); useEffect(() => { @@ -40,66 +82,118 @@ const BeatShell: React.FC<{ transition: { ...SPRING, delay }, }); - return ( -
-
-
- {onBack && ( - armed && onBack()} - style={{ - display: 'inline-flex', alignItems: 'center', gap: 6, alignSelf: 'flex-start', - marginBottom: 18, padding: 0, border: 'none', background: 'transparent', - color: c.text.inverse + '77', fontSize: '0.85rem', cursor: 'pointer', fontFamily: 'inherit', - }} - > - Back - - )} - - {title} - - - {body} - - + const panel = ( +
+
+ {onBack && ( + armed && onBack()} + style={{ + display: 'inline-flex', alignItems: 'center', gap: 6, alignSelf: 'flex-start', + marginBottom: 18, padding: 0, border: 'none', background: 'transparent', + color: 'rgba(255,255,255,0.47)', fontSize: '0.85rem', cursor: 'pointer', fontFamily: 'inherit', + position: wide ? 'absolute' : 'relative', top: wide ? 24 : undefined, left: wide ? 28 : undefined, + }} + > + Back + + )} + {logo && {logo}} + + {title} + + + {body} + + {/* Arc pins the CTA to the panel's bottom on card beats; on the wide account beat it follows the content. */} + + + {/* Arc's quiet escape hatch under the primary CTA. */} + {secondaryLabel && onSecondary && ( - + )} + +
+ ); + + const stage = ( + + {/* Arc's stage always wears texture; the slider can add more but never strips it during onboarding. */} +
+
+ {children}
+ + ); + + if (wide) { + return ( +
+ {panel} + {stage} +
+ ); + } + + return ( +
+
-
-
- {children} -
+ {panel} + {stage}
); diff --git a/frontend/src/app/components/OnboardingV3/beats/BeatTheme.tsx b/frontend/src/app/components/OnboardingV3/beats/BeatTheme.tsx index e8599311..62ad72b0 100644 --- a/frontend/src/app/components/OnboardingV3/beats/BeatTheme.tsx +++ b/frontend/src/app/components/OnboardingV3/beats/BeatTheme.tsx @@ -51,13 +51,15 @@ const BeatTheme: React.FC<{ nextLabel="Next" onNext={onNext} onBack={onBack} + stageDark > diff --git a/frontend/src/app/components/OnboardingV3/providerLogos.tsx b/frontend/src/app/components/OnboardingV3/providerLogos.tsx new file mode 100644 index 00000000..caf0e692 --- /dev/null +++ b/frontend/src/app/components/OnboardingV3/providerLogos.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +// Real brand marks (simple-icons paths) for the connect rows; the dots read as placeholders. +const CLAUDE_PATH = 'M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.583.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312z'; + +const GEMINI_PATH = 'M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81z'; + +const OPENAI_PATH = 'M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z'; + +export function providerLogo(providerId: string, size = 24): React.ReactElement | null { + if (providerId === 'claude') { + return ( + + ); + } + if (providerId === 'antigravity') { + return ( + + + + + + + + + + + ); + } + if (providerId === 'codex') { + return ( + + ); + } + return null; +} diff --git a/frontend/src/app/components/theme/AccentColorPad.tsx b/frontend/src/app/components/theme/AccentColorPad.tsx index afc76983..7a51de91 100644 --- a/frontend/src/app/components/theme/AccentColorPad.tsx +++ b/frontend/src/app/components/theme/AccentColorPad.tsx @@ -1,9 +1,14 @@ -import React, { useCallback, useRef } from 'react'; -import { Minus, Plus } from 'lucide-react'; +import React, { useCallback, useRef, useState } from 'react'; +import { ChevronLeft, ChevronRight, Minus, Plus } from 'lucide-react'; import { hexToHsl, hslToHex } from '@/shared/styles/claudeTokens'; import type { ClaudeTokens } from '@/shared/styles/claudeTokens'; +import { Knob, SquiggleSlider } from './WashDials'; -export const ACCENT_PRESETS = ['#ae5630', '#b0453c', '#8e5cb8', '#3a6fc4', '#2e8f6f', '#b08b2e', '#c2588f', '#5c6470']; +export const ACCENT_PRESETS = [ + '#ae5630', '#b0453c', '#8e5cb8', '#3a6fc4', '#2e8f6f', '#b08b2e', '#c2588f', '#5c6470', + '#e8b4b8', '#f2d0a4', '#a8d8b9', '#9ec5e8', '#c3aed6', '#f7e8a4', '#87d1c6', '#d98cb3', +]; +const PRESETS_PER_PAGE = 8; const MAX_STOPS = 3; function stopToXY(hex: string): { x: number; y: number } | null { @@ -30,6 +35,8 @@ const AccentColorPad: React.FC<{ const padRef = useRef(null); const grabbedRef = useRef(null); const lastApplyRef = useRef(0); + const [presetPage, setPresetPage] = useState(0); + const presetPages = Math.ceil(ACCENT_PRESETS.length / PRESETS_PER_PAGE); const pointToHex = useCallback((clientX: number, clientY: number): string | null => { const pad = padRef.current; @@ -104,9 +111,16 @@ const AccentColorPad: React.FC<{ onPointerMove={onPointerMove} onPointerUp={onPointerUp} style={{ + // Arc's pad: dark dot-grid field with the spectrum only ghosting through, the picked dots carry the color. position: 'relative', height, borderRadius: c.radius.lg, cursor: 'crosshair', border: `1px solid ${c.border.medium}`, touchAction: 'none', - background: 'linear-gradient(to bottom, rgba(255,255,255,0.55), rgba(0,0,0,0.45)), linear-gradient(to right, hsl(0,72%,55%), hsl(60,72%,55%), hsl(120,72%,55%), hsl(180,72%,55%), hsl(240,72%,55%), hsl(300,72%,55%), hsl(360,72%,55%))', + background: [ + 'radial-gradient(rgba(255,255,255,0.13) 1px, transparent 1.4px)', + 'linear-gradient(rgba(30,29,27,0.84), rgba(30,29,27,0.84))', + 'linear-gradient(to bottom, rgba(255,255,255,0.55), rgba(0,0,0,0.45))', + 'linear-gradient(to right, hsl(0,72%,55%), hsl(60,72%,55%), hsl(120,72%,55%), hsl(180,72%,55%), hsl(240,72%,55%), hsl(300,72%,55%), hsl(360,72%,55%))', + ].join(', '), + backgroundSize: '14px 14px, auto, auto, auto', }} > {stops.map((hex, i) => { @@ -150,8 +164,16 @@ const AccentColorPad: React.FC<{
-
- {ACCENT_PRESETS.map((hex) => ( + {/* Arc's preset carousel: a page of dots between chevrons. */} +
+ + {ACCENT_PRESETS.slice(presetPage * PRESETS_PER_PAGE, (presetPage + 1) * PRESETS_PER_PAGE).map((hex) => (
+ {/* Arc's dials row: wavy line = grain, round knob = intensity. */} {wash && ( -
- - +
+ +
)}
diff --git a/frontend/src/app/components/theme/WashDials.tsx b/frontend/src/app/components/theme/WashDials.tsx new file mode 100644 index 00000000..88543815 --- /dev/null +++ b/frontend/src/app/components/theme/WashDials.tsx @@ -0,0 +1,62 @@ +import React, { useCallback, useRef } from 'react'; + +// Arc's theme-device dials: grain is a wavy line with a pill thumb, intensity is a round dotted +// knob. Strokes ride currentColor so the same dials read on the dark device and light Settings. + +export const SquiggleSlider: React.FC<{ value: number; onChange: (v: number) => void; width?: number }> = ({ value, onChange, width = 170 }) => { + const ref = useRef(null); + const dragging = useRef(false); + const apply = useCallback((clientX: number) => { + const el = ref.current; + if (!el) return; + const r = el.getBoundingClientRect(); + onChange(Math.min(1, Math.max(0, (clientX - r.left) / r.width))); + }, [onChange]); + const H = 30; + const wavePath = Array.from({ length: 61 }, (unused, i) => { + const x = (i / 60) * width; + const y = H / 2 + Math.sin((i / 60) * Math.PI * 2 * 5) * 6; + return `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`; + }).join(' '); + return ( +
{ dragging.current = true; (e.target as HTMLElement).setPointerCapture?.(e.pointerId); apply(e.clientX); }} + onPointerMove={(e) => { if (dragging.current) apply(e.clientX); }} + onPointerUp={() => { dragging.current = false; }} + style={{ position: 'relative', width, height: H, cursor: 'pointer', touchAction: 'none', flexShrink: 0 }} + > + + + +
+
+ ); +}; + +export const Knob: React.FC<{ value: number; onChange: (v: number) => void; size?: number }> = ({ value, onChange, size = 34 }) => { + const dragging = useRef<{ startY: number; startV: number } | null>(null); + const angle = -135 + value * 270; + return ( +
{ dragging.current = { startY: e.clientY, startV: value }; (e.target as HTMLElement).setPointerCapture?.(e.pointerId); }} + onPointerMove={(e) => { const d = dragging.current; if (!d) return; onChange(Math.min(1, Math.max(0, d.startV + (d.startY - e.clientY) / 120))); }} + onPointerUp={() => { dragging.current = null; }} + style={{ + position: 'relative', width: size + 10, height: size + 10, display: 'flex', alignItems: 'center', + justifyContent: 'center', cursor: 'ns-resize', touchAction: 'none', flexShrink: 0, + }} + > +
+
+
+
+
+ ); +}; diff --git a/frontend/src/shared/styles/grainTexture.ts b/frontend/src/shared/styles/grainTexture.ts new file mode 100644 index 00000000..30382438 --- /dev/null +++ b/frontend/src/shared/styles/grainTexture.ts @@ -0,0 +1,4 @@ +// Zen-style film grain: a static 220px PNG tile of sparse BIPOLAR speckle (some pixels lighten, +// some darken, most near-transparent), so at full slider it reads as texture in the paint rather +// than gray haze on top. Opacity IS the grain slider, exactly like Zen's --zen-grainy-background-opacity. +export const GRAIN_URL = 'url("./grain-texture.png")';