[eric] onboarding: small-detail pass (themed checkbox, chunky torn edge, themed stage, real traffic lights, crossfade transitions)

This commit is contained in:
ciregenz
2026-07-15 13:07:45 -07:00
parent 410ba60b0e
commit a771280fa9
5 changed files with 30 additions and 12 deletions
@@ -55,7 +55,7 @@ const IntroBeat: React.FC<{ c: ClaudeTokens; line: string; sub?: string; onNext:
<div style={{ position: 'relative', width: '100%', height: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', background: c.bg.inverse, overflow: 'hidden' }}>
<motion.div
initial={{ scale: 0.35, opacity: 0, x: 0, y: 0 }}
animate={{ scale: [0.35, 1, 1.06, 1], opacity: [0, 0.55, 0.5, 0.55], x: [0, 0, 22, 0], y: [0, 0, -14, 0] }}
animate={{ scale: [0.35, 1, 1.06, 1], opacity: [0, 0.62, 0.56, 0.62], x: [0, 0, 22, 0], y: [0, 0, -14, 0] }}
transition={{ duration: 9, times: [0, 0.16, 0.6, 1], ease: 'easeInOut' }}
style={{
position: 'absolute', width: 560, height: 560, borderRadius: 999,
@@ -173,14 +173,14 @@ const OnboardingV3Root: React.FC = () => {
background: c.bg.page,
}}
>
<AnimatePresence mode="wait">
<AnimatePresence initial={false}>
<motion.div
key={beat}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.32 }}
style={{ width: '100%', height: '100%' }}
transition={{ duration: 0.35, ease: 'easeInOut' }}
style={{ position: 'absolute', inset: 0 }}
>
{beat === 'welcome' && <IntroBeat c={c} line="Welcome." onNext={() => setBeat('newos')} />}
{beat === 'newos' && <IntroBeat c={c} line="This is your new OS." onNext={() => setBeat('connect')} />}
@@ -206,8 +206,8 @@ const OnboardingV3Root: React.FC = () => {
transition={{ duration: 0.3 }}
style={{ position: 'absolute', top: 13, left: 14, display: 'flex', gap: 7, pointerEvents: 'none' }}
>
{[0, 1, 2].map((i) => (
<span key={i} style={{ width: 11, height: 11, borderRadius: 999, background: 'rgba(255,255,255,0.22)' }} />
{['#FF5F57', '#FEBC2E', '#28C840'].map((dot) => (
<span key={dot} style={{ width: 11, height: 11, borderRadius: 999, background: dot, opacity: 0.9 }} />
))}
</motion.div>
{finishing && (
@@ -43,8 +43,8 @@ const BeatApps: React.FC<{
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 7, padding: '9px 12px', borderBottom: `1px solid ${c.border.subtle}`, background: c.bg.elevated }}>
{[0, 1, 2].map((i) => (
<span key={i} style={{ width: 10, height: 10, borderRadius: 999, background: c.border.strong }} />
{['#FF5F57', '#FEBC2E', '#28C840'].map((dot) => (
<span key={dot} style={{ width: 10, height: 10, borderRadius: 999, background: dot, opacity: 0.85 }} />
))}
<span style={{ marginLeft: 8, fontSize: '0.72rem', fontWeight: 600, color: c.text.tertiary, letterSpacing: '0.04em' }}>OpenSwarm</span>
</div>
@@ -1,5 +1,6 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { motion } from 'framer-motion';
import { Check } from 'lucide-react';
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
import { API_BASE } from '@/shared/config';
import { fetchModels } from '@/shared/state/modelsSlice';
@@ -101,7 +102,6 @@ const BeatConnect: React.FC<{
display: 'flex', alignItems: 'center', gap: 14, padding: '13px 14px 13px 16px', textAlign: 'left',
borderRadius: c.radius.md, border: `1px solid ${filled ? c.accent.primary : c.border.medium}`,
background: c.bg.surface, cursor: connected ? 'default' : 'pointer', fontFamily: 'inherit',
boxShadow: c.shadow.sm,
}}
>
<span style={{
@@ -135,7 +135,18 @@ const BeatConnect: React.FC<{
</motion.div>
)}
<label style={{ display: 'flex', alignItems: 'flex-start', gap: 9, marginTop: 8, cursor: 'pointer', fontSize: '0.82rem', color: c.text.tertiary, lineHeight: 1.5 }}>
<input type="checkbox" checked={scanConsent} onChange={(e) => setScanConsent(e.target.checked)} style={{ marginTop: 2 }} />
<button
onClick={(e) => { e.preventDefault(); setScanConsent(!scanConsent); }}
style={{
width: 16, height: 16, marginTop: 2, borderRadius: 5, padding: 0, cursor: 'pointer', flexShrink: 0,
border: `1.5px solid ${scanConsent ? c.accent.primary : c.border.strong}`,
background: scanConsent ? c.accent.primary : 'transparent',
display: 'flex', alignItems: 'center', justifyContent: 'center',
transition: 'background 120ms ease, border-color 120ms ease',
}}
>
{scanConsent && <Check size={11} color="#fff" strokeWidth={3.2} />}
</button>
<span>Take a quick local look around to personalize my setup. Nothing leaves this Mac.</span>
</label>
<div style={{ display: 'flex', gap: 18, marginTop: 2 }}>
@@ -1,13 +1,14 @@
import React, { useEffect, useState } from 'react';
import { motion } from 'framer-motion';
import { ArrowLeft } from 'lucide-react';
import { useThemeAccent } from '@/shared/styles/ThemeContext';
import type { ClaudeTokens } from '@/shared/styles/claudeTokens';
// 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: 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: 50 }, (unused, i) => `calc(100% - 6px) ${i * 2 + 1}%, 100% ${i * 2 + 2}%`).join(', ')}, 0 100%)`;
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%)`;
const SPRING = { type: 'spring' as const, stiffness: 260, damping: 26 };
@@ -22,6 +23,10 @@ const BeatShell: React.FC<{
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.
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}`;
// 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(() => {
@@ -87,8 +92,10 @@ const BeatShell: React.FC<{
style={{
position: 'relative', flex: 1, minWidth: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
padding: 36, boxSizing: 'border-box', overflow: 'auto',
background: stageWash,
}}
>
<div style={{ position: 'absolute', inset: 0, backgroundImage: GRAIN_URL, opacity: 0.08, pointerEvents: 'none' }} />
<div style={{ position: 'relative', width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{children}
</div>
@@ -224,7 +224,7 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
position: 'absolute',
inset: 0,
pointerEvents: 'none',
background: `linear-gradient(115deg, ${gradient.map((hex, i) => `${hex}24 ${(i / (gradient.length - 1)) * 100}%`).join(', ')})`,
background: `linear-gradient(115deg, ${gradient.map((hex, i) => `${hex}2b ${(i / (gradient.length - 1)) * 100}%`).join(', ')})`,
}}
/>
)}