diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index 97719fcb2..fc93e49c0 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -1808,12 +1808,18 @@ html.onboarding-tour-start-transition::view-transition-new(root) { /* Frame 2 — Orchestration: child cards flash as bubbles land along the dashed paths fanning down from the coordinator. */ +/* Why: bubbles use percent-based left/top so they always travel from the + root card's right edge (34%) to the child card's left edge (64%), + regardless of preview width. The previous offset-path used hardcoded + pixels and drifted off the dashed lines once the preview resized. */ .feature-tour-orch-bubble { position: absolute; - top: 0; - left: 0; + left: 34%; + top: 50%; width: 12px; height: 12px; + margin-left: -6px; + margin-top: -6px; border-radius: 3px; background: var(--primary); color: var(--primary-foreground); @@ -1821,58 +1827,124 @@ html.onboarding-tour-start-transition::view-transition-new(root) { align-items: center; justify-content: center; opacity: 0; - offset-rotate: 0deg; z-index: 4; - animation: feature-tour-orch-travel var(--feature-tour-cycle) cubic-bezier(0.4, 0, 0.2, 1) +} +.feature-tour-orch-bubble.top { + animation: feature-tour-orch-travel-top var(--feature-tour-cycle) cubic-bezier(0.4, 0, 0.2, 1) infinite; -} -.feature-tour-orch-bubble.left { - offset-path: path('M 168 36 C 132 56, 102 66, 72 86'); - offset-anchor: center; animation-delay: 4s; } -.feature-tour-orch-bubble.right { - offset-path: path('M 168 36 C 204 56, 234 66, 264 86'); - offset-anchor: center; - animation-delay: 4s; +.feature-tour-orch-bubble.mid { + animation: feature-tour-orch-travel-mid var(--feature-tour-cycle) cubic-bezier(0.4, 0, 0.2, 1) + infinite; + animation-delay: calc(4s + 0.2s); } -.feature-tour-orch-bubble.left.b2 { - animation-delay: calc(4s + 0.55s); +.feature-tour-orch-bubble.bot { + animation: feature-tour-orch-travel-bot var(--feature-tour-cycle) cubic-bezier(0.4, 0, 0.2, 1) + infinite; + animation-delay: calc(4s + 0.4s); } -.feature-tour-orch-bubble.right.b2 { - animation-delay: calc(4s + 0.75s); -} -@keyframes feature-tour-orch-travel { +@keyframes feature-tour-orch-travel-top { 0% { - offset-distance: 0%; + left: 34%; + top: 50%; opacity: 0; } 2% { - offset-distance: 0%; + opacity: 1; + } + 3.5% { + left: 49%; + top: 50%; opacity: 1; } 6.5% { - offset-distance: 100%; + left: 64%; + top: 18%; opacity: 1; } 7.5% { - offset-distance: 100%; + left: 64%; + top: 18%; opacity: 0; } 25%, 100% { - offset-distance: 100%; + left: 64%; + top: 18%; + opacity: 0; + } +} +@keyframes feature-tour-orch-travel-mid { + 0% { + left: 34%; + top: 50%; + opacity: 0; + } + 2% { + opacity: 1; + } + 6.5% { + left: 64%; + top: 50%; + opacity: 1; + } + 7.5% { + left: 64%; + top: 50%; + opacity: 0; + } + 25%, + 100% { + left: 64%; + top: 50%; + opacity: 0; + } +} +@keyframes feature-tour-orch-travel-bot { + 0% { + left: 34%; + top: 50%; + opacity: 0; + } + 2% { + opacity: 1; + } + 3.5% { + left: 49%; + top: 50%; + opacity: 1; + } + 6.5% { + left: 64%; + top: 82%; + opacity: 1; + } + 7.5% { + left: 64%; + top: 82%; + opacity: 0; + } + 25%, + 100% { + left: 64%; + top: 82%; opacity: 0; } } .feature-tour-orch-child { animation: feature-tour-orch-child-flash var(--feature-tour-cycle) linear infinite; +} +.feature-tour-orch-child.top { animation-delay: 4s; } -.feature-tour-orch-child.right { +.feature-tour-orch-child.mid { animation-delay: calc(4s + 0.2s); } +.feature-tour-orch-child.bot { + animation-delay: calc(4s + 0.4s); +} @keyframes feature-tour-orch-child-flash { 0%, 6% { @@ -1897,14 +1969,18 @@ html.onboarding-tour-start-transition::view-transition-new(root) { /* Frame 3 — Tasks: cursor lands on the second row, the "Open" pill morphs to a dark "Start workspace →" pill, then a workspace card slides in. */ .feature-tour-tasks-cursor { + /* Why: anchored from the row's right edge so the cursor sits just past the + pill's right edge (12px row padding + ~6px past pill end) instead of + covering the label text. The arrow tip (top-left of the SVG) lands on the + dark pill's trailing edge with the body of the cursor extending right. */ position: absolute; - top: 0; - left: 0; + top: 50%; + right: 14px; width: 14px; height: 14px; pointer-events: none; z-index: 8; - transform: translate(20px, 8px); + transform: translate(120px, -30%); opacity: 0; animation: feature-tour-tasks-cursor var(--feature-tour-cycle) cubic-bezier(0.45, 0.05, 0.2, 1) infinite; @@ -1917,22 +1993,30 @@ html.onboarding-tour-start-transition::view-transition-new(root) { } @keyframes feature-tour-tasks-cursor { 0% { - transform: translate(20px, 8px); + transform: translate(160px, -10%); opacity: 0; } 2% { opacity: 1; } 5% { - transform: translate(120px, 60px); + transform: translate(60px, -30%); opacity: 1; } 8% { - transform: translate(238px, 60px); + transform: translate(0px, -30%); + opacity: 1; + } + 9% { + transform: translate(0px, -10%); + opacity: 1; + } + 10.5% { + transform: translate(0px, -30%); opacity: 1; } 23.5% { - transform: translate(238px, 60px); + transform: translate(0px, -30%); opacity: 1; } 24.5% { @@ -1940,7 +2024,7 @@ html.onboarding-tour-start-transition::view-transition-new(root) { } 25%, 100% { - transform: translate(238px, 60px); + transform: translate(0px, -30%); opacity: 0; } } @@ -1977,25 +2061,29 @@ html.onboarding-tour-start-transition::view-transition-new(root) { @keyframes feature-tour-tasks-pill-morph { 0%, 6% { - width: 36px; + width: 28px; background: rgb(34 197 94 / 0.14); border-color: rgb(34 197 94 / 0.3); + box-shadow: none; } 8% { - width: 110px; + width: 78px; background: var(--primary); border-color: var(--primary); + box-shadow: 0 1px 2px rgb(0 0 0 / 0.18); } 23.5% { - width: 110px; + width: 78px; background: var(--primary); border-color: var(--primary); + box-shadow: 0 1px 2px rgb(0 0 0 / 0.18); } 25%, 100% { - width: 36px; + width: 28px; background: rgb(34 197 94 / 0.14); border-color: rgb(34 197 94 / 0.3); + box-shadow: none; } } @@ -2027,11 +2115,15 @@ html.onboarding-tour-start-transition::view-transition-new(root) { /* Click ring fires when the cursor "clicks" the morphed pill on the tasks row. Positioned to overlay the right-side pill of the second task. */ .feature-tour-tasks-click-ring { + /* Why: ring is centered on the cursor tip (right=28 from row's right edge) + so the click feedback lands where the cursor is, on the arrow icon — not + centered on the pill, which would visually disconnect from the cursor. */ position: absolute; + top: 50%; right: 14px; - top: 50px; - width: 22px; - height: 22px; + width: 28px; + height: 28px; + margin-top: -14px; border-radius: 9999px; border: 1.5px solid color-mix(in srgb, var(--foreground) 55%, transparent); opacity: 0; diff --git a/src/renderer/src/components/feature-wall/FeatureTourPreview.tsx b/src/renderer/src/components/feature-wall/FeatureTourPreview.tsx index a0406b443..6297877cc 100644 --- a/src/renderer/src/components/feature-wall/FeatureTourPreview.tsx +++ b/src/renderer/src/components/feature-wall/FeatureTourPreview.tsx @@ -1,24 +1,13 @@ import type { JSX } from 'react' import { cn } from '@/lib/utils' import { ClaudeIcon, OpenCodeGoIcon } from '../status-bar/icons' - -function CodexInlineIcon(): JSX.Element { - return ( - - - - ) -} +import { + CodexInlineIcon, + CursorIcon, + MailGlyph, + WorkingSpinner +} from './feature-tour-preview-glyphs' +import { FeatureTourWorkspaceCard } from './FeatureTourWorkspaceCard' type FrameId = 1 | 2 | 3 | 4 @@ -54,214 +43,277 @@ export const FEATURE_TOUR_PREVIEW_COPY: readonly FeatureTourPreviewFrameCopy[] = } ] -function WorkingSpinner({ size = 'sm' }: { size?: 'sm' | 'xs' }): JSX.Element { - // Why: matches AgentStateDot's working indicator so the preview teaches - // the same state language users see in the real app. - const ring = size === 'xs' ? 'size-1.5 border' : 'size-2 border-2' - return ( - - ) -} - -function CursorIcon(): JSX.Element { - return ( - - - - ) -} - -function MailGlyph(): JSX.Element { - return ( - - - - - ) -} - function WorkspaceFrame(): JSX.Element { return ( -
-
+
+
Isolated workspaces
-
-
- - - fix login race condition - -
-
-
- - - -
-
- - - -
-
-
-
-
- - - speed up CI pipeline - -
-
-
- - - -
-
+ {/* Why: 3 cards in a row tells the "ship several at once" story by + composition; the wide preview aspect (~4.9:1) makes a vertical stack + read as wasted space. The grid auto-sizes (no flex-1) so the cards + don't stretch to the container's bottom edge — keeps headroom under + the dot indicators. */} +
+ + +
) } +type OrchChildAgent = 'claude' | 'codex' | 'opencode-go' + +const ORCH_CHILDREN: readonly { + key: 'top' | 'mid' | 'bot' + position: string + label: string + agent: OrchChildAgent +}[] = [ + // Why: card vertical centers anchor to 18% / 50% / 82% — the same Y + // endpoints the dashed SVG paths terminate at — so the connectors land on + // each card's center regardless of card height. + { key: 'top', position: 'top-[18%] -translate-y-1/2', label: 'PR 1/3', agent: 'claude' }, + { key: 'mid', position: 'top-1/2 -translate-y-1/2', label: 'PR 2/3', agent: 'codex' }, + { key: 'bot', position: 'top-[82%] -translate-y-1/2', label: 'PR 3/3', agent: 'opencode-go' } +] + function OrchestrationFrame(): JSX.Element { + // Why: a horizontal fan (root → 3 children L→R) reads naturally as + // "fans out and ships parallel PRs" at the wide aspect; the previous + // top-down tree wasted the horizontal space. SVG paths are sized to a + // 600×130 viewBox and stretched non-uniformly, so the dashed lines flex + // with the container while the absolutely-positioned cards stay aligned + // to viewport-relative anchors (root left, children right column). return ( -
-
+
+
Agent orchestration
+ {/* Why: viewBox is percent-units (100×100, preserveAspectRatio="none") + so endpoints anchor to the same percentage anchors as the cards + and the bubbles — root right edge at 34%, child left edge at 64%, + child Y centers at 18%/50%/82%. Explicit width/height attrs are + required because an SVG with a 1:1 viewBox and only inset-0 + otherwise picks its intrinsic 1:1 aspect for height. */} + {/* Why: vectorEffect is NOT inheritable in SVG, so the + non-scaling-stroke attribute must live on each path. Hoisting + it onto let preserveAspectRatio="none" stretch the dashes + into trapezoids on the diagonal connectors. */} + -
- redesign auth flow - - + {/* Why: parent matches WorkspaceCard composition — spinner + title row, + then a Claude agent row underneath — so the user reads it as "a + workspace running Claude as the orchestrator," consistent with how + workspaces look elsewhere in the app. */} +
+
+ + + redesign auth flow + +
+
+ + + + orchestrating 3 agents + +
-
- 2 children -
+ {/* Why: children mirror the parent's WorkspaceCard composition so the + fan reads as "coordinator workspace dispatches to 3 child + workspaces, each running its own agent." */} + {ORCH_CHILDREN.map(({ key, position, label, agent }) => ( +
+
+ + + {label} + +
+
+ + {agent === 'claude' ? ( + + ) : agent === 'codex' ? ( + + ) : ( + + )} + +
+
+ ))} -
- PR 1/2 - -
-
- PR 2/2 - -
- -
- -
-
- -
-
- -
-
- -
+ {ORCH_CHILDREN.map(({ key }) => ( +
+ +
+ ))}
) } function TasksFrame(): JSX.Element { + // Why: a left→right pipeline reads as "pick from backlog → workspace + // appears" in one glance. The wide aspect lets the backlog and the + // resulting workspace card sit side-by-side instead of stacked, which + // makes the cause/effect visible in the composition itself. return ( -
-
- GitHub & Linear tasks +
+
+ GitHub & Linear tasks
-
- - #1799 - - - Bulk archive in source control - - +
+
+
+ + GH #1799 + + {/* Why: surrounding rows show only the issue number + a skeleton + so the user's eye is drawn to the row that has real text — the + one the cursor clicks on. */} + +
+
+ + GH #1842 + + + Worktree picker truncates + + + + Start + + + + + + + {/* Why: cursor + ring live inside the row so they anchor to the + pill's right-edge ml-auto, instead of using fixed pixel offsets + that drift when the preview is resized. */} + + + + +
+
+ + LIN-329 + + +
+
+ +
+
+ + + fix/worktree-picker-truncates + + + + +
+
+ + + +
+
Linked to GH #1842
+
-
- - #1842 - - - Worktree picker truncates - - - - Start workspace → - - -
-
- - - fix/worktree-picker-truncates - - - - -
- - - -
) } function TerminalFrame(): JSX.Element { return ( -
-
+
+
Splittable terminal
-
+
- + orca · zsh
-
+
$ @@ -302,7 +354,7 @@ function TerminalFrame(): JSX.Element { className="feature-tour-terminal-output flex min-w-0 items-center gap-1.5" data-line="1" > - + session started
+
+ {status === 'working' ? ( + + ) : ( + + )} + + {title} + +
+
+ {agents.map((agent, idx) => ( +
+ {agent.state === 'working' ? ( + + ) : ( + + )} + {agent.kind === 'claude' ? ( + + ) : agent.kind === 'codex' ? ( + + ) : ( + + )} + +
+ ))} +
+
+ ) +} diff --git a/src/renderer/src/components/feature-wall/feature-tour-preview-glyphs.tsx b/src/renderer/src/components/feature-wall/feature-tour-preview-glyphs.tsx new file mode 100644 index 000000000..9acf5a321 --- /dev/null +++ b/src/renderer/src/components/feature-wall/feature-tour-preview-glyphs.tsx @@ -0,0 +1,57 @@ +import type { JSX } from 'react' +import { cn } from '@/lib/utils' + +export function CodexInlineIcon(): JSX.Element { + return ( + + + + ) +} + +export function WorkingSpinner({ size = 'sm' }: { size?: 'sm' | 'xs' }): JSX.Element { + // Why: matches AgentStateDot's working indicator so the preview teaches + // the same state language users see in the real app. + const ring = size === 'xs' ? 'size-1.5 border' : 'size-2 border-2' + return ( + + ) +} + +export function CursorIcon(): JSX.Element { + return ( + + + + ) +} + +export function MailGlyph(): JSX.Element { + return ( + + + + + ) +} diff --git a/src/renderer/src/components/onboarding/OnboardingFlow.test.tsx b/src/renderer/src/components/onboarding/OnboardingFlow.test.tsx index df74d4760..106524714 100644 --- a/src/renderer/src/components/onboarding/OnboardingFlow.test.tsx +++ b/src/renderer/src/components/onboarding/OnboardingFlow.test.tsx @@ -32,14 +32,14 @@ describe('OnboardingFlow', () => { ) expect(html).toContain('Explore Orca') - expect(html).toContain('Preview the core workflow.') - expect(html).toContain('Run agents in isolated worktrees.') - expect(html).toContain('Orchestrate agents to finish larger tasks.') - expect(html).toContain('Start tasks from GitHub or Linear.') - expect(html).toContain('Send webpage elements to agents from the Orca browser.') - expect(html).not.toContain('Write and preview Markdown.') - expect(html).toContain('items-start') - expect(html).toContain('text-left') + expect(html).toContain('Take a 60-second tour of Orca's advanced features.') + expect(html).toContain('Take the tour') + // Why: the prior intro carried a redundant lead, a four-item checklist, and + // a help-menu footnote. The tour animation already conveys all of that, so + // the body is now just the preview + a single CTA — guard against drift. + expect(html).not.toContain('Preview the core workflow.') + expect(html).not.toContain('Run agents in isolated worktrees.') + expect(html).not.toContain('Available later under Help') expect(html).toContain('Continue') expect(html).toContain('Skip to project setup') expect(html).not.toContain('Skip the tour') diff --git a/src/renderer/src/components/onboarding/OnboardingFlow.tsx b/src/renderer/src/components/onboarding/OnboardingFlow.tsx index b6a87412f..eefacda5f 100644 --- a/src/renderer/src/components/onboarding/OnboardingFlow.tsx +++ b/src/renderer/src/components/onboarding/OnboardingFlow.tsx @@ -40,7 +40,7 @@ const stepCopy = { }, tour: { title: 'Explore Orca', - subtitle: '' + subtitle: "Take a 60-second tour of Orca's advanced features." }, repo: { title: 'Point Orca at some code', diff --git a/src/renderer/src/components/onboarding/OnboardingTourStep.tsx b/src/renderer/src/components/onboarding/OnboardingTourStep.tsx index af99cd7b2..b5a809390 100644 --- a/src/renderer/src/components/onboarding/OnboardingTourStep.tsx +++ b/src/renderer/src/components/onboarding/OnboardingTourStep.tsx @@ -1,19 +1,12 @@ import type { JSX } from 'react' import { flushSync } from 'react-dom' -import { ArrowRight, Check } from 'lucide-react' +import { ArrowRight } from 'lucide-react' import { Button } from '@/components/ui/button' import type { FeatureWallTourDepthSummary } from '../../../../shared/feature-wall-tour-depth' import { FeatureTourPreview } from '../feature-wall/FeatureTourPreview' import { FeatureWallTourSurface } from '../feature-wall/FeatureWallTourSurface' import { usePrefersReducedMotion } from '../feature-wall/feature-wall-modal-helpers' -const TOUR_LEARNING_POINTS: readonly string[] = [ - 'Run agents in isolated worktrees.', - 'Orchestrate agents to finish larger tasks.', - 'Start tasks from GitHub or Linear.', - 'Send webpage elements to agents from the Orca browser.' -] - type OnboardingTourStepProps = { tourStarted: boolean busyLabel: string | null @@ -100,41 +93,12 @@ export function OnboardingTourStep({ } return ( -
-
-
-

Preview the core workflow.

-
    - {TOUR_LEARNING_POINTS.map((point) => ( -
  • - - - - {point} -
  • - ))} -
-
-
- -
- -
~ 60 seconds
-
-
-
- -

- Available later under Help > Explore Orca. -

+
+ +
) }