From 0d821879241c18f98fa28e95a0c36ebf8bef10c0 Mon Sep 17 00:00:00 2001 From: Makisuo Date: Sat, 31 Oct 2026 10:58:01 +0100 Subject: [PATCH 1/3] style(web): polish onboarding from the first screen to the topbar checklist Create org shares the quick start's column and header, so nothing jumps between them. Region drops the second mascot and the red selected state, plan leads with the trial instead of repeating "Pick a plan", the team list no longer bleeds to the panel edge, and the last screen keeps amber for its one button. The topbar checklist loses its numbered circles and most of its amber; the overview setup card drops the rocket. New /lab/onboarding steps (org, region) and /lab/onboarding-checklist show every state without an API. --- .../src/lab/entries/onboarding-checklist.ts | 135 +++++++++++++++ apps/web/src/lab/entries/onboarding.ts | 97 ++++++++--- apps/web/src/lab/registry.ts | 8 + apps/web/src/lib/onboarding-stack.ts | 4 +- .../src/page/onboarding/create-org-view.ts | 46 +++--- .../page/onboarding/create-org.scene.test.ts | 2 +- apps/web/src/page/onboarding/option-card.ts | 33 ++-- .../src/page/onboarding/pricing-cards-view.ts | 8 +- .../onboarding/quick-start-live.scene.test.ts | 6 +- .../onboarding/quick-start-region-view.ts | 62 +++---- .../page/onboarding/quick-start-team-view.ts | 52 +++--- .../src/page/onboarding/quick-start-view.ts | 156 +++++++++++------- .../src/page/overview/setup-checklist-view.ts | 42 +++-- .../overview/setup-checklist.scene.test.ts | 8 +- .../src/shell/onboarding-checklist-view.ts | 115 +++++++++---- 15 files changed, 524 insertions(+), 250 deletions(-) create mode 100644 apps/web/src/lab/entries/onboarding-checklist.ts diff --git a/apps/web/src/lab/entries/onboarding-checklist.ts b/apps/web/src/lab/entries/onboarding-checklist.ts new file mode 100644 index 0000000000..56a1d1666d --- /dev/null +++ b/apps/web/src/lab/entries/onboarding-checklist.ts @@ -0,0 +1,135 @@ +import type { V2OnboardingChecklist, V2OnboardingChecklistStep } from "@maple/domain/http/v2" +import { Effect, Schema } from "effect" +import type { Html, HtmlBuilder } from "foldkit/html" +import * as OnboardingChecklist from "../../shell/onboarding-checklist" +import * as OnboardingChecklistView from "../../shell/onboarding-checklist-view" +import { defineLabWithFlags } from "../define" + +// `/lab/onboarding-checklist`: the top bar's credits pill with its one-time pointer, then the panel in +// each state, over fixtures. Nothing is claimed. + +const HOUR = 3_600_000 + +const step = ( + id: V2OnboardingChecklistStep["id"], + title: string, + completed: boolean, + optional = false, +): V2OnboardingChecklistStep => ({ + object: "onboarding_checklist_step", + id, + title, + completed, + href: "/lab/onboarding-checklist", + optional, +}) + +const checklistOf = ( + status: V2OnboardingChecklist["status"], + done: ReadonlyArray, + nowMs: number, +): V2OnboardingChecklist => { + const steps = [ + step("send_telemetry", "Send your first telemetry", done[0] ?? false), + step("connect_github", "Connect GitHub", done[1] ?? false), + step("create_alert_rule", "Create an alert with a destination", done[2] ?? false), + step("connect_mcp_agent", "Connect an MCP agent", done[3] ?? false), + step("invite_teammate", "Invite a teammate", done[4] ?? false, true), + ] + const required = steps.filter((entry) => !entry.optional) + return { + object: "onboarding_checklist", + status, + reward_amount_usd: 30, + deadline_at: new Date(nowMs + 17 * HOUR + 42 * 60_000).toISOString(), + claimed_at: status === "claimed" ? new Date(nowMs).toISOString() : null, + completed_count: required.filter((entry) => entry.completed).length, + total_count: required.length, + steps, + } +} + +export const Model = Schema.Struct({ nowMs: Schema.Number }) +export type Model = typeof Model.Type + +/** The pill's own messages, dropped: the lab only draws its states. */ +type Message = OnboardingChecklist.Message + +const modelOf = ( + nowMs: number, + checklist: V2OnboardingChecklist, + isAdmin = true, +): OnboardingChecklist.Model => ({ + ...OnboardingChecklist.init(), + checklist, + isAdmin, + isSeen: false, + nowMs, +}) + +const frame = (h: HtmlBuilder, label: string, children: ReadonlyArray): Html => + h.section( + [h.Class("flex flex-col gap-2")], + [ + h.span([h.Class("text-xs text-muted-foreground")], [label]), + h.div( + [ + h.Class( + "w-[22rem] overflow-hidden rounded-lg border bg-popover text-popover-foreground shadow-popup", + ), + ], + children, + ), + ], + ) + +const panelOf = (h: HtmlBuilder, model: OnboardingChecklist.Model): ReadonlyArray => + model.checklist === null ? [] : OnboardingChecklistView.checklistPanel(h, model, model.checklist) + +export const view = (model: Model, h: HtmlBuilder): Html => { + const inProgress = modelOf( + model.nowMs, + checklistOf("in_progress", [true, true, false, false, false], model.nowMs), + ) + const claimable = modelOf( + model.nowMs, + checklistOf("claimable", [true, true, true, true, false], model.nowMs), + ) + const claimed = { + ...modelOf(model.nowMs, checklistOf("claimed", [true, true, true, true, true], model.nowMs)), + isJustClaimed: true, + } + return h.div( + [h.Class("flex min-h-dvh flex-col gap-10 bg-background p-10 text-foreground")], + [ + h.div( + [h.Class("flex justify-end pb-24")], + [ + h.submodel({ + slotId: "lab-onboarding-checklist-pill", + model: inProgress, + view: OnboardingChecklistView.view, + toParentMessage: (message) => message, + }), + ], + ), + h.div( + [h.Class("flex flex-wrap items-start gap-8")], + [ + frame(h, "In progress", panelOf(h, inProgress)), + frame(h, "Every step done", panelOf(h, claimable)), + frame(h, "Claimed", panelOf(h, claimed)), + ], + ), + ], + ) +} + +export const lab = defineLabWithFlags("Onboarding checklist", { + Model, + Flags: Schema.Struct({ nowMs: Schema.Number }), + flags: Effect.sync(() => ({ nowMs: Date.now() })), + init: (flags) => ({ model: { nowMs: flags.nowMs } }), + update: (model) => ({ model }), + view, +}) diff --git a/apps/web/src/lab/entries/onboarding.ts b/apps/web/src/lab/entries/onboarding.ts index 5a47c91b44..a6ea758fd3 100644 --- a/apps/web/src/lab/entries/onboarding.ts +++ b/apps/web/src/lab/entries/onboarding.ts @@ -2,6 +2,8 @@ import { CatalogPlan, CatalogPlanItem, CatalogPlanItemPrice } from "@maple/domai import { Effect, Schema } from "effect" import { Command, Submodel } from "foldkit" import type { Html, HtmlBuilder } from "foldkit/html" +import * as CreateOrg from "../../page/onboarding/create-org" +import * as CreateOrgView from "../../page/onboarding/create-org-view" import * as PricingCardsView from "../../page/onboarding/pricing-cards-view" import * as QuickStartTeamView from "../../page/onboarding/quick-start-team-view" import * as QuickStartView from "../../page/onboarding/quick-start-view" @@ -18,7 +20,8 @@ import { DEFAULT_QUICK_START_STATE, STEP_IDS } from "../../page/onboarding/quick import { ready, TeamMessage, updateTeam } from "../../page/onboarding/quick-start-team" // `/lab/onboarding`: the real quick-start screen with a sample catalog; checkout, rename and invites are -// simulated and nothing is saved. `?step=stack` opens a later step, Restart starts over. +// simulated and nothing is saved. `?step=stack` opens a later step (`org` is the create-org screen, +// `region` the region step), Restart starts over. const meteredItem = ( featureId: string, @@ -51,8 +54,14 @@ export const PLANS = [ }), ] +const LAB_STEPS = ["org", "region", ...STEP_IDS] as const +type LabStep = (typeof LAB_STEPS)[number] + export const Model = Schema.Struct({ quickStart: QuickStart.Model, + /** The create-org screen, shown instead of the quick start until its form is sent. */ + createOrg: Schema.NullOr(CreateOrg.Model), + needsRegion: Schema.Boolean, /** Picking a sample plan stands in for checkout. */ hasPlan: Schema.Boolean, isComplete: Schema.Boolean, @@ -65,18 +74,38 @@ export const Message = defineMessageUnion({ GotStepMessage: { message: QuickStart.Message }, ClickedRestart: {}, SimulatedTeamOperation: { message: TeamMessage }, + GotCreateOrgMessage: { message: CreateOrg.Message }, }) export type Message = typeof Message.Type -const Step = Schema.Literals(STEP_IDS) +const Step = Schema.Literals(LAB_STEPS) + +const SAMPLE_ORG: CreateOrg.Model = { + _tag: "OrgRequired", + name: "Acme", + suggestedFrom: "acme.com", + region: "us", + isCreating: false, + errorMessage: null, + invitations: [], + joiningId: null, + openingId: null, + createdOrgId: null, + invitationsError: null, +} + +const quickStartStep = (step: LabStep): (typeof STEP_IDS)[number] => + step === "org" || step === "region" ? "plan" : step -export const init = (step: (typeof STEP_IDS)[number], run = 0): Model => ({ +export const init = (step: LabStep, run = 0): Model => ({ quickStart: { ...QuickStart.init("lab-onboarding").model, - state: { ...DEFAULT_QUICK_START_STATE, activeStep: step }, + state: { ...DEFAULT_QUICK_START_STATE, activeStep: quickStartStep(step) }, team: ready("Acme", [], true, null), }, - hasPlan: step !== "plan", + createOrg: step === "org" ? SAMPLE_ORG : null, + needsRegion: step === "org" || step === "region", + hasPlan: quickStartStep(step) !== "plan", isComplete: false, run, }) @@ -138,6 +167,8 @@ const stepMessage = (model: Model, message: QuickStart.Message): Return => { return message.message._tag === "GotSnippetCopyMessage" ? copyStep(model, message.message.snippet, message.message.message) : { model: screenStep(model, message) } + case "ClickedRegionContinue": + return { model: { ...model, needsRegion: false } } case "GotPricingMessage": return message.message._tag === "ClickedPlan" ? { model: { ...model, hasPlan: true } } : { model } case "ClickedContinue": @@ -151,10 +182,25 @@ const stepMessage = (model: Model, message: QuickStart.Message): Return => { export const update = (model: Model, message: Message): Return => Message.match(message, { GotStepMessage: ({ message: child }) => stepMessage(model, child), - ClickedRestart: () => ({ model: init("plan", model.run + 1) }), + ClickedRestart: () => ({ model: init("org", model.run + 1) }), SimulatedTeamOperation: ({ message: child }) => teamStep(model, child), + GotCreateOrgMessage: ({ message: child }) => createOrgStep(model, child), }) +/** The create-org form's own edits; sending it moves on to the region step, nothing is created. */ +const createOrgStep = (model: Model, message: CreateOrg.Message): Return => { + if (model.createOrg === null) return { model } + switch (message._tag) { + case "ChangedName": + case "PickedRegion": + return { model: { ...model, createOrg: CreateOrg.update(model.createOrg, message).model } } + case "SubmittedCreate": + return model.createOrg.name.trim() === "" ? { model } : { model: { ...model, createOrg: null } } + default: + return { model } + } +} + // VIEW const plansView = Submodel.defineView((_, h) => @@ -188,7 +234,7 @@ const stepView = Submodel.defineView( separator(h, "vertical", "h-4"), linkButton(h, { href: "/lab", size: "xs", variant: "ghost" }, ["Exit preview"]), ]), - needsRegion: false, + needsRegion: model.needsRegion, hasPlan: model.hasPlan, orgName: "Acme", teamStep: () => QuickStartTeamView.teamStep(h, model.quickStart.team), @@ -205,25 +251,36 @@ const stepView = Submodel.defineView( }) ?? h.empty, ) +const createOrgView = Submodel.defineView((model, h) => + CreateOrgView.view(h, model, "you@acme.com", []), +) + export const view = (model: Model, h: HtmlBuilder): Html => model.isComplete ? complete(h) - : h.div( - [h.Key(String(model.run))], - [ - h.submodel({ - slotId: "lab-onboarding-step", - model, - view: stepView, - toParentMessage: (message) => Message.GotStepMessage({ message }), - }), - ], - ) + : model.createOrg !== null + ? h.submodel({ + slotId: "lab-onboarding-org", + model: model.createOrg, + view: createOrgView, + toParentMessage: (message) => Message.GotCreateOrgMessage({ message }), + }) + : h.div( + [h.Key(String(model.run))], + [ + h.submodel({ + slotId: "lab-onboarding-step", + model, + view: stepView, + toParentMessage: (message) => Message.GotStepMessage({ message }), + }), + ], + ) /** `?step=stack` opens the preview on a later step. */ -const initialStep = (search: string): (typeof STEP_IDS)[number] => +const initialStep = (search: string): LabStep => Schema.decodeUnknownOption(Step)(new URLSearchParams(search).get("step")).pipe((step) => - step._tag === "Some" ? step.value : "plan", + step._tag === "Some" ? step.value : "org", ) export const lab = defineLabWithFlags("Onboarding", { diff --git a/apps/web/src/lab/registry.ts b/apps/web/src/lab/registry.ts index 581abf464a..52cd26bb83 100644 --- a/apps/web/src/lab/registry.ts +++ b/apps/web/src/lab/registry.ts @@ -108,6 +108,14 @@ export const LAB_ENTRIES: ReadonlyArray = [ kind: "lab", session: "none", }, + { + path: "/lab/onboarding-checklist", + title: "Onboarding checklist", + description: + "The top bar's credits pill and its checklist panel in progress, claimable and claimed, over fixtures.", + kind: "lab", + session: "none", + }, { path: "/lab/loaders", title: "Chat loaders", diff --git a/apps/web/src/lib/onboarding-stack.ts b/apps/web/src/lib/onboarding-stack.ts index 61d99c894f..7523851833 100644 --- a/apps/web/src/lib/onboarding-stack.ts +++ b/apps/web/src/lib/onboarding-stack.ts @@ -172,7 +172,7 @@ export const STACK_PLATFORMS = { aws: { label: "AWS", description: "CloudWatch", icon: "AwsIcon", fills: ["infrastructure", "metrics"] }, gcp: { label: "Google Cloud", - description: "Logging, monitoring", + description: "Logs, metrics", icon: "GoogleCloudIcon", fills: ["logs", "metrics"], }, @@ -196,7 +196,7 @@ export const STACK_PLATFORMS = { }, prometheus: { label: "Prometheus", - description: "Scrape an endpoint", + description: "Scrape targets", icon: "PrometheusIcon", fills: ["metrics"], }, diff --git a/apps/web/src/page/onboarding/create-org-view.ts b/apps/web/src/page/onboarding/create-org-view.ts index 1328d00ff0..847d79c96f 100644 --- a/apps/web/src/page/onboarding/create-org-view.ts +++ b/apps/web/src/page/onboarding/create-org-view.ts @@ -71,7 +71,7 @@ const nameField = (h: H, model: Model): Html => [ h.AriaHidden(true), h.Class( - "flex size-12 shrink-0 items-center justify-center rounded-lg bg-primary font-display text-xl font-semibold text-primary-foreground", + "flex size-12 shrink-0 items-center justify-center rounded-lg border border-border bg-surface-strong font-display text-xl font-semibold text-foreground", ), ], [initialOf(model.name)], @@ -80,7 +80,7 @@ const nameField = (h: H, model: Model): Html => id: "org-required-name", size: "lg", className: "flex-1", - controlClassName: "h-12 text-base", + controlClassName: "h-12 leading-12 text-base sm:h-12 sm:leading-12 sm:text-base", isDisabled: model.isCreating || model.createdOrgId !== null, attributes: [ h.Maxlength(100), @@ -213,7 +213,7 @@ const membershipRow = (h: H, model: Model, organization: ClerkOrganization): Htm const form = (h: H, model: Model, memberships: ReadonlyArray): Html => h.form( [ - h.Class("flex flex-col gap-9"), + h.Class("flex flex-col gap-8"), h.AriaLabel("Create your organization"), h.OnSubmit(Message.SubmittedCreate()), ], @@ -221,12 +221,11 @@ const form = (h: H, model: Model, memberships: ReadonlyArray) nameField(h, model), ...(hasMultipleRegions ? [regionField(h, model)] : []), h.div( - [h.Class("flex flex-col gap-4")], + [h.Class("flex flex-col gap-3")], [ - h.p( - [h.AriaLive("polite"), h.Class(cn("min-h-4 text-xs", TONE_TEXT.crit))], - model.errorMessage === null ? [] : [model.errorMessage], - ), + ...(model.errorMessage === null + ? [] + : [h.p([h.Class(cn("text-xs", TONE_TEXT.crit))], [model.errorMessage])]), button( h, { @@ -273,14 +272,10 @@ const workspacePreview = (h: H, model: Model): Html => h.div( [h.AriaHidden(true), h.Class("relative flex w-full max-w-[640px] flex-col items-center gap-5")], [ - h.span([h.Class("text-sm font-medium text-foreground")], ["A peek at your new workspace"]), + h.span([h.Class("text-xs text-muted-foreground")], ["A peek at your new workspace"]), h.div( [h.Class("w-full overflow-hidden rounded-md border border-border bg-background")], [ - h.div( - [h.Class("flex h-9 items-center gap-1.5 border-b border-border bg-sidebar px-3.5")], - [0, 1, 2].map(() => h.span([h.Class("size-2.5 rounded-full bg-input")], [])), - ), h.div( [h.Class("flex h-[420px]")], [ @@ -294,14 +289,14 @@ const workspacePreview = (h: H, model: Model): Html => h.div( [ h.Class( - "flex items-center gap-2 rounded-md border border-primary bg-sidebar-accent p-1.5", + "flex items-center gap-2 rounded-md border border-border bg-sidebar-accent p-1.5", ), ], [ h.span( [ h.Class( - "flex size-6 shrink-0 items-center justify-center rounded-sm bg-primary text-xs font-semibold text-primary-foreground", + "flex size-6 shrink-0 items-center justify-center rounded-sm border border-border bg-surface-strong text-xs font-semibold text-foreground", ), ], [initialOf(model.name)], @@ -350,7 +345,7 @@ const workspacePreview = (h: H, model: Model): Html => ], [ h.span( - [h.Class("text-primary")], + [h.Class("text-muted-foreground")], [icon(h, "FrameworkOpenTelemetryIcon", { size: 28 })], ), h.span( @@ -383,30 +378,33 @@ export const view = ( [h.Class("flex h-dvh overflow-hidden bg-background text-foreground")], [ h.div( - [h.Class("flex h-dvh w-full shrink-0 flex-col lg:w-[640px]")], + [h.Class("flex h-dvh w-full shrink-0 flex-col lg:w-[760px]")], [ - h.div([h.Class("shrink-0 px-6 pt-8 sm:px-12 lg:px-20 lg:pt-12")], [header(h, email)]), + h.div([h.Class("shrink-0 px-6 py-5 sm:px-12 lg:px-16")], [header(h, email)]), h.div( [ h.Class( - "min-h-0 flex-1 overflow-y-auto overscroll-contain px-6 pb-10 sm:px-12 lg:px-20", + "min-h-0 flex-1 overflow-y-auto overscroll-contain px-6 pb-10 sm:px-12 lg:px-16", ), ], [ h.div( - [h.Class("flex flex-col gap-10 pt-12")], + [h.Class("flex max-w-lg flex-col gap-10 pt-4")], [ h.div( [h.Class("flex flex-col gap-3")], [ - h.p([h.Class("text-sm text-primary")], ["Welcome to Maple"]), + h.p( + [h.Class("text-sm text-muted-foreground")], + ["Welcome to Maple"], + ), h.h1( [ h.Class( - "font-display text-4xl font-semibold leading-[1.1] tracking-tight", + "font-display text-3xl font-semibold leading-tight tracking-tight", ), ], - ["Let's give your team a home."], + ["Give your team a home"], ), h.p( [ @@ -415,7 +413,7 @@ export const view = ( ), ], [ - "Your traces, logs, dashboards and alerts all live in one organization. It takes ten seconds.", + "Your traces, logs, dashboards and alerts live in one organization.", ], ), ], diff --git a/apps/web/src/page/onboarding/create-org.scene.test.ts b/apps/web/src/page/onboarding/create-org.scene.test.ts index 24e2adc56c..4f09756d0f 100644 --- a/apps/web/src/page/onboarding/create-org.scene.test.ts +++ b/apps/web/src/page/onboarding/create-org.scene.test.ts @@ -62,7 +62,7 @@ describe("create your first organization", () => { { update, view }, given(init(user).model), mounted, - expectView(role("heading", { name: "Let's give your team a home." })).toExist(), + expectView(role("heading", { name: "Give your team a home" })).toExist(), expectView(nameField).toHaveValue("Acme"), expectView(text("We guessed from acme.dev. Rename it whenever you like.")).toExist(), type(nameField, "Acme Labs"), diff --git a/apps/web/src/page/onboarding/option-card.ts b/apps/web/src/page/onboarding/option-card.ts index fcbd55a431..8dcb294ae5 100644 --- a/apps/web/src/page/onboarding/option-card.ts +++ b/apps/web/src/page/onboarding/option-card.ts @@ -82,10 +82,6 @@ export type OptionCardOptions = Readonly<{ layout?: "row" | "stacked" /** Content pinned to the bottom of a `stacked` card. */ footer?: Html - /** Hex colour that replaces primary for the selected border and tint. */ - accent?: string - /** Classes for the selected check, paired with `accent`. */ - checkClassName?: string }> /** `OptionCard`: a hidden native input behind a bordered card. */ @@ -94,7 +90,7 @@ export const optionCard = (h: HtmlBuilder, options: OptionCardOptions): const check = drawnCheck( h, options.isChecked, - cn(layout === "row" && "mt-0.5", options.isChecked ? options.checkClassName : options.type === "radio" && "opacity-0"), + cn(layout === "row" && "mt-0.5", !options.isChecked && options.type === "radio" && "opacity-0"), ) const body: ReadonlyArray = layout === "row" @@ -104,7 +100,10 @@ export const optionCard = (h: HtmlBuilder, options: OptionCardOptions): [h.Class("min-w-0 flex-1 pt-px")], [ h.span([h.Class("block text-sm font-semibold")], [options.title]), - h.span([h.Class("mt-1 block text-xs leading-relaxed text-muted-foreground")], [options.description]), + h.span( + [h.Class("mt-1 block text-xs leading-relaxed text-muted-foreground")], + [options.description], + ), ], ), check, @@ -114,13 +113,18 @@ export const optionCard = (h: HtmlBuilder, options: OptionCardOptions): h.div( [h.Class("space-y-1")], [ - h.span([h.Class("block text-base font-semibold tracking-tight")], [options.title]), + h.span( + [h.Class("block text-base font-semibold tracking-tight")], + [options.title], + ), h.span([h.Class("block text-sm text-muted-foreground")], [options.description]), ], ), - ...(options.footer === undefined ? [] : [h.div([h.Class("mt-auto flex")], [options.footer])]), + ...(options.footer === undefined + ? [] + : [h.div([h.Class("mt-auto flex")], [options.footer])]), ] - const isAccented = options.isChecked && options.accent !== undefined + return h.label( [h.Class("group relative cursor-pointer")], [ @@ -135,14 +139,13 @@ export const optionCard = (h: HtmlBuilder, options: OptionCardOptions): ]), h.div( [ - ...(isAccented ? [h.Style({ "border-color": options.accent ?? "", "background-color": `color-mix(in oklab, ${options.accent ?? ""} 6%, transparent)` })] : []), h.Class( cn( "flex h-full rounded-md border transition-colors duration-150 motion-reduce:transition-none peer-focus-visible:ring-2 peer-focus-visible:ring-ring peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-background", layout === "row" ? "items-start gap-3 p-4" : "flex-col gap-5 p-5", !options.isChecked ? "border-border group-hover:border-border-strong group-hover:bg-surface-faint" - : options.accent === undefined && "border-primary bg-primary-a1", + : "border-primary bg-primary-a1", ), ), ], @@ -179,7 +182,9 @@ export const pixelGlyph = (h: HtmlBuilder, name: PixelGlyphName, isSelecte h.Class( cn( "transition-colors duration-200 motion-reduce:transition-none", - isSelected ? "text-primary/40" : "text-muted-foreground group-hover:text-foreground", + isSelected + ? "text-primary/40" + : "text-muted-foreground group-hover:text-foreground", ), ), ], @@ -187,7 +192,9 @@ export const pixelGlyph = (h: HtmlBuilder, name: PixelGlyphName, isSelecte ), h.g( [h.Class("text-primary")], - segments.map((d) => segmentPath(h, d, [h.Style({ opacity: isSelected ? "1" : "0" })])), + segments.map((d) => + segmentPath(h, d, [h.Style({ opacity: isSelected ? "1" : "0" })]), + ), ), ], ), diff --git a/apps/web/src/page/onboarding/pricing-cards-view.ts b/apps/web/src/page/onboarding/pricing-cards-view.ts index b0160d1494..a57d49b5d2 100644 --- a/apps/web/src/page/onboarding/pricing-cards-view.ts +++ b/apps/web/src/page/onboarding/pricing-cards-view.ts @@ -55,9 +55,7 @@ const dataRow = (h: H, feature: FeatureRow, isDetailed: boolean): Html => { h.div( [h.Class("text-muted-foreground flex items-center gap-2.5")], [ - ...(iconName === undefined - ? [] - : [icon(h, iconName, { className: "size-4" })]), + ...(iconName === undefined ? [] : [icon(h, iconName, { className: "size-4" })]), h.span([h.Class("font-medium")], [feature.label]), ], ), @@ -214,7 +212,7 @@ const enterpriseCard = (h: H): Html => cardHeader(h, [ h.div( [h.Class("flex items-center justify-between gap-2")], - [cardTitle(h, ["Enterprise"], eyebrowVariants({ className: "text-primary" }))], + [cardTitle(h, ["Enterprise"], eyebrowVariants())], ), h.div( [h.Class("mt-3 flex items-baseline gap-1")], @@ -256,7 +254,7 @@ const enterpriseCard = (h: H): Html => "mt-auto flex-col gap-2 items-stretch", ), ], - "flex flex-col border-primary-a3 bg-primary/[0.02]", + "flex flex-col", ) /** `PricingCards`: skeletons while the catalog loads, its error row, or the plan grid. */ diff --git a/apps/web/src/page/onboarding/quick-start-live.scene.test.ts b/apps/web/src/page/onboarding/quick-start-live.scene.test.ts index 9fe381e843..2962ee34b4 100644 --- a/apps/web/src/page/onboarding/quick-start-live.scene.test.ts +++ b/apps/web/src/page/onboarding/quick-start-live.scene.test.ts @@ -40,7 +40,7 @@ describe("you're live step", () => { scene( { update, view }, given(atLive([], {})), - expectView(role("heading", { name: "Acme is ready for its first data." })).toExist(), + expectView(role("heading", { name: "Acme is ready for its first data" })).toExist(), expectView(text("Connect Cloudflare")).toExist(), expectView(text("Your first span, log or metric")).toExist(), ) @@ -50,7 +50,7 @@ describe("you're live step", () => { scene( { update, view }, given(atLive([], { cloudflare: true })), - expectView(role("heading", { name: "Acme is ready for its first data." })).toExist(), + expectView(role("heading", { name: "Acme is ready for its first data" })).toExist(), expectView(text("Connect Cloudflare")).not.toExist(), expectView(text("Connect AWS")).toExist(), ) @@ -60,7 +60,7 @@ describe("you're live step", () => { scene( { update, view }, given(atLive(["traces"], {})), - expectView(role("heading", { name: "Acme is live." })).toExist(), + expectView(role("heading", { name: "Acme is live" })).toExist(), expectView(text("Your first span, log or metric")).not.toExist(), ) }) diff --git a/apps/web/src/page/onboarding/quick-start-region-view.ts b/apps/web/src/page/onboarding/quick-start-region-view.ts index aa6409a328..ee7a97a4df 100644 --- a/apps/web/src/page/onboarding/quick-start-region-view.ts +++ b/apps/web/src/page/onboarding/quick-start-region-view.ts @@ -3,7 +3,6 @@ import { TONE_TEXT } from "@maple/ui/lib/tone" import { cn } from "@maple/ui/lib/utils" import type { Html, HtmlBuilder } from "foldkit/html" import { button } from "../../ui/button" -import { eyebrow } from "../../ui/eyebrow" import { icon } from "../../ui/icon" import { optionCard } from "./option-card" import { Message, type RegionChoice } from "./quick-start" @@ -19,39 +18,25 @@ export const REGION_CARDS = { name: "United States", place: "North America", residency: "Data stays in the US", - accent: "var(--color-region-us)", - check: "border-region-us bg-region-us text-white", + dot: "bg-region-us", }, eu: { name: "European Union", place: "Frankfurt, Germany", residency: "Data stays in the EU", - accent: "var(--color-region-eu)", - check: "border-region-eu bg-region-eu text-white", + dot: "bg-region-eu", }, -} as const satisfies Record +} as const satisfies Record -const residency = (h: H, region: Region, isActive: boolean): Html => { - const card = REGION_CARDS[region] - return h.span( +/** The residency line, its dot in the region's map colour. */ +const residency = (h: H, region: Region): Html => + h.span( + [h.Class("inline-flex items-center gap-1.5 text-xs text-muted-foreground")], [ - h.Class( - "inline-flex items-center gap-1.5 rounded-full border border-border px-2 py-0.5 text-2xs font-medium text-muted-foreground", - ), - ...(isActive - ? [h.Style({ - color: card.accent, - "border-color": `color-mix(in oklab, ${card.accent} 40%, transparent)`, - "background-color": `color-mix(in oklab, ${card.accent} 8%, transparent)`, - })] - : []), - ], - [ - h.span([h.AriaHidden(true), h.Class("size-1.5 rounded-full"), h.Style({ "background-color": card.accent })], []), - card.residency, + h.span([h.AriaHidden(true), h.Class(cn("size-1.5 rounded-full", REGION_CARDS[region].dot))], []), + REGION_CARDS[region].residency, ], ) -} const regionCard = (h: H, choice: RegionChoice, region: Region): Html => { const isActive = choice.selected === region @@ -66,8 +51,7 @@ const regionCard = (h: H, choice: RegionChoice, region: Region): Html => { label: card.name, title: card.name, description: card.place, - accent: card.accent, - checkClassName: card.check, + media: regionFlag( h, region, @@ -76,26 +60,27 @@ const regionCard = (h: H, choice: RegionChoice, region: Region): Html => { !isActive && "opacity-70 saturate-50 group-hover:opacity-100 group-hover:saturate-100", ), ), - footer: residency(h, region, isActive), + footer: residency(h, region), }) } /** `StepRegion`. */ export const regionStep = (h: H, choice: RegionChoice): Html => h.div( - [h.Class("flex flex-1 flex-col items-center justify-center overflow-auto px-6 py-12")], + [h.Class("flex flex-1 flex-col items-center justify-center px-6 pb-12 pt-4 sm:px-12")], [ h.div( [h.Class("flex w-full max-w-3xl flex-col gap-8")], [ h.div( - [h.Class("space-y-3 text-center")], + [h.Class("flex flex-col items-center gap-2.5 text-center")], [ - h.div([h.AriaHidden(true), h.Class("mx-auto mb-6 w-fit text-primary")], [icon(h, "MapleMark", { size: 56 })]), - eyebrow(h, { variant: "label", className: "text-primary" }, ["Before you start"]), - h.h1([h.Class("text-3xl font-semibold tracking-tight")], ["Where should your data live?"]), + h.h1( + [h.Class("font-display text-3xl font-semibold tracking-tight")], + ["Where should your data live?"], + ), h.p( - [h.Class("mx-auto max-w-md text-sm leading-relaxed text-muted-foreground")], + [h.Class("max-w-md text-title leading-relaxed text-muted-foreground")], [ "Each region is a separate Maple. Your telemetry, dashboards and alerts are stored and processed only in the one you pick.", ], @@ -104,7 +89,10 @@ export const regionStep = (h: H, choice: RegionChoice): Html => ), h.fieldset( [h.Class("grid min-w-0 gap-3 sm:grid-cols-2")], - [h.legend([h.Class("sr-only")], ["Data region"]), ...MapleRegion.literals.map((region) => regionCard(h, choice, region))], + [ + h.legend([h.Class("sr-only")], ["Data region"]), + ...MapleRegion.literals.map((region) => regionCard(h, choice, region)), + ], ), h.p( [ @@ -112,7 +100,7 @@ export const regionStep = (h: H, choice: RegionChoice): Html => h.AriaAtomic(true), h.Class( cn( - "min-h-10 text-center text-xs leading-relaxed", + "min-h-5 text-center text-xs leading-relaxed", choice.errorMessage !== null ? TONE_TEXT.crit : "text-muted-foreground", ), ), @@ -120,7 +108,7 @@ export const regionStep = (h: H, choice: RegionChoice): Html => [choice.errorMessage ?? "An organization stays in the region it starts in."], ), h.div( - [h.Class("flex items-center justify-end")], + [h.Class("flex justify-center")], [ button( h, @@ -130,7 +118,7 @@ export const regionStep = (h: H, choice: RegionChoice): Html => isLoading: choice.isSaving, attributes: [h.OnClick(Message.ClickedRegionContinue())], }, - ["Continue", h.span([h.Class("ml-2")], ["→"])], + ["Continue", icon(h, "ArrowRightIcon", { size: 16 })], ), ], ), diff --git a/apps/web/src/page/onboarding/quick-start-team-view.ts b/apps/web/src/page/onboarding/quick-start-team-view.ts index 9b6caf0db5..736f6f436f 100644 --- a/apps/web/src/page/onboarding/quick-start-team-view.ts +++ b/apps/web/src/page/onboarding/quick-start-team-view.ts @@ -66,7 +66,7 @@ const TOOLS: ReadonlyArray = [ const toolRow = (h: H, tool: Tool): Html => h.li( - [h.Class("flex items-center gap-3.5 px-4 py-4")], + [h.Class("flex flex-wrap items-center gap-x-3.5 gap-y-3 px-4 py-4")], [ h.span( [ @@ -78,7 +78,7 @@ const toolRow = (h: H, tool: Tool): Html => [icon(h, tool.icon, { size: 20 })], ), h.div( - [h.Class("flex min-w-0 flex-1 flex-col gap-1")], + [h.Class("flex min-w-0 flex-1 basis-48 flex-col gap-1")], [ h.span([h.Class("text-sm font-medium")], [tool.name]), h.span([h.Class("text-xs leading-snug text-muted-foreground")], [tool.line]), @@ -90,6 +90,7 @@ const toolRow = (h: H, tool: Tool): Html => href: tool.href, variant: "outline", size: "sm", + className: "max-sm:ml-[3.375rem]", attributes: [ h.Target("_blank"), h.Rel("noopener"), @@ -120,14 +121,20 @@ export const teamStep = (h: H, team: Team): Html => { ), ], ), - h.ul( + // Wrapped, so the column's side padding lands outside the list's border, not inside it. + h.div( + [], [ - h.AriaLabel("Tools"), - h.Class( - "flex flex-col divide-y divide-border overflow-hidden rounded-md border border-border bg-card", + h.ul( + [ + h.AriaLabel("Tools"), + h.Class( + "flex flex-col divide-y divide-border overflow-hidden rounded-md border border-border bg-card", + ), + ], + TOOLS.map((tool) => toolRow(h, tool)), ), ], - TOOLS.map((tool) => toolRow(h, tool)), ), team.isLoaded ? h.div( @@ -270,27 +277,22 @@ const pricingSkeleton = (h: H): Html => /** `StepPlan` / `StepPlanLayout`: the trial pitch over the pricing cards; it is the first step, so no Back. */ export const planStep = (h: H, isCustomerLoading: boolean, pricing: Html): Html => h.div( - [h.Class("flex-1 flex flex-col items-center px-6 py-12 overflow-auto")], + [h.Class("flex flex-1 flex-col items-center px-6 pb-12 pt-4 sm:px-12 lg:px-16")], [ h.div( [h.Class("w-full max-w-5xl")], [ h.div( - [h.Class("text-center mb-10")], + [h.Class("mb-10 flex flex-col items-center gap-2.5 text-center")], [ - eyebrow(h, { variant: "label", className: "text-primary" }, ["Pick a plan"]), - h.h2( - [h.Class("font-display text-3xl font-semibold tracking-tight mt-2")], - ["Pick a plan to keep going"], + h.h1( + [h.Class("font-display text-3xl font-semibold tracking-tight")], + [`Start your ${TRIAL_DURATION_DAYS}-day free trial`], ), h.p( - [h.Class("font-display text-muted-foreground text-sm mt-3 max-w-lg mx-auto")], + [h.Class("max-w-lg text-title leading-relaxed text-muted-foreground")], [ - "Start a ", - String(TRIAL_DURATION_DAYS), - "-day free trial: we'll save your card now and won't charge until day ", - String(TRIAL_DURATION_DAYS), - ". Cancel anytime from settings.", + `Pick a plan to keep going. Your card is saved now and charged on day ${TRIAL_DURATION_DAYS}; cancel anytime from settings.`, ], ), ], @@ -303,20 +305,16 @@ export const planStep = (h: H, isCustomerLoading: boolean, pricing: Html): Html const inviteForm = (h: H, team: Team, isBusy: boolean): Html => h.form( - [h.Class("space-y-3 border-t pt-6"), h.OnSubmit(toTeam(TeamMessage.SubmittedInvite()))], + [h.Class("space-y-3"), h.OnSubmit(toTeam(TeamMessage.SubmittedInvite()))], [ - label(h, { attributes: [h.For("onboarding-teammate-email")] }, [ - "Invite a teammate", - " ", - h.span([h.Class("font-normal text-muted-foreground")], ["(optional)"]), - ]), + label(h, { attributes: [h.For("onboarding-teammate-email")] }, ["Invite a teammate"]), h.div( [h.Class("flex flex-wrap gap-2")], [ input(h, { id: "onboarding-teammate-email", type: "email", - className: "min-w-40 flex-1", + className: "min-w-56 flex-1", isDisabled: !team.canManage || isBusy, attributes: [ h.Autocomplete("email"), @@ -348,7 +346,7 @@ const inviteForm = (h: H, team: Team, isBusy: boolean): Html => [ isAlreadyInvited(team) ? "This teammate already has a pending invitation." - : "They’ll receive an email invitation to join this workspace as a member.", + : "They get an email invitation to join as a member.", ], ), ], diff --git a/apps/web/src/page/onboarding/quick-start-view.ts b/apps/web/src/page/onboarding/quick-start-view.ts index 340a85bfdf..163ed1958f 100644 --- a/apps/web/src/page/onboarding/quick-start-view.ts +++ b/apps/web/src/page/onboarding/quick-start-view.ts @@ -17,6 +17,7 @@ import { AppRoute, hrefOf } from "../../route" import { button } from "../../ui/button" import { icon } from "../../ui/icon" import { searchInput } from "../../ui/search-input" +import { textLink } from "../../ui/text-link" import { type PixelGlyphName, pixelGlyph } from "./option-card" import * as CopyButton from "../../ui/copy-button" import * as GuidedSetup from "../kit/guided-setup" @@ -100,7 +101,10 @@ const trail = (h: H, items: ReadonlyArray): Html => : h.span( [h.Class("flex items-center gap-1.5 text-muted-foreground")], [ - icon(h, "CheckIcon", { size: 12, className: "text-severity-info" }), + icon(h, "CheckIcon", { + size: 12, + className: "text-severity-info", + }), item.answer, ], ), @@ -432,7 +436,7 @@ const stackStep = (h: H, state: QuickStartState, query: string, hasPlan: boolean ...(picked === 0 ? [ h.span( - [h.Class("text-sm text-muted-foreground")], + [h.Class("hidden text-sm text-muted-foreground sm:inline")], ["Not sure yet? You can skip this."], ), ] @@ -476,16 +480,21 @@ const stackPreview = (h: H, orgName: string, state: QuickStartState): Html => { eyebrow(h, { variant: "label" }, [`What ${orgName} gets`]), h.span( [h.Class("font-display text-xl font-semibold")], - [ - surfaces.length === 0 - ? "Pick what you run to see it here" - : `${surfaces.length} places light up`, - ], + [surfaces.length === 0 ? "Nothing picked yet" : `${surfaces.length} places light up`], ), ], ), ...(surfaces.length === 0 - ? [] + ? [ + h.p( + [ + h.Class( + "rounded-md border border-dashed border-border px-4 py-8 text-center text-xs leading-relaxed text-muted-foreground", + ), + ], + ["Pick a language or platform to see the pages it fills."], + ), + ] : [ h.ul( [ @@ -636,7 +645,7 @@ export const connectStep = (h: H, model: Model, state: QuickStartState): Html => h.p( [h.Class("text-title leading-relaxed text-muted-foreground")], [ - "Connect each provider without leaving setup. Your code sends its traces, logs and metrics with OpenTelemetry: one prompt to your coding agent, or a few lines by hand.", + "Your code sends traces, logs and metrics with OpenTelemetry. Hand one prompt to your coding agent, or add a few lines by hand.", ], ), ], @@ -670,7 +679,7 @@ export const connectStep = (h: H, model: Model, state: QuickStartState): Html => ]), // The languages picked, or every SDK there are snippets for when none was. h.span( - [h.AriaHidden(true), h.Class("flex items-center gap-1.5")], + [h.AriaHidden(true), h.Class("flex items-center gap-1.5 max-sm:hidden")], (state.stack.languages.length === 0 ? ([ "FrameworkNodejsIcon", @@ -733,7 +742,7 @@ export const connectStep = (h: H, model: Model, state: QuickStartState): Html => h, [ h.span( - [h.Class("text-sm text-muted-foreground")], + [h.Class("hidden text-sm text-muted-foreground sm:inline")], ["We keep listening while you move on."], ), button( @@ -953,7 +962,7 @@ export const liveStep = (h: H, model: Model, state: QuickStartState, orgName: st ), h.h1( [h.Class("font-display text-4xl font-semibold tracking-tight")], - [isLive ? `${orgName} is live.` : `${orgName} is ready for its first data.`], + [isLive ? `${orgName} is live` : `${orgName} is ready for its first data`], ), h.ul( [ @@ -997,33 +1006,53 @@ export const liveStep = (h: H, model: Model, state: QuickStartState, orgName: st "grid gap-px overflow-hidden rounded-md border border-border bg-border sm:grid-cols-3", ), ], + // The whole card is the link; its glyph and arrow answer the hover. startCards(findings).map((card) => h.li( - [h.Class("flex flex-col gap-3.5 bg-background p-5")], + [h.Class("flex bg-background")], [ - pixelGlyph(h, card.glyph, true), - h.div( - [h.Class("flex flex-col gap-1")], + h.a( + [ + h.Href(card.href), + h.Class( + "group flex flex-1 flex-col gap-3.5 p-5 outline-none transition-colors duration-150 hover:bg-surface-faint focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring active:bg-surface-faint motion-reduce:transition-none", + ), + ], [ - h.span([h.Class("text-title font-medium")], [card.title]), + pixelGlyph(h, card.glyph, false), h.span( + [h.Class("flex flex-col gap-1")], [ - h.Class( - "text-ui leading-relaxed text-muted-foreground", + h.span( + [h.Class("text-title font-medium")], + [card.title], + ), + h.span( + [ + h.Class( + "text-ui leading-relaxed text-muted-foreground", + ), + ], + [card.finding], ), ], - [card.finding], ), - ], - ), - h.a( - [ - h.Href(card.href), - h.Class( - "mt-auto flex items-center gap-1.5 text-ui text-primary hover:underline active:underline", + h.span( + [ + h.Class( + "mt-auto flex items-center gap-1.5 text-ui text-muted-foreground transition-colors duration-150 group-hover:text-foreground motion-reduce:transition-none", + ), + ], + [ + card.action, + icon(h, "ArrowRightIcon", { + size: 12, + className: + "transition-transform duration-150 group-hover:translate-x-0.5 motion-reduce:transition-none", + }), + ], ), ], - [card.action, icon(h, "ArrowRightIcon", { size: 12 })], ), ], ), @@ -1063,19 +1092,22 @@ export const liveStep = (h: H, model: Model, state: QuickStartState, orgName: st `Connect ${STACK_PLATFORMS[platform].label}`, ], ), - h.button( - [ - h.Type("button"), - h.AriaLabel( - `Set up ${STACK_PLATFORMS[platform].label}`, - ), - h.OnClick( - Message.ClickedSetUp({ platform }), - ), - h.Class( - "text-xs text-primary hover:underline active:underline", - ), - ], + button( + h, + { + variant: "outline", + size: "xs", + attributes: [ + h.AriaLabel( + `Set up ${STACK_PLATFORMS[platform].label}`, + ), + h.OnClick( + Message.ClickedSetUp({ + platform, + }), + ), + ], + }, ["Set up"], ), ], @@ -1102,10 +1134,13 @@ export const liveStep = (h: H, model: Model, state: QuickStartState, orgName: st h.span( [ h.Class( - "text-xs text-muted-foreground", + "flex items-center gap-1.5 text-xs text-muted-foreground", ), ], - ["Listening"], + [ + statusDot(h, { tone: "ok" }), + "Listening", + ], ), ], ), @@ -1116,26 +1151,33 @@ export const liveStep = (h: H, model: Model, state: QuickStartState, orgName: st ), ]), h.div( - [h.Class("flex flex-col items-center gap-3")], + [h.Class("flex flex-col items-center gap-4")], [ - button( - h, - { - size: "lg", - attributes: [h.OnClick(Message.ClickedContinue({ step: "live" }))], - }, - [`Go to ${orgName}`, icon(h, "ArrowRightIcon", { size: 16 })], - ), - h.a( + h.div( + [h.Class("flex items-center gap-2")], [ - h.Href(hrefOf(AppRoute.Alerts({ search: {} }))), - h.Class( - "text-xs text-muted-foreground underline-offset-4 hover:underline active:underline", + backButton(h, "team"), + button( + h, + { + size: "lg", + attributes: [ + h.OnClick(Message.ClickedContinue({ step: "live" })), + ], + }, + [`Go to ${orgName}`, icon(h, "ArrowRightIcon", { size: 16 })], ), ], + ), + textLink( + h, + { + tone: "muted", + className: "text-xs", + href: hrefOf(AppRoute.Alerts({ search: {} })), + }, ["Or set up your first alert"], ), - backButton(h, "team"), ], ), ], diff --git a/apps/web/src/page/overview/setup-checklist-view.ts b/apps/web/src/page/overview/setup-checklist-view.ts index 064f526902..20158bd22d 100644 --- a/apps/web/src/page/overview/setup-checklist-view.ts +++ b/apps/web/src/page/overview/setup-checklist-view.ts @@ -12,6 +12,7 @@ import { icon } from "../../ui/icon" import { iconButton } from "../../ui/icon-button" import { listRow } from "../../ui/list-row" import { skeleton } from "../../ui/skeleton" +import { statusDot } from "../../ui/status-dot" import * as GuidedSetup from "../kit/guided-setup" import { Message, type Model } from "./setup-checklist" import { frameworkOf, isMounted, realServices } from "./setup-checklist-update" @@ -31,7 +32,9 @@ const keysUnavailable = (h: H): Html => h.a( [ h.Href(hrefOf(AppRoute.Settings({ search: { tab: "ingestion" } }))), - h.Class("font-medium text-foreground underline underline-offset-2 hover:no-underline active:no-underline"), + h.Class( + "font-medium text-foreground underline underline-offset-2 hover:no-underline active:no-underline", + ), ], ["Settings → Ingestion"], ), @@ -109,17 +112,13 @@ const guidedSetup = (h: H, model: Model, framework: string): Html => ], ) -/** "Watching for your first trace…" while the card waits for data. */ +/** "Listening for your first trace" while the card waits for data. */ const watchingStrip = (h: H): Html => h.div( + [h.Class("flex items-center gap-2.5 rounded-md border border-dashed border-border px-4 py-3")], [ - h.Class( - "flex items-center gap-2.5 rounded-md border border-dashed border-primary-a3 bg-primary-a1 p-4", - ), - ], - [ - icon(h, "PulseIcon", { size: 14, className: "text-primary" }), - h.span([h.Class("font-prose text-xs text-muted-foreground")], ["Watching for your first trace…"]), + statusDot(h, { tone: "ok" }), + h.span([h.Class("text-xs text-muted-foreground")], ["Listening for your first trace"]), ], ) @@ -134,7 +133,7 @@ const celebration = (h: H, serviceName: string | undefined): Html => h.div( [ h.Class( - "flex size-10 shrink-0 items-center justify-center rounded-lg bg-primary-a2 text-primary", + "flex size-10 shrink-0 items-center justify-center rounded-lg bg-severity-info-a2 text-severity-info", ), ], [icon(h, "CircleCheckIcon", { size: 20 })], @@ -142,16 +141,13 @@ const celebration = (h: H, serviceName: string | undefined): Html => h.div( [h.Class("min-w-0 flex-1 basis-40")], [ + h.p([h.Class("text-sm font-semibold tracking-tight")], ["First trace received"]), h.p( - [h.Class("text-sm font-semibold tracking-tight")], - ["First trace received. You're live"], - ), - h.p( - [h.Class("mt-0.5 font-prose text-xs text-muted-foreground")], + [h.Class("mt-0.5 text-xs text-muted-foreground")], [ serviceName === undefined - ? "We're seeing your telemetry. Jump in to explore." - : `We're seeing telemetry from ${serviceName}. Open it to explore.`, + ? "Your telemetry is arriving." + : `${serviceName} is sending telemetry.`, ], ), ], @@ -163,7 +159,7 @@ const celebration = (h: H, serviceName: string | undefined): Html => className: "gap-2 shrink-0 max-sm:order-last max-sm:w-full", attributes: [h.OnClick(Message.ClickedExploreTraces())], }, - ["Explore your traces", icon(h, "RocketIcon", {})], + ["Explore your traces", icon(h, "ArrowRightIcon", {})], ), button( h, @@ -182,7 +178,7 @@ const celebration = (h: H, serviceName: string | undefined): Html => "flex flex-wrap items-center gap-4 p-5", ), ], - "mb-4 shrink-0 border-primary-a4 bg-primary/[0.04] overflow-hidden", + "mb-4 shrink-0 overflow-hidden", ) /** `SetupChecklistCard` while waiting: the collapsible "Connect your app" card. */ @@ -205,13 +201,13 @@ const waitingCard = ( leading: h.span( [ h.Class( - "flex size-9 items-center justify-center rounded-lg bg-primary-a2 text-primary", + "flex size-9 items-center justify-center rounded-lg border border-border bg-surface-strong text-foreground", ), ], [icon(h, "CodeIcon", { size: 16 })], ), title: "Connect your app to see real data", - meta: h.span([h.Class("font-prose")], [getOnboardingSetupHint(intents)]), + meta: getOnboardingSetupHint(intents), }), h.div( [h.Class("flex items-center gap-1 shrink-0")], @@ -250,14 +246,14 @@ const waitingCard = ( cardPanel( h, [guidedSetup(h, model, framework), watchingStrip(h)], - "border-t border-primary-a3 p-5 space-y-5", + "border-t p-5 space-y-5", ), ], ), ], ), ], - "mb-4 shrink-0 border-primary-a3 bg-primary/[0.02] overflow-hidden", + "mb-4 shrink-0 overflow-hidden", ) /** The first read's failure while the card is mounted; the page's error strip shows it. */ diff --git a/apps/web/src/page/overview/setup-checklist.scene.test.ts b/apps/web/src/page/overview/setup-checklist.scene.test.ts index ec9cdbca9b..42316ba888 100644 --- a/apps/web/src/page/overview/setup-checklist.scene.test.ts +++ b/apps/web/src/page/overview/setup-checklist.scene.test.ts @@ -102,7 +102,7 @@ describe("overview setup checklist", () => { ...pageMounts, ...tabMounts, expectView(text("Connect your app to see real data")).toExist(), - expectView(text("Watching for your first trace…")).toExist(), + expectView(text("Listening for your first trace")).toExist(), expectView(role("button", { name: "Dismiss setup checklist" })).toExist(), ) }) @@ -112,7 +112,7 @@ describe("overview setup checklist", () => { { update: pageUpdate, view }, given(withServices(entered(true).read.model, ["checkout"])), ...pageMounts, - expectView(text("We're seeing telemetry from checkout. Open it to explore.")).toExist(), + expectView(text("checkout is sending telemetry.")).toExist(), expectView(text("Connect your app to see real data")).not.toExist(), ) }) @@ -174,7 +174,7 @@ describe("overview setup checklist", () => { given(entered(true).read.model), ...pageMounts, expectView(text("Connect your app to see real data")).not.toExist(), - expectView(text(/We're seeing telemetry/)).not.toExist(), + expectView(text(/is sending telemetry/)).not.toExist(), ) }) @@ -184,7 +184,7 @@ describe("overview setup checklist", () => { given(failOverview(entered(true).read.model)), ...pageMounts, expectView(text(failureSentence(toLoadFailure("boom")), { exact: false })).toExist(), - expectView(text("Watching for your first trace…")).not.toExist(), + expectView(text("Listening for your first trace")).not.toExist(), ) }) diff --git a/apps/web/src/shell/onboarding-checklist-view.ts b/apps/web/src/shell/onboarding-checklist-view.ts index 29a633acba..b7d2ffb1cb 100644 --- a/apps/web/src/shell/onboarding-checklist-view.ts +++ b/apps/web/src/shell/onboarding-checklist-view.ts @@ -19,7 +19,10 @@ const credits = (checklist: V2OnboardingChecklist): string => `$${checklist.rewa const deadlineOf = (checklist: V2OnboardingChecklist): number | null => checklist.deadline_at === null ? null - : Option.match(DateTime.make(checklist.deadline_at), { onNone: () => null, onSome: DateTime.toEpochMillis }) + : Option.match(DateTime.make(checklist.deadline_at), { + onNone: () => null, + onSome: DateTime.toEpochMillis, + }) /** `ProgressChip`: `2/5`, written out. */ const progressChip = (h: H, completed: number, total: number): Html => @@ -43,13 +46,13 @@ const rewardCallout = (h: H, checklist: V2OnboardingChecklist): Html => [ h.Role("status"), h.Class( - "absolute top-full right-0 z-40 mt-3 w-max max-w-[calc(100vw-1rem)] rounded-lg border border-primary-a4 bg-popover text-popover-foreground shadow-popup animate-in fade-in-0 slide-in-from-top-1", + "absolute top-full right-0 z-40 mt-3 w-max max-w-[calc(100vw-1rem)] rounded-lg border bg-popover text-popover-foreground shadow-popup animate-in fade-in-0 slide-in-from-top-1", ), ], [ h.span( [ - h.Class("absolute -top-1.5 right-6 size-3 rotate-45 border-t border-l border-primary-a4 bg-popover"), + h.Class("absolute -top-1.5 right-6 size-3 rotate-45 border-t border-l bg-popover"), h.AriaHidden(true), ], [], @@ -66,10 +69,13 @@ const rewardCallout = (h: H, checklist: V2OnboardingChecklist): Html => ), ], [ - h.span([h.Class("block whitespace-nowrap text-sm font-semibold")], ["Finish the onboarding list"]), + h.span( + [h.Class("block whitespace-nowrap text-sm font-semibold")], + ["Finish setting up"], + ), h.span( [h.Class("mt-0.5 block text-xs text-muted-foreground")], - ["and get ", credits(checklist), " in credits."], + [credits(checklist), " in credits when you are done."], ), ], ), @@ -90,12 +96,12 @@ const rewardCallout = (h: H, checklist: V2OnboardingChecklist): Html => const optionalTag = (h: H): Html => badge(h, { variant: "meta", size: "xs", pill: true }, ["Optional"]) /** `StepRow`: a done step struck through, an open one linking to where it is done. */ -const stepRow = (h: H, step: V2OnboardingChecklistStep, index: number): Html => +const stepRow = (h: H, step: V2OnboardingChecklistStep): Html => step.completed ? h.li( [h.Class("flex items-center gap-3 rounded-md px-2 py-2 text-sm text-muted-foreground")], [ - icon(h, "CircleCheckIcon", { size: 18, className: "shrink-0 text-primary" }), + icon(h, "CircleCheckIcon", { size: 16, className: "shrink-0 text-severity-info" }), h.span( [h.Class("min-w-0 flex-1 leading-snug line-through decoration-muted-foreground/40")], [step.title], @@ -118,11 +124,11 @@ const stepRow = (h: H, step: V2OnboardingChecklistStep, index: number): Html => h.span( [ h.Class( - "flex size-4.5 shrink-0 items-center justify-center rounded-full border border-foreground-faint font-mono text-3xs text-muted-foreground transition-colors group-hover:border-primary group-hover:text-primary", + "size-4 shrink-0 rounded-full border border-dashed border-foreground-faint transition-colors group-hover:border-solid group-hover:border-foreground", ), h.AriaHidden(true), ], - [String(index)], + [], ), h.span([h.Class("min-w-0 flex-1 leading-snug")], [step.title]), ...(step.optional ? [optionalTag(h)] : []), @@ -137,10 +143,11 @@ const stepRow = (h: H, step: V2OnboardingChecklistStep, index: number): Html => ) /** `OnboardingChecklistPanel`: the reward, the progress meter, the steps and the footer. */ -const panel = (h: H, model: Model, checklist: V2OnboardingChecklist): ReadonlyArray => { +export const checklistPanel = (h: H, model: Model, checklist: V2OnboardingChecklist): ReadonlyArray => { const isClaimed = model.isJustClaimed || checklist.status === "claimed" const deadlineMs = deadlineOf(checklist) - const timeLeft = deadlineMs === null ? null : formatCountdown(deadlineMs - model.nowMs, { fixedHours: true }) + const timeLeft = + deadlineMs === null ? null : formatCountdown(deadlineMs - model.nowMs, { fixedHours: true }) const progress = checklist.total_count === 0 ? 0 : checklist.completed_count / checklist.total_count const amount = credits(checklist) return [ @@ -148,7 +155,7 @@ const panel = (h: H, model: Model, checklist: V2OnboardingChecklist): ReadonlyAr [], [ h.div( - [h.Class("relative space-y-3 border-b bg-primary/[0.06] px-4 pt-4 pb-3")], + [h.Class("relative space-y-3 border-b px-4 pt-4 pb-3")], [ h.div( [h.Class("flex items-start gap-3")], @@ -156,34 +163,46 @@ const panel = (h: H, model: Model, checklist: V2OnboardingChecklist): ReadonlyAr h.span( [ h.Class( - "flex size-9 shrink-0 items-center justify-center rounded-lg border border-primary-a3 bg-primary-a2 text-primary", + "flex size-9 shrink-0 items-center justify-center rounded-lg border border-border bg-surface-strong text-foreground", ), ], - [icon(h, isClaimed ? "CircleCheckIcon" : "RocketIcon", { size: 18 })], + [icon(h, isClaimed ? "CircleCheckIcon" : "StarIcon", { size: 18 })], ), h.div( [h.Class("min-w-0 space-y-1")], [ h.h3( [h.Class("text-base font-semibold leading-tight")], - [isClaimed ? `${amount} in credits added` : `Get ${amount} in credits`], + [ + isClaimed + ? `${amount} in credits added` + : `Get ${amount} in credits`, + ], ), h.p( [h.Class("text-xs leading-relaxed text-muted-foreground")], [ isClaimed ? `${amount} in credits applied. It comes off your next invoice.` - : "Finish these steps within 24 hours of creating your org and we'll credit your balance.", + : "Finish these within 24 hours of creating your org and we credit your balance.", ], ), ], ), ], ), - meter(h, { value: progress, minVisible: 0, className: "bg-primary-a2", fillClassName: "bg-primary" }), + meter(h, { + value: progress, + minVisible: 0, + className: "bg-primary-a2", + fillClassName: "bg-primary", + }), ], ), - h.ol([h.Class("space-y-0.5 p-2")], checklist.steps.map((step, index) => stepRow(h, step, index + 1))), + h.ol( + [h.Class("space-y-0.5 p-2")], + checklist.steps.map((step) => stepRow(h, step)), + ), h.div( [h.Class("flex items-center justify-between gap-3 border-t px-4 py-3 text-xs")], panelFooter(h, model, checklist, isClaimed, timeLeft), @@ -199,7 +218,8 @@ export const view = Submodel.defineView((model, h) => { if (checklist === null) return null const isActive = checklist.status === "in_progress" || checklist.status === "claimable" const deadlineMs = deadlineOf(checklist) - const isWindowOpen = checklist.status === "claimable" || (deadlineMs !== null && deadlineMs >= model.nowMs) + const isWindowOpen = + checklist.status === "claimable" || (deadlineMs !== null && deadlineMs >= model.nowMs) if (!model.isJustClaimed && !(isActive && isWindowOpen)) return null const isClaimable = checklist.status === "claimable" return h.submodel({ @@ -208,7 +228,7 @@ export const view = Submodel.defineView((model, h) => { view: PopoverView.view, viewInputs: { className: "w-[22rem] max-w-[calc(100vw-1rem)] p-0", - toContent: () => (model.popover.isOpen ? panel(h, model, checklist) : []), + toContent: () => (model.popover.isOpen ? checklistPanel(h, model, checklist) : []), toView: ({ trigger, withTrigger }) => h.div( [h.Class("relative")], @@ -219,10 +239,7 @@ export const view = Submodel.defineView((model, h) => { { variant: "outline", size: "sm", - className: cn( - "gap-2 border-primary-a4 bg-primary-a2 text-foreground hover:bg-primary-a3 active:bg-primary-a3", - isClaimable && "border-primary bg-primary-a3", - ), + className: cn("gap-2", isClaimable && "border-primary"), slot: "popover-trigger", attributes: trigger, }, @@ -232,7 +249,11 @@ export const view = Submodel.defineView((model, h) => { ? h.span([], ["Claim ", credits(checklist), " credits"]) : h.span( [h.Class("font-mono tabular-nums")], - [formatCountdown(deadlineMs - model.nowMs, { fixedHours: true })], + [ + formatCountdown(deadlineMs - model.nowMs, { + fixedHours: true, + }), + ], ), progressChip(h, checklist.completed_count, checklist.total_count), ], @@ -246,7 +267,13 @@ export const view = Submodel.defineView((model, h) => { }) }) -const panelFooter = (h: H, model: Model, checklist: V2OnboardingChecklist, isClaimed: boolean, timeLeft: string | null): ReadonlyArray => { +const panelFooter = ( + h: H, + model: Model, + checklist: V2OnboardingChecklist, + isClaimed: boolean, + timeLeft: string | null, +): ReadonlyArray => { if (isClaimed) return [ h.span([h.Class("text-muted-foreground")], ["You're all set."]), @@ -256,16 +283,32 @@ const panelFooter = (h: H, model: Model, checklist: V2OnboardingChecklist, isCla return model.isAdmin ? [ h.span( - [h.Class(cn("text-muted-foreground", model.claimError !== null && "text-severity-error"))], + [ + h.Class( + cn( + "text-muted-foreground", + model.claimError !== null && "text-severity-error", + ), + ), + ], [model.claimError ?? "Every step is done."], ), - button(h, { size: "sm", isLoading: model.isClaimPending, attributes: [h.OnClick(Message.ClickedClaim())] }, [ - "Claim ", - credits(checklist), - " credits", - ]), + button( + h, + { + size: "sm", + isLoading: model.isClaimPending, + attributes: [h.OnClick(Message.ClickedClaim())], + }, + ["Claim ", credits(checklist), " credits"], + ), + ] + : [ + h.span( + [h.Class("text-muted-foreground")], + ["All done. Ask an org admin to claim the credits."], + ), ] - : [h.span([h.Class("text-muted-foreground")], ["All done. Ask an org admin to claim the credits."])] return [ h.span( [h.Class("font-mono tabular-nums text-muted-foreground")], @@ -275,7 +318,11 @@ const panelFooter = (h: H, model: Model, checklist: V2OnboardingChecklist, isCla ? [] : [ h.span( - [h.Class("inline-flex items-center gap-1 font-mono font-medium tabular-nums text-primary")], + [ + h.Class( + "inline-flex items-center gap-1 font-mono tabular-nums text-muted-foreground", + ), + ], [icon(h, "ClockIcon", { size: 12 }), timeLeft], ), ]), From 3b0b5ab975a212403946699ea6c7fe3df2557cae Mon Sep 17 00:00:00 2001 From: Makisuo Date: Sat, 31 Oct 2026 11:06:07 +0100 Subject: [PATCH 2/3] style(web): review fixes for onboarding polish Hover states on the live step's cards and the checklist ring are instant again (instant-state), the create-org error is a role=alert so it is announced, and the start cards carry a short accessible name. --- apps/web/src/lab/entries/onboarding-checklist.ts | 2 +- apps/web/src/page/onboarding/create-org-view.ts | 7 ++++++- apps/web/src/page/onboarding/quick-start-region-view.ts | 1 - apps/web/src/page/onboarding/quick-start-team-view.ts | 2 +- apps/web/src/page/onboarding/quick-start-view.ts | 5 +++-- apps/web/src/shell/onboarding-checklist-view.ts | 4 ++-- 6 files changed, 13 insertions(+), 8 deletions(-) diff --git a/apps/web/src/lab/entries/onboarding-checklist.ts b/apps/web/src/lab/entries/onboarding-checklist.ts index 56a1d1666d..e5d4d4c3d3 100644 --- a/apps/web/src/lab/entries/onboarding-checklist.ts +++ b/apps/web/src/lab/entries/onboarding-checklist.ts @@ -127,7 +127,7 @@ export const view = (model: Model, h: HtmlBuilder): Html => { export const lab = defineLabWithFlags("Onboarding checklist", { Model, - Flags: Schema.Struct({ nowMs: Schema.Number }), + Flags: Model, flags: Effect.sync(() => ({ nowMs: Date.now() })), init: (flags) => ({ model: { nowMs: flags.nowMs } }), update: (model) => ({ model }), diff --git a/apps/web/src/page/onboarding/create-org-view.ts b/apps/web/src/page/onboarding/create-org-view.ts index 847d79c96f..be8a05e3a4 100644 --- a/apps/web/src/page/onboarding/create-org-view.ts +++ b/apps/web/src/page/onboarding/create-org-view.ts @@ -225,7 +225,12 @@ const form = (h: H, model: Model, memberships: ReadonlyArray) [ ...(model.errorMessage === null ? [] - : [h.p([h.Class(cn("text-xs", TONE_TEXT.crit))], [model.errorMessage])]), + : [ + h.p( + [h.Role("alert"), h.Class(cn("text-xs", TONE_TEXT.crit))], + [model.errorMessage], + ), + ]), button( h, { diff --git a/apps/web/src/page/onboarding/quick-start-region-view.ts b/apps/web/src/page/onboarding/quick-start-region-view.ts index ee7a97a4df..e5ff04c08c 100644 --- a/apps/web/src/page/onboarding/quick-start-region-view.ts +++ b/apps/web/src/page/onboarding/quick-start-region-view.ts @@ -51,7 +51,6 @@ const regionCard = (h: H, choice: RegionChoice, region: Region): Html => { label: card.name, title: card.name, description: card.place, - media: regionFlag( h, region, diff --git a/apps/web/src/page/onboarding/quick-start-team-view.ts b/apps/web/src/page/onboarding/quick-start-team-view.ts index 736f6f436f..f0db9fb064 100644 --- a/apps/web/src/page/onboarding/quick-start-team-view.ts +++ b/apps/web/src/page/onboarding/quick-start-team-view.ts @@ -90,7 +90,7 @@ const toolRow = (h: H, tool: Tool): Html => href: tool.href, variant: "outline", size: "sm", - className: "max-sm:ml-[3.375rem]", + className: "max-sm:ml-13.5", attributes: [ h.Target("_blank"), h.Rel("noopener"), diff --git a/apps/web/src/page/onboarding/quick-start-view.ts b/apps/web/src/page/onboarding/quick-start-view.ts index 163ed1958f..06ffc8a15b 100644 --- a/apps/web/src/page/onboarding/quick-start-view.ts +++ b/apps/web/src/page/onboarding/quick-start-view.ts @@ -1014,8 +1014,9 @@ export const liveStep = (h: H, model: Model, state: QuickStartState, orgName: st h.a( [ h.Href(card.href), + h.AriaLabel(`${card.action}: ${card.title}`), h.Class( - "group flex flex-1 flex-col gap-3.5 p-5 outline-none transition-colors duration-150 hover:bg-surface-faint focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring active:bg-surface-faint motion-reduce:transition-none", + "group flex flex-1 flex-col gap-3.5 p-5 outline-none hover:bg-surface-faint focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring active:bg-surface-faint", ), ], [ @@ -1040,7 +1041,7 @@ export const liveStep = (h: H, model: Model, state: QuickStartState, orgName: st h.span( [ h.Class( - "mt-auto flex items-center gap-1.5 text-ui text-muted-foreground transition-colors duration-150 group-hover:text-foreground motion-reduce:transition-none", + "mt-auto flex items-center gap-1.5 text-ui text-muted-foreground group-hover:text-foreground", ), ], [ diff --git a/apps/web/src/shell/onboarding-checklist-view.ts b/apps/web/src/shell/onboarding-checklist-view.ts index b7d2ffb1cb..d1d35bd6c9 100644 --- a/apps/web/src/shell/onboarding-checklist-view.ts +++ b/apps/web/src/shell/onboarding-checklist-view.ts @@ -124,7 +124,7 @@ const stepRow = (h: H, step: V2OnboardingChecklistStep): Html => h.span( [ h.Class( - "size-4 shrink-0 rounded-full border border-dashed border-foreground-faint transition-colors group-hover:border-solid group-hover:border-foreground", + "size-4 shrink-0 rounded-full border border-dashed border-foreground-faint group-hover:border-solid group-hover:border-foreground", ), h.AriaHidden(true), ], @@ -184,7 +184,7 @@ export const checklistPanel = (h: H, model: Model, checklist: V2OnboardingCheckl [ isClaimed ? `${amount} in credits applied. It comes off your next invoice.` - : "Finish these within 24 hours of creating your org and we credit your balance.", + : "Finish these within 24 hours of creating your org and we'll credit your balance.", ], ), ], From 39a59e21075b971c2963404011fe004a2158698a Mon Sep 17 00:00:00 2001 From: Makisuo Date: Sat, 31 Oct 2026 11:16:50 +0100 Subject: [PATCH 3/3] style(web): onboarding review follow-ups The live step's cards read their findings to screen readers again, its buttons wrap and a long org name truncates, the create-org preview waits for a width it fits, the checklist says the credits are claimed rather than applied, and the lab carries the created name and region into the quick start. --- apps/web/src/lab/entries/onboarding.ts | 19 +++++++++++++++++-- .../src/page/onboarding/create-org-view.ts | 4 ++-- .../src/page/onboarding/quick-start-view.ts | 8 +++++--- .../src/shell/onboarding-checklist-view.ts | 4 ++-- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/apps/web/src/lab/entries/onboarding.ts b/apps/web/src/lab/entries/onboarding.ts index a6ea758fd3..f82ba84f0d 100644 --- a/apps/web/src/lab/entries/onboarding.ts +++ b/apps/web/src/lab/entries/onboarding.ts @@ -65,6 +65,8 @@ export const Model = Schema.Struct({ /** Picking a sample plan stands in for checkout. */ hasPlan: Schema.Boolean, isComplete: Schema.Boolean, + /** The name sent from the create-org screen, shown by the steps after it. */ + orgName: Schema.String, /** Bumped by Restart, so every step remounts. */ run: Schema.Number, }) @@ -107,6 +109,7 @@ export const init = (step: LabStep, run = 0): Model => ({ needsRegion: step === "org" || step === "region", hasPlan: quickStartStep(step) !== "plan", isComplete: false, + orgName: SAMPLE_ORG.name, run, }) @@ -195,7 +198,19 @@ const createOrgStep = (model: Model, message: CreateOrg.Message): Return => { case "PickedRegion": return { model: { ...model, createOrg: CreateOrg.update(model.createOrg, message).model } } case "SubmittedCreate": - return model.createOrg.name.trim() === "" ? { model } : { model: { ...model, createOrg: null } } + return model.createOrg.name.trim() === "" + ? { model } + : { + model: { + ...model, + orgName: model.createOrg.name.trim(), + createOrg: null, + quickStart: { + ...model.quickStart, + region: { ...model.quickStart.region, selected: model.createOrg.region }, + }, + }, + } default: return { model } } @@ -236,7 +251,7 @@ const stepView = Submodel.defineView( ]), needsRegion: model.needsRegion, hasPlan: model.hasPlan, - orgName: "Acme", + orgName: model.orgName, teamStep: () => QuickStartTeamView.teamStep(h, model.quickStart.team), planStep: QuickStartTeamView.planStep( h, diff --git a/apps/web/src/page/onboarding/create-org-view.ts b/apps/web/src/page/onboarding/create-org-view.ts index be8a05e3a4..1eeafb4aa8 100644 --- a/apps/web/src/page/onboarding/create-org-view.ts +++ b/apps/web/src/page/onboarding/create-org-view.ts @@ -383,7 +383,7 @@ export const view = ( [h.Class("flex h-dvh overflow-hidden bg-background text-foreground")], [ h.div( - [h.Class("flex h-dvh w-full shrink-0 flex-col lg:w-[760px]")], + [h.Class("flex h-dvh w-full shrink-0 flex-col xl:w-[760px]")], [ h.div([h.Class("shrink-0 px-6 py-5 sm:px-12 lg:px-16")], [header(h, email)]), h.div( @@ -431,7 +431,7 @@ export const view = ( ], ), h.div( - [h.Class("relative hidden h-dvh flex-1 border-l border-border bg-sidebar lg:block")], + [h.Class("relative hidden h-dvh flex-1 border-l border-border bg-sidebar xl:block")], [ h.div( [h.Class("pointer-events-none absolute inset-0 overflow-hidden")], diff --git a/apps/web/src/page/onboarding/quick-start-view.ts b/apps/web/src/page/onboarding/quick-start-view.ts index 06ffc8a15b..811f4c4e1d 100644 --- a/apps/web/src/page/onboarding/quick-start-view.ts +++ b/apps/web/src/page/onboarding/quick-start-view.ts @@ -1014,7 +1014,6 @@ export const liveStep = (h: H, model: Model, state: QuickStartState, orgName: st h.a( [ h.Href(card.href), - h.AriaLabel(`${card.action}: ${card.title}`), h.Class( "group flex flex-1 flex-col gap-3.5 p-5 outline-none hover:bg-surface-faint focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring active:bg-surface-faint", ), @@ -1155,7 +1154,7 @@ export const liveStep = (h: H, model: Model, state: QuickStartState, orgName: st [h.Class("flex flex-col items-center gap-4")], [ h.div( - [h.Class("flex items-center gap-2")], + [h.Class("flex max-w-full flex-wrap items-center justify-center gap-2")], [ backButton(h, "team"), button( @@ -1166,7 +1165,10 @@ export const liveStep = (h: H, model: Model, state: QuickStartState, orgName: st h.OnClick(Message.ClickedContinue({ step: "live" })), ], }, - [`Go to ${orgName}`, icon(h, "ArrowRightIcon", { size: 16 })], + [ + h.span([h.Class("max-w-64 truncate")], [`Go to ${orgName}`]), + icon(h, "ArrowRightIcon", { size: 16 }), + ], ), ], ), diff --git a/apps/web/src/shell/onboarding-checklist-view.ts b/apps/web/src/shell/onboarding-checklist-view.ts index d1d35bd6c9..43ab0ef761 100644 --- a/apps/web/src/shell/onboarding-checklist-view.ts +++ b/apps/web/src/shell/onboarding-checklist-view.ts @@ -75,7 +75,7 @@ const rewardCallout = (h: H, checklist: V2OnboardingChecklist): Html => ), h.span( [h.Class("mt-0.5 block text-xs text-muted-foreground")], - [credits(checklist), " in credits when you are done."], + ["Then claim ", credits(checklist), " in credits."], ), ], ), @@ -184,7 +184,7 @@ export const checklistPanel = (h: H, model: Model, checklist: V2OnboardingCheckl [ isClaimed ? `${amount} in credits applied. It comes off your next invoice.` - : "Finish these within 24 hours of creating your org and we'll credit your balance.", + : "Finish these within 24 hours of creating your org, then claim the credits.", ], ), ],