mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-11-10 04:41:59 -10:00
refactor(frontend): atoms, molecules, organisms, tour and public pages on design tokens; data-slot on the remaining primitives; zero strict violations (#16402)
### Why / What / How **Why:** P9 of the SECRT-3097 design-system cleanup. The atoms, molecules and organisms, the public tour and the `(no-navbar)` pages (login, signup, onboarding, link, share) still used raw palette classes, `dark:` classes that never render, `legacy-*` buttons, arbitrary sizes and inline styles. Under the guardrails lint (#16394) they had 1,035 violations. **What:** these directories now use the colour roles, the type scale, `Text` tones and variants, and design-system buttons. Every `dark:` class is gone and no `LegacyButton` call sites remain. Every primitive that renders a DOM root now sets `data-slot`. **How:** 1. A codemod run with the approved section 5.1/5.2 snap tables. The extended tables live in P2's `codemod-tokens.ts`; that branch doesn't exist yet, so I used an uncommitted local copy with the same mappings. Variant prefixes and opacity modifiers are kept. 2. A script deleted every `dark:` class. 3. A manual pass per directory for `no-restyle`, arbitrary values, inline styles, unknown classes, dynamic classes and the legacy buttons. 4. Resolved `TODO(tokens)` comments and the "DS6's to delete" comments are deleted. Where the only fix would break the owner's visual limit, the look is kept and the line carries a one-line `oxlint-disable-next-line design-system/<rule> -- <reason>` (listed under *Deferred visual changes*). ### Changes 🏗️ | Measure (atoms, molecules, organisms, tour, no-navbar) | dev | this PR | |---|--:|--:| | design-system lint violations, baseline off (#16394 rules) | 1,035 | **19**, all `hugeicons-only`, left for P11 | | `dark:` classes | 165 | 0 | | `TODO(tokens)` | 207 | 11 (no role exists: `text-lg`, `text-2xl`, `tracking-tight`/`widest`, the `bg-black` scrim) | | LegacyButton / `legacyButtonVariants` call sites | 5 (`Calendar`, `DateInput`, `DrawerWrap`, `PaginationControls` ×2) | 0 | | primitives without `data-slot` | 5 | 2. Neither renders a DOM node of its own: `Dialog.tsx` renders only Radix/vaul roots, and its popups already carry `dialog-popup`/`drawer-popup`. `RelativeTime` returns bare text inside its caller's sentence; a wrapper span split that text and broke a library test. | - **`data-slot`** added to `RunStatusBadge` (`run-status-badge`), `Sheet` (`sheet-*`) and `Sidebar` (`sidebar`, `sidebar-wrapper`). - **Legacy buttons:** - `DateInput`, `DrawerWrap` and `PaginationControls` now render plain `<button>`s with token classes that keep today's rounded-md shape and size. No Button atom variant keeps the shape, and a pill would break the visual limit. The Drawer close and Pagination buttons also gain the Button atom's keyboard focus ring (`ring-1 ring-focus`); LegacyButton had none. - `Calendar` writes the ghost classes out instead of calling `legacyButtonVariants`. - `Button.test.tsx`, `LegacyButton.tsx` and `Button/helpers.ts` are untouched: P10 deletes the legacy layer, and P2 owns the helpers. - **Excluded:** `Text/helpers.ts` and `Button/helpers.ts` (P2), and icon imports (P11). - **Suppressions:** 99 suppressions, each scoped to one rule and given a reason: - logo colours, gradients, glows and glass; - the tour's restyled `Sidebar*` primitives; - motion wrappers; - runtime custom properties in arbitrary classes; - marker classes (`day-outside`, `is-user`, `rtl`/`ltr`); - the tour hero's 64px headline (named exception, SECRT-3152). - **Size:** 188 files, +923 / −1,256 (2,179 lines). About 40% of the deletions are comments, mostly `TODO(tokens)`. #### Visible changes (old → new) *Dark fills in atoms → `action`* - Badge `default`, Checkbox checked, Switch on, Calendar selected day, compact-primary button: #171717 → **#3E3E43**, hover #2C2C30. Text on them is #FAFAFA → #FEFEFE. *Greys* - Secondary text (`text-neutral-500` ×23, `text-gray-500` ×19): #737373 / #6B7280 → #83838C. - Input, textarea and select placeholders: #737373 / #6B7280 → #83838C. - `text-neutral-900` ×27: #171717 → #2C2C30. - `text-neutral-950` ×18: #0A0A0A → #141414. - `text-gray-600`: #4B5563 → #68686F. `text-gray-700`: #374151 → #505057. - Hover and chip fills (`bg-neutral-100` ×35, `bg-gray-100` ×9): #F5F5F5 / #F3F4F6 → #EFEFF0. - Borders (`border-neutral-200` ×16): #E5E5E5 → #DADADC. - `bg-neutral-200`: #E5E5E5 → #DADADC. `bg-gray-50`: #F9FAFB → #F9F9FA. - `bg-zinc-300` fills → `surface-active`: #C5C5C9 → #DADADC (×4: onboarding inactive step dots, the tour's composer caret and disabled send button). - `border-zinc-400` → `border-strong`: #ADADB3 → #C5C5C9 (×3, e.g. the onboarding `SelectableCard` selected border). - shadcn names on tour and link pages: `text-muted-foreground` #71717A → #83838C; spinner `text-primary` #18181B → #2C2C30. *Tour* - Sidebar panel #F3F3F4 → #EFEFF0. - Chat inset #FAFAFA → #F9F9FA. - Plan-card step title #09090B → #141414. - Block name and cube icon #71717A → #83838C. - "Enter" key hint 11.2px → 11px. *Share chat* - Background #F8F8F9 → #F9F9FA. *Status and accent* - `text-purple-700` ×7: #5424AE → #6D28D9. - `text-violet-600` ×6: #7C3AED → #7733F5. - `bg-purple-50/100`: #F1EBFE / #EFE8FE → #F5F3FF. - `text-red-600` ×4: #D93636 → #AA3030. - `text-amber-800/900` (AttentionCard warning variants, share-chat banner): #92400E / #78350F → #B45309. - `text-emerald-700`: #047857 → #107435. - `bg-red-500`: #EF4444 → #EF4444, no change. - AttentionCard caution surface #FFFCF3 → #FFFBEB; its border → #FDE68A. - Alert warning background #FFF9F2 → #FFFBEB; error background ≈#FEF6F6 → #FEF5F5. - Dark code blocks (FieldValue, SkillLearningSheet): text #EFEFF0 → #FEFEFE. - Toast description #F4F4F5 → ≈#F3F3F3. *Sizes, all ≤4px* - ProActivationDialog title 26 → 28px. - TrialTitle 17 → 16px. - PaymentRecoveryCard heading 23 → 22px. - Calendar weekday labels 12.8 → 13px. - Subscription CTA label 16 → 15px. - IntroStep body line-height (md+) 21.6 → 22.5px. - Login "What's new" tracking 0.14em → 0.12em. - TabsLine pill track padding 5 → 4px. - Toast vertical padding 12.8 → 12px. - Subscription welcome card radius 28 → 24px. *Buttons* - Pagination buttons: border #E5E5E5 → #DADADC, hover #F5F5F5 → #EFEFF0. Shape and size unchanged. - Drawer close icon #171717 → #2C2C30. DateInput trigger hover text #171717 → #2C2C30. - Onboarding Back, Log out and HireStep secondary action now use the `toggle` variant: hover text #2C2C30 → #3E3E43, and the faint #F9F9FA hover fill is gone. - BrainDump "Skip for now": text #ADADB3 → #83838C, hover #505057 → #3E3E43. - ReadyView "Not you? Sign out" and the BrainDump question links: underline offset → browser default. *Shadows* - IntegrationsMarquee card shadow opacity 0.03 → 0.05. - AttentionCard elevated and BriefingCard arrow shadow colour #09090B → #141414. #### Deferred visual changes (today's look kept, line suppressed with a reason) - **Hues with no role:** - UsageMeter full-meter fill `orange-500`, which would become amber. - AttentionCard caution icon `fill-yellow-600`. - Toast warning `orange-600` #E77B00. - VoiceDots `violet-400` dot. - Tour scenario title `to-indigo-500`, PlanHeader indigo stop, BriefingCard purple-800 gradient text. - **Gradients, glows and glass:** - GlassOrb blobs and timings; GlassSurface rim and violet glow; GlassPixelBackdrop 0.5px hairline. - AttentionCard featured grey gradient border, 15px inner radius and glow. - Tour hero badge ring, glow and highlight. - Tour emerald, violet and indigo shadows. - Key-cap shadows in SearchCommandModal. - The ErrorCard's 135° sRGB gradient border: `bg-linear-*` would interpolate in oklab, which the Tailwind 3 compat test forbids. - WelcomeShell lift shadow; OttoCover `#FDFCF8`, which matches the cover image; Otto's `color-mix` tint in ReadyLine and Timeline. - **AutoGPTLogo colours:** blue dots and gradient stops. - **Sizes and shapes:** - Tour hero 64px headline (SECRT-3152, awaiting product decision) and the hero CTA's wider padding. - TrialCard 18px skeleton radius, ScrollableTabs `pb-[200px]`, the dialog's 48% slide, the Sidebar easing curve and outline shadow ring. - The `AuthCard` `p-8 gap-6` on the link and Slack pages; the defaults would move content by 8px. - **Tour sidebar:** the restyled `SidebarMenuButton`, `SidebarGroup` and `SidebarFooter` padding and hover; the purple user bubble and thread spacing. - **Onboarding recording controls:** the dark send fill and press scale stay on `variant="icon"`, which keeps the tooltip. - **GlassSurface:** the runtime glass knobs stay as inline styles; class opacities read from custom properties mix in oklab. #### Screenshots (before = `dev`, after = this branch; dev server, `NEXT_PUBLIC_PW_TEST=true`) `/tour` hero: pixel-identical at 1440 and 390. **/tour/chat (1440)**: sidebar grey #F3F3F4 → #EFEFF0, block names #71717A → #83838C  **/tour/chat (390)**  **/login (1440)**: "What's new" tracking only  **/signup (1440 / 390)**: legal line grey #64748B → #83838C   The atom fill changes (Badge, Checkbox, Switch, Calendar) appear on signed-in screens. I couldn't capture those here because no backend was running. ### Agents and large language models used - Claude Code with Claude Opus 5.5 (1M context): one coordinating session and four parallel sub-agents, one per directory. ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] `pnpm lint`, `pnpm types`, `prettier --check` pass - [x] Full `vitest run` passes on the head merged with dev: 1,135 files, 12,430 tests - [x] #16394's design-system rules with the baseline off report only the `hugeicons-only` imports in these directories - [x] Before/after screenshots of `/tour`, `/tour/chat`, `/login` and `/signup` at 1440 and 390, pixel-diffed **#16394 (guardrails) is merged into this branch.** `pnpm lint` passes with its rules. `pnpm lint:ds-baseline` lowered `design-system-baseline.json` by 1,016 violations across 170 files, all in this PR's directories, with no count raised. `RunStatusBadge`, `Sheet` and `Sidebar` are removed from `UNSLOTTED_PRIMITIVES` in the contract test. `docs/engineering/design-system.md` still says the list has "five"; that file belongs to the docs PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- conductor-workspace-link --> --- [Open workspace in Conductor](https://app.conductor.build/pro-user-8d964eae-256d-46a9-b469-fd3c36fa6673/w/199) --------- Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
b94d58480c
commit
acfdad9b62
190 files changed
+997
-2174
No files matched your search
File diff suppressed because it is too large.
Load diff
+4
-15
@@ -11,24 +11,13 @@ export function ErrorView({ message }: Props) {
|
||||
return (
|
||||
<AuthCard title="Setup failed">
|
||||
<div className="flex w-full flex-col items-center gap-6">
|
||||
<div
|
||||
// TODO(tokens): no exact role for bg-red-100 (#FDECEC)
|
||||
className="flex h-16 w-16 items-center justify-center rounded-full bg-red-100"
|
||||
>
|
||||
<Icon
|
||||
icon={Unlink01Icon}
|
||||
size={40}
|
||||
// TODO(tokens): no fitting role for text-red-600 #D93636; #D93636 is error-hover (a hover fill), not a text colour
|
||||
className="text-red-600"
|
||||
/>
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-error-surface text-error-foreground">
|
||||
<Icon icon={Unlink01Icon} size={40} />
|
||||
</div>
|
||||
<Text
|
||||
variant="body-medium"
|
||||
className="text-center text-muted-foreground"
|
||||
>
|
||||
<Text variant="body-medium" tone="muted" className="text-center">
|
||||
{message}
|
||||
</Text>
|
||||
<Text variant="small" className="text-center text-muted-foreground">
|
||||
<Text variant="small" tone="muted" className="text-center">
|
||||
Go back to your chat and ask the bot for a new setup link.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
+4
-6
@@ -15,12 +15,10 @@ export function LoadingView({
|
||||
return (
|
||||
<AuthCard title={title}>
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<Icon
|
||||
icon={Loading03Icon}
|
||||
size={48}
|
||||
className="animate-spin text-primary"
|
||||
/>
|
||||
<Text variant="body-medium" className="text-muted-foreground">
|
||||
<div className="animate-spin text-foreground-strong">
|
||||
<Icon icon={Loading03Icon} size={48} />
|
||||
</div>
|
||||
<Text variant="body-medium" tone="muted">
|
||||
{message}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
+1
-4
@@ -11,10 +11,7 @@ export function NotAuthenticatedView({ token, loginRedirect }: Props) {
|
||||
return (
|
||||
<AuthCard title="Sign in to continue">
|
||||
<div className="flex w-full flex-col items-center gap-6">
|
||||
<Text
|
||||
variant="body-medium"
|
||||
className="text-center text-muted-foreground"
|
||||
>
|
||||
<Text variant="body-medium" tone="muted" className="text-center">
|
||||
Sign in to your AutoGPT account to finish setting up AutoGPT.
|
||||
</Text>
|
||||
<Button as="NextLink" href={loginRedirect} className="w-full">
|
||||
|
||||
+16
-16
@@ -32,12 +32,14 @@ export function ReadyView({
|
||||
: (serverName ?? `this ${platform} ${serverNoun}`);
|
||||
|
||||
return (
|
||||
<AuthCard title={title} className="max-w-[38rem] gap-6 p-8">
|
||||
<AuthCard
|
||||
title={title}
|
||||
// oxlint-disable-next-line design-system/no-restyle -- AuthCard has no roomy size; keeps today's p-8 padding and gap-6
|
||||
className="max-w-[38rem] gap-6 p-8"
|
||||
>
|
||||
<div className="flex w-full flex-col items-center gap-6">
|
||||
<div className="w-full rounded-xl bg-muted px-6 py-5 text-left">
|
||||
<Text variant="body-medium" className="font-medium">
|
||||
What happens when you confirm:
|
||||
</Text>
|
||||
<Text variant="body-medium">What happens when you confirm:</Text>
|
||||
{forUser ? (
|
||||
<ul className="mt-4 space-y-2.5 text-sm leading-relaxed text-muted-foreground">
|
||||
<li>{contextLabel} will be linked to your AutoGPT account</li>
|
||||
@@ -62,10 +64,7 @@ export function ReadyView({
|
||||
</div>
|
||||
|
||||
<div className="w-full rounded-xl border border-border bg-muted px-5 py-4">
|
||||
<Text
|
||||
variant="small"
|
||||
className="leading-relaxed text-muted-foreground"
|
||||
>
|
||||
<Text variant="small-relaxed" tone="muted">
|
||||
Usage from {contextLabel} is billed to your AutoGPT account. You can
|
||||
unlink at any time in Settings → Bots.
|
||||
</Text>
|
||||
@@ -84,16 +83,17 @@ export function ReadyView({
|
||||
|
||||
{userEmail && (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<Text variant="small" className="text-muted-foreground">
|
||||
<Text variant="small" tone="muted">
|
||||
Signed in as {userEmail}
|
||||
</Text>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onClick={onSwitchAccount}
|
||||
className="text-xs text-muted-foreground underline underline-offset-2"
|
||||
>
|
||||
Not you? Sign out
|
||||
<Button variant="ghost" size="small" onClick={onSwitchAccount}>
|
||||
<Text
|
||||
variant="small-medium-loose-underline"
|
||||
as="span"
|
||||
tone="muted"
|
||||
>
|
||||
Not you? Sign out
|
||||
</Text>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
+10
-13
@@ -32,27 +32,24 @@ export function SuccessView({
|
||||
: `Everyone in the ${serverNoun} can start using AutoGPT right away.`;
|
||||
|
||||
return (
|
||||
<AuthCard title="AutoGPT is ready!" className="max-w-[38rem] gap-6 p-8">
|
||||
<AuthCard
|
||||
title="AutoGPT is ready!"
|
||||
// oxlint-disable-next-line design-system/no-restyle -- AuthCard has no roomy size; keeps today's p-8 padding and gap-6
|
||||
className="max-w-[38rem] gap-6 p-8"
|
||||
>
|
||||
<div className="flex w-full flex-col items-center gap-7">
|
||||
<div
|
||||
// TODO(tokens): no exact role for bg-green-100 (#B7E2C7)
|
||||
className="flex h-16 w-16 items-center justify-center rounded-full bg-green-100"
|
||||
>
|
||||
<Icon
|
||||
icon={CheckmarkCircle02Icon}
|
||||
size={40}
|
||||
className="text-success"
|
||||
/>
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-success-surface text-success">
|
||||
<Icon icon={CheckmarkCircle02Icon} size={40} />
|
||||
</div>
|
||||
|
||||
<div className="w-full rounded-xl bg-muted px-6 py-5 text-center">
|
||||
<Text variant="body-medium" className="text-muted-foreground">
|
||||
<Text variant="body-medium" tone="muted">
|
||||
<strong>{label}</strong> is now connected to your AutoGPT account.
|
||||
<br />
|
||||
{detail}
|
||||
</Text>
|
||||
{returnUrl && forUser ? (
|
||||
<Text variant="small" className="mt-3 block text-muted-foreground">
|
||||
<Text variant="small" tone="muted" className="mt-3 block">
|
||||
Try it now — “research a topic”, “build me a
|
||||
workflow”, or “draft a doc”.
|
||||
</Text>
|
||||
@@ -81,7 +78,7 @@ export function SuccessView({
|
||||
</div>
|
||||
|
||||
{returnUrl ? null : (
|
||||
<Text variant="small" className="text-center text-muted-foreground">
|
||||
<Text variant="small" tone="muted" className="text-center">
|
||||
You can close this page and go back to your chat.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -10,14 +10,18 @@ export default function SlackInstalledPage() {
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-[85vh] flex-col items-center justify-center py-10">
|
||||
<AuthCard title="Slack connected" className="max-w-[38rem] gap-6 p-8">
|
||||
<AuthCard
|
||||
title="Slack connected"
|
||||
// oxlint-disable-next-line design-system/no-restyle -- AuthCard has no roomy size; keeps today's p-8 padding and gap-6
|
||||
className="max-w-[38rem] gap-6 p-8"
|
||||
>
|
||||
<div className="flex w-full flex-col items-center gap-7">
|
||||
<div className="w-full rounded-xl bg-muted px-6 py-5 text-center">
|
||||
{/* The page navigates on its own, so announce it rather than
|
||||
letting a screen reader land somewhere unexplained. */}
|
||||
<Text
|
||||
variant="body-medium"
|
||||
className="text-muted-foreground"
|
||||
tone="muted"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
|
||||
+8
-13
@@ -1,6 +1,4 @@
|
||||
import { Icon } from "@/components/atoms/Icon/Icon";
|
||||
import { Link } from "@/components/atoms/Link/Link";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { CHANGELOG_BASE_URL } from "@/components/molecules/ChangelogPopup/changelog-constants";
|
||||
import { ArrowUpRight01Icon } from "@hugeicons/core-free-icons";
|
||||
import { useId } from "react";
|
||||
@@ -21,26 +19,23 @@ export function LoginWhatsNew() {
|
||||
className="flex max-w-lg flex-col gap-3"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<Text
|
||||
as="h2"
|
||||
<h2
|
||||
id={headingID}
|
||||
variant="small-medium"
|
||||
tone="secondary"
|
||||
// TODO(tokens): no exact text token for tracking-[0.14em]
|
||||
className="uppercase tracking-[0.14em]"
|
||||
className="font-sans text-caption font-medium uppercase tracking-caps text-foreground-secondary"
|
||||
>
|
||||
What's new
|
||||
</Text>
|
||||
</h2>
|
||||
{isUnavailable ? null : (
|
||||
<Link
|
||||
<a
|
||||
href={CHANGELOG_BASE_URL}
|
||||
isExternal
|
||||
className="inline-flex items-center gap-1 text-caption leading-4 text-foreground-secondary hover:text-foreground-strong"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 rounded-sm font-sans text-caption font-medium leading-4 text-foreground-secondary hover:text-foreground-strong hover:underline focus:outline-none focus:ring-2 focus:ring-info focus:ring-offset-2"
|
||||
>
|
||||
Full changelog
|
||||
<span className="sr-only">(opens in a new tab)</span>
|
||||
<Icon icon={ArrowUpRight01Icon} size={14} aria-hidden />
|
||||
</Link>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<WhatsNewBody isLoading={isLoading} releases={releases} />
|
||||
|
||||
+4
-4
@@ -27,12 +27,12 @@ export function ChangelogFallback() {
|
||||
<span className="sr-only"> (opens in a new tab)</span>
|
||||
</Text>
|
||||
</span>
|
||||
<Icon
|
||||
icon={ArrowUpRight01Icon}
|
||||
size={16}
|
||||
<span
|
||||
aria-hidden
|
||||
className="shrink-0 text-foreground-tertiary transition-[color,transform] duration-150 group-hover:-translate-y-px group-hover:translate-x-px group-hover:text-foreground-strong motion-reduce:transition-none motion-reduce:group-hover:transform-none"
|
||||
/>
|
||||
>
|
||||
<Icon icon={ArrowUpRight01Icon} size={16} />
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
+6
-14
@@ -5,9 +5,7 @@ import { ArrowUpRight01Icon } from "@hugeicons/core-free-icons";
|
||||
import { useId } from "react";
|
||||
import { getReleaseLabel, Release } from "../helpers";
|
||||
import {
|
||||
RELEASE_DETAILS_GAP_CLASS,
|
||||
RELEASE_DOT_COLUMN_CLASS,
|
||||
RELEASE_HEADLINE_GAP_CLASS,
|
||||
RELEASE_ROW_CLASS,
|
||||
} from "./releaseRowLayout";
|
||||
|
||||
@@ -64,22 +62,19 @@ export function ReleaseItem({ release, isLatest, isLast }: Props) {
|
||||
Latest
|
||||
</span>
|
||||
) : null}
|
||||
<Icon
|
||||
icon={ArrowUpRight01Icon}
|
||||
size={16}
|
||||
<span
|
||||
aria-hidden
|
||||
className="ml-auto shrink-0 text-foreground-tertiary transition-[color,transform] duration-150 group-hover:-translate-y-px group-hover:translate-x-px group-hover:text-foreground-strong motion-reduce:transition-none motion-reduce:group-hover:transform-none"
|
||||
/>
|
||||
>
|
||||
<Icon icon={ArrowUpRight01Icon} size={16} />
|
||||
</span>
|
||||
</span>
|
||||
<Text
|
||||
as="span"
|
||||
variant="body-medium"
|
||||
tone="primary"
|
||||
title={headline.length > CLAMP_HINT_CHARS ? headline : undefined}
|
||||
className={cn(
|
||||
RELEASE_HEADLINE_GAP_CLASS,
|
||||
"line-clamp-3 [overflow-wrap:anywhere]",
|
||||
)}
|
||||
className="mt-1 line-clamp-3 [overflow-wrap:anywhere]"
|
||||
>
|
||||
{headline}
|
||||
</Text>
|
||||
@@ -90,10 +85,7 @@ export function ReleaseItem({ release, isLatest, isLast }: Props) {
|
||||
variant="body-tight"
|
||||
tone="secondary"
|
||||
title={details.length > CLAMP_HINT_CHARS ? details : undefined}
|
||||
className={cn(
|
||||
RELEASE_DETAILS_GAP_CLASS,
|
||||
"line-clamp-3 [overflow-wrap:anywhere]",
|
||||
)}
|
||||
className="mt-0.5 line-clamp-3 [overflow-wrap:anywhere]"
|
||||
>
|
||||
{details}
|
||||
</Text>
|
||||
|
||||
+3
-5
@@ -5,9 +5,7 @@ import {
|
||||
DETAILS_LINE_HEIGHT_CLASS,
|
||||
HEADLINE_LINE_HEIGHT_CLASS,
|
||||
LATEST_DATE_LINE_HEIGHT_CLASS,
|
||||
RELEASE_DETAILS_GAP_CLASS,
|
||||
RELEASE_DOT_COLUMN_CLASS,
|
||||
RELEASE_HEADLINE_GAP_CLASS,
|
||||
RELEASE_LIST_CLASS,
|
||||
RELEASE_ROW_CLASS,
|
||||
} from "./releaseRowLayout";
|
||||
@@ -42,14 +40,13 @@ export function ReleaseListSkeleton() {
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
RELEASE_HEADLINE_GAP_CLASS,
|
||||
HEADLINE_LINE_HEIGHT_CLASS,
|
||||
"flex items-center",
|
||||
"mt-1 flex items-center",
|
||||
)}
|
||||
>
|
||||
<Bar className="h-3.5 w-1/2" />
|
||||
</div>
|
||||
<div className={cn(RELEASE_DETAILS_GAP_CLASS, "flex flex-col")}>
|
||||
<div className="mt-0.5 flex flex-col">
|
||||
{Array.from({ length: detailLines }, (_, line) => (
|
||||
<div
|
||||
key={line}
|
||||
@@ -74,6 +71,7 @@ export function ReleaseListSkeleton() {
|
||||
function Bar({ className }: { className: string }) {
|
||||
return (
|
||||
<Skeleton
|
||||
// oxlint-disable-next-line design-system/no-restyle -- Skeleton has no reduced-motion variant; keeps the pulse off under prefers-reduced-motion
|
||||
className={cn(className, "rounded-full motion-reduce:animate-none")}
|
||||
/>
|
||||
);
|
||||
|
||||
+2
-5
@@ -9,11 +9,8 @@ export const RELEASE_ROW_CLASS = "flex gap-3 px-3.5 py-2.5";
|
||||
export const RELEASE_DOT_COLUMN_CLASS =
|
||||
"flex w-2.5 shrink-0 justify-center pt-1";
|
||||
|
||||
// Spacing above the headline and the details.
|
||||
export const RELEASE_HEADLINE_GAP_CLASS = "mt-1";
|
||||
export const RELEASE_DETAILS_GAP_CLASS = "mt-0.5";
|
||||
|
||||
// Line boxes of the Text variants the row uses: the date line
|
||||
// Line boxes of the Text variants the row uses (ReleaseItem's headline and
|
||||
// details sit mt-1 and mt-0.5 below the line above, as the skeleton's bars do): the date line
|
||||
// (small-medium-tight, 18px with the "Latest" pill), the headline
|
||||
// (body-medium) and one line of details (body-tight).
|
||||
export const DATE_LINE_HEIGHT_CLASS = "h-4";
|
||||
|
||||
@@ -71,7 +71,7 @@ export default function LoginPage() {
|
||||
return (
|
||||
<AuthSplitLayout marketing={<LoginMarketingPanel />}>
|
||||
<div className="mb-8">
|
||||
<Text variant="h3" as="h1" className="!text-foreground-strong">
|
||||
<Text variant="h3" as="h1" tone="primary">
|
||||
Log in to your account to continue
|
||||
</Text>
|
||||
</div>
|
||||
@@ -160,7 +160,7 @@ export default function LoginPage() {
|
||||
</FormProvider>
|
||||
|
||||
<div className="mt-6 inline-flex w-full items-center justify-center gap-1">
|
||||
<Text variant="body-medium" className="!text-foreground-tertiary">
|
||||
<Text variant="body-medium" tone="muted">
|
||||
Don't have an account?
|
||||
</Text>
|
||||
<Link href={signupHref} variant="secondary">
|
||||
|
||||
@@ -13,8 +13,8 @@ export function ProgressBar({ currentStep, totalSteps }: Props) {
|
||||
return (
|
||||
<div className="absolute left-0 top-0 h-0.5 w-full bg-surface-hover">
|
||||
<div
|
||||
className="h-full bg-action-hover transition-all duration-500 ease-out"
|
||||
style={{ width: `${percent}%` }}
|
||||
className="h-full w-[var(--progress)] bg-action-hover transition-all duration-500 ease-out"
|
||||
style={{ "--progress": `${percent}%` } as React.CSSProperties}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
+3
-8
@@ -28,17 +28,12 @@ export function SelectableCard({
|
||||
className={cn(
|
||||
"relative flex h-24 w-[10.375rem] shrink-0 flex-col items-center justify-center gap-2 rounded-lg border bg-surface p-4 transition-colors hover:bg-surface-muted md:shrink",
|
||||
className,
|
||||
// TODO(tokens): no fitting role for border-zinc-400 #ADADB3; #ADADB3 is foreground-subtle (a text colour), not a border
|
||||
selected ? "border-zinc-400 bg-surface-muted" : "border-separator",
|
||||
selected ? "border-border-strong bg-surface-muted" : "border-separator",
|
||||
)}
|
||||
>
|
||||
{selected && (
|
||||
<span className="absolute right-2 top-2 flex size-4 items-center justify-center rounded-full bg-action-hover">
|
||||
<Icon
|
||||
icon={Tick02Icon}
|
||||
size={10}
|
||||
className="text-action-foreground"
|
||||
/>
|
||||
<span className="absolute right-2 top-2 flex size-4 items-center justify-center rounded-full bg-action-hover text-action-foreground">
|
||||
<Icon icon={Tick02Icon} size={10} />
|
||||
</span>
|
||||
)}
|
||||
<span className="flex items-center justify-center text-foreground-tertiary">
|
||||
|
||||
+3
-2
@@ -13,8 +13,9 @@ export function StepIndicator({ totalSteps, currentStep }: Props) {
|
||||
key={i}
|
||||
className={cn(
|
||||
"h-1.5 rounded-full transition-all",
|
||||
// TODO(tokens): no fitting role for bg-zinc-300 #C5C5C9; #C5C5C9 is foreground-disabled (a text colour) and border-strong (a border colour), not a fill
|
||||
i + 1 === currentStep ? "w-4 bg-action-hover" : "w-1.5 bg-zinc-300",
|
||||
i + 1 === currentStep
|
||||
? "w-4 bg-action-hover"
|
||||
: "w-1.5 bg-surface-active",
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -6,10 +6,7 @@ export default function OnboardingLayout({
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
// TODO(tokens): no exact role for bg-gray-100 (#F3F4F6)
|
||||
className="relative flex min-h-screen w-full flex-col bg-gray-100"
|
||||
>
|
||||
<div className="relative flex min-h-screen w-full flex-col bg-surface-hover">
|
||||
<main className="flex w-full flex-1 flex-col items-center justify-center">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
@@ -111,11 +111,11 @@ export default function OnboardingPage() {
|
||||
{showBack && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
variant="toggle"
|
||||
size="xs"
|
||||
onClick={prevStep}
|
||||
leadingIcon={ArrowLeft01Icon}
|
||||
className="absolute left-6 top-6 text-foreground-tertiary hover:text-foreground-strong"
|
||||
className="absolute left-6 top-6"
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
@@ -159,10 +159,10 @@ export default function OnboardingPage() {
|
||||
<Button
|
||||
as="NextLink"
|
||||
href="/logout"
|
||||
variant="ghost"
|
||||
variant="toggle"
|
||||
size="xs"
|
||||
leadingIcon={Logout03Icon}
|
||||
className="absolute bottom-6 left-6 text-foreground-tertiary hover:text-foreground-strong"
|
||||
className="absolute bottom-6 left-6"
|
||||
>
|
||||
Log out
|
||||
</Button>
|
||||
|
||||
+5
-1
@@ -72,9 +72,11 @@ export function BrainDumpStep() {
|
||||
/>
|
||||
<RevealGroup
|
||||
className={cn(
|
||||
// oxlint-disable-next-line design-system/no-restyle -- RevealGroup/RevealItem are motion wrappers that lay out their children; a wrapper div would split the stagger
|
||||
"relative z-10 flex w-full flex-col items-center gap-6 px-4",
|
||||
isTyping
|
||||
? "w-[calc(100vw-3rem)] max-w-3xl [@media(max-height:800px)]:gap-4"
|
||||
? // oxlint-disable-next-line design-system/no-restyle -- see above
|
||||
"w-[calc(100vw-3rem)] max-w-3xl [@media(max-height:800px)]:gap-4"
|
||||
: "max-w-2xl",
|
||||
)}
|
||||
>
|
||||
@@ -91,7 +93,9 @@ export function BrainDumpStep() {
|
||||
<RevealItem
|
||||
blur={false}
|
||||
className={cn(
|
||||
// oxlint-disable-next-line design-system/no-restyle -- RevealGroup/RevealItem are motion wrappers that lay out their children; a wrapper div would split the stagger
|
||||
"flex w-full flex-col items-center gap-4",
|
||||
// oxlint-disable-next-line design-system/no-restyle -- see above
|
||||
isTyping && "[@media(max-height:800px)]:gap-2",
|
||||
)}
|
||||
>
|
||||
|
||||
+4
-14
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import type { AmbientContent } from "./useAmbientPrompts";
|
||||
@@ -63,21 +62,12 @@ export function AmbientCard({
|
||||
transition={{ duration: reduceMotion ? 0 : 0.45, ease: EASE }}
|
||||
className={cn(content.isQuote && "italic")}
|
||||
>
|
||||
<Text
|
||||
variant="small-semibold"
|
||||
as="span"
|
||||
className="block text-caption-sm uppercase tracking-eyebrow text-accent-strong"
|
||||
>
|
||||
<span className="block font-sans text-caption-sm font-semibold uppercase tracking-eyebrow text-accent-strong">
|
||||
{content.label}
|
||||
</Text>
|
||||
<Text
|
||||
variant="body"
|
||||
as="span"
|
||||
tone="secondary"
|
||||
className="block text-body-sm"
|
||||
>
|
||||
</span>
|
||||
<span className="block font-sans text-body-sm font-normal text-foreground-secondary">
|
||||
{content.text}
|
||||
</Text>
|
||||
</span>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
|
||||
+5
-2
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { formatElapsed } from "../helpers";
|
||||
|
||||
// Each digit is its own slot, so only the digits that actually changed pop:
|
||||
@@ -18,8 +19,10 @@ export function ElapsedTime({ seconds }: { seconds: number }) {
|
||||
{characters.map((character, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="relative inline-flex justify-center"
|
||||
style={{ width: character === ":" ? "0.3em" : "0.62em" }}
|
||||
className={cn(
|
||||
"relative inline-flex justify-center",
|
||||
character === ":" ? "w-[0.3em]" : "w-[0.62em]",
|
||||
)}
|
||||
aria-hidden
|
||||
>
|
||||
<AnimatePresence initial={false} mode="popLayout">
|
||||
|
||||
+5
-12
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Icon } from "@/components/atoms/Icon/Icon";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { MicOff01Icon } from "@hugeicons/core-free-icons";
|
||||
@@ -52,27 +51,21 @@ export function MicBlockedNotice({
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
className="flex w-full max-w-[640px] items-start gap-2.5 self-start rounded-xl bg-surface-hover px-3 py-2.5 text-left"
|
||||
className="flex w-full max-w-[640px] items-start gap-2.5 self-start rounded-xl bg-surface-hover px-3 py-2.5 text-left text-warning-foreground"
|
||||
>
|
||||
<Icon
|
||||
icon={MicOff01Icon}
|
||||
size={18}
|
||||
className="mt-0.5 shrink-0 text-warning-foreground"
|
||||
/>
|
||||
<Icon icon={MicOff01Icon} size={18} className="mt-0.5 shrink-0" />
|
||||
<Text variant="body" tone="secondary">
|
||||
<Text variant="body-medium" as="span">
|
||||
{copy.reason}
|
||||
</Text>{" "}
|
||||
{copy.howTo}{" "}
|
||||
<Button
|
||||
<button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={onRetry}
|
||||
className="inline h-auto rounded-sm p-0 align-baseline text-body text-foreground-base underline underline-offset-4 hover:bg-transparent hover:text-foreground-strong"
|
||||
className="inline whitespace-nowrap rounded-sm border border-transparent align-baseline font-sans text-body font-medium text-foreground-base underline underline-offset-4 transition-colors hover:text-foreground-strong focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-focus"
|
||||
>
|
||||
try the microphone again
|
||||
</Button>
|
||||
</button>
|
||||
.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
+3
-6
@@ -128,12 +128,9 @@ export function MicButton({
|
||||
isActive={isFolding && !reduceMotion}
|
||||
>
|
||||
<motion.div
|
||||
style={{
|
||||
scale: bodyScale,
|
||||
opacity: bodyOpacity,
|
||||
transformOrigin: `${CENTRE.x}px ${CENTRE.y}px`,
|
||||
willChange: "transform, opacity",
|
||||
}}
|
||||
className="origin-center will-change-[transform,opacity]"
|
||||
// oxlint-disable-next-line design-system/no-inline-styles -- framer-motion MotionValues
|
||||
style={{ scale: bodyScale, opacity: bodyOpacity }}
|
||||
>
|
||||
<AutopilotAvatar size={AVATAR_SIZE} transparent />
|
||||
</motion.div>
|
||||
|
||||
+5
-7
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { SwapFade } from "@/components/atoms/SwapFade/SwapFade";
|
||||
import type { ScreenState } from "../useBrainDumpStep";
|
||||
|
||||
@@ -55,13 +56,10 @@ function LinkButton({
|
||||
children: string;
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={onClick}
|
||||
className="underline underline-offset-4"
|
||||
>
|
||||
{children}
|
||||
<Button variant="ghost" size="xs" onClick={onClick}>
|
||||
<Text variant="small-medium-loose-underline" as="span">
|
||||
{children}
|
||||
</Text>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -27,7 +27,7 @@ export function OrbControlButton({ screen, onClick }: Props) {
|
||||
size="icon"
|
||||
onClick={onClick}
|
||||
aria-label={ariaLabel}
|
||||
className="mt-4 border border-border-base bg-surface hover:border-border-strong hover:bg-surface-muted"
|
||||
className="mt-4"
|
||||
>
|
||||
<SwapFade swapKey={screen} className="flex items-center justify-center">
|
||||
{screen === "failed" ? (
|
||||
|
||||
+7
-10
@@ -44,16 +44,14 @@ export function OttoMightAsk({
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Text
|
||||
variant="small-semibold"
|
||||
as="span"
|
||||
<span
|
||||
className={cn(
|
||||
"text-caption-sm uppercase tracking-eyebrow text-accent-strong",
|
||||
"font-sans text-caption-sm font-semibold uppercase tracking-eyebrow text-accent-strong",
|
||||
isLeft && "col-start-1 row-start-1",
|
||||
)}
|
||||
>
|
||||
Otto might ask
|
||||
</Text>
|
||||
</span>
|
||||
<SwapFade
|
||||
swapKey={question}
|
||||
className={cn("mt-0.5", isLeft && "col-span-2 row-start-2")}
|
||||
@@ -73,12 +71,11 @@ export function OttoMightAsk({
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={onAnother}
|
||||
className={cn(
|
||||
"underline underline-offset-4",
|
||||
isLeft ? "col-start-2 row-start-1 -mr-2.5" : "mt-1",
|
||||
)}
|
||||
className={isLeft ? "col-start-2 row-start-1 -mr-2.5" : "mt-1"}
|
||||
>
|
||||
Another question
|
||||
<Text variant="small-medium-loose-underline" as="span">
|
||||
Another question
|
||||
</Text>
|
||||
</Button>
|
||||
)}
|
||||
{/* The only announcement of the prompt layer: the ambient cards are
|
||||
|
||||
+4
-7
@@ -9,15 +9,12 @@ const PRIVACY_COPY =
|
||||
|
||||
export function PrivacyNote() {
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-2 px-4">
|
||||
<Icon
|
||||
icon={LockIcon}
|
||||
size={14}
|
||||
className="shrink-0 text-foreground-subtle"
|
||||
/>
|
||||
<div className="flex items-center justify-center gap-2 px-4 text-foreground-subtle">
|
||||
<Icon icon={LockIcon} size={14} className="shrink-0" />
|
||||
<Text
|
||||
variant="small"
|
||||
className="text-center text-foreground-subtle sm:whitespace-nowrap"
|
||||
tone="subtle"
|
||||
className="text-center sm:whitespace-nowrap"
|
||||
>
|
||||
{PRIVACY_COPY}
|
||||
</Text>
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ export function CancelRecordingDialog({
|
||||
return (
|
||||
<Dialog
|
||||
title="Discard recording?"
|
||||
className="min-w-0 max-w-[30rem] rounded-2xlarge"
|
||||
className="min-w-0 max-w-[30rem]"
|
||||
controlled={{ isOpen, set: onOpenChange }}
|
||||
>
|
||||
<Dialog.Content>
|
||||
|
||||
+9
-6
@@ -177,11 +177,13 @@ function RecordingControlButton({
|
||||
aria-label={isLoading ? pendingLabel : label}
|
||||
aria-busy={isLoading}
|
||||
className={cn(
|
||||
/* oxlint-disable design-system/no-restyle -- the recording controls' press scale and dark send fill; the icon variant keeps the tooltip, primary has none */
|
||||
"border transition-[transform,opacity,background-color] duration-150 ease-out active:scale-[0.97]",
|
||||
primary
|
||||
? "border-action-hover bg-action-hover text-action-foreground hover:bg-action"
|
||||
: "border-border-base bg-surface hover:bg-surface-muted",
|
||||
isInactive && "opacity-40",
|
||||
/* oxlint-enable design-system/no-restyle */
|
||||
)}
|
||||
>
|
||||
<span className="grid size-[22px] place-items-center [&>*]:[grid-area:1/1]">
|
||||
@@ -191,12 +193,13 @@ function RecordingControlButton({
|
||||
className="flex size-[22px] items-center justify-center"
|
||||
>
|
||||
{isLoading ? (
|
||||
<Icon
|
||||
icon={Loading03Icon}
|
||||
size={22}
|
||||
className="motion-safe:animate-spin"
|
||||
data-testid="recording-control-loader"
|
||||
/>
|
||||
<span className="flex motion-safe:animate-spin">
|
||||
<Icon
|
||||
icon={Loading03Icon}
|
||||
size={22}
|
||||
data-testid="recording-control-loader"
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
<Icon icon={icon} size={22} />
|
||||
)}
|
||||
|
||||
+2
-6
@@ -42,12 +42,8 @@ export function RecordingStatus({
|
||||
{/* A dropped connection is not a lost recording, and saying so is the
|
||||
whole point of writing every chunk to the device first. */}
|
||||
{isOffline && (
|
||||
<div className="flex items-center gap-2 rounded-md bg-surface-hover px-2.5 py-1">
|
||||
<Icon
|
||||
icon={CloudOffIcon}
|
||||
size={14}
|
||||
className="shrink-0 text-foreground-subtle"
|
||||
/>
|
||||
<div className="flex items-center gap-2 rounded-md bg-surface-hover px-2.5 py-1 text-foreground-subtle">
|
||||
<Icon icon={CloudOffIcon} size={14} className="shrink-0" />
|
||||
<Text variant="small" tone="muted">
|
||||
{OFFLINE_COPY}
|
||||
</Text>
|
||||
|
||||
+1
-7
@@ -23,13 +23,7 @@ export function SkipButton({ isHidden, isProcessing, onSkip }: Props) {
|
||||
behind the finalize that is already in flight, landing past
|
||||
the last step on a blank screen. */}
|
||||
{!isProcessing && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
onClick={onSkip}
|
||||
className="text-foreground-subtle hover:text-foreground-body"
|
||||
>
|
||||
<Button type="button" variant="toggle" size="xs" onClick={onSkip}>
|
||||
Skip for now
|
||||
</Button>
|
||||
)}
|
||||
|
||||
+17
-15
@@ -52,23 +52,25 @@ export function StepHeader({
|
||||
</SwapFade>
|
||||
</RevealItem>
|
||||
{screen === "rest" && (
|
||||
<RevealItem className="flex flex-col items-center gap-4">
|
||||
<Text variant="large" tone="muted" className="max-w-xl md:text-lead">
|
||||
Tell me what you do and what eats up your week.{" "}
|
||||
<Text variant="body-medium" as="span">
|
||||
Otto uses it to pick your experts and plan your first week.
|
||||
</Text>{" "}
|
||||
A minute is plenty, and you can type instead.
|
||||
</Text>
|
||||
{/* On wide screens the ambient cards carry these questions, and
|
||||
<RevealItem>
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<p className="max-w-xl font-sans text-body-lg font-normal text-foreground-tertiary md:text-lead">
|
||||
Tell me what you do and what eats up your week.{" "}
|
||||
<Text variant="body-medium" as="span">
|
||||
Otto uses it to pick your experts and plan your first week.
|
||||
</Text>{" "}
|
||||
A minute is plenty, and you can type instead.
|
||||
</p>
|
||||
{/* On wide screens the ambient cards carry these questions, and
|
||||
they are hidden from assistive tech, so this one stays in the
|
||||
accessibility tree. */}
|
||||
<OttoMightAsk
|
||||
question={question}
|
||||
compact
|
||||
onTogglePause={onTogglePause}
|
||||
className="xl:sr-only"
|
||||
/>
|
||||
<OttoMightAsk
|
||||
question={question}
|
||||
compact
|
||||
onTogglePause={onTogglePause}
|
||||
className="xl:sr-only"
|
||||
/>
|
||||
</div>
|
||||
</RevealItem>
|
||||
)}
|
||||
{screen === "typing" && (
|
||||
|
||||
-1
@@ -37,7 +37,6 @@ export function TypedFallback({
|
||||
label="Your brain dump"
|
||||
hideLabel
|
||||
rows={rows}
|
||||
className="rounded-xl border-border-base"
|
||||
placeholder="What repeats every week? What would you hand off first?"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
|
||||
+2
-2
@@ -33,8 +33,8 @@ export function VoiceAura({ colors, size, levels, isActive, children }: Props) {
|
||||
return (
|
||||
<div
|
||||
data-testid="voice-aura"
|
||||
className="relative flex items-center justify-center"
|
||||
style={{ width: size, height: size }}
|
||||
className="relative flex size-[var(--aura-size)] items-center justify-center"
|
||||
style={{ "--aura-size": `${size}px` } as React.CSSProperties}
|
||||
>
|
||||
<VoiceStreaks
|
||||
field={fieldRef.current.field}
|
||||
|
||||
+7
-14
@@ -2,6 +2,7 @@
|
||||
|
||||
import { motion, type MotionValue, useTransform } from "framer-motion";
|
||||
|
||||
// The dot is w-[14px] wide; keep the two in step.
|
||||
export const DOT = 14;
|
||||
const GAP = 22;
|
||||
const BAR_HEIGHTS = [40, 64, 64, 40];
|
||||
@@ -56,7 +57,7 @@ function Dot({
|
||||
slot,
|
||||
barHeight,
|
||||
centre,
|
||||
color,
|
||||
color: backgroundColor,
|
||||
reduceMotion,
|
||||
}: {
|
||||
level: MotionValue<number>;
|
||||
@@ -77,24 +78,16 @@ function Dot({
|
||||
DOT + (reduceMotion ? 0 : voice * spread * (barHeight - DOT)),
|
||||
);
|
||||
const y = useTransform(height, (h) => (DOT - h) / 2);
|
||||
const left = centre.x - DOT / 2;
|
||||
const top = centre.y - DOT / 2;
|
||||
|
||||
return (
|
||||
<motion.span
|
||||
data-testid="voice-dot"
|
||||
aria-hidden
|
||||
className="absolute rounded-full"
|
||||
style={{
|
||||
width: DOT,
|
||||
height,
|
||||
left: centre.x - DOT / 2,
|
||||
top: centre.y - DOT / 2,
|
||||
x,
|
||||
y,
|
||||
scale,
|
||||
opacity,
|
||||
backgroundColor: color,
|
||||
willChange: "transform, height",
|
||||
}}
|
||||
className="absolute w-[14px] rounded-full will-change-[transform,height]"
|
||||
// oxlint-disable-next-line design-system/no-inline-styles -- framer-motion MotionValues, a position from the centre prop and the dot colour prop (violet-400, no role)
|
||||
style={{ height, x, y, scale, opacity, left, top, backgroundColor }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+11
-7
@@ -2,6 +2,7 @@
|
||||
|
||||
import type { MotionValue } from "framer-motion";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
envelope,
|
||||
headAngle,
|
||||
@@ -89,13 +90,16 @@ export function VoiceStreaks({ field, levels, isActive, layer, store }: Props) {
|
||||
ref={canvasRef}
|
||||
data-testid={`voice-streaks-${layer}`}
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute"
|
||||
style={{
|
||||
inset: -field.pad,
|
||||
width: field.box,
|
||||
height: field.box,
|
||||
zIndex: layer === "front" ? 20 : 0,
|
||||
}}
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-[var(--streak-inset)] size-[var(--streak-box)]",
|
||||
layer === "front" ? "z-20" : "z-0",
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--streak-inset": `${-field.pad}px`,
|
||||
"--streak-box": `${field.box}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+3
-1
@@ -205,6 +205,8 @@ it("keeps the voice aura layers around their child and resizes with the avatar",
|
||||
<span>Avatar</span>
|
||||
</VoiceAura>,
|
||||
);
|
||||
expect(container.querySelector("canvas")?.style.width).toBe("480px");
|
||||
expect(
|
||||
container.querySelector("canvas")?.style.getPropertyValue("--streak-box"),
|
||||
).toBe("480px");
|
||||
tick(400);
|
||||
});
|
||||
+1
-2
@@ -19,8 +19,7 @@ const SKELETON_CARDS = 3;
|
||||
// Three across at the container's max width, two on tablets, one on phones.
|
||||
const CARD_WIDTH_CLASS = "w-full sm:w-[calc(50%-0.375rem)] lg:w-72";
|
||||
const EDGE_FADE_CLASS =
|
||||
// TODO(tokens): no exact role for from-gray-100 (#F3F4F6), via-gray-100/70 (#F3F4F6)
|
||||
"pointer-events-none absolute inset-x-0 z-10 h-16 from-gray-100 via-gray-100/70 to-transparent transition-opacity duration-200";
|
||||
"pointer-events-none absolute inset-x-0 z-10 h-16 from-surface-hover via-surface-hover/70 to-transparent transition-opacity duration-200";
|
||||
|
||||
// Otto's first act as Head of AI: right after the brain dump, before the
|
||||
// user is asked to pay for anything, it proposes the experts that take the
|
||||
|
||||
+1
-2
@@ -46,11 +46,10 @@ export function HireStepActions({
|
||||
{secondaryLabel && !isHiringAll ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
variant="toggle"
|
||||
size="xs"
|
||||
onClick={onSecondary}
|
||||
disabled={isCardHiring}
|
||||
className="text-foreground-tertiary hover:text-foreground-strong"
|
||||
>
|
||||
{secondaryLabel}
|
||||
</Button>
|
||||
|
||||
+16
-25
@@ -57,19 +57,13 @@ export function RecommendedExpertCard({
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"flex size-4 items-center justify-center rounded-full border transition-colors",
|
||||
"flex size-4 items-center justify-center rounded-full border text-action-foreground transition-colors",
|
||||
isIncluded
|
||||
? "border-action-hover bg-action-hover"
|
||||
: "border-border-strong bg-surface",
|
||||
)}
|
||||
>
|
||||
{isIncluded ? (
|
||||
<Icon
|
||||
icon={Tick02Icon}
|
||||
size={10}
|
||||
className="text-action-foreground"
|
||||
/>
|
||||
) : null}
|
||||
{isIncluded ? <Icon icon={Tick02Icon} size={10} /> : null}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
@@ -94,26 +88,23 @@ export function RecommendedExpertCard({
|
||||
</div>
|
||||
</div>
|
||||
{expert.reason ? (
|
||||
<Text
|
||||
variant="body"
|
||||
tone="secondary"
|
||||
className={cn(
|
||||
"line-clamp-2 text-pretty transition-opacity",
|
||||
isLeftOut && "opacity-50",
|
||||
)}
|
||||
>
|
||||
{expert.reason}
|
||||
</Text>
|
||||
<div className={cn("transition-opacity", isLeftOut && "opacity-50")}>
|
||||
<Text
|
||||
variant="body"
|
||||
tone="secondary"
|
||||
className="line-clamp-2 text-pretty"
|
||||
>
|
||||
{expert.reason}
|
||||
</Text>
|
||||
</div>
|
||||
) : null}
|
||||
{isHired ? (
|
||||
<Text
|
||||
variant="small"
|
||||
as="span"
|
||||
className="mt-auto inline-flex items-center gap-1 self-start pt-1 text-foreground-strong"
|
||||
>
|
||||
<span className="mt-auto inline-flex items-center gap-1 self-start pt-1 text-foreground-strong">
|
||||
<Icon icon={Tick02Icon} size={14} />
|
||||
Hired
|
||||
</Text>
|
||||
<Text variant="small" as="span" tone="primary">
|
||||
Hired
|
||||
</Text>
|
||||
</span>
|
||||
) : (
|
||||
<Button
|
||||
variant="primary"
|
||||
|
||||
+28
-35
@@ -2,7 +2,6 @@
|
||||
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { FadeIn } from "@/components/atoms/FadeIn/FadeIn";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useOnboardingWizardStore } from "../../store";
|
||||
import { AutopilotScene } from "./components/AutopilotScene";
|
||||
@@ -27,44 +26,38 @@ export function IntroStep({ slide }: Props) {
|
||||
|
||||
return (
|
||||
<FadeIn
|
||||
className={cn(
|
||||
"flex w-full flex-col items-center gap-8 px-4",
|
||||
slide === "team" ? "max-w-2xl" : "max-w-lg",
|
||||
)}
|
||||
className={cn("w-full", slide === "team" ? "max-w-2xl" : "max-w-lg")}
|
||||
>
|
||||
<Text variant="h4" as="h1" className="text-center leading-tight">
|
||||
{title}
|
||||
</Text>
|
||||
<div className="flex w-full flex-col items-center gap-8 px-4">
|
||||
<h1 className="text-center font-poppins text-heading-sm font-medium leading-tight text-foreground-base">
|
||||
{title}
|
||||
</h1>
|
||||
|
||||
<div
|
||||
data-testid="intro-stage"
|
||||
className={cn(
|
||||
"relative w-full overflow-hidden",
|
||||
slide === "team" ? "h-[420px]" : "h-[340px]",
|
||||
)}
|
||||
>
|
||||
<Scene />
|
||||
</div>
|
||||
|
||||
{body && (
|
||||
<Text
|
||||
variant="large"
|
||||
as="p"
|
||||
tone="muted"
|
||||
className="max-w-md text-center md:max-w-full md:text-[1.125rem] md:leading-[1.2]"
|
||||
<div
|
||||
data-testid="intro-stage"
|
||||
className={cn(
|
||||
"relative w-full overflow-hidden",
|
||||
slide === "team" ? "h-[420px]" : "h-[340px]",
|
||||
)}
|
||||
>
|
||||
{body}
|
||||
</Text>
|
||||
)}
|
||||
<Scene />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
size="small"
|
||||
onClick={nextStep}
|
||||
className="h-10 w-56"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
{body && (
|
||||
<p className="max-w-md text-center font-sans text-body-lg font-normal text-foreground-tertiary md:max-w-full md:text-lg md:leading-tight">
|
||||
{body}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
size="small"
|
||||
onClick={nextStep}
|
||||
className="h-10 w-56"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</FadeIn>
|
||||
);
|
||||
}
|
||||
+6
-11
@@ -1,5 +1,4 @@
|
||||
import { Icon } from "@/components/atoms/Icon/Icon";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { ArrowRight01Icon } from "@hugeicons/core-free-icons";
|
||||
import { motion } from "framer-motion";
|
||||
import { AutopilotAvatar } from "@/components/molecules/AutopilotAvatar/AutopilotAvatar";
|
||||
@@ -30,16 +29,12 @@ export function AutopilotScene() {
|
||||
initial={{ opacity: 0, x: -8 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={reveal(i)}
|
||||
className="flex items-center gap-1.5"
|
||||
className="flex items-center gap-1.5 text-brand"
|
||||
>
|
||||
<Icon
|
||||
icon={ArrowRight01Icon}
|
||||
size={18}
|
||||
className="shrink-0 text-brand"
|
||||
/>
|
||||
<Text variant="large" as="span" className="md:text-lead">
|
||||
<Icon icon={ArrowRight01Icon} size={18} className="shrink-0" />
|
||||
<span className="font-sans text-body-lg font-normal text-foreground-base md:text-lead">
|
||||
{point}
|
||||
</Text>
|
||||
</span>
|
||||
</motion.li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -48,9 +43,9 @@ export function AutopilotScene() {
|
||||
animate={{ opacity: 1 }}
|
||||
transition={reveal(POINTS.length)}
|
||||
>
|
||||
<Text variant="large" as="p" tone="muted" className="md:text-lead">
|
||||
<p className="font-sans text-body-lg font-normal text-foreground-tertiary md:text-lead">
|
||||
and more…
|
||||
</Text>
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { AutopilotAvatar } from "@/components/molecules/AutopilotAvatar/AutopilotAvatar";
|
||||
import { TypingText } from "@/components/molecules/TypingText/TypingText";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
ChecklistMark,
|
||||
type ChecklistState,
|
||||
@@ -34,8 +35,8 @@ export function PreparingStep({ onComplete }: Props) {
|
||||
|
||||
<div className="h-0.5 w-full overflow-hidden bg-surface-hover">
|
||||
<div
|
||||
className="h-full bg-action-hover transition-all duration-100 ease-linear"
|
||||
style={{ width: `${progress}%` }}
|
||||
className="h-full w-[var(--progress)] bg-action-hover transition-all duration-100 ease-linear"
|
||||
style={{ "--progress": `${progress}%` } as React.CSSProperties}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -45,14 +46,17 @@ export function PreparingStep({ onComplete }: Props) {
|
||||
return (
|
||||
<li key={item} className="flex items-center gap-3">
|
||||
<ChecklistMark key={state} state={state} />
|
||||
<Text
|
||||
variant={state === "active" ? "large-medium" : "large"}
|
||||
as="span"
|
||||
tone={state === "pending" ? "muted" : "primary"}
|
||||
className="transition-colors"
|
||||
<span
|
||||
className={cn(
|
||||
"font-sans text-body-lg transition-colors",
|
||||
state === "active" ? "font-medium" : "font-normal",
|
||||
state === "pending"
|
||||
? "text-foreground-tertiary"
|
||||
: "text-foreground-strong",
|
||||
)}
|
||||
>
|
||||
{item}
|
||||
</Text>
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ export function SubscriptionStep() {
|
||||
if (trial.isLoading)
|
||||
return (
|
||||
<div className="w-full px-4">
|
||||
<Skeleton className="mx-auto h-[34rem] w-full max-w-[60rem] rounded-[1.75rem]" />
|
||||
<Skeleton className="mx-auto h-[34rem] w-full max-w-[60rem] rounded-3xl" />
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
+1
@@ -29,6 +29,7 @@ interface Props {
|
||||
export function OttoCover({ art }: Props) {
|
||||
const image = COVER_ART[art];
|
||||
return (
|
||||
// oxlint-disable-next-line design-system/no-arbitrary-values -- the warm studio colour baked into the cover art; no role has it
|
||||
<div className="flex items-center justify-center bg-[#FDFCF8] px-6 pb-2 pt-6 sm:py-10 lg:px-10">
|
||||
<Image
|
||||
src={image.src}
|
||||
|
||||
+2
-1
@@ -54,7 +54,8 @@ export function PaidWelcome({
|
||||
onClick={onContinue}
|
||||
loading={isProcessing}
|
||||
disabled={isProcessing || isBlocked}
|
||||
className="h-12 w-full text-base"
|
||||
textSize="body-md"
|
||||
className="h-12 w-full"
|
||||
>
|
||||
Continue to checkout
|
||||
</Button>
|
||||
|
||||
+6
-3
@@ -14,22 +14,25 @@ interface Props {
|
||||
export function ReadyLine({ experts }: Props) {
|
||||
return (
|
||||
<p
|
||||
// oxlint-disable-next-line design-system/no-inline-styles -- Otto's tint is mixed from the expert palette (ExpertAvatar/colors); no role has it
|
||||
style={{ backgroundColor: OTTO_TINT }}
|
||||
className="inline-flex max-w-full items-center gap-2 rounded-2xl py-1 pl-1 pr-3 font-sans text-caption font-medium text-foreground-emphasis sm:rounded-full"
|
||||
>
|
||||
<span aria-hidden className="flex shrink-0 -space-x-1">
|
||||
<AutopilotAvatar size={AVATAR_SIZE} className="rounded-full" />
|
||||
<span className="flex overflow-hidden rounded-full">
|
||||
<AutopilotAvatar size={AVATAR_SIZE} />
|
||||
</span>
|
||||
{experts.slice(0, MAX_NAMED_EXPERTS).map((expert) => (
|
||||
<span
|
||||
key={expert.id}
|
||||
className="rounded-full"
|
||||
className="flex overflow-hidden rounded-full"
|
||||
// oxlint-disable-next-line design-system/no-inline-styles -- a ring in Otto's palette tint (see above); no role has it
|
||||
style={{ boxShadow: `0 0 0 2px ${OTTO_TINT}` }}
|
||||
>
|
||||
<ExpertAvatar
|
||||
name={expert.name}
|
||||
avatarUrl={expert.avatarUrl}
|
||||
size={AVATAR_SIZE}
|
||||
className="rounded-full"
|
||||
/>
|
||||
</span>
|
||||
))}
|
||||
|
||||
+1
@@ -48,6 +48,7 @@ export function Timeline({ label, milestones }: Props) {
|
||||
)}
|
||||
<span
|
||||
aria-hidden
|
||||
// oxlint-disable-next-line design-system/no-inline-styles -- Otto's tint is mixed from the expert palette (ExpertAvatar/colors); no role has it
|
||||
style={isFirst ? { backgroundColor: OTTO_TINT } : undefined}
|
||||
className={cn(
|
||||
"relative flex size-9 items-center justify-center rounded-full",
|
||||
|
||||
+2
-1
@@ -51,7 +51,8 @@ export function TrialWelcome({
|
||||
onClick={onStart}
|
||||
loading={isStarting}
|
||||
disabled={isStarting || isBlocked}
|
||||
className="h-12 w-full text-base"
|
||||
textSize="body-md"
|
||||
className="h-12 w-full"
|
||||
>
|
||||
Start my free trial
|
||||
</Button>
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ const EXPERTS: ReadyExpert[] = [
|
||||
|
||||
function OnboardingFrame({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="relative flex min-h-screen w-full flex-col items-center bg-gray-100">
|
||||
<div className="relative flex min-h-screen w-full flex-col items-center bg-surface-hover">
|
||||
<ProgressBar currentStep={6} totalSteps={6} />
|
||||
<div className="flex w-full min-w-0 flex-1 items-center justify-center pb-8 pt-14">
|
||||
{children}
|
||||
|
||||
+13
-10
@@ -9,16 +9,19 @@ interface Props {
|
||||
|
||||
export function WelcomeShell({ art, children }: Props) {
|
||||
return (
|
||||
<FadeIn className="w-full px-4 pt-12 lg:pt-0">
|
||||
<section
|
||||
aria-labelledby="welcome-title"
|
||||
className="mx-auto grid w-full max-w-[60rem] overflow-hidden rounded-[1.75rem] border border-border-base/80 bg-surface shadow-[0_1px_2px_rgba(24,24,27,0.04),0_32px_64px_-32px_rgba(24,24,27,0.18)] lg:grid-cols-2"
|
||||
>
|
||||
<OttoCover art={art} />
|
||||
<div className="flex flex-col gap-6 px-5 pb-6 pt-5 sm:gap-7 sm:px-10 sm:py-9">
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
<FadeIn className="w-full">
|
||||
<div className="px-4 pt-12 lg:pt-0">
|
||||
<section
|
||||
aria-labelledby="welcome-title"
|
||||
// oxlint-disable-next-line design-system/no-arbitrary-values -- the card's soft lift shadow has no shadow token yet
|
||||
className="mx-auto grid w-full max-w-[60rem] overflow-hidden rounded-3xl border border-border-base/80 bg-surface shadow-[0_1px_2px_rgba(24,24,27,0.04),0_32px_64px_-32px_rgba(24,24,27,0.18)] lg:grid-cols-2"
|
||||
>
|
||||
<OttoCover art={art} />
|
||||
<div className="flex flex-col gap-6 px-5 pb-6 pt-5 sm:gap-7 sm:px-10 sm:py-9">
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</FadeIn>
|
||||
);
|
||||
}
|
||||
@@ -67,8 +67,8 @@ export default function SharePage() {
|
||||
<ExecutionShareChrome>
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<div className="mx-auto w-full max-w-md p-6">
|
||||
<Card variant="outlined" className="border-dashed">
|
||||
<CardContent className="pt-6">
|
||||
<div className="rounded-2xl border border-dashed border-border-strong bg-surface p-px text-foreground-base">
|
||||
<div className="p-6">
|
||||
<div className="space-y-4 text-center">
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-muted">
|
||||
<InfoIcon className="h-6 w-6 text-muted-foreground" />
|
||||
@@ -92,8 +92,8 @@ export default function SharePage() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 text-center text-xs text-muted-foreground">
|
||||
<p>Powered by AutoGPT Platform</p>
|
||||
</div>
|
||||
@@ -118,9 +118,9 @@ export default function SharePage() {
|
||||
|
||||
<Card variant="outlined" className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">
|
||||
<h3 className="text-2xl font-semibold tracking-tight">
|
||||
{executionData.graph_name}
|
||||
</CardTitle>
|
||||
</h3>
|
||||
{executionData.graph_description && (
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
{executionData.graph_description}
|
||||
|
||||
+2
-6
@@ -11,12 +11,8 @@ export function SharedChatErrorState({ onRetry }: Props) {
|
||||
<div className="flex h-full w-full flex-1 items-center justify-center">
|
||||
<div className="mx-auto w-full max-w-md p-6">
|
||||
<div className="space-y-4 rounded-lg border border-dashed border-border-strong p-6 text-center">
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-surface-hover">
|
||||
<Icon
|
||||
icon={InformationCircleIcon}
|
||||
size={24}
|
||||
className="text-foreground-tertiary"
|
||||
/>
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-surface-hover text-foreground-tertiary">
|
||||
<Icon icon={InformationCircleIcon} size={24} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-semibold">Share link not found</h3>
|
||||
|
||||
@@ -147,24 +147,17 @@ export default function SharedChatPage() {
|
||||
|
||||
return (
|
||||
<SharedChatChrome title={title} subtitle={`Shared ${sharedOn}`}>
|
||||
<div
|
||||
// TODO(tokens): no exact role for bg-[#f8f8f9] (#F8F8F9)
|
||||
className="relative flex min-w-0 flex-1 flex-col overflow-hidden bg-[#f8f8f9]"
|
||||
>
|
||||
<div className="relative flex min-w-0 flex-1 flex-col overflow-hidden bg-surface-muted">
|
||||
{messagesError && (
|
||||
// Session loaded but messages failed — transient 5xx, not a
|
||||
// revoked link. Keep the chrome (so the viewer knows the
|
||||
// share is real) and show an inline retry instead of the
|
||||
// permanent not-found card.
|
||||
<div
|
||||
// TODO(tokens): no exact role for text-amber-900 (#78350F)
|
||||
className="flex shrink-0 items-center justify-between gap-3 border-b border-warning-border bg-warning-surface px-4 py-2 text-xs text-amber-900"
|
||||
>
|
||||
<div className="flex shrink-0 items-center justify-between gap-3 border-b border-warning-border bg-warning-surface px-4 py-2 text-xs text-warning-foreground">
|
||||
<span>Couldn't load messages. The share link is valid.</span>
|
||||
<button
|
||||
onClick={retry}
|
||||
// TODO(tokens): no exact role for border-amber-300 (#FCD34D), hover:bg-amber-100 (#FEF3C7)
|
||||
className="rounded border border-amber-300 bg-surface px-2 py-0.5 font-medium hover:bg-amber-100"
|
||||
className="rounded border border-warning-border bg-surface px-2 py-0.5 font-medium hover:bg-warning-surface"
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
@@ -179,8 +172,7 @@ export default function SharedChatPage() {
|
||||
<div
|
||||
data-testid="shared-chat-content-column"
|
||||
className={cn(
|
||||
// TODO(tokens): no exact role for bg-[#f8f8f9] (#F8F8F9)
|
||||
"mx-auto flex min-h-0 w-full flex-1 flex-col bg-[#f8f8f9] px-2 lg:px-0",
|
||||
"mx-auto flex min-h-0 w-full flex-1 flex-col bg-surface-muted px-2 lg:px-0",
|
||||
!isArtifactPanelOpen && "max-w-3xl",
|
||||
)}
|
||||
>
|
||||
|
||||
+2
-2
@@ -62,14 +62,14 @@ function Logo() {
|
||||
alt="AutoGPT"
|
||||
width={120}
|
||||
height={54}
|
||||
className="hidden h-7 w-auto dark:block"
|
||||
className="hidden h-7 w-auto"
|
||||
/>
|
||||
<Image
|
||||
src="/autogpt-logo-light-bg.png"
|
||||
alt="AutoGPT"
|
||||
width={120}
|
||||
height={54}
|
||||
className="block h-7 w-auto dark:hidden"
|
||||
className="block h-7 w-auto"
|
||||
priority
|
||||
/>
|
||||
</Link>
|
||||
|
||||
+3
-11
@@ -15,11 +15,7 @@ interface Props {
|
||||
|
||||
export function SignupLegalLine({ optedOut, onToggle, disabled }: Props) {
|
||||
return (
|
||||
<Text
|
||||
variant="body"
|
||||
// TODO(tokens): no exact role for !text-slate-500 (#64748B)
|
||||
className="mt-6 text-center !text-slate-500"
|
||||
>
|
||||
<Text variant="body" tone="muted" className="mt-6 text-center">
|
||||
By continuing you agree to our{" "}
|
||||
<Link href={TERMS_OF_USE_URL} variant="secondary" isExternal>
|
||||
Terms of Use
|
||||
@@ -31,10 +27,7 @@ export function SignupLegalLine({ optedOut, onToggle, disabled }: Props) {
|
||||
.{" "}
|
||||
<span aria-live="polite">
|
||||
{optedOut ? (
|
||||
<span
|
||||
// TODO(tokens): no exact role for text-slate-950 (#020617)
|
||||
className="text-slate-950"
|
||||
>
|
||||
<span className="text-foreground-base">
|
||||
You won't get marketing emails.
|
||||
</span>
|
||||
) : (
|
||||
@@ -48,8 +41,7 @@ export function SignupLegalLine({ optedOut, onToggle, disabled }: Props) {
|
||||
className={cn(
|
||||
linkBaseClasses,
|
||||
linkVariantClasses.secondary,
|
||||
// TODO(tokens): no fitting role for focus-visible:ring-blue-500 #3B82F6; #3B82F6 is info (a status colour) and sidebar-ring (a focus ring scoped to the shadcn sidebar), not this link's focus ring
|
||||
"rounded-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2",
|
||||
"rounded-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-info focus-visible:ring-offset-2",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
)}
|
||||
>
|
||||
|
||||
+1
-7
@@ -12,13 +12,7 @@ export function SignupMarketingPanel() {
|
||||
headingLines={[
|
||||
"Your ambition",
|
||||
<span key="line-2">
|
||||
deserves{" "}
|
||||
<span
|
||||
// TODO(tokens): no exact role for text-violet-600 (#7C3AED)
|
||||
className="text-violet-600"
|
||||
>
|
||||
a team.
|
||||
</span>
|
||||
deserves <span className="text-brand">a team.</span>
|
||||
</span>,
|
||||
]}
|
||||
description="Hire AI experts who research, write, sell and take work off your plate. Otto, your Head of AI, hands out the jobs and reports back to you."
|
||||
|
||||
@@ -72,10 +72,10 @@ export default function SignupPage() {
|
||||
return (
|
||||
<AuthSplitLayout marketing={<SignupMarketingPanel />}>
|
||||
<div className="mb-8">
|
||||
<Text variant="h3" as="h1" className="!text-foreground-strong">
|
||||
<Text variant="h3" as="h1" tone="primary">
|
||||
Create your account
|
||||
</Text>
|
||||
<Text variant="body" className="mt-1 !text-foreground-tertiary">
|
||||
<Text variant="body" tone="muted" className="mt-1">
|
||||
Already a member?{" "}
|
||||
<Link href={loginHref} variant="secondary">
|
||||
Log in
|
||||
|
||||
@@ -102,8 +102,8 @@ export function TourCopilot() {
|
||||
className="h-dvh min-h-0"
|
||||
>
|
||||
<TourSidebar />
|
||||
<SidebarInset className="min-h-0 overflow-hidden bg-[#fafafa]">
|
||||
<div className="relative flex h-full min-h-0 w-full">
|
||||
<SidebarInset className="min-h-0 overflow-hidden">
|
||||
<div className="relative flex h-full min-h-0 w-full bg-surface-muted">
|
||||
{chatColumn}
|
||||
{artifactPanels}
|
||||
</div>
|
||||
|
||||
+2
-11
@@ -9,7 +9,6 @@ interface DotDistortionShaderProps {
|
||||
dotSize?: number;
|
||||
dotColor?: string;
|
||||
glowColor?: string;
|
||||
backgroundColor?: string;
|
||||
mouseRadius?: number;
|
||||
distortionStrength?: number;
|
||||
breathingSpeed?: number;
|
||||
@@ -42,7 +41,6 @@ export const DotDistortionShader: React.FC<DotDistortionShaderProps> = ({
|
||||
dotSize = 1,
|
||||
dotColor = "var(--color-sky-700)",
|
||||
glowColor = "var(--color-sky-700)",
|
||||
backgroundColor,
|
||||
mouseRadius = 100,
|
||||
distortionStrength = 1,
|
||||
breathingSpeed = 1,
|
||||
@@ -358,18 +356,11 @@ export const DotDistortionShader: React.FC<DotDistortionShaderProps> = ({
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn(
|
||||
"relative overflow-hidden bg-white dark:bg-black",
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
background: backgroundColor,
|
||||
}}
|
||||
className={cn("relative overflow-hidden bg-surface", className)}
|
||||
>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="absolute inset-0 h-full w-full"
|
||||
style={{ display: "block" }}
|
||||
className="absolute inset-0 block h-full w-full"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
+13
-13
@@ -17,16 +17,17 @@ interface Props {
|
||||
|
||||
export function TourAgentCard({ agent, runCompleted }: Props) {
|
||||
return (
|
||||
<div className="flex flex-col gap-3 rounded-xl border border-zinc-200/70 bg-white p-4 shadow-sm">
|
||||
<div className="flex flex-col gap-3 rounded-xl border border-border-base/70 bg-surface p-4 shadow-sm">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<Text variant="large-medium" className="text-zinc-900">
|
||||
<Text variant="large-medium" tone="primary">
|
||||
{agent.name}
|
||||
</Text>
|
||||
<span
|
||||
className={cn(
|
||||
"relative isolate flex shrink-0 items-center gap-2 rounded-full bg-emerald-100 px-3 py-1.5 text-sm font-semibold text-emerald-700",
|
||||
"relative isolate flex shrink-0 items-center gap-2 rounded-full bg-success-surface px-3 py-1.5 text-sm font-semibold text-success-foreground",
|
||||
!runCompleted &&
|
||||
"shadow-[0_0_14px_-2px_rgba(16,185,129,0.65),0_0_28px_-4px_rgba(16,185,129,0.5)] ring-1 ring-emerald-300/70",
|
||||
// oxlint-disable-next-line design-system/no-arbitrary-values -- the live run's emerald glow, no shadow token
|
||||
"shadow-[0_0_14px_-2px_rgba(16,185,129,0.65),0_0_28px_-4px_rgba(16,185,129,0.5)] ring-1 ring-success/70",
|
||||
)}
|
||||
>
|
||||
{runCompleted ? (
|
||||
@@ -39,11 +40,11 @@ export function TourAgentCard({ agent, runCompleted }: Props) {
|
||||
{/* Breathing halo behind the badge while the run is live. */}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="absolute -inset-1.5 -z-10 animate-pulse rounded-full bg-emerald-400/50 blur-md"
|
||||
className="absolute -inset-1.5 -z-10 animate-pulse rounded-full bg-success/50 blur-md"
|
||||
/>
|
||||
<span className="relative flex size-2 shrink-0">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-500 opacity-75" />
|
||||
<span className="relative inline-flex size-2 rounded-full bg-emerald-500" />
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-success opacity-75" />
|
||||
<span className="relative inline-flex size-2 rounded-full bg-success" />
|
||||
</span>
|
||||
Running
|
||||
</>
|
||||
@@ -51,7 +52,7 @@ export function TourAgentCard({ agent, runCompleted }: Props) {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5 text-sm text-zinc-600">
|
||||
<div className="flex items-center gap-1.5 text-sm text-foreground-secondary">
|
||||
<Icon icon={Clock01Icon} className="size-4 shrink-0" />
|
||||
<span>{agent.schedule} · created from your sentence</span>
|
||||
</div>
|
||||
@@ -60,12 +61,11 @@ export function TourAgentCard({ agent, runCompleted }: Props) {
|
||||
{agent.blocks.map((block, index) => (
|
||||
<Fragment key={`${block}-${index}`}>
|
||||
{index > 0 && (
|
||||
<Icon
|
||||
icon={ArrowRight02Icon}
|
||||
className="size-3.5 shrink-0 text-zinc-400"
|
||||
/>
|
||||
<span className="contents text-foreground-subtle">
|
||||
<Icon icon={ArrowRight02Icon} className="size-3.5 shrink-0" />
|
||||
</span>
|
||||
)}
|
||||
<span className="rounded-md bg-zinc-100 px-2.5 py-1 font-mono text-xs text-zinc-700">
|
||||
<span className="rounded-md bg-surface-hover px-2.5 py-1 font-mono text-xs text-foreground-body">
|
||||
{block}
|
||||
</span>
|
||||
</Fragment>
|
||||
|
||||
+14
-13
@@ -9,24 +9,24 @@ interface Props {
|
||||
|
||||
export function TourArtifactCard({ artifact }: Props) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2.5 rounded-xl border border-zinc-200/70 bg-white p-4 shadow-sm">
|
||||
<div className="flex items-center gap-1.5 text-sm text-zinc-500">
|
||||
<div className="flex flex-col gap-2.5 rounded-xl border border-border-base/70 bg-surface p-4 shadow-sm">
|
||||
<div className="flex items-center gap-1.5 text-sm text-foreground-tertiary">
|
||||
<Icon icon={Mail02Icon} className="size-4 shrink-0" />
|
||||
<span>Artifact · {artifact.caption}</span>
|
||||
</div>
|
||||
|
||||
<Text variant="large-medium" className="text-zinc-900">
|
||||
<Text variant="large-medium" tone="primary">
|
||||
{artifact.title}
|
||||
</Text>
|
||||
|
||||
{artifact.subtitle && (
|
||||
<Text variant="body" className="text-zinc-600">
|
||||
<Text variant="body" tone="secondary">
|
||||
{artifact.subtitle}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{artifact.bullets && artifact.bullets.length > 0 && (
|
||||
<ul className="flex list-disc flex-col gap-1.5 pl-5 text-sm text-zinc-700">
|
||||
<ul className="flex list-disc flex-col gap-1.5 pl-5 text-sm text-foreground-body">
|
||||
{artifact.bullets.map((bullet) => (
|
||||
<li key={bullet}>{bullet}</li>
|
||||
))}
|
||||
@@ -34,18 +34,19 @@ export function TourArtifactCard({ artifact }: Props) {
|
||||
)}
|
||||
|
||||
{artifact.diff && (
|
||||
<div className="flex items-center justify-between gap-3 rounded-lg bg-zinc-50 px-4 py-3">
|
||||
<div className="flex items-center justify-between gap-3 rounded-lg bg-surface-muted px-4 py-3">
|
||||
<div className="flex items-center gap-3 text-base font-medium">
|
||||
<span className="text-red-700 line-through decoration-red-700/70">
|
||||
<span className="text-error-foreground line-through decoration-error-foreground/70">
|
||||
{artifact.diff.from}
|
||||
</span>
|
||||
<Icon
|
||||
icon={ArrowRight02Icon}
|
||||
className="size-4 shrink-0 text-zinc-500"
|
||||
/>
|
||||
<span className="text-emerald-700">{artifact.diff.to}</span>
|
||||
<span className="contents text-foreground-tertiary">
|
||||
<Icon icon={ArrowRight02Icon} className="size-4 shrink-0" />
|
||||
</span>
|
||||
<span className="text-success-foreground">{artifact.diff.to}</span>
|
||||
</div>
|
||||
<span className="text-sm text-zinc-500">{artifact.diff.delta}</span>
|
||||
<span className="text-sm text-foreground-tertiary">
|
||||
{artifact.diff.delta}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@ export function TourChatContainer({ chat }: Props) {
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 w-full flex-col px-2 lg:px-0">
|
||||
<div className="mx-auto flex h-full min-h-0 w-full max-w-3xl flex-col bg-[#fafafa] pb-8">
|
||||
<div className="mx-auto flex h-full min-h-0 w-full max-w-3xl flex-col bg-surface-muted pb-8">
|
||||
<TourMessageList
|
||||
messages={chat.messages}
|
||||
isStreaming={chat.isStreaming}
|
||||
@@ -32,7 +32,7 @@ export function TourChatContainer({ chat }: Props) {
|
||||
chat.currentUserPrompt && chat.onSend(chat.currentUserPrompt)
|
||||
}
|
||||
/>
|
||||
<p className="mt-2 flex items-center justify-center gap-1 text-sm text-zinc-400">
|
||||
<p className="mt-2 flex items-center justify-center gap-1 text-sm text-foreground-subtle">
|
||||
<Icon icon={FlashIcon} className="size-3.5 shrink-0" />
|
||||
Simulated demo — sign up to put Otto to work on your own tasks.
|
||||
</p>
|
||||
|
||||
+10
-9
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { SidebarTrigger } from "@/components/molecules/Sidebar/Sidebar";
|
||||
import { useTourChatHeader } from "./useTourChatHeader";
|
||||
import { Link02Icon, Tick02Icon } from "@hugeicons/core-free-icons";
|
||||
@@ -17,19 +16,19 @@ export function TourChatHeader({ scenarioLabel, scenarioIcon }: Props) {
|
||||
const { isCopied, handleShare } = useTourChatHeader();
|
||||
|
||||
return (
|
||||
<header className="flex shrink-0 items-center justify-between gap-2 border-b border-zinc-200/70 bg-white/70 px-3 py-2 backdrop-blur-sm md:px-4">
|
||||
<header className="flex shrink-0 items-center justify-between gap-2 border-b border-border-base/70 bg-surface/70 px-3 py-2 backdrop-blur-sm md:px-4">
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
{/* On mobile this is the only way to reach the sidebar. */}
|
||||
<div className="md:hidden">
|
||||
<SidebarTrigger />
|
||||
</div>
|
||||
<Icon icon={scenarioIcon} className="size-4 shrink-0 text-violet-600" />
|
||||
<Text
|
||||
variant="body-medium"
|
||||
className="truncate bg-gradient-to-r from-violet-600 to-indigo-500 bg-clip-text text-transparent"
|
||||
>
|
||||
<span className="contents text-brand">
|
||||
<Icon icon={scenarioIcon} className="size-4 shrink-0" />
|
||||
</span>
|
||||
{/* oxlint-disable-next-line design-system/no-raw-colors -- the scenario title's purple-to-indigo gradient, no indigo role */}
|
||||
<p className="truncate bg-gradient-to-r from-brand-hover to-indigo-500 bg-clip-text font-sans text-body font-medium text-transparent">
|
||||
{scenarioLabel}
|
||||
</Text>
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
@@ -37,7 +36,9 @@ export function TourChatHeader({ scenarioLabel, scenarioIcon }: Props) {
|
||||
onClick={handleShare}
|
||||
leftIcon={
|
||||
isCopied ? (
|
||||
<Icon icon={Tick02Icon} className="size-4 text-emerald-600" />
|
||||
<span className="contents text-success">
|
||||
<Icon icon={Tick02Icon} className="size-4" />
|
||||
</span>
|
||||
) : (
|
||||
<Icon icon={Link02Icon} className="size-4" />
|
||||
)
|
||||
|
||||
+14
-11
@@ -16,21 +16,21 @@ export function TourEndCard() {
|
||||
useTourEndCard();
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md rounded-2xl border border-zinc-200/70 bg-white p-5 shadow-sm duration-500 animate-in fade-in slide-in-from-bottom-2 sm:p-6">
|
||||
<div className="mx-auto w-full max-w-md rounded-2xl border border-border-base/70 bg-surface p-5 shadow-sm duration-500 animate-in fade-in slide-in-from-bottom-2 sm:p-6">
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-emerald-50 px-3 py-1">
|
||||
<Icon icon={Tick02Icon} className="size-3.5 text-emerald-700" />
|
||||
<Text variant="small-medium" className="!text-emerald-700">
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-success-surface px-3 py-1 text-success-foreground">
|
||||
<Icon icon={Tick02Icon} className="size-3.5" />
|
||||
<Text variant="small-medium" tone="inherit">
|
||||
Workflow built & first run complete — {TOUR_DEMO_CLAIM_SECONDS}{" "}
|
||||
seconds
|
||||
</Text>
|
||||
</span>
|
||||
<Text variant="h4" as="h3" className="mt-1 text-zinc-900">
|
||||
<Text variant="h4" as="h3" tone="primary" className="mt-1">
|
||||
That took {TOUR_DEMO_CLAIM_SECONDS} seconds.
|
||||
<br />
|
||||
Yours will too.
|
||||
</Text>
|
||||
<Text variant="body" className="text-zinc-500">
|
||||
<Text variant="body" tone="muted">
|
||||
Same chat, your real tasks — hosted and running for you.
|
||||
</Text>
|
||||
</div>
|
||||
@@ -43,11 +43,14 @@ export function TourEndCard() {
|
||||
rel="noopener noreferrer"
|
||||
variant="primary"
|
||||
onClick={handlePricingClick}
|
||||
className="h-auto w-full flex-col gap-0 py-2.5 shadow-[0_0_20px_-6px_rgba(124,58,237,0.6)]"
|
||||
// oxlint-disable-next-line design-system/no-restyle, design-system/no-arbitrary-values -- the end card CTA's violet glow, no shadow token or Button variant
|
||||
className="h-auto w-full shadow-[0_0_20px_-6px_rgba(124,58,237,0.6)]"
|
||||
>
|
||||
<span className="font-medium">Make this workflow yours</span>
|
||||
<span className="text-xs font-normal opacity-80">
|
||||
Start with Pro · $42.50/mo · cancel anytime
|
||||
<span className="flex flex-col items-center">
|
||||
<span className="font-medium">Make this workflow yours</span>
|
||||
<span className="text-xs font-normal opacity-80">
|
||||
Start with Pro · $42.50/mo · cancel anytime
|
||||
</span>
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
@@ -63,7 +66,7 @@ export function TourEndCard() {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={handleSelfHostClick}
|
||||
className="mt-1 text-center text-sm text-zinc-500 underline underline-offset-2 hover:text-zinc-700"
|
||||
className="mt-1 text-center text-sm text-foreground-tertiary underline underline-offset-2 hover:text-foreground-body"
|
||||
>
|
||||
or self-host free
|
||||
</a>
|
||||
|
||||
+20
-19
@@ -19,9 +19,8 @@ import { TourStreamingText } from "./TourStreamingText";
|
||||
// Mirrors the real copilot's MessageContent overrides (purple user bubble,
|
||||
// 1rem text) so the tour chat reads exactly like the product.
|
||||
const MESSAGE_CONTENT_CLASSES =
|
||||
"text-base leading-relaxed " +
|
||||
"group-[.is-user]:rounded-xl group-[.is-user]:bg-purple-100 group-[.is-user]:px-3 group-[.is-user]:py-2.5 group-[.is-user]:text-slate-900 group-[.is-user]:[border-bottom-right-radius:0] " +
|
||||
"group-[.is-assistant]:bg-transparent group-[.is-assistant]:text-slate-900";
|
||||
// oxlint-disable-next-line design-system/no-restyle -- the copilot's purple user bubble; MessageContent has no variant for it
|
||||
"text-body-lg leading-relaxed group-[.is-user]:rounded-xl group-[.is-user]:rounded-br-none group-[.is-user]:bg-accent-surface group-[.is-user]:px-3 group-[.is-user]:py-2.5 group-[.is-user]:text-foreground-strong group-[.is-assistant]:text-foreground-strong";
|
||||
|
||||
const CARD_ANIMATION_CLASSES =
|
||||
"animate-in fade-in slide-in-from-bottom-2 fill-mode-both duration-500";
|
||||
@@ -41,27 +40,29 @@ export function TourMessageList({ messages, isStreaming, footer }: Props) {
|
||||
|
||||
return (
|
||||
<Conversation className="min-h-0 flex-1">
|
||||
{/* oxlint-disable-next-line design-system/no-restyle -- the tour thread's tighter spacing; ConversationContent has no size for it */}
|
||||
<ConversationContent className="gap-6 px-3 pb-6 pt-4">
|
||||
{messages.map((message) => (
|
||||
<Message
|
||||
<div
|
||||
key={message.id}
|
||||
from={message.role}
|
||||
className="duration-300 animate-in fade-in slide-in-from-bottom-2"
|
||||
>
|
||||
<MessageContent className={MESSAGE_CONTENT_CLASSES}>
|
||||
{message.parts.map((part, index) => (
|
||||
<TourPartRenderer
|
||||
key={`${message.id}-${index}`}
|
||||
part={part}
|
||||
role={message.role}
|
||||
hasFollowingPart={index < message.parts.length - 1}
|
||||
followsCard={
|
||||
index > 0 && message.parts[index - 1].type !== "text"
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</MessageContent>
|
||||
</Message>
|
||||
<Message from={message.role}>
|
||||
<MessageContent className={MESSAGE_CONTENT_CLASSES}>
|
||||
{message.parts.map((part, index) => (
|
||||
<TourPartRenderer
|
||||
key={`${message.id}-${index}`}
|
||||
part={part}
|
||||
role={message.role}
|
||||
hasFollowingPart={index < message.parts.length - 1}
|
||||
followsCard={
|
||||
index > 0 && message.parts[index - 1].type !== "text"
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</MessageContent>
|
||||
</Message>
|
||||
</div>
|
||||
))}
|
||||
{/* The demo has no backend status stream, so it names the state
|
||||
itself — the indicator shows no label otherwise. */}
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ export function TourStreamingText({ text }: { text: string }) {
|
||||
<p>
|
||||
{revealed}
|
||||
{!isDone && (
|
||||
<span className="ml-0.5 inline-block h-4 w-2 animate-pulse rounded-sm bg-zinc-300 align-middle" />
|
||||
<span className="ml-0.5 inline-block h-4 w-2 animate-pulse rounded-sm bg-surface-active align-middle" />
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
|
||||
+17
-16
@@ -6,6 +6,7 @@ import {
|
||||
} from "@/app/(platform)/copilot/components/ToolAccordion/AccordionContent";
|
||||
import { ToolAccordion } from "@/app/(platform)/copilot/components/ToolAccordion/ToolAccordion";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import type { CSSProperties } from "react";
|
||||
import type { TourPlan, TourPlanStep } from "../../script/types";
|
||||
import {
|
||||
CheckmarkCircle02Icon,
|
||||
@@ -57,26 +58,26 @@ function TourPlanStepItem({
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-start gap-3 py-1.5 duration-500 animate-in fade-in slide-in-from-bottom-2 fill-mode-both"
|
||||
style={{ animationDelay: `${index * STEP_STAGGER_MS}ms` }}
|
||||
// oxlint-disable-next-line design-system/no-arbitrary-values -- per-row stagger delay set at runtime through --step-delay
|
||||
className="flex items-start gap-3 py-1.5 duration-500 animate-in fade-in slide-in-from-bottom-2 fill-mode-both [animation-delay:var(--step-delay)]"
|
||||
style={
|
||||
{
|
||||
"--step-delay": `${index * STEP_STAGGER_MS}ms`,
|
||||
"--check-delay": `${index * STEP_STAGGER_MS + 200}ms`,
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
icon={CheckmarkCircle02Icon}
|
||||
size={18}
|
||||
aria-label="completed"
|
||||
className="mt-0.5 shrink-0 text-emerald-500 duration-300 animate-in fade-in zoom-in-50 fill-mode-both"
|
||||
style={{ animationDelay: `${index * STEP_STAGGER_MS + 200}ms` }}
|
||||
/>
|
||||
{/* oxlint-disable-next-line design-system/no-arbitrary-values -- per-row check pop-in delay set at runtime through --check-delay */}
|
||||
<span className="mt-0.5 flex shrink-0 text-success duration-300 animate-in fade-in zoom-in-50 fill-mode-both [animation-delay:var(--check-delay)]">
|
||||
<Icon icon={CheckmarkCircle02Icon} size={18} aria-label="completed" />
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<Text variant="body-medium" className="text-base text-foreground">
|
||||
<Text variant="large-medium-tight">
|
||||
{index + 1}. {step.description}
|
||||
</Text>
|
||||
<div className="mt-0.5 flex items-center gap-1">
|
||||
<Icon icon={CubeIcon} size={12} className="text-muted-foreground" />
|
||||
<Text
|
||||
variant="small"
|
||||
className="font-mono text-xs text-muted-foreground"
|
||||
>
|
||||
<div className="mt-0.5 flex items-center gap-1 text-foreground-tertiary">
|
||||
<Icon icon={CubeIcon} size={12} />
|
||||
<Text variant="small-mono-tight" as="p" tone="muted">
|
||||
{step.blockName}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
+8
-7
@@ -52,20 +52,21 @@ export function TourPromptBar({ prompt, isStreaming, onSend }: Props) {
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={send}
|
||||
className={cn(
|
||||
"flex cursor-pointer items-center gap-3 rounded-xl border border-zinc-200 bg-white px-5 py-3.5 shadow-[0_2px_8px_rgba(0,0,0,0.04),0_0_32px_-4px_rgba(99,102,241,0.4)] outline-none transition-shadow focus-visible:border-zinc-300 focus-visible:shadow-[0_2px_8px_rgba(0,0,0,0.04),0_0_44px_-2px_rgba(99,102,241,0.55)]",
|
||||
// oxlint-disable-next-line design-system/no-arbitrary-values -- the prompt bar's indigo glow, no shadow token
|
||||
"flex cursor-pointer items-center gap-3 rounded-xl border border-border-base bg-surface px-5 py-3.5 shadow-[0_2px_8px_rgba(0,0,0,0.04),0_0_32px_-4px_rgba(99,102,241,0.4)] outline-none transition-shadow focus-visible:border-border-strong focus-visible:shadow-[0_2px_8px_rgba(0,0,0,0.04),0_0_44px_-2px_rgba(99,102,241,0.55)]",
|
||||
isDisabled && "cursor-default opacity-60",
|
||||
)}
|
||||
>
|
||||
<span className="min-h-6 flex-1 truncate text-base text-zinc-700">
|
||||
<span className="min-h-6 flex-1 truncate text-base text-foreground-body">
|
||||
{visibleText}
|
||||
{prompt && !isTyped && (
|
||||
<span className="ml-0.5 inline-block h-4 w-2 animate-pulse rounded-sm bg-zinc-300 align-middle" />
|
||||
<span className="ml-0.5 inline-block h-4 w-2 animate-pulse rounded-sm bg-surface-active align-middle" />
|
||||
)}
|
||||
</span>
|
||||
{!isDisabled && isTyped && (
|
||||
<span className="hidden shrink-0 items-center gap-1 text-xs text-zinc-400 sm:flex">
|
||||
<span className="hidden shrink-0 items-center gap-1 text-xs text-foreground-subtle sm:flex">
|
||||
Press
|
||||
<kbd className="rounded border border-zinc-200 bg-zinc-50 px-1.5 py-0.5 font-sans text-[0.7rem] text-zinc-500">
|
||||
<kbd className="rounded border border-border-base bg-surface-muted px-1.5 py-0.5 font-sans text-caption-sm text-foreground-tertiary">
|
||||
Enter
|
||||
</kbd>
|
||||
</span>
|
||||
@@ -73,8 +74,8 @@ export function TourPromptBar({ prompt, isStreaming, onSend }: Props) {
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"flex size-10 shrink-0 items-center justify-center rounded-full text-white transition-colors",
|
||||
isDisabled ? "bg-zinc-200" : "bg-zinc-800",
|
||||
"flex size-10 shrink-0 items-center justify-center rounded-full text-action-foreground transition-colors",
|
||||
isDisabled ? "bg-surface-active" : "bg-action",
|
||||
)}
|
||||
>
|
||||
<Icon icon={ArrowUp02Icon} className="size-4" />
|
||||
|
||||
+20
-15
@@ -53,7 +53,8 @@ function DisabledMenuItem({
|
||||
<SidebarMenuButton
|
||||
aria-disabled="true"
|
||||
tooltip={label}
|
||||
className="cursor-not-allowed font-normal opacity-50 hover:!bg-transparent group-data-[collapsible=icon]:!p-1.5 [&>svg]:size-5"
|
||||
// oxlint-disable-next-line design-system/no-restyle -- 20px icons need the tighter collapsed padding; SidebarMenuButton has no size for it
|
||||
className="cursor-not-allowed group-data-[collapsible=icon]:!p-1.5 [&>svg]:size-5"
|
||||
>
|
||||
<Icon icon={icon} className="size-5" />
|
||||
<span className="truncate">{label}</span>
|
||||
@@ -89,11 +90,12 @@ export function TourSidebar({ variant = "tour" }: Props) {
|
||||
return (
|
||||
<Sidebar
|
||||
collapsible="icon"
|
||||
className="[&_[data-sidebar=sidebar]]:bg-[#F3F3F4]"
|
||||
// oxlint-disable-next-line design-system/no-restyle -- the tour sidebar's grey panel; Sidebar has no variant for it
|
||||
className="[&_[data-sidebar=sidebar]]:bg-surface-hover"
|
||||
>
|
||||
<TourSidebarHeader />
|
||||
|
||||
<SidebarContent className="gap-2 overflow-hidden">
|
||||
<SidebarContent className="overflow-hidden">
|
||||
<motion.div
|
||||
variants={sidebarContainerVariants}
|
||||
initial="hidden"
|
||||
@@ -101,6 +103,7 @@ export function TourSidebar({ variant = "tour" }: Props) {
|
||||
className="flex min-h-0 flex-1 flex-col gap-2"
|
||||
>
|
||||
<motion.div variants={itemVariants}>
|
||||
{/* oxlint-disable-next-line design-system/no-restyle -- the copilot sidebar's tighter group padding; SidebarGroup has no size for it */}
|
||||
<SidebarGroup className="mt-2 py-1 group-data-[collapsible=icon]:mt-0">
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
@@ -108,7 +111,8 @@ export function TourSidebar({ variant = "tour" }: Props) {
|
||||
<SidebarMenuButton
|
||||
aria-disabled="true"
|
||||
tooltip="New Task"
|
||||
className="cursor-not-allowed justify-center rounded-lg bg-zinc-800 font-medium text-white opacity-50 hover:!bg-zinc-800 hover:!text-white group-data-[collapsible=icon]:justify-start"
|
||||
// oxlint-disable-next-line design-system/no-restyle -- the copilot's dark New Task button; SidebarMenuButton has no primary variant
|
||||
className="cursor-not-allowed justify-center rounded-lg bg-action font-medium text-action-foreground group-data-[collapsible=icon]:justify-start"
|
||||
>
|
||||
<Icon icon={SparklesIcon} className="size-4" />
|
||||
<span className="truncate">New Task</span>
|
||||
@@ -120,8 +124,10 @@ export function TourSidebar({ variant = "tour" }: Props) {
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={itemVariants}>
|
||||
{/* oxlint-disable-next-line design-system/no-restyle -- the copilot sidebar's tighter group padding; SidebarGroup has no size for it */}
|
||||
<SidebarGroup className="mt-2 py-1 group-data-[collapsible=icon]:mt-0">
|
||||
<SidebarGroupContent>
|
||||
{/* oxlint-disable-next-line design-system/no-restyle -- the copilot sidebar's collapsed item gap; SidebarMenu has no size for it */}
|
||||
<SidebarMenu className="group-data-[collapsible=icon]:gap-1">
|
||||
<DisabledMenuItem icon={Search01Icon} label="Search" />
|
||||
<DisabledMenuItem icon={GridViewIcon} label="Workflows" />
|
||||
@@ -129,7 +135,8 @@ export function TourSidebar({ variant = "tour" }: Props) {
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
tooltip="Marketplace"
|
||||
className="font-normal hover:!bg-zinc-200 group-data-[collapsible=icon]:!p-1.5 [&>svg]:size-5"
|
||||
// oxlint-disable-next-line design-system/no-restyle -- the tour sidebar's darker hover and 20px-icon collapsed padding; SidebarMenuButton has no variant for them
|
||||
className="hover:!bg-surface-active group-data-[collapsible=icon]:!p-1.5 [&>svg]:size-5"
|
||||
>
|
||||
<Link href="/marketplace">
|
||||
<Icon icon={Store01Icon} className="size-5" />
|
||||
@@ -145,13 +152,9 @@ export function TourSidebar({ variant = "tour" }: Props) {
|
||||
|
||||
<motion.div variants={itemVariants}>
|
||||
<CollapsibleRoot defaultOpen className="group/collapsible">
|
||||
{/* oxlint-disable-next-line design-system/no-restyle -- the copilot sidebar's tighter group padding; SidebarGroup has no size for it */}
|
||||
<SidebarGroup className="py-1">
|
||||
<SidebarGroupLabel
|
||||
asChild
|
||||
// The copy's line box follows the parent's line height, as
|
||||
// before; the primitive's own size would make it 16px.
|
||||
className="leading-inherit"
|
||||
>
|
||||
<SidebarGroupLabel asChild>
|
||||
<CollapsibleTrigger>
|
||||
<Text
|
||||
as="span"
|
||||
@@ -160,14 +163,15 @@ export function TourSidebar({ variant = "tour" }: Props) {
|
||||
>
|
||||
Workspace
|
||||
</Text>
|
||||
<Icon
|
||||
icon={ArrowDown01Icon}
|
||||
className="ease-[cubic-bezier(0.33,1,0.68,1)] ml-auto size-4 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-180 motion-reduce:transition-none"
|
||||
/>
|
||||
<span className="ml-auto flex shrink-0 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-180 motion-reduce:transition-none">
|
||||
<Icon icon={ArrowDown01Icon} className="size-4" />
|
||||
</span>
|
||||
</CollapsibleTrigger>
|
||||
</SidebarGroupLabel>
|
||||
{/* oxlint-disable-next-line design-system/no-restyle -- Radix sets the open state and height on this element, so the collapse animation can't move to a wrapper */}
|
||||
<CollapsibleContent className="overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down motion-reduce:animate-none">
|
||||
<SidebarGroupContent>
|
||||
{/* oxlint-disable-next-line design-system/no-restyle -- the copilot sidebar's collapsed item gap; SidebarMenu has no size for it */}
|
||||
<SidebarMenu className="group-data-[collapsible=icon]:gap-1">
|
||||
<DisabledMenuItem icon={Folder01Icon} label="Files" />
|
||||
</SidebarMenu>
|
||||
@@ -179,6 +183,7 @@ export function TourSidebar({ variant = "tour" }: Props) {
|
||||
</motion.div>
|
||||
</SidebarContent>
|
||||
|
||||
{/* oxlint-disable-next-line design-system/no-restyle -- the upsell card's 12px inset; SidebarFooter has no size for it */}
|
||||
<SidebarFooter className="p-3 group-data-[collapsible=icon]:hidden">
|
||||
{(variant === "marketplace" || !isDemoComplete) && (
|
||||
<TourUpsellCard surface={variant} />
|
||||
|
||||
+4
-3
@@ -20,18 +20,19 @@ export function TourSidebarHeader() {
|
||||
aria-label={isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
||||
onClick={toggleSidebar}
|
||||
className={cn(
|
||||
"size-8 shrink-0 items-center justify-center rounded-md transition-colors hover:bg-zinc-200",
|
||||
"size-8 shrink-0 items-center justify-center rounded-md text-sidebar-foreground transition-colors hover:bg-surface-active",
|
||||
isCollapsed
|
||||
? "absolute inset-0 hidden group-focus-within:flex group-hover:flex"
|
||||
: "flex",
|
||||
)}
|
||||
>
|
||||
<Icon icon={SidebarLeftIcon} className="size-5 text-sidebar-foreground" />
|
||||
<Icon icon={SidebarLeftIcon} className="size-5" />
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<SidebarHeader className="flex animate-fade-in flex-row items-center justify-between gap-2 p-2 group-data-[collapsible=icon]:flex-col">
|
||||
// oxlint-disable-next-line design-system/no-restyle -- one-off mount fade; SidebarHeader has no motion variant
|
||||
<SidebarHeader className="flex animate-fade-in flex-row items-center justify-between group-data-[collapsible=icon]:flex-col">
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex items-center",
|
||||
|
||||
+5
-5
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Card } from "@/components/atoms/Card/Card";
|
||||
import { Icon } from "@/components/atoms/Icon/Icon";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { environment } from "@/services/environment";
|
||||
@@ -32,9 +31,9 @@ export function TourUpsellCard({ surface }: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="flex flex-col gap-4 border border-zinc-200 p-4 shadow-subtle">
|
||||
<div className="flex flex-col gap-4 rounded-large border border-border-base bg-surface p-4 shadow-subtle">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="flex size-9 items-center justify-center rounded-medium bg-purple-100 text-purple-700">
|
||||
<span className="flex size-9 items-center justify-center rounded-medium bg-accent-surface text-accent-strong">
|
||||
<Icon icon={SparklesIcon} size={18} aria-hidden />
|
||||
</span>
|
||||
<Text variant="eyebrow" tone="secondary">
|
||||
@@ -70,11 +69,12 @@ export function TourUpsellCard({ surface }: Props) {
|
||||
size="small"
|
||||
onClick={handleSelfHostClick}
|
||||
leftIcon={<Icon icon={GithubIcon} size={14} aria-hidden />}
|
||||
className="w-full text-xs text-zinc-600"
|
||||
// oxlint-disable-next-line design-system/no-restyle -- quiet 12px secondary link; no Button variant has small muted text at this height
|
||||
className="w-full text-xs text-foreground-secondary"
|
||||
>
|
||||
Self-host instead
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+10
-18
@@ -4,28 +4,26 @@ import type { CSSProperties } from "react";
|
||||
const rectangleSVG = `<svg width='40' height='40' viewBox='0 0 40 40' xmlns='http://www.w3.org/2000/svg'><rect width='40' height='40' x='0' y='0' stroke='rgba(0,0,0,0.08)' fill='none' /></svg>`;
|
||||
const encodedRectangleSVG = encodeURIComponent(rectangleSVG);
|
||||
|
||||
const gridStyle = {
|
||||
"--tour-grid": `url("data:image/svg+xml,${encodedRectangleSVG}")`,
|
||||
} as CSSProperties;
|
||||
|
||||
interface RectanglesProps {
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
function Rectangles({ className, style }: RectanglesProps) {
|
||||
function Rectangles({ className }: RectanglesProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-0 z-0 h-full w-full overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
<div
|
||||
className="h-full w-full"
|
||||
style={{
|
||||
backgroundImage: `url("data:image/svg+xml,${encodedRectangleSVG}")`,
|
||||
backgroundSize: "40px 40px",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "repeat",
|
||||
}}
|
||||
// oxlint-disable-next-line design-system/no-arbitrary-values -- decorative hero grid image and its 40px tile, no token
|
||||
className="h-full w-full bg-[image:var(--tour-grid)] bg-[length:40px_40px] bg-center bg-repeat"
|
||||
style={gridStyle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -34,14 +32,8 @@ function Rectangles({ className, style }: RectanglesProps) {
|
||||
export function TourBackground() {
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-0 z-0 h-full w-full overflow-hidden opacity-40 [perspective:1000px] [transform-style:preserve-3d]">
|
||||
<Rectangles
|
||||
style={{ transform: "rotateX(45deg)" }}
|
||||
className="[mask-image:linear-gradient(to_top,white,transparent)]"
|
||||
/>
|
||||
<Rectangles
|
||||
style={{ transform: "rotateX(-45deg)" }}
|
||||
className="[mask-image:linear-gradient(to_bottom,white,transparent)]"
|
||||
/>
|
||||
<Rectangles className="[mask-image:linear-gradient(to_top,white,transparent)] [transform:rotateX(45deg)]" />
|
||||
<Rectangles className="[mask-image:linear-gradient(to_bottom,white,transparent)] [transform:rotateX(-45deg)]" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import { Icon } from "@/components/atoms/Icon/Icon";
|
||||
|
||||
export function TourHero() {
|
||||
return (
|
||||
<section className="relative flex flex-1 flex-col overflow-hidden border-b border-neutral-100">
|
||||
<section className="relative flex flex-1 flex-col overflow-hidden border-b border-separator">
|
||||
<TourBackground />
|
||||
<div className="relative z-10 flex flex-1 flex-col items-center overflow-y-auto px-6 pb-4 pt-16">
|
||||
<div className="flex w-full max-w-2xl flex-1 flex-col items-center justify-center text-center">
|
||||
@@ -18,10 +18,14 @@ export function TourHero() {
|
||||
className="mb-6 h-20 w-20"
|
||||
/>
|
||||
|
||||
{/* oxlint-disable-next-line design-system/no-arbitrary-values -- hero glass badge's indigo gradient ring and glow, no token */}
|
||||
<span className="relative mb-6 inline-flex overflow-hidden rounded-full bg-[linear-gradient(135deg,rgba(99,102,241,0.6),rgba(59,130,246,0.35),rgba(165,180,252,0.6))] p-px shadow-[0_6px_20px_-6px_rgba(59,130,246,0.35)]">
|
||||
<span className="relative inline-flex items-center gap-2 rounded-full bg-white/50 px-4 py-1.5 shadow-[inset_0_1px_0_rgba(255,255,255,0.75)] backdrop-blur-xl">
|
||||
<Icon icon={SparklesIcon} className="h-4 w-4 text-violet-600" />
|
||||
<Text variant="small-medium" className="text-sm text-zinc-700">
|
||||
{/* oxlint-disable-next-line design-system/no-arbitrary-values -- glass highlight, no shadow token */}
|
||||
<span className="relative inline-flex items-center gap-2 rounded-full bg-surface/50 px-4 py-1.5 shadow-[inset_0_1px_0_rgba(255,255,255,0.75)] backdrop-blur-xl">
|
||||
<span className="contents text-brand">
|
||||
<Icon icon={SparklesIcon} className="h-4 w-4" />
|
||||
</span>
|
||||
<Text variant="body-medium-tight" tone="body">
|
||||
Live demo, no signup required
|
||||
</Text>
|
||||
</span>
|
||||
@@ -30,14 +34,16 @@ export function TourHero() {
|
||||
<Text
|
||||
as="h1"
|
||||
variant="h1"
|
||||
className="text-balance text-[4rem] font-[600] leading-[1]"
|
||||
// oxlint-disable-next-line design-system/no-restyle, design-system/no-arbitrary-values -- marketing hero size, awaiting product decision (SECRT-3152)
|
||||
className="text-balance text-[4rem] leading-none"
|
||||
>
|
||||
Build AI agents by just chatting
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
variant="lead"
|
||||
className="mt-5 max-w-xl text-balance text-zinc-600"
|
||||
tone="secondary"
|
||||
className="mt-5 max-w-xl text-balance"
|
||||
>
|
||||
Describe a goal in plain English and watch Otto build and run a
|
||||
working AutoGPT workflow for you, in seconds, right in your browser.
|
||||
@@ -49,11 +55,11 @@ export function TourHero() {
|
||||
href="/tour/chat"
|
||||
size="large"
|
||||
rightIcon={
|
||||
<Icon
|
||||
icon={ArrowRight02Icon}
|
||||
className="h-4 w-4 transition-transform duration-200 ease-out group-hover:translate-x-1"
|
||||
/>
|
||||
<span className="flex transition-transform duration-200 ease-out group-hover:translate-x-1">
|
||||
<Icon icon={ArrowRight02Icon} className="h-4 w-4" />
|
||||
</span>
|
||||
}
|
||||
// oxlint-disable-next-line design-system/no-restyle -- the hero CTA is wider than the large size; no Button size for it
|
||||
className="group w-full sm:w-auto sm:px-8"
|
||||
>
|
||||
Try the demo
|
||||
|
||||
@@ -111,9 +111,6 @@ describe("components.json", () => {
|
||||
const UNSLOTTED_PRIMITIVES = [
|
||||
"src/components/atoms/RelativeTime/RelativeTime.tsx",
|
||||
"src/components/molecules/Dialog/Dialog.tsx",
|
||||
"src/components/molecules/RunStatusBadge/RunStatusBadge.tsx",
|
||||
"src/components/molecules/Sheet/Sheet.tsx",
|
||||
"src/components/molecules/Sidebar/Sidebar.tsx",
|
||||
];
|
||||
|
||||
function primitiveFiles() {
|
||||
|
||||
@@ -5,8 +5,6 @@ interface Props extends React.SVGProps<SVGSVGElement> {
|
||||
wordmarkColor?: string;
|
||||
}
|
||||
|
||||
// TODO(tokens): brand artwork. The logo's own colours (#669CF6, the gradient
|
||||
// stops #000030, #9900FF, #4285F4, and the clip-path fill) are not UI roles.
|
||||
export function AutoGPTLogo({
|
||||
hideText = false,
|
||||
wordmarkColor = "#000030",
|
||||
@@ -56,10 +54,12 @@ export function AutoGPTLogo({
|
||||
)}
|
||||
<path
|
||||
d="M87.4713 32.257C88.2434 32.257 88.8693 31.6311 88.8693 30.859C88.8693 30.0869 88.2434 29.4609 87.4713 29.4609C86.6992 29.4609 86.0732 30.0869 86.0732 30.859C86.0732 31.6311 86.6992 32.257 87.4713 32.257Z"
|
||||
// oxlint-disable-next-line design-system/no-raw-colors -- AutoGPT logo brand colour, no role
|
||||
fill="#669CF6"
|
||||
/>
|
||||
<path
|
||||
d="M49.2167 39.9475C49.9888 39.9475 50.6147 39.3215 50.6147 38.5494C50.6147 37.7773 49.9888 37.1514 49.2167 37.1514C48.4445 37.1514 47.8186 37.7773 47.8186 38.5494C47.8186 39.3215 48.4445 39.9475 49.2167 39.9475Z"
|
||||
// oxlint-disable-next-line design-system/no-raw-colors -- AutoGPT logo brand colour, no role
|
||||
fill="#669CF6"
|
||||
/>
|
||||
</g>
|
||||
@@ -72,7 +72,9 @@ export function AutoGPTLogo({
|
||||
y2="33.2932"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
{/* oxlint-disable-next-line design-system/no-raw-colors -- AutoGPT logo brand colour, no role */}
|
||||
<stop stopColor="#000030" />
|
||||
{/* oxlint-disable-next-line design-system/no-raw-colors -- AutoGPT logo brand colour, no role */}
|
||||
<stop offset="1" stopColor="#9900FF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
@@ -83,7 +85,9 @@ export function AutoGPTLogo({
|
||||
y2="33.2951"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
{/* oxlint-disable-next-line design-system/no-raw-colors -- AutoGPT logo brand colour, no role */}
|
||||
<stop stopColor="#000030" />
|
||||
{/* oxlint-disable-next-line design-system/no-raw-colors -- AutoGPT logo brand colour, no role */}
|
||||
<stop offset="1" stopColor="#4285F4" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
@@ -94,7 +98,9 @@ export function AutoGPTLogo({
|
||||
y2="-3.94009"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
{/* oxlint-disable-next-line design-system/no-raw-colors -- AutoGPT logo brand colour, no role */}
|
||||
<stop stopColor="#4285F4" />
|
||||
{/* oxlint-disable-next-line design-system/no-raw-colors -- AutoGPT logo brand colour, no role */}
|
||||
<stop offset="1" stopColor="#9900FF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
@@ -105,7 +111,9 @@ export function AutoGPTLogo({
|
||||
y2="34.5465"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
{/* oxlint-disable-next-line design-system/no-raw-colors -- AutoGPT logo brand colour, no role */}
|
||||
<stop stopColor="#000030" />
|
||||
{/* oxlint-disable-next-line design-system/no-raw-colors -- AutoGPT logo brand colour, no role */}
|
||||
<stop offset="1" stopColor="#4285F4" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
@@ -116,11 +124,13 @@ export function AutoGPTLogo({
|
||||
y2="30.9756"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
{/* oxlint-disable-next-line design-system/no-raw-colors -- AutoGPT logo brand colour, no role */}
|
||||
<stop stopColor="#4285F4" />
|
||||
{/* oxlint-disable-next-line design-system/no-raw-colors -- AutoGPT logo brand colour, no role */}
|
||||
<stop offset="1" stopColor="#9900FF" />
|
||||
</linearGradient>
|
||||
<clipPath id={clipId}>
|
||||
<rect width="88.8696" height="40" fill="white" />
|
||||
<rect width="88.8696" height="40" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props extends React.SVGProps<SVGSVGElement> {
|
||||
hideText?: boolean;
|
||||
}
|
||||
|
||||
// TODO(tokens): brand artwork. The fills are #FFFFFF, which only shadcn's
|
||||
// card and popover names hold (surface is #FEFEFE); a logo is neither.
|
||||
export function AutoGPTLogoWhite({
|
||||
hideText = false,
|
||||
className,
|
||||
@@ -16,42 +16,45 @@ export function AutoGPTLogoWhite({
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-label="AutoGPT Logo"
|
||||
className={className ?? "h-[3.375rem] w-auto"}
|
||||
className={cn(
|
||||
"text-action-foreground",
|
||||
className ?? "h-[3.375rem] w-auto",
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M92.5796 38.9702V52.1619C92.5796 53.1347 91.7927 53.9281 90.8293 53.9281C89.789 53.9281 88.9297 53.2639 88.9297 52.1619V32.4754C88.9297 28.297 92.2917 24.906 96.4377 24.906C100.584 24.906 103.946 28.294 103.946 32.4754C103.946 36.6553 100.585 40.0448 96.4377 40.0448C95.0281 40.0448 93.7104 39.6511 92.5827 38.9702H92.5796ZM94.1159 30.1377C95.3959 30.1377 96.4347 31.1834 96.4347 32.4739C96.4347 33.7643 95.3974 34.8116 94.1159 34.8116C93.8777 34.8116 93.6471 34.7751 93.4315 34.7082C94.1114 35.6369 95.2044 36.2388 96.4362 36.2388C98.4986 36.2388 100.169 34.5547 100.169 32.4754C100.169 30.3961 98.4986 28.712 96.4362 28.712C95.2044 28.712 94.1129 29.3139 93.4315 30.2426C93.6471 30.1757 93.8777 30.1392 94.1159 30.1392V30.1377Z"
|
||||
fill="white"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M83.2017 38.9701V47.4454C83.2017 48.2145 82.8896 48.9152 82.3875 49.4229C81.2824 50.5355 79.2336 50.5355 78.13 49.4229C76.6812 47.9622 78.0546 45.4847 75.4585 42.8673C72.9392 40.3275 68.5566 40.3275 66.0403 42.8673C64.8765 44.0407 64.1543 45.661 64.1543 47.4454C64.1543 48.4182 64.9413 49.2116 65.9062 49.2116C66.9479 49.2116 67.8058 48.5474 67.8058 47.4454C67.8058 46.6763 68.1179 45.9772 68.6199 45.4695C69.725 44.3569 71.7738 44.3569 72.8774 45.4695C74.4589 47.0639 72.8623 49.3165 75.5489 52.0236C78.0682 54.5634 82.4494 54.5634 84.9671 52.0236C86.131 50.8501 86.8531 49.2299 86.8531 47.4454V32.5255V32.4769C86.8531 28.2985 83.4926 24.9075 79.3452 24.9075C75.1992 24.9075 71.8372 28.2955 71.8372 32.4769C71.8372 36.6568 75.1977 40.0463 79.3452 40.0463C80.7563 40.0447 82.0755 39.6511 83.2017 38.9701ZM79.3452 36.2388C78.1134 36.2388 77.0219 35.6369 76.3404 34.7082C76.556 34.775 76.7867 34.8115 77.0249 34.8115C78.3049 34.8115 79.3436 33.7643 79.3436 32.4738C79.3436 31.1834 78.3049 30.1377 77.0249 30.1377C76.7867 30.1377 76.556 30.1741 76.3404 30.241C77.0204 29.3123 78.1119 28.7104 79.3452 28.7104C81.4061 28.7104 83.078 30.3945 83.078 32.4738C83.078 34.5531 81.4061 36.2373 79.3452 36.2373V36.2388Z"
|
||||
fill="white"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M109.414 17.4963C109.414 12.8437 107.577 8.42514 104.318 5.13746C101.058 1.85131 96.6757 0 92.0594 0C87.4445 0 83.0634 1.85131 79.8008 5.13746C76.5414 8.42362 74.7051 12.8437 74.7051 17.4963V18.2882C74.7051 19.3005 75.5192 20.1228 76.5248 20.1228C77.5289 20.1228 78.3445 19.302 78.3445 18.2882V17.4963C78.3445 13.8195 79.7994 10.3281 82.3744 7.73052C84.9494 5.13442 88.4139 3.66614 92.0609 3.66614C95.7078 3.66614 99.1694 5.1329 101.744 7.73052C104.319 10.3266 105.776 13.8195 105.776 17.4963C105.776 18.5086 106.59 19.3309 107.595 19.3309C108.598 19.3309 109.414 18.5101 109.414 17.4963Z"
|
||||
fill="white"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M109.412 23.0335V25.2937H112.342C113.317 25.2937 114.113 26.0962 114.113 27.0812C114.113 28.1132 113.414 28.972 112.342 28.972H109.412V47.517C109.412 48.2861 109.726 48.9853 110.228 49.493C111.332 50.6071 113.38 50.6071 114.484 49.493C114.988 48.9853 115.3 48.2846 115.3 47.517V46.8984C115.3 45.7964 116.159 45.1322 117.199 45.1322C118.164 45.1322 118.951 45.9256 118.951 46.8984V47.517C118.951 49.303 118.229 50.9233 117.065 52.0951C114.546 54.635 110.177 54.635 107.658 52.0951C106.494 50.9217 105.771 49.3015 105.771 47.517V23.0335C105.771 21.9695 106.61 21.2476 107.636 21.2476C108.612 21.2476 109.408 22.0516 109.408 23.0335H109.412Z"
|
||||
fill="white"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M101.004 52.1604C101.004 53.1317 100.217 53.9251 99.2516 53.9251C98.2111 53.9251 97.3516 53.2609 97.3516 52.1604C97.3516 46.7706 97.3516 51.56 97.3516 46.1702C97.3516 45.1975 98.1387 44.4041 99.1038 44.4041C100.144 44.4041 101.004 45.0683 101.004 46.1702C101.004 51.56 101.004 46.7706 101.004 52.1604Z"
|
||||
fill="white"
|
||||
fill="currentColor"
|
||||
/>
|
||||
{!hideText && (
|
||||
<path
|
||||
d="M15.7577 30.0936V42.6956H11.9751V36.3696H3.78263V42.6956H0V30.0936C0.00301526 19.7108 15.7577 19.574 15.7577 30.0936ZM59.4292 42.6941C65.027 42.6941 69.565 38.119 69.565 32.4754C69.565 26.8318 65.027 22.2567 59.4292 22.2567C53.8314 22.2567 49.2934 26.8318 49.2934 32.4754C49.2934 38.119 53.8314 42.6941 59.4292 42.6941ZM59.4292 38.7848C55.9722 38.7848 53.171 35.9607 53.171 32.4754C53.171 28.9902 55.9722 26.1661 59.4292 26.1661C62.8862 26.1661 65.6873 28.9902 65.6873 32.4754C65.6873 35.9607 62.8862 38.7848 59.4292 38.7848ZM49.9748 22.2552V25.9335H44.469V42.6941H40.8205V25.9335H35.3147V22.2552C40.2024 22.2552 45.0886 22.2552 49.9748 22.2552ZM33.4919 34.8572V22.2552H29.7093V34.8572C29.7093 40.5312 21.5169 40.4294 21.5169 34.8572V22.2552H17.7342V34.8572C17.7357 45.237 33.4919 45.3753 33.4919 34.8572ZM11.9766 32.6897V30.0936C11.9766 24.4196 3.78414 24.5215 3.78414 30.0936V32.6897H11.9766Z"
|
||||
fill="white"
|
||||
fill="currentColor"
|
||||
/>
|
||||
)}
|
||||
<path
|
||||
d="M117.129 43.5454C118.163 43.5454 119.002 42.7002 119.002 41.6576C119.002 40.615 118.163 39.7698 117.129 39.7698C116.094 39.7698 115.256 40.615 115.256 41.6576C115.256 42.7002 116.094 43.5454 117.129 43.5454Z"
|
||||
fill="white"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M65.9022 53.9282C66.9366 53.9282 67.7751 53.083 67.7751 52.0404C67.7751 50.9978 66.9366 50.1526 65.9022 50.1526C64.8678 50.1526 64.0293 50.9978 64.0293 52.0404C64.0293 53.083 64.8678 53.9282 65.9022 53.9282Z"
|
||||
fill="white"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -129,7 +129,7 @@ export function AvatarImage({
|
||||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
// eslint-disable-next-line @next/next/no-img-element -- avatar URLs come from any host, so next/image does not fit
|
||||
<img
|
||||
data-slot="avatar-image"
|
||||
src={normalizedSrc}
|
||||
@@ -230,9 +230,8 @@ export function AvatarFallback({
|
||||
className={cn(
|
||||
// absolute so the fallback overlays (not flows beside) the image while it loads;
|
||||
// svg stretched to fill so the marble always matches the avatar size.
|
||||
// TODO(tokens): no text role is 18px/28px (text-lg) and no colour
|
||||
// role is neutral-600 #525252.
|
||||
"absolute inset-0 flex items-center justify-center rounded-full bg-transparent text-lg text-neutral-600",
|
||||
// TODO(tokens): no text role is 18px/28px (text-lg).
|
||||
"absolute inset-0 flex items-center justify-center rounded-full bg-transparent text-lg text-foreground-secondary",
|
||||
!hasCustomFallback && "[&>svg]:h-full [&>svg]:w-full",
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -15,12 +15,10 @@ interface BadgeProps<V extends BadgeVariant> {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// TODO(tokens): no role is emerald-50 #ECFDF5, emerald-700 #047857,
|
||||
// emerald-600 #059669 or amber-800 #92400E.
|
||||
const tintVariants: Record<TintVariant, string> = {
|
||||
success: "bg-emerald-50 text-emerald-700 ring-emerald-600/20",
|
||||
success: "bg-success-surface text-success-foreground ring-success/20",
|
||||
error: "bg-error-surface text-error-foreground ring-error-hover/10",
|
||||
warning: "bg-warning-surface text-amber-800 ring-warning/20",
|
||||
warning: "bg-warning-surface text-warning-foreground ring-warning/20",
|
||||
info: "bg-surface-muted text-foreground-secondary ring-foreground-tertiary/10",
|
||||
};
|
||||
|
||||
@@ -30,44 +28,24 @@ const badgeSizes: Record<TintSize, string> = {
|
||||
};
|
||||
|
||||
// Bordered chips, which render a <div> like the legacy badge they replace.
|
||||
// Their sizes only set the type. `ring-focus` is a role that `.dark` does not
|
||||
// redefine. The legacy dark: classes are moved verbatim; DS6 (SECRT-3116)
|
||||
// deletes them.
|
||||
// Their sizes only set the type.
|
||||
const chipLayout =
|
||||
"inline-flex cursor-default items-center rounded-md border px-2.5 py-0.5";
|
||||
|
||||
const chipBehaviour =
|
||||
"transition-colors focus:outline-none focus:ring-2 focus:ring-focus focus:ring-offset-2 dark:border-neutral-800 dark:focus:ring-neutral-300";
|
||||
"transition-colors focus:outline-none focus:ring-2 focus:ring-focus focus:ring-offset-2";
|
||||
|
||||
// TODO(tokens): no role is 0.75rem/1rem (text-xs), so caption plus leading-4,
|
||||
// or 0.625rem at the parent's line height (text-[10px]), so tiny plus
|
||||
// leading-inherit.
|
||||
const chipSizes: Record<ChipSize, string> = {
|
||||
default: "text-caption font-semibold leading-4",
|
||||
tiny: "text-tiny font-semibold leading-inherit",
|
||||
"tiny-normal": "text-tiny font-normal leading-inherit",
|
||||
};
|
||||
|
||||
// TODO(tokens): no role is neutral-900 #171717, neutral-100 #F5F5F5 or
|
||||
// neutral-200 #E5E5E5. neutral-950 #0A0A0A is the `focus` role, a ring
|
||||
// colour, not a text colour.
|
||||
// TODO(tokens): primary-foreground holds #FAFAFA in :root but #18181B
|
||||
// (24 24 27) in .dark (text on the light .dark primary fill), so it doesn't
|
||||
// fit the solid chip's neutral-50.
|
||||
// TODO(tokens): destructive holds #EF4444 in :root but #7F1D1D (127 29 29) in
|
||||
// .dark (the dark destructive fill), so it doesn't fit the destructive chip's
|
||||
// red-500. The `error` role also holds #EF4444 in both themes, but it is the
|
||||
// error-state colour for text and borders, not a chip fill (rule 13 pair for
|
||||
// SECRT-3154). destructive-foreground stays: it is #FAFAFA (250 250 250) in :root
|
||||
// and .dark alike, the text on the destructive fill.
|
||||
const chipVariants: Record<ChipVariant, string> = {
|
||||
solid:
|
||||
"border-transparent bg-neutral-900 text-neutral-50 shadow dark:bg-neutral-50 dark:text-neutral-900",
|
||||
secondary:
|
||||
"border-transparent bg-neutral-100 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-50",
|
||||
destructive:
|
||||
"border-transparent bg-red-500 text-destructive-foreground shadow dark:bg-red-900 dark:text-neutral-50",
|
||||
outline: "border-neutral-200 text-neutral-950 dark:text-neutral-50",
|
||||
solid: "border-transparent bg-action text-action-foreground shadow",
|
||||
secondary: "border-transparent bg-surface-hover text-foreground-strong",
|
||||
destructive: "border-transparent bg-error text-destructive-foreground shadow",
|
||||
outline: "border-border-base text-foreground-base",
|
||||
};
|
||||
|
||||
function isChip(variant: BadgeVariant): variant is ChipVariant {
|
||||
|
||||
@@ -411,7 +411,7 @@ function renderActionButtons() {
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
leadingIcon={FilterHorizontalIcon}
|
||||
className="text-zinc-600"
|
||||
className="text-foreground-secondary"
|
||||
>
|
||||
All work
|
||||
</Button>
|
||||
@@ -494,11 +494,11 @@ function renderToggleButtons() {
|
||||
variant="toggle"
|
||||
size="xs"
|
||||
aria-pressed
|
||||
className="border-zinc-200 bg-white aria-pressed:border-amber-200 aria-pressed:bg-amber-100 aria-pressed:text-amber-700"
|
||||
className="border-border-base bg-surface aria-pressed:border-warning-border aria-pressed:bg-warning-surface aria-pressed:text-warning-foreground"
|
||||
>
|
||||
Needs review (3)
|
||||
</Button>
|
||||
<div className="flex h-7 items-center rounded-md border border-zinc-200 p-0.5">
|
||||
<div className="flex h-7 items-center rounded-md border border-border-base p-0.5">
|
||||
<Button
|
||||
variant="toggle"
|
||||
size="icon-xs"
|
||||
@@ -524,7 +524,7 @@ function renderContextualLoadingExamples() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="mb-4 text-base font-medium text-neutral-900">
|
||||
<h3 className="mb-4 text-base font-medium text-foreground-strong">
|
||||
✅ Good Examples - Contextual Loading Text
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
@@ -556,7 +556,7 @@ function renderContextualLoadingExamples() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-4 text-base font-medium text-red-600">
|
||||
<h3 className="mb-4 text-base font-medium text-error-foreground">
|
||||
❌ Avoid - Generic Loading Text
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
@@ -570,7 +570,7 @@ function renderContextualLoadingExamples() {
|
||||
Working...
|
||||
</Button>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-neutral-600">
|
||||
<p className="mt-2 text-sm text-foreground-secondary">
|
||||
These examples are disabled to show what NOT to do. Use specific
|
||||
action-based text instead.
|
||||
</p>
|
||||
@@ -650,13 +650,13 @@ function renderAllVariants() {
|
||||
<div className="space-y-12 p-8">
|
||||
{/* Large buttons section */}
|
||||
<div className="space-y-8">
|
||||
<h2 className="text-3xl font-semibold text-neutral-900">
|
||||
<h2 className="text-3xl font-semibold text-foreground-strong">
|
||||
Large buttons
|
||||
</h2>
|
||||
<div className="flex flex-wrap gap-20">
|
||||
{/* Primary */}
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="font-['Geist'] text-base font-medium text-neutral-900">
|
||||
<div className="font-['Geist'] text-base font-medium text-foreground-strong">
|
||||
Primary
|
||||
</div>
|
||||
<div className="flex flex-col gap-8">
|
||||
@@ -681,7 +681,7 @@ function renderAllVariants() {
|
||||
|
||||
{/* Secondary */}
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="font-['Geist'] text-base font-medium text-neutral-900">
|
||||
<div className="font-['Geist'] text-base font-medium text-foreground-strong">
|
||||
Secondary
|
||||
</div>
|
||||
<div className="flex flex-col gap-8">
|
||||
@@ -706,7 +706,7 @@ function renderAllVariants() {
|
||||
|
||||
{/* Destructive */}
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="font-['Geist'] text-base font-medium text-neutral-900">
|
||||
<div className="font-['Geist'] text-base font-medium text-foreground-strong">
|
||||
Destructive
|
||||
</div>
|
||||
<div className="flex flex-col gap-8">
|
||||
@@ -731,7 +731,7 @@ function renderAllVariants() {
|
||||
|
||||
{/* Outline */}
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="font-['Geist'] text-base font-medium text-neutral-900">
|
||||
<div className="font-['Geist'] text-base font-medium text-foreground-strong">
|
||||
Outline
|
||||
</div>
|
||||
<div className="flex flex-col gap-8">
|
||||
@@ -756,7 +756,7 @@ function renderAllVariants() {
|
||||
|
||||
{/* Ghost */}
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="font-['Geist'] text-base font-medium text-neutral-900">
|
||||
<div className="font-['Geist'] text-base font-medium text-foreground-strong">
|
||||
Save
|
||||
</div>
|
||||
<div className="flex flex-col gap-8">
|
||||
@@ -783,13 +783,13 @@ function renderAllVariants() {
|
||||
|
||||
{/* Small buttons section */}
|
||||
<div className="space-y-8">
|
||||
<h2 className="text-3xl font-semibold text-neutral-900">
|
||||
<h2 className="text-3xl font-semibold text-foreground-strong">
|
||||
Small buttons
|
||||
</h2>
|
||||
<div className="flex flex-wrap gap-20">
|
||||
{/* Primary Small */}
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="font-['Geist'] text-base font-medium text-neutral-900">
|
||||
<div className="font-['Geist'] text-base font-medium text-foreground-strong">
|
||||
Primary
|
||||
</div>
|
||||
<div className="flex flex-col gap-8">
|
||||
@@ -814,7 +814,7 @@ function renderAllVariants() {
|
||||
|
||||
{/* Secondary Small */}
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="font-['Geist'] text-base font-medium text-neutral-900">
|
||||
<div className="font-['Geist'] text-base font-medium text-foreground-strong">
|
||||
Secondary
|
||||
</div>
|
||||
<div className="flex flex-col gap-8">
|
||||
@@ -839,7 +839,7 @@ function renderAllVariants() {
|
||||
|
||||
{/* Destructive Small */}
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="font-['Geist'] text-base font-medium text-neutral-900">
|
||||
<div className="font-['Geist'] text-base font-medium text-foreground-strong">
|
||||
Destructive
|
||||
</div>
|
||||
<div className="flex flex-col gap-8">
|
||||
@@ -864,7 +864,7 @@ function renderAllVariants() {
|
||||
|
||||
{/* Outline Small */}
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="font-['Geist'] text-base font-medium text-neutral-900">
|
||||
<div className="font-['Geist'] text-base font-medium text-foreground-strong">
|
||||
Outline
|
||||
</div>
|
||||
<div className="flex flex-col gap-8">
|
||||
@@ -889,7 +889,7 @@ function renderAllVariants() {
|
||||
|
||||
{/* Ghost Small */}
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="font-['Geist'] text-base font-medium text-neutral-900">
|
||||
<div className="font-['Geist'] text-base font-medium text-foreground-strong">
|
||||
Ghost
|
||||
</div>
|
||||
<div className="flex flex-col gap-8">
|
||||
@@ -916,19 +916,19 @@ function renderAllVariants() {
|
||||
|
||||
{/* Other button types */}
|
||||
<div className="space-y-8">
|
||||
<h2 className="text-3xl font-semibold text-neutral-900">
|
||||
<h2 className="text-3xl font-semibold text-foreground-strong">
|
||||
Other button types
|
||||
</h2>
|
||||
<div className="flex gap-20">
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="font-['Geist'] text-base font-medium text-zinc-800">
|
||||
<div className="font-['Geist'] text-base font-medium text-foreground-emphasis">
|
||||
Icon
|
||||
</div>
|
||||
<div className="flex flex-col gap-8">
|
||||
<Button variant="icon" size="icon">
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="primary" size="icon" className="bg-zinc-700">
|
||||
<Button variant="primary" size="icon" className="bg-action">
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="icon" size="icon" disabled>
|
||||
@@ -937,7 +937,7 @@ function renderAllVariants() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="font-['Geist'] text-base font-medium text-zinc-800">
|
||||
<div className="font-['Geist'] text-base font-medium text-foreground-emphasis">
|
||||
Link
|
||||
</div>
|
||||
<div className="flex flex-col gap-8">
|
||||
|
||||
@@ -7,27 +7,18 @@ import { extendedButtonVariants } from "./helpers";
|
||||
// base (extendedButtonVariants with no variant and no size), so a compact
|
||||
// button renders the same class string, in the same order, as it did when
|
||||
// these entries lived in extendedButtonVariants.
|
||||
// The deleted file's dark: classes are carried verbatim.
|
||||
// TODO(tokens): no role holds neutral-900 #171717, neutral-100 #F5F5F5 or
|
||||
// neutral-200 #E5E5E5 (border is #E4E4E7), so they stay raw. Four colours
|
||||
// whose role matches in light mode stay raw too, because the role's .dark
|
||||
// value differs from this button's dark colour (SECRT-3154):
|
||||
// ring-neutral-950 (focus: no .dark value, dark ring is neutral-300),
|
||||
// text-neutral-50 on primary (primary-foreground: .dark #18181B, dark text is
|
||||
// neutral-900 #171717), bg-red-500 (error: no .dark value, dark bg is red-900)
|
||||
// and bg-white (surface: no .dark value, dark bg is neutral-950).
|
||||
const COMPACT_BASE =
|
||||
"gap-2 rounded-md border-0 min-w-0 text-body leading-5 focus-visible:ring-neutral-950 dark:focus-visible:ring-neutral-300 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0";
|
||||
"gap-2 rounded-md border-0 min-w-0 text-body leading-5 focus-visible:ring-focus [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0";
|
||||
|
||||
export const compactButtonVariants = cva(
|
||||
extendedButtonVariants({ variant: null, size: null }),
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
"compact-primary": `${COMPACT_BASE} bg-neutral-900 text-neutral-50 shadow hover:bg-neutral-900/90 dark:bg-neutral-50 dark:text-neutral-900 dark:hover:bg-neutral-50/90`,
|
||||
"compact-destructive": `${COMPACT_BASE} bg-red-500 text-destructive-foreground shadow-sm hover:bg-red-500/90 dark:bg-red-900 dark:text-neutral-50 dark:hover:bg-red-900/90`,
|
||||
"compact-outline": `${COMPACT_BASE} border border-neutral-200 bg-white shadow-sm hover:bg-neutral-100 hover:text-neutral-900 dark:border-neutral-800 dark:bg-neutral-950 dark:hover:bg-neutral-800 dark:hover:text-neutral-50`,
|
||||
"compact-ghost": `${COMPACT_BASE} hover:bg-neutral-100 hover:text-neutral-900 dark:hover:bg-neutral-800 dark:hover:text-neutral-50`,
|
||||
"compact-primary": `${COMPACT_BASE} bg-action text-action-foreground shadow hover:bg-action-hover`,
|
||||
"compact-destructive": `${COMPACT_BASE} bg-error text-destructive-foreground shadow-sm hover:bg-error/90`,
|
||||
"compact-outline": `${COMPACT_BASE} border border-border-base bg-surface shadow-sm hover:bg-surface-hover hover:text-foreground-strong`,
|
||||
"compact-ghost": `${COMPACT_BASE} hover:bg-surface-hover hover:text-foreground-strong`,
|
||||
},
|
||||
size: {
|
||||
compact: "h-9 px-4 py-2",
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
} from "react-day-picker";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { legacyButtonVariants } from "@/components/atoms/Button/helpers";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -99,44 +98,26 @@ function Calendar({
|
||||
button_next: "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
||||
month_grid: "w-full border-collapse space-y-1",
|
||||
weekdays: "flex",
|
||||
// TODO(tokens): no exact role: text-neutral-500 #737373. SECRT-3152
|
||||
// rare size: text-[0.8rem].
|
||||
weekday:
|
||||
"text-neutral-500 rounded-md w-8 font-normal text-[0.8rem] dark:text-neutral-400",
|
||||
"text-foreground-tertiary rounded-md w-8 font-normal text-body-sm leading-inherit",
|
||||
week: "flex w-full mt-2",
|
||||
// TODO(tokens): no exact role: bg-neutral-100 #F5F5F5.
|
||||
day: cn(
|
||||
"relative p-0 text-center text-body leading-5 focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-neutral-100 [&:has([aria-selected].day-outside)]:bg-neutral-100/50 [&:has([aria-selected].day-range-end)]:rounded-r-md dark:[&:has([aria-selected])]:bg-neutral-800 dark:[&:has([aria-selected].day-outside)]:bg-neutral-800/50",
|
||||
"relative p-0 text-center text-body leading-5 focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-surface-hover [&:has([aria-selected].day-outside)]:bg-surface-hover/50 [&:has([aria-selected].day-range-end)]:rounded-r-md",
|
||||
props.mode === "range"
|
||||
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
|
||||
: "[&:has([aria-selected])]:rounded-md",
|
||||
),
|
||||
day_button: cn(
|
||||
legacyButtonVariants({ variant: "legacy-ghost" }),
|
||||
"h-8 w-8 p-0 font-normal aria-selected:opacity-100",
|
||||
),
|
||||
range_start: "range-start",
|
||||
range_end: "range-end",
|
||||
// TODO(tokens): no exact role: bg-neutral-900 #171717,
|
||||
// text-neutral-100 #F5F5F5, bg-neutral-700 #404040. text-neutral-50
|
||||
// #FAFAFA is `primary-foreground` in :root only (#18181B in .dark),
|
||||
// so it stays raw. SECRT-3154
|
||||
day_button:
|
||||
"inline-flex h-8 w-8 items-center justify-center whitespace-nowrap rounded-md p-0 text-body leading-5 font-normal transition-colors hover:bg-surface-hover hover:text-foreground-strong focus-visible:outline-none focus-visible:ring-0 disabled:pointer-events-none disabled:opacity-50 aria-selected:opacity-100",
|
||||
selected:
|
||||
"bg-neutral-900 text-neutral-100 hover:bg-neutral-900 hover:text-neutral-50 focus:bg-neutral-700 focus:text-neutral-50 dark:bg-neutral-50 dark:text-neutral-900 dark:hover:bg-neutral-50 dark:hover:text-neutral-900 dark:focus:bg-neutral-50 dark:focus:text-neutral-900",
|
||||
// TODO(tokens): no exact role: bg-neutral-100 #F5F5F5,
|
||||
// text-neutral-900 #171717.
|
||||
today:
|
||||
"bg-neutral-100 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-50",
|
||||
// TODO(tokens): no exact role: text-neutral-500 #737373,
|
||||
// bg-neutral-100 #F5F5F5.
|
||||
"bg-action text-action-foreground hover:bg-action hover:text-action-foreground focus:bg-action focus:text-action-foreground",
|
||||
today: "bg-surface-hover text-foreground-strong",
|
||||
outside:
|
||||
"day-outside text-neutral-500 opacity-50 aria-selected:bg-neutral-100/50 aria-selected:text-neutral-500 aria-selected:opacity-30 dark:text-neutral-400 dark:aria-selected:bg-neutral-800/50 dark:aria-selected:text-neutral-400",
|
||||
// TODO(tokens): no exact role: text-neutral-500 #737373.
|
||||
disabled: "text-neutral-500 opacity-50 dark:text-neutral-400",
|
||||
// TODO(tokens): no exact role: bg-neutral-100 #F5F5F5,
|
||||
// text-neutral-900 #171717.
|
||||
// oxlint-disable-next-line design-system/no-unknown-classes -- day-outside is a marker the day selector above matches
|
||||
"day-outside text-foreground-tertiary opacity-50 aria-selected:bg-surface-hover/50 aria-selected:text-foreground-tertiary aria-selected:opacity-30",
|
||||
disabled: "text-foreground-tertiary opacity-50",
|
||||
range_middle:
|
||||
"aria-selected:bg-neutral-100 aria-selected:text-neutral-900 dark:aria-selected:bg-neutral-800 dark:aria-selected:text-neutral-50",
|
||||
"aria-selected:bg-surface-hover aria-selected:text-foreground-strong",
|
||||
hidden: "invisible",
|
||||
...classNames,
|
||||
}}
|
||||
|
||||
@@ -4,9 +4,7 @@ import { HTMLAttributes, ReactNode } from "react";
|
||||
|
||||
const variants = {
|
||||
default: "rounded-large bg-surface p-6 shadow-md",
|
||||
// TODO(tokens): text-neutral-950 #0A0A0A is the `focus` role, a ring
|
||||
// colour, not a text colour (SECRT-3154).
|
||||
outlined: "agpt-card text-neutral-950",
|
||||
outlined: "agpt-card text-foreground-base",
|
||||
} as const;
|
||||
|
||||
interface Props {
|
||||
@@ -60,9 +58,8 @@ export function CardDescription({
|
||||
variant="body-tight"
|
||||
as="p"
|
||||
data-slot="card-description"
|
||||
// TODO(tokens): no exact role: text-neutral-500 #737373. The dark: class
|
||||
// is the legacy card's, kept verbatim.
|
||||
className={cn("text-neutral-500 dark:text-neutral-400", className)}
|
||||
tone="muted"
|
||||
className={className}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -14,12 +14,7 @@ const Checkbox = React.forwardRef<
|
||||
ref={ref}
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
// TODO(tokens): no role is neutral-900 #171717.
|
||||
// TODO(tokens): primary-foreground holds #FAFAFA in :root but 24 24 27
|
||||
// in .dark (text on the primary fill), so it doesn't fit; neutral-50
|
||||
// stays.
|
||||
// Legacy dark: classes, moved verbatim; DS6 (SECRT-3116) deletes them.
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-neutral-900 shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-focus disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-neutral-900 data-[state=checked]:text-neutral-50 dark:border-neutral-50 dark:border-neutral-800 dark:focus-visible:ring-neutral-300 dark:data-[state=checked]:bg-neutral-50 dark:data-[state=checked]:text-neutral-900",
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-action-hover shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-focus disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-action data-[state=checked]:bg-action data-[state=checked]:text-action-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import * as React from "react";
|
||||
import { Calendar as CalendarIcon } from "lucide-react";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Popover,
|
||||
@@ -83,14 +82,15 @@ export const DateInput = ({
|
||||
const isDisabled = disabled || readonly;
|
||||
|
||||
const triggerStyles = cn(
|
||||
"inline-flex h-9 items-center whitespace-nowrap py-2 transition-colors hover:bg-surface-hover hover:text-foreground-strong focus-visible:outline-none focus-visible:ring-0 disabled:pointer-events-none disabled:opacity-50",
|
||||
// Base styles matching other form components
|
||||
"rounded-3xl border border-border-base bg-surface px-4 shadow-none",
|
||||
"font-normal text-foreground-base w-full text-body leading-5",
|
||||
"placeholder:font-normal !placeholder:text-placeholder",
|
||||
"placeholder:font-normal",
|
||||
// Focus and hover states
|
||||
"focus:border-foreground-subtle focus:shadow-none focus:outline-none focus:ring-1 focus:ring-foreground-subtle focus:ring-offset-0",
|
||||
// Error state
|
||||
error && "border-1.5 border-error focus:border-error focus:ring-error",
|
||||
error && "border-error focus:border-error focus:ring-error",
|
||||
// Placeholder styling
|
||||
!selected && "text-foreground-subtle",
|
||||
"justify-start text-left",
|
||||
@@ -108,21 +108,19 @@ export const DateInput = ({
|
||||
return (
|
||||
<div data-slot="date-input" className="flex flex-col gap-1">
|
||||
{label && !hideLabel && (
|
||||
// TODO(tokens): no role is gray-700 #374151.
|
||||
<label
|
||||
data-slot="date-input-label"
|
||||
htmlFor={id}
|
||||
className="text-body font-medium leading-5 text-gray-700"
|
||||
className="text-body font-medium leading-5 text-foreground-body"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
<button
|
||||
data-slot="date-input-trigger"
|
||||
type="button"
|
||||
variant="legacy-ghost"
|
||||
className={triggerStyles}
|
||||
disabled={isDisabled}
|
||||
autoFocus={autoFocus}
|
||||
@@ -136,14 +134,11 @@ export const DateInput = ({
|
||||
className={cn("mr-2", size === "default" ? "h-4 w-4" : "h-3 w-3")}
|
||||
/>
|
||||
{buttonText}
|
||||
</Button>
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
{/* TODO(tokens): today's popover look. No exact role: border-neutral-200
|
||||
#E5E5E5; text-neutral-950 #0A0A0A is the `focus` role, a ring colour,
|
||||
not a text colour (SECRT-3154). */}
|
||||
<PopoverContent
|
||||
data-slot="date-input-popup"
|
||||
className="w-auto rounded-medium border-neutral-200 p-0 text-neutral-950 dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50"
|
||||
className="w-auto rounded-medium border-border-base p-0 text-foreground-base"
|
||||
sideOffset={6}
|
||||
>
|
||||
<Calendar
|
||||
|
||||
@@ -136,11 +136,11 @@ export const DateTimeInput = ({
|
||||
// Base styles matching other form components
|
||||
"rounded-3xl border border-border-base bg-surface px-4 shadow-none",
|
||||
"font-normal text-foreground-base w-full text-body leading-5",
|
||||
"placeholder:font-normal !placeholder:text-placeholder",
|
||||
"placeholder:font-normal",
|
||||
// Focus and hover states
|
||||
"focus:border-foreground-subtle focus:shadow-none focus:outline-none focus:ring-1 focus:ring-foreground-subtle focus:ring-offset-0",
|
||||
// Error state
|
||||
error && "border-1.5 border-error focus:border-error focus:ring-error",
|
||||
error && "border-error focus:border-error focus:ring-error",
|
||||
// Placeholder styling
|
||||
!selected && "text-foreground-subtle",
|
||||
"justify-start text-left",
|
||||
@@ -205,12 +205,9 @@ export const DateTimeInput = ({
|
||||
{buttonText}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
{/* TODO(tokens): today's popover look. No exact role: border-neutral-200
|
||||
#E5E5E5; text-neutral-950 #0A0A0A is the `focus` role, a ring colour,
|
||||
not a text colour (SECRT-3154). */}
|
||||
<PopoverContent
|
||||
data-slot="date-time-input-popup"
|
||||
className="w-auto rounded-medium border-neutral-200 p-0 text-neutral-950 dark:border-neutral-800 dark:bg-neutral-950 dark:text-neutral-50"
|
||||
className="w-auto rounded-medium border-border-base p-0 text-foreground-base"
|
||||
sideOffset={6}
|
||||
>
|
||||
<div data-slot="date-time-input-popup-body" className="p-3">
|
||||
@@ -228,11 +225,10 @@ export const DateTimeInput = ({
|
||||
data-slot="date-time-input-time"
|
||||
className="mt-3 border-t pt-3"
|
||||
>
|
||||
{/* TODO(tokens): no role is gray-700 #374151. */}
|
||||
<label
|
||||
data-slot="date-time-input-time-label"
|
||||
htmlFor={timeInputId}
|
||||
className="mb-2 block text-body font-medium leading-5 text-gray-700"
|
||||
className="mb-2 block text-body font-medium leading-5 text-foreground-body"
|
||||
>
|
||||
Time
|
||||
</label>
|
||||
|
||||
@@ -22,9 +22,9 @@ type Story = StoryObj<typeof meta>;
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<FadeIn>
|
||||
<div className="rounded-lg border border-zinc-200 bg-white p-8 text-center">
|
||||
<div className="rounded-lg border border-border-base bg-surface p-8 text-center">
|
||||
<p className="text-lg font-medium">This content fades in</p>
|
||||
<p className="text-sm text-zinc-500">
|
||||
<p className="text-sm text-foreground-tertiary">
|
||||
With a subtle upward slide animation
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -35,7 +35,7 @@ function PreviewButton({
|
||||
data-slot="file-input-preview"
|
||||
variant="outline"
|
||||
size="small"
|
||||
className="h-7 w-7 min-w-0 flex-shrink-0 border-border-strong p-0 text-gray-500 hover:text-info-foreground dark:text-gray-400 dark:hover:text-blue-500"
|
||||
className="h-7 w-7 min-w-0 flex-shrink-0 border-border-strong p-0 text-foreground-tertiary hover:text-info-foreground"
|
||||
type="button"
|
||||
aria-label="Preview file"
|
||||
>
|
||||
@@ -112,10 +112,6 @@ interface Base64ModeProps extends BaseProps {
|
||||
|
||||
type Props = UploadModeProps | Base64ModeProps;
|
||||
|
||||
// TODO(tokens): no role holds Tailwind's gray-50/300/500/600/700/900
|
||||
// (#F9FAFB, #D1D5DB, #6B7280, #4B5563, #374151, #111827) or blue-200
|
||||
// #BFDBFE, blue-300 #93C5FD, blue-700 #1D4ED8. The dark: classes never apply
|
||||
// (forcedTheme="light") and are DS6's (SECRT-3116) to delete.
|
||||
export function FileInput(props: Props) {
|
||||
const {
|
||||
value,
|
||||
@@ -344,16 +340,16 @@ export function FileInput(props: Props) {
|
||||
{isUploading ? (
|
||||
<div
|
||||
data-slot="file-input-progress"
|
||||
className="flex items-center gap-2 rounded-md border border-blue-200 bg-info-surface p-2 dark:border-blue-800 dark:bg-blue-950"
|
||||
className="flex items-center gap-2 rounded-md border border-info/30 bg-info-surface p-2"
|
||||
>
|
||||
<div
|
||||
data-slot="file-input-spinner"
|
||||
className="h-4 w-4 animate-spin rounded-full border-2 border-blue-300 border-t-info-foreground dark:border-blue-700 dark:border-t-blue-400"
|
||||
className="h-4 w-4 animate-spin rounded-full border-2 border-info/50 border-t-info-foreground"
|
||||
/>
|
||||
<Text
|
||||
data-slot="file-input-progress-text"
|
||||
variant="small"
|
||||
className="text-blue-700 dark:text-blue-300"
|
||||
className="text-info-foreground"
|
||||
>
|
||||
{mode === "base64" ? "Processing..." : "Uploading..."}
|
||||
</Text>
|
||||
@@ -365,18 +361,18 @@ export function FileInput(props: Props) {
|
||||
>
|
||||
<div
|
||||
data-slot="file-input-file"
|
||||
className="flex flex-1 items-center gap-2 rounded-xlarge border border-gray-300 bg-gray-50 p-2 dark:border-gray-600 dark:bg-gray-800"
|
||||
className="flex flex-1 items-center gap-2 rounded-xlarge border border-border-strong bg-surface-muted p-2"
|
||||
>
|
||||
<Icon
|
||||
data-slot="file-input-file-icon"
|
||||
icon={File02Icon}
|
||||
className="h-4 w-4 flex-shrink-0 text-gray-600 dark:text-gray-400"
|
||||
className="h-4 w-4 flex-shrink-0 text-foreground-secondary"
|
||||
/>
|
||||
|
||||
<Text
|
||||
data-slot="file-input-file-name"
|
||||
variant="small-medium"
|
||||
className="truncate text-gray-900 dark:text-gray-100"
|
||||
className="truncate text-foreground-base"
|
||||
>
|
||||
{fileInfo
|
||||
? getFileLabel(fileInfo.name, fileInfo.content_type)
|
||||
@@ -386,7 +382,7 @@ export function FileInput(props: Props) {
|
||||
<Text
|
||||
data-slot="file-input-file-size"
|
||||
variant="small"
|
||||
className="text-gray-500 dark:text-gray-400"
|
||||
className="text-foreground-tertiary"
|
||||
>
|
||||
{formatFileSize(fileInfo.size)}
|
||||
</Text>
|
||||
@@ -407,7 +403,7 @@ export function FileInput(props: Props) {
|
||||
data-slot="file-input-clear"
|
||||
variant="outline"
|
||||
size="small"
|
||||
className="h-7 w-7 min-w-0 flex-shrink-0 border-border-strong p-0 text-gray-500 hover:text-error-hover dark:text-gray-400 dark:hover:text-red-500"
|
||||
className="h-7 w-7 min-w-0 flex-shrink-0 border-border-strong p-0 text-foreground-tertiary hover:text-error-hover"
|
||||
onClick={handleClear}
|
||||
type="button"
|
||||
aria-label="Clear file"
|
||||
@@ -457,7 +453,7 @@ export function FileInput(props: Props) {
|
||||
data-slot="file-input-error"
|
||||
variant="small"
|
||||
tone="danger"
|
||||
className="dark:text-red-400"
|
||||
className=""
|
||||
>
|
||||
{uploadError}
|
||||
</Text>
|
||||
@@ -480,11 +476,11 @@ export function FileInput(props: Props) {
|
||||
>
|
||||
<div
|
||||
data-slot="file-input-spinner"
|
||||
className="h-5 w-5 animate-spin rounded-full border-2 border-blue-300 border-t-info-foreground"
|
||||
className="h-5 w-5 animate-spin rounded-full border-2 border-info/50 border-t-info-foreground"
|
||||
/>
|
||||
<span
|
||||
data-slot="file-input-progress-text"
|
||||
className="text-gray-700"
|
||||
className="text-foreground-body"
|
||||
>
|
||||
{mode === "base64" ? "Processing..." : "Uploading..."}
|
||||
</span>
|
||||
@@ -499,7 +495,7 @@ export function FileInput(props: Props) {
|
||||
>
|
||||
<div
|
||||
data-slot="file-input-file"
|
||||
className="agpt-border-input flex min-h-14 w-full items-center justify-between rounded-xl bg-surface-muted p-4 text-body leading-5 text-gray-500"
|
||||
className="agpt-border-input flex min-h-14 w-full items-center justify-between rounded-xl bg-surface-muted p-4 text-body leading-5 text-foreground-tertiary"
|
||||
>
|
||||
<div
|
||||
data-slot="file-input-file-info"
|
||||
@@ -549,7 +545,7 @@ export function FileInput(props: Props) {
|
||||
type="button"
|
||||
onClick={handleClear}
|
||||
aria-label="Clear file"
|
||||
className="h-7 w-7 min-w-0 flex-shrink-0 border-border-strong p-0 text-gray-500 hover:text-error-hover dark:text-gray-400 dark:hover:text-red-500"
|
||||
className="h-7 w-7 min-w-0 flex-shrink-0 border-border-strong p-0 text-foreground-tertiary hover:text-error-hover"
|
||||
>
|
||||
<Icon
|
||||
data-slot="file-input-clear-icon"
|
||||
@@ -563,7 +559,7 @@ export function FileInput(props: Props) {
|
||||
{showStorageNote && mode === "upload" && (
|
||||
<p
|
||||
data-slot="file-input-note"
|
||||
className="text-caption leading-4 text-gray-500"
|
||||
className="text-caption leading-4 text-foreground-tertiary"
|
||||
>
|
||||
{storageNote}
|
||||
</p>
|
||||
@@ -579,7 +575,7 @@ export function FileInput(props: Props) {
|
||||
data-slot="file-input-dropzone"
|
||||
onDrop={handleFileDrop}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
className="agpt-border-input flex min-h-14 w-full items-center justify-center rounded-xl border-dashed bg-surface-muted text-body leading-5 text-gray-500"
|
||||
className="agpt-border-input flex min-h-14 w-full items-center justify-center rounded-xl border-dashed bg-surface-muted text-body leading-5 text-foreground-tertiary"
|
||||
>
|
||||
Choose a file or drag and drop it here
|
||||
</div>
|
||||
@@ -606,7 +602,7 @@ export function FileInput(props: Props) {
|
||||
{showStorageNote && mode === "upload" && (
|
||||
<p
|
||||
data-slot="file-input-note"
|
||||
className="text-caption leading-4 text-gray-500"
|
||||
className="text-caption leading-4 text-foreground-tertiary"
|
||||
>
|
||||
{storageNote}
|
||||
</p>
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ type Story = StoryObj<typeof meta>;
|
||||
export const Basic: Story = {
|
||||
render: function BasicStory() {
|
||||
return (
|
||||
<div className="relative h-64 w-96 overflow-hidden rounded-2xl bg-gradient-to-br from-violet-500 via-purple-500 to-indigo-600">
|
||||
<div className="relative h-64 w-96 overflow-hidden rounded-2xl bg-gradient-to-br from-brand via-brand to-indigo-600">
|
||||
<GlassPixelBackdrop />
|
||||
</div>
|
||||
);
|
||||
|
||||
+2
-2
@@ -24,8 +24,8 @@ export function GlassPixelBackdrop() {
|
||||
<span
|
||||
key={index}
|
||||
data-slot="glass-pixel-backdrop-cell"
|
||||
// TODO(tokens): no border width is 0.5px. The opacity differs per
|
||||
// cell, so it is a custom property read by an arbitrary value.
|
||||
// The opacity differs per cell, so it is a custom property.
|
||||
// oxlint-disable-next-line design-system/no-arbitrary-values -- hairline glass tile edge, no 0.5px border width on the scale
|
||||
className="opacity-(--cell-opacity) border-[0.5px] border-surface/10 bg-surface"
|
||||
style={
|
||||
{ "--cell-opacity": cellOpacity(index) } as React.CSSProperties
|
||||
|
||||
@@ -10,11 +10,6 @@ export interface BareInputProps
|
||||
// no label, wrapper or error line. Every prop and handler reaches the element
|
||||
// unchanged (no IME keydown guard, no value filtering), so it behaves exactly
|
||||
// as that element did; a consumer's onKeyDown must use isKey itself.
|
||||
// TODO(tokens): no role holds neutral-200 #E5E5E5 (border is #E4E4E7) or
|
||||
// neutral-500 #737373. neutral-950 #0A0A0A is the focus role, a ring colour,
|
||||
// not a text colour; the ring stays raw too, because focus has no .dark value
|
||||
// and this ring is neutral-300 in dark mode (SECRT-3154). The deleted file's
|
||||
// dark: classes are carried verbatim.
|
||||
export const BareInput = forwardRef<HTMLInputElement, BareInputProps>(
|
||||
function BareInput({ bare: _bare, className, type, ...props }, ref) {
|
||||
return (
|
||||
@@ -22,7 +17,7 @@ export const BareInput = forwardRef<HTMLInputElement, BareInputProps>(
|
||||
data-slot="input-control"
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-9 w-full rounded-md border border-neutral-200 bg-transparent px-3 py-1 text-body-lg leading-6 shadow-sm transition-colors file:border-0 file:bg-transparent file:text-body file:font-medium file:leading-5 file:text-neutral-950 placeholder:text-neutral-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-800 dark:file:text-neutral-50 dark:placeholder:text-neutral-400 dark:focus-visible:ring-neutral-300 md:text-body md:leading-5",
|
||||
"flex h-9 w-full rounded-md border border-border-base bg-transparent px-3 py-1 text-body-lg leading-6 shadow-sm transition-colors file:border-0 file:bg-transparent file:text-body file:font-medium file:leading-5 file:text-foreground-base placeholder:text-placeholder focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-focus disabled:cursor-not-allowed disabled:opacity-50 md:text-body md:leading-5",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
@@ -40,10 +35,6 @@ export interface BareTextareaProps
|
||||
|
||||
// The look of the deleted components/ui/textarea (SECRT-3113), on the same
|
||||
// terms as BareInput above.
|
||||
// TODO(tokens): no role holds neutral-200 #E5E5E5 (border is #E4E4E7) or
|
||||
// neutral-500 #737373; the ring stays raw neutral-950 because focus has no
|
||||
// .dark value and this ring is neutral-300 in dark mode (SECRT-3154). The
|
||||
// deleted file's dark: classes are carried verbatim.
|
||||
export const BareTextarea = forwardRef<HTMLTextAreaElement, BareTextareaProps>(
|
||||
function BareTextarea(
|
||||
{ bare: _bare, type: _type, className, ...props },
|
||||
@@ -53,7 +44,7 @@ export const BareTextarea = forwardRef<HTMLTextAreaElement, BareTextareaProps>(
|
||||
<textarea
|
||||
data-slot="input-control"
|
||||
className={cn(
|
||||
"flex min-h-[60px] w-full rounded-md border border-neutral-200 bg-transparent px-3 py-2 text-body-lg leading-6 shadow-sm placeholder:text-neutral-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-800 dark:placeholder:text-neutral-400 dark:focus-visible:ring-neutral-300 md:text-body md:leading-5",
|
||||
"flex min-h-[60px] w-full rounded-md border border-border-base bg-transparent px-3 py-2 text-body-lg leading-6 shadow-sm placeholder:text-placeholder focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-focus disabled:cursor-not-allowed disabled:opacity-50 md:text-body md:leading-5",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
@@ -5,16 +5,13 @@ export type BaseInputProps = React.InputHTMLAttributes<HTMLInputElement>;
|
||||
|
||||
// The bare <input> under the Input atom, for fields that bring their own label
|
||||
// and layout.
|
||||
// TODO(tokens): no role is gray-500 #6B7280 (`placeholder` is #83838C). The
|
||||
// dark: class never applies (forcedTheme="light") and is DS6's (SECRT-3116) to
|
||||
// delete.
|
||||
export const BaseInput = forwardRef<HTMLInputElement, BaseInputProps>(
|
||||
function BaseInput({ className, type, ...props }, ref) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"agpt-border-input agpt-shadow-input flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-body leading-5 transition-colors file:border-0 file:bg-transparent file:text-body file:font-medium file:leading-5 placeholder:text-gray-500 disabled:cursor-not-allowed disabled:opacity-50 dark:placeholder:text-gray-400",
|
||||
"agpt-border-input agpt-shadow-input flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-body leading-5 transition-colors file:border-0 file:bg-transparent file:text-body file:font-medium file:leading-5 placeholder:text-placeholder disabled:cursor-not-allowed disabled:opacity-50",
|
||||
type == "file" ? "pb-0.5 pt-1.5" : "",
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -139,8 +139,7 @@ const TextField = forwardRef<InputElement, TextFieldProps>(function TextField(
|
||||
"font-normal text-foreground-base",
|
||||
"placeholder:font-normal placeholder:text-placeholder",
|
||||
// Focus and hover states
|
||||
// TODO(tokens): no role is purple-400 #925CF7, the focus border and ring.
|
||||
"focus:border-purple-400 focus:shadow-none focus:outline-none focus:ring-1 focus:ring-purple-400 focus:ring-offset-0",
|
||||
"focus:border-brand focus:shadow-none focus:outline-none focus:ring-1 focus:ring-brand focus:ring-offset-0",
|
||||
className,
|
||||
);
|
||||
|
||||
@@ -345,6 +344,7 @@ const TextField = forwardRef<InputElement, TextFieldProps>(function TextField(
|
||||
data-slot="input-label-text"
|
||||
variant={labelVariant}
|
||||
as="span"
|
||||
// oxlint-disable-next-line design-system/require-static-classes -- labelClassName is a caller override that existing call sites outside the design system still use
|
||||
className={cn("text-foreground-base", labelClassName)}
|
||||
>
|
||||
{label}
|
||||
|
||||
+10
-4
@@ -52,7 +52,7 @@ export const Large: Story = {
|
||||
|
||||
export const CustomColor: Story = {
|
||||
args: {
|
||||
className: "text-emerald-500",
|
||||
className: "text-success",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -71,15 +71,21 @@ function renderAllSizes() {
|
||||
<div className="flex items-center gap-8 text-indigo-500">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<LoadingSpinner size="small" aria-label="loading-small" />
|
||||
<span className="text-xs capitalize text-zinc-500">Small</span>
|
||||
<span className="text-xs capitalize text-foreground-tertiary">
|
||||
Small
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<LoadingSpinner size="medium" aria-label="loading-medium" />
|
||||
<span className="text-xs capitalize text-zinc-500">Medium</span>
|
||||
<span className="text-xs capitalize text-foreground-tertiary">
|
||||
Medium
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<LoadingSpinner size="large" aria-label="loading-large" />
|
||||
<span className="text-xs capitalize text-zinc-500">Large</span>
|
||||
<span className="text-xs capitalize text-foreground-tertiary">
|
||||
Large
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,13 +4,7 @@ import { Loading03Icon } from "@hugeicons/core-free-icons";
|
||||
import type { IconSvgElement } from "@hugeicons/react";
|
||||
import { Icon } from "@/components/atoms/Icon/Icon";
|
||||
|
||||
const sizeClassNameMap = {
|
||||
small: "h-4 w-4",
|
||||
medium: "h-6 w-6",
|
||||
large: "h-10 w-10",
|
||||
} as const;
|
||||
|
||||
type SpinnerSize = keyof typeof sizeClassNameMap;
|
||||
type SpinnerSize = "small" | "medium" | "large";
|
||||
|
||||
// The open-ring spinner the old shadcn `loading` primitive drew (lucide's
|
||||
// loader-circle), kept so its callers render exactly as before.
|
||||
@@ -38,22 +32,22 @@ type LoadingSpinnerProps = {
|
||||
cover?: boolean;
|
||||
} & Omit<React.ComponentPropsWithoutRef<typeof Icon>, "icon" | "size">;
|
||||
|
||||
export function LoadingSpinner(props: LoadingSpinnerProps) {
|
||||
const {
|
||||
size = "medium",
|
||||
variant = "default",
|
||||
className,
|
||||
cover = false,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
export function LoadingSpinner({
|
||||
size = "medium",
|
||||
variant = "default",
|
||||
className,
|
||||
cover = false,
|
||||
...restProps
|
||||
}: LoadingSpinnerProps) {
|
||||
const spinner = (
|
||||
<Icon
|
||||
data-slot="loading-spinner"
|
||||
icon={iconMap[variant]}
|
||||
className={cn(
|
||||
"animate-spin text-inherit",
|
||||
sizeClassNameMap[size],
|
||||
size === "small" && "h-4 w-4",
|
||||
size === "medium" && "h-6 w-6",
|
||||
size === "large" && "h-10 w-10",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
|
||||
+4
-4
@@ -108,19 +108,19 @@ export const DifferentWidths: Story = {
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs text-zinc-500">Width: 200px</span>
|
||||
<span className="text-xs text-foreground-tertiary">Width: 200px</span>
|
||||
<div className="w-[200px]">
|
||||
<OverflowText value={longText} variant="body" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs text-zinc-500">Width: 300px</span>
|
||||
<span className="text-xs text-foreground-tertiary">Width: 300px</span>
|
||||
<div className="w-[300px]">
|
||||
<OverflowText value={longText} variant="body" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-xs text-zinc-500">Width: 400px</span>
|
||||
<span className="text-xs text-foreground-tertiary">Width: 400px</span>
|
||||
<div className="w-[400px]">
|
||||
<OverflowText value={longText} variant="body" />
|
||||
</div>
|
||||
@@ -150,7 +150,7 @@ export const URLExample: Story = {
|
||||
render: function URLExampleStory(args) {
|
||||
return (
|
||||
<div className="w-64">
|
||||
<OverflowText {...args} className="text-blue-600" />
|
||||
<OverflowText {...args} className="text-info-foreground" />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -13,7 +13,12 @@ interface Props extends Omit<TextProps, "children"> {
|
||||
value: string | ReactNode;
|
||||
}
|
||||
|
||||
export function OverflowText(props: Props) {
|
||||
export function OverflowText({
|
||||
value,
|
||||
className,
|
||||
variant = "body",
|
||||
...restProps
|
||||
}: Props) {
|
||||
const elementRef = useRef<HTMLSpanElement | null>(null);
|
||||
const [isTruncated, setIsTruncated] = useState(false);
|
||||
|
||||
@@ -62,14 +67,12 @@ export function OverflowText(props: Props) {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof props.value === "string") updateTruncation();
|
||||
}, [props.value]);
|
||||
if (typeof value === "string") updateTruncation();
|
||||
}, [value]);
|
||||
|
||||
useEffect(setupResizeListener, []);
|
||||
useEffect(setupObserver, []);
|
||||
|
||||
const { value, className, variant = "body", ...restProps } = props;
|
||||
|
||||
const content = (
|
||||
<span
|
||||
ref={elementRef}
|
||||
|
||||
@@ -14,10 +14,8 @@ const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
|
||||
<div
|
||||
ref={ref}
|
||||
data-slot="progress"
|
||||
// TODO(tokens): no role is gray-200 #E5E7EB (sidebar-border has the
|
||||
// hex but belongs to the sidebar).
|
||||
className={cn(
|
||||
"relative h-2 w-full overflow-hidden rounded-full bg-gray-200",
|
||||
"relative h-2 w-full overflow-hidden rounded-full bg-surface-active",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -27,17 +27,17 @@ export const Basic: Story = {
|
||||
return (
|
||||
<RevealGroup {...args} className="flex w-72 flex-col gap-3">
|
||||
<RevealItem>
|
||||
<h3 className="text-lg font-semibold text-zinc-900">
|
||||
<h3 className="text-lg font-semibold text-foreground-strong">
|
||||
Tell us about your work
|
||||
</h3>
|
||||
</RevealItem>
|
||||
<RevealItem>
|
||||
<p className="text-sm text-zinc-600">
|
||||
<p className="text-sm text-foreground-secondary">
|
||||
Most people talk for two to three minutes.
|
||||
</p>
|
||||
</RevealItem>
|
||||
<RevealItem>
|
||||
<p className="text-sm text-zinc-600">
|
||||
<p className="text-sm text-foreground-secondary">
|
||||
Each item reveals on its own, even if it mounts later.
|
||||
</p>
|
||||
</RevealItem>
|
||||
|
||||
@@ -192,7 +192,9 @@ export const WithRenderItem: Story = {
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{o.label}</span>
|
||||
{o.disabled && (
|
||||
<span className="text-xs text-zinc-400">(disabled)</span>
|
||||
<span className="text-xs text-foreground-subtle">
|
||||
(disabled)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Loaded 100 of 190 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user