Redesign tour intro preview (#2852)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
69986f2ff5
commit
656dab3afd
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<svg
|
||||
width={12}
|
||||
height={12}
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden
|
||||
focusable="false"
|
||||
className="text-foreground"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M9.205 8.658v-2.26c0-.19.072-.333.238-.428l4.543-2.616c.619-.357 1.356-.523 2.117-.523 2.854 0 4.662 2.212 4.662 4.566 0 .167 0 .357-.024.547l-4.71-2.759a.797.797 0 00-.856 0l-5.97 3.473zm10.609 8.8V12.06c0-.333-.143-.57-.429-.737l-5.97-3.473 1.95-1.118a.433.433 0 01.476 0l4.543 2.617c1.309.76 2.189 2.378 2.189 3.948 0 1.808-1.07 3.473-2.76 4.163zM7.802 12.703l-1.95-1.142c-.167-.095-.239-.238-.239-.428V5.899c0-2.545 1.95-4.472 4.591-4.472 1 0 1.927.333 2.712.928L8.23 5.067c-.285.166-.428.404-.428.737v6.898zM12 15.128l-2.795-1.57v-3.33L12 8.658l2.795 1.57v3.33L12 15.128zm1.796 7.23c-1 0-1.927-.332-2.712-.927l4.686-2.712c.285-.166.428-.404.428-.737v-6.898l1.974 1.142c.167.095.238.238.238.428v5.233c0 2.545-1.974 4.472-4.614 4.472zm-5.637-5.303l-4.544-2.617c-1.308-.761-2.188-2.378-2.188-3.948A4.482 4.482 0 014.21 6.327v5.423c0 .333.143.571.428.738l5.947 3.449-1.95 1.118a.432.432 0 01-.476 0zm-.262 3.9c-2.688 0-4.662-2.021-4.662-4.519 0-.19.024-.38.047-.57l4.686 2.71c.286.167.571.167.856 0l5.97-3.448v2.26c0 .19-.07.333-.237.428l-4.543 2.616c-.619.357-1.356.523-2.117.523zm5.899 2.83a5.947 5.947 0 005.827-4.756C22.287 18.339 24 15.84 24 13.296c0-1.665-.713-3.282-1.998-4.448.119-.5.19-.999.19-1.498 0-3.401-2.759-5.947-5.946-5.947-.642 0-1.26.095-1.88.31A5.962 5.962 0 0010.205 0a5.947 5.947 0 00-5.827 4.757C1.713 5.447 0 7.945 0 10.49c0 1.666.713 3.283 1.998 4.448-.119.5-.19 1-.19 1.499 0 3.401 2.759 5.946 5.946 5.946.642 0 1.26-.095 1.88-.309a5.96 5.96 0 004.162 1.713z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
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 (
|
||||
<span
|
||||
className={cn(
|
||||
'inline-block shrink-0 rounded-full border-yellow-500 border-t-transparent animate-spin',
|
||||
ring
|
||||
)}
|
||||
aria-hidden
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CursorIcon(): JSX.Element {
|
||||
return (
|
||||
<svg viewBox="0 0 16 16" aria-hidden focusable="false">
|
||||
<path
|
||||
d="M2 1.5 L2 12 L5 9 L7.2 14.5 L9.5 13.6 L7.3 8 L11.5 8 Z"
|
||||
className="fill-background stroke-foreground"
|
||||
strokeWidth={1}
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function MailGlyph(): JSX.Element {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden width="9" height="9" fill="none" strokeWidth={1.6}>
|
||||
<rect x="3" y="5" width="18" height="14" rx="2" stroke="currentColor" />
|
||||
<path d="M3 7l9 6 9-6" stroke="currentColor" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function WorkspaceFrame(): JSX.Element {
|
||||
return (
|
||||
<div className="absolute inset-0 flex flex-col gap-1.5 bg-card p-3">
|
||||
<div className="text-[10px] font-semibold uppercase tracking-[0.07em] leading-none text-muted-foreground">
|
||||
<div className="absolute inset-0 flex flex-col gap-5 bg-card px-4 py-4">
|
||||
<div className="text-[14.5px] font-semibold uppercase tracking-[0.07em] leading-none text-muted-foreground">
|
||||
Isolated workspaces
|
||||
</div>
|
||||
<div className="rounded-md border border-border bg-background px-2 py-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<WorkingSpinner />
|
||||
<span className="truncate text-[11px] font-medium leading-none text-foreground">
|
||||
fix login race condition
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1.5 flex flex-col gap-1.5 pl-3.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<WorkingSpinner size="xs" />
|
||||
<ClaudeIcon size={12} />
|
||||
<span className="h-1.5 w-[55%] rounded-full bg-foreground/15" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<WorkingSpinner size="xs" />
|
||||
<CodexInlineIcon />
|
||||
<span className="h-1.5 w-[48%] rounded-full bg-foreground/15" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-md border border-border/70 bg-background px-2 py-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="size-2 rounded-full bg-emerald-500" />
|
||||
<span className="truncate text-[11px] leading-none text-muted-foreground">
|
||||
speed up CI pipeline
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1.5 flex flex-col gap-1.5 pl-3.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-block size-1.5 rounded-full bg-emerald-500" />
|
||||
<OpenCodeGoIcon size={12} />
|
||||
<span className="h-1.5 w-[42%] rounded-full bg-foreground/15" />
|
||||
</div>
|
||||
</div>
|
||||
{/* 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. */}
|
||||
<div className="grid grid-cols-3 gap-3 px-4">
|
||||
<FeatureTourWorkspaceCard
|
||||
status="working"
|
||||
title="fix login race condition"
|
||||
agents={[
|
||||
{ kind: 'claude', barWidth: '60%', state: 'working' },
|
||||
{ kind: 'codex', barWidth: '52%', state: 'working' }
|
||||
]}
|
||||
/>
|
||||
<FeatureTourWorkspaceCard
|
||||
status="done"
|
||||
title="speed up CI pipeline"
|
||||
agents={[{ kind: 'opencode-go', barWidth: '70%', state: 'done' }]}
|
||||
/>
|
||||
<FeatureTourWorkspaceCard
|
||||
status="working"
|
||||
title="refactor billing webhook"
|
||||
agents={[{ kind: 'claude', barWidth: '38%', state: 'working' }]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="absolute inset-0 flex flex-col gap-1.5 bg-card p-3">
|
||||
<div className="text-[10px] font-semibold uppercase tracking-[0.07em] leading-none text-muted-foreground">
|
||||
<div className="absolute inset-0 flex flex-col gap-5 bg-card px-4 py-4">
|
||||
<div className="text-[14.5px] font-semibold uppercase tracking-[0.07em] leading-none text-muted-foreground">
|
||||
Agent orchestration
|
||||
</div>
|
||||
<div className="relative w-full flex-1">
|
||||
{/* 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. */}
|
||||
<svg
|
||||
className="pointer-events-none absolute inset-0 text-foreground/20"
|
||||
viewBox="0 0 332 130"
|
||||
className="pointer-events-none absolute inset-0 text-foreground/30"
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="none"
|
||||
width="100%"
|
||||
height="100%"
|
||||
aria-hidden
|
||||
>
|
||||
{/* Why: vectorEffect is NOT inheritable in SVG, so the
|
||||
non-scaling-stroke attribute must live on each path. Hoisting
|
||||
it onto <g> let preserveAspectRatio="none" stretch the dashes
|
||||
into trapezoids on the diagonal connectors. */}
|
||||
<path
|
||||
d="M 166 36 C 130 56, 100 66, 72 86"
|
||||
d="M 34 50 C 49 50, 49 18, 64 18"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
strokeDasharray="2 3"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
<path
|
||||
d="M 166 36 C 202 56, 232 66, 260 86"
|
||||
d="M 34 50 L 64 50"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
strokeDasharray="2 3"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
<path
|
||||
d="M 34 50 C 49 50, 49 82, 64 82"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
strokeDasharray="2 3"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div className="absolute left-12 right-12 top-1 flex h-7 items-center gap-2 rounded-md border border-border bg-background px-2.5">
|
||||
<span className="font-mono text-[9.5px] text-muted-foreground">redesign auth flow</span>
|
||||
<span className="h-1.5 flex-1 rounded-full bg-foreground/12" />
|
||||
<WorkingSpinner />
|
||||
{/* 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. */}
|
||||
<div className="absolute left-0 top-1/2 flex w-[34%] -translate-y-1/2 flex-col rounded-md border border-border bg-background px-3 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<WorkingSpinner />
|
||||
<span className="truncate text-[15px] font-medium leading-none text-foreground">
|
||||
redesign auth flow
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-1.5 pl-3.5">
|
||||
<WorkingSpinner size="xs" />
|
||||
<ClaudeIcon size={13} />
|
||||
<span className="truncate text-[12.5px] leading-none text-muted-foreground">
|
||||
orchestrating 3 agents
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute left-1/2 top-[42px] -translate-x-1/2 rounded-full border border-border bg-muted px-1.5 py-px font-sans text-[9.5px] leading-none text-muted-foreground">
|
||||
2 children
|
||||
</div>
|
||||
{/* 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 }) => (
|
||||
<div
|
||||
key={key}
|
||||
className={cn(
|
||||
'feature-tour-orch-child absolute right-0 flex w-[36%] flex-col rounded-md border border-border bg-background px-3 py-2',
|
||||
key,
|
||||
position
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<WorkingSpinner />
|
||||
<span className="truncate font-mono text-[14px] font-medium leading-none text-foreground">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-1.5 pl-3.5">
|
||||
<WorkingSpinner size="xs" />
|
||||
{agent === 'claude' ? (
|
||||
<ClaudeIcon size={12} />
|
||||
) : agent === 'codex' ? (
|
||||
<CodexInlineIcon />
|
||||
) : (
|
||||
<OpenCodeGoIcon size={12} />
|
||||
)}
|
||||
<span className="h-2 flex-1 rounded-full bg-foreground/15" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="feature-tour-orch-child absolute left-0 top-[78px] flex h-7 w-[120px] items-center gap-2 rounded-md border border-border bg-background px-2.5">
|
||||
<span className="font-mono text-[10px] leading-none text-muted-foreground">PR 1/2</span>
|
||||
<span className="h-1.5 flex-1 rounded-full bg-foreground/12" />
|
||||
</div>
|
||||
<div className="feature-tour-orch-child right absolute right-0 top-[78px] flex h-7 w-[120px] items-center gap-2 rounded-md border border-border bg-background px-2.5">
|
||||
<span className="font-mono text-[10px] leading-none text-muted-foreground">PR 2/2</span>
|
||||
<span className="h-1.5 flex-1 rounded-full bg-foreground/12" />
|
||||
</div>
|
||||
|
||||
<div className="feature-tour-orch-bubble left">
|
||||
<MailGlyph />
|
||||
</div>
|
||||
<div className="feature-tour-orch-bubble right">
|
||||
<MailGlyph />
|
||||
</div>
|
||||
<div className="feature-tour-orch-bubble left b2">
|
||||
<MailGlyph />
|
||||
</div>
|
||||
<div className="feature-tour-orch-bubble right b2">
|
||||
<MailGlyph />
|
||||
</div>
|
||||
{ORCH_CHILDREN.map(({ key }) => (
|
||||
<div key={`bubble-${key}`} className={cn('feature-tour-orch-bubble', key)}>
|
||||
<MailGlyph />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="absolute inset-0 flex flex-col gap-1 bg-card px-3 pb-3 pt-2.5">
|
||||
<div className="px-2.5 pb-1 text-[10px] font-semibold uppercase tracking-[0.07em] leading-none text-muted-foreground">
|
||||
GitHub & Linear tasks
|
||||
<div className="absolute inset-0 flex flex-col gap-5 bg-card px-4 py-4">
|
||||
<div className="text-[14.5px] font-semibold uppercase tracking-[0.07em] leading-none text-muted-foreground">
|
||||
GitHub & Linear tasks
|
||||
</div>
|
||||
<div className="grid h-[26px] grid-cols-[34px_minmax(0,1fr)_auto] items-center gap-2.5 rounded-md px-2.5">
|
||||
<span className="inline-flex h-3.5 w-[30px] items-center justify-center rounded-[3px] border border-border bg-muted font-mono text-[9px] leading-none text-muted-foreground">
|
||||
#1799
|
||||
</span>
|
||||
<span className="truncate text-[11px] leading-none text-muted-foreground">
|
||||
Bulk archive in source control
|
||||
</span>
|
||||
<span className="h-4 w-9 rounded-full border border-emerald-500/30 bg-emerald-500/15" />
|
||||
<div className="relative grid flex-1 grid-cols-[minmax(0,1fr)_minmax(0,1fr)] items-center gap-4 px-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex h-9 items-center gap-2.5 rounded-md border border-border bg-background px-3">
|
||||
<span className="inline-flex h-5 items-center justify-center rounded-[3px] border border-border bg-muted px-1.5 font-mono text-[13px] leading-none text-muted-foreground">
|
||||
GH #1799
|
||||
</span>
|
||||
{/* 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. */}
|
||||
<span className="h-2 w-[60%] rounded-full bg-foreground/12" />
|
||||
</div>
|
||||
<div className="feature-tour-tasks-row relative flex h-9 items-center gap-2.5 rounded-md border border-border bg-background px-3">
|
||||
<span className="inline-flex h-5 items-center justify-center rounded-[3px] border border-border bg-muted px-1.5 font-mono text-[13px] leading-none text-muted-foreground">
|
||||
GH #1842
|
||||
</span>
|
||||
<span className="truncate text-[15px] font-medium leading-none text-foreground">
|
||||
Worktree picker truncates
|
||||
</span>
|
||||
<span className="feature-tour-tasks-pill relative ml-auto flex h-6 items-center justify-center overflow-hidden rounded-full border border-emerald-500/30 bg-emerald-500/15">
|
||||
<span className="feature-tour-tasks-pill-label flex items-center gap-1 whitespace-nowrap pl-3 pr-2.5 text-[13px] font-semibold leading-none tracking-[0.01em] text-primary-foreground">
|
||||
Start
|
||||
<svg
|
||||
width="11"
|
||||
height="11"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden
|
||||
>
|
||||
<path d="M3 8h10" />
|
||||
<path d="M9 4l4 4-4 4" />
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
{/* 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. */}
|
||||
<span className="feature-tour-tasks-cursor">
|
||||
<CursorIcon />
|
||||
</span>
|
||||
<span className="feature-tour-tasks-click-ring" aria-hidden />
|
||||
</div>
|
||||
<div className="flex h-9 items-center gap-2.5 rounded-md border border-border bg-background px-3">
|
||||
<span className="inline-flex h-5 items-center justify-center rounded-[3px] border border-border bg-muted px-1.5 font-mono text-[13px] leading-none text-muted-foreground">
|
||||
LIN-329
|
||||
</span>
|
||||
<span className="h-2 w-[45%] rounded-full bg-foreground/12" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="feature-tour-tasks-workspace flex flex-col gap-2 rounded-md border border-border bg-background px-4 py-2.5">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<WorkingSpinner />
|
||||
<span className="truncate text-[15.5px] font-medium leading-none text-foreground">
|
||||
fix/worktree-picker-truncates
|
||||
</span>
|
||||
<span className="ml-auto inline-flex">
|
||||
<ClaudeIcon size={13} />
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5 pl-4">
|
||||
<WorkingSpinner size="xs" />
|
||||
<ClaudeIcon size={12} />
|
||||
<span className="h-2 w-[55%] rounded-full bg-foreground/15" />
|
||||
</div>
|
||||
<div className="text-[13.5px] leading-none text-muted-foreground">Linked to GH #1842</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="feature-tour-tasks-row relative grid h-[26px] grid-cols-[34px_minmax(0,1fr)_auto] items-center gap-2.5 rounded-md px-2.5">
|
||||
<span className="inline-flex h-3.5 w-[30px] items-center justify-center rounded-[3px] border border-border bg-muted font-mono text-[9px] leading-none text-muted-foreground">
|
||||
#1842
|
||||
</span>
|
||||
<span className="truncate text-[11px] font-medium leading-none text-foreground">
|
||||
Worktree picker truncates
|
||||
</span>
|
||||
<span className="feature-tour-tasks-pill relative flex h-4 items-center justify-center overflow-hidden rounded-full border border-emerald-500/30 bg-emerald-500/15">
|
||||
<span className="feature-tour-tasks-pill-label whitespace-nowrap text-[10px] font-medium leading-none text-primary-foreground">
|
||||
Start workspace →
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="feature-tour-tasks-workspace mt-1.5 flex items-center gap-2 rounded-md border border-border bg-background px-2.5 py-1.5">
|
||||
<WorkingSpinner />
|
||||
<span className="truncate text-[10.5px] font-medium leading-none text-foreground">
|
||||
fix/worktree-picker-truncates
|
||||
</span>
|
||||
<span className="ml-auto inline-flex items-center gap-1">
|
||||
<ClaudeIcon size={11} />
|
||||
</span>
|
||||
</div>
|
||||
<span className="feature-tour-tasks-cursor">
|
||||
<CursorIcon />
|
||||
</span>
|
||||
<span className="feature-tour-tasks-click-ring" aria-hidden />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TerminalFrame(): JSX.Element {
|
||||
return (
|
||||
<div className="absolute inset-0 flex flex-col gap-1.5 bg-card p-3">
|
||||
<div className="text-[10px] font-semibold uppercase tracking-[0.07em] leading-none text-muted-foreground">
|
||||
<div className="absolute inset-0 flex flex-col gap-5 bg-card px-4 py-4">
|
||||
<div className="text-[14.5px] font-semibold uppercase tracking-[0.07em] leading-none text-muted-foreground">
|
||||
Splittable terminal
|
||||
</div>
|
||||
<div className="flex h-[136px] flex-col overflow-hidden rounded-md border border-border bg-background">
|
||||
<div className="mx-4 flex min-h-0 flex-1 flex-col overflow-hidden rounded-md border border-border bg-background">
|
||||
<div className="flex items-center gap-1.5 border-b border-border bg-muted/40 px-2 py-1">
|
||||
<span className="size-1.5 rounded-full bg-foreground/15" />
|
||||
<span className="size-1.5 rounded-full bg-foreground/15" />
|
||||
<span className="size-1.5 rounded-full bg-foreground/15" />
|
||||
<span className="ml-2 font-mono text-[9px] leading-none text-muted-foreground">
|
||||
<span className="ml-2 font-mono text-[13.5px] leading-none text-muted-foreground">
|
||||
orca · zsh
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid flex-1 grid-cols-2 divide-x divide-border font-mono text-[10px] leading-[1.4] text-foreground">
|
||||
<div className="grid flex-1 grid-cols-2 divide-x divide-border font-mono text-[14.5px] leading-[1.4] text-foreground">
|
||||
<div className="min-w-0 p-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-emerald-500">$</span>
|
||||
|
|
@ -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"
|
||||
>
|
||||
<ClaudeIcon size={10} />
|
||||
<ClaudeIcon size={12} />
|
||||
<span className="truncate text-muted-foreground">session started</span>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -331,7 +383,7 @@ export function FeatureTourPreview(props: { className?: string }): JSX.Element {
|
|||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative h-[180px] w-full overflow-hidden rounded-lg border border-border bg-muted/40',
|
||||
'relative h-[260px] w-full overflow-hidden rounded-lg border border-border bg-muted/40',
|
||||
props.className
|
||||
)}
|
||||
aria-hidden
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
import type { JSX } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { ClaudeIcon, OpenCodeGoIcon } from '../status-bar/icons'
|
||||
import { CodexInlineIcon, WorkingSpinner } from './feature-tour-preview-glyphs'
|
||||
|
||||
export type FeatureTourWorkspaceCardAgent = {
|
||||
kind: 'claude' | 'codex' | 'opencode-go'
|
||||
barWidth: string
|
||||
state: 'working' | 'done'
|
||||
}
|
||||
|
||||
export function FeatureTourWorkspaceCard({
|
||||
status,
|
||||
title,
|
||||
agents
|
||||
}: {
|
||||
status: 'working' | 'done'
|
||||
title: string
|
||||
agents: readonly FeatureTourWorkspaceCardAgent[]
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex min-w-0 flex-col rounded-md border bg-background px-3 py-2',
|
||||
status === 'done' ? 'border-border/70' : 'border-border'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{status === 'working' ? (
|
||||
<WorkingSpinner />
|
||||
) : (
|
||||
<span className="size-2 rounded-full bg-emerald-500" />
|
||||
)}
|
||||
<span
|
||||
className={cn(
|
||||
'truncate text-[16.5px] leading-none',
|
||||
status === 'done' ? 'text-muted-foreground' : 'font-medium text-foreground'
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-col gap-1.5 pl-3.5">
|
||||
{agents.map((agent, idx) => (
|
||||
<div key={idx} className="flex items-center gap-1.5">
|
||||
{agent.state === 'working' ? (
|
||||
<WorkingSpinner size="xs" />
|
||||
) : (
|
||||
<span className="inline-block size-1.5 rounded-full bg-emerald-500" />
|
||||
)}
|
||||
{agent.kind === 'claude' ? (
|
||||
<ClaudeIcon size={13} />
|
||||
) : agent.kind === 'codex' ? (
|
||||
<CodexInlineIcon />
|
||||
) : (
|
||||
<OpenCodeGoIcon size={13} />
|
||||
)}
|
||||
<span className="h-2 rounded-full bg-foreground/15" style={{ width: agent.barWidth }} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import type { JSX } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function CodexInlineIcon(): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width={14}
|
||||
height={14}
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden
|
||||
focusable="false"
|
||||
className="text-foreground"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M9.205 8.658v-2.26c0-.19.072-.333.238-.428l4.543-2.616c.619-.357 1.356-.523 2.117-.523 2.854 0 4.662 2.212 4.662 4.566 0 .167 0 .357-.024.547l-4.71-2.759a.797.797 0 00-.856 0l-5.97 3.473zm10.609 8.8V12.06c0-.333-.143-.57-.429-.737l-5.97-3.473 1.95-1.118a.433.433 0 01.476 0l4.543 2.617c1.309.76 2.189 2.378 2.189 3.948 0 1.808-1.07 3.473-2.76 4.163zM7.802 12.703l-1.95-1.142c-.167-.095-.239-.238-.239-.428V5.899c0-2.545 1.95-4.472 4.591-4.472 1 0 1.927.333 2.712.928L8.23 5.067c-.285.166-.428.404-.428.737v6.898zM12 15.128l-2.795-1.57v-3.33L12 8.658l2.795 1.57v3.33L12 15.128zm1.796 7.23c-1 0-1.927-.332-2.712-.927l4.686-2.712c.285-.166.428-.404.428-.737v-6.898l1.974 1.142c.167.095.238.238.238.428v5.233c0 2.545-1.974 4.472-4.614 4.472zm-5.637-5.303l-4.544-2.617c-1.308-.761-2.188-2.378-2.188-3.948A4.482 4.482 0 014.21 6.327v5.423c0 .333.143.571.428.738l5.947 3.449-1.95 1.118a.432.432 0 01-.476 0zm-.262 3.9c-2.688 0-4.662-2.021-4.662-4.519 0-.19.024-.38.047-.57l4.686 2.71c.286.167.571.167.856 0l5.97-3.448v2.26c0 .19-.07.333-.237.428l-4.543 2.616c-.619.357-1.356.523-2.117.523zm5.899 2.83a5.947 5.947 0 005.827-4.756C22.287 18.339 24 15.84 24 13.296c0-1.665-.713-3.282-1.998-4.448.119-.5.19-.999.19-1.498 0-3.401-2.759-5.947-5.946-5.947-.642 0-1.26.095-1.88.31A5.962 5.962 0 0010.205 0a5.947 5.947 0 00-5.827 4.757C1.713 5.447 0 7.945 0 10.49c0 1.666.713 3.283 1.998 4.448-.119.5-.19 1-.19 1.499 0 3.401 2.759 5.946 5.946 5.946.642 0 1.26-.095 1.88-.309a5.96 5.96 0 004.162 1.713z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<span
|
||||
className={cn(
|
||||
'inline-block shrink-0 rounded-full border-yellow-500 border-t-transparent animate-spin',
|
||||
ring
|
||||
)}
|
||||
aria-hidden
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function CursorIcon(): JSX.Element {
|
||||
return (
|
||||
<svg viewBox="0 0 16 16" aria-hidden focusable="false">
|
||||
<path
|
||||
d="M2 1.5 L2 12 L5 9 L7.2 14.5 L9.5 13.6 L7.3 8 L11.5 8 Z"
|
||||
className="fill-background stroke-foreground"
|
||||
strokeWidth={1}
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function MailGlyph(): JSX.Element {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden width="9" height="9" fill="none" strokeWidth={1.6}>
|
||||
<rect x="3" y="5" width="18" height="14" rx="2" stroke="currentColor" />
|
||||
<path d="M3 7l9 6 9-6" stroke="currentColor" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="flex h-full min-h-[430px] flex-col">
|
||||
<div className="grid w-full grid-cols-1 items-start gap-10 md:grid-cols-[1fr_minmax(0,340px)]">
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-sm font-medium text-foreground">Preview the core workflow.</p>
|
||||
<ul className="flex flex-col gap-2.5">
|
||||
{TOUR_LEARNING_POINTS.map((point) => (
|
||||
<li key={point} className="flex items-start gap-3">
|
||||
<span className="mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full bg-foreground/[0.06] text-foreground">
|
||||
<Check className="size-2.5" strokeWidth={3} />
|
||||
</span>
|
||||
<span className="text-sm leading-snug text-foreground">{point}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-3">
|
||||
<FeatureTourPreview className="w-full" />
|
||||
<div className="rounded-lg border border-border bg-muted/40 p-3">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={handleStartTour}
|
||||
disabled={Boolean(busyLabel)}
|
||||
className="w-full gap-2"
|
||||
>
|
||||
Take the tour
|
||||
<ArrowRight className="size-4" />
|
||||
</Button>
|
||||
<div className="mt-2 text-center text-xs text-muted-foreground">~ 60 seconds</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mt-auto max-w-[560px] text-left text-xs leading-relaxed text-muted-foreground">
|
||||
Available later under Help > Explore Orca.
|
||||
</p>
|
||||
<div className="flex flex-col gap-5">
|
||||
<FeatureTourPreview />
|
||||
<Button onClick={handleStartTour} disabled={Boolean(busyLabel)} className="gap-2 self-start">
|
||||
Take the tour
|
||||
<ArrowRight className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue