diff --git a/frontend/documentation/pages/onboarding/OnboardingFlagsTable.stories.tsx b/frontend/documentation/pages/onboarding/OnboardingFlagsTable.stories.tsx new file mode 100644 index 000000000000..bf37298058af --- /dev/null +++ b/frontend/documentation/pages/onboarding/OnboardingFlagsTable.stories.tsx @@ -0,0 +1,49 @@ +import type { Meta, StoryObj } from 'storybook' + +import OnboardingFlagsTable, { + OnboardingFlagRow, +} from 'components/pages/onboarding/OnboardingFlagsTable' + +const demoFlag: OnboardingFlagRow = { + description: 'Controls the demo button shown to your users', + enabled: true, + name: 'show_demo_button', +} + +const meta: Meta = { + args: { + flags: [demoFlag], + onToggle: () => {}, + status: 'connected', + }, + component: OnboardingFlagsTable, + parameters: { + docs: { + description: { + component: + 'The "Your flags" card from the onboarding flow, reusing the product FeatureName / Tag / Switch. Prop-driven: the page owns the flag data and the persisted Dev toggle. `connected` lifts the card with the accent border and glow; `waiting` dims it until the first evaluation arrives.', + }, + }, + layout: 'padded', + }, + title: 'Pages/Onboarding/OnboardingFlagsTable', +} +export default meta + +type Story = StoryObj + +export const Connected: Story = {} + +export const Waiting: Story = { + args: { status: 'waiting' }, +} + +export const Off: Story = { + args: { flags: [{ ...demoFlag, enabled: false }] }, +} + +export const WithTag: Story = { + args: { + flags: [{ ...demoFlag, tags: [{ color: '#6837FC', label: 'demo' }] }], + }, +} diff --git a/frontend/documentation/pages/onboarding/OnboardingTerminal.stories.tsx b/frontend/documentation/pages/onboarding/OnboardingTerminal.stories.tsx new file mode 100644 index 000000000000..8320c402bdce --- /dev/null +++ b/frontend/documentation/pages/onboarding/OnboardingTerminal.stories.tsx @@ -0,0 +1,40 @@ +import type { Meta, StoryObj } from 'storybook' + +import OnboardingTerminal from 'components/pages/onboarding/OnboardingTerminal' + +const meta: Meta = { + args: { + connected: false, + featureName: 'show_demo_button', + installCopied: false, + snippetCopied: false, + }, + component: OnboardingTerminal, + parameters: { + docs: { + description: { + component: + 'The onboarding verify console. The checklist ticks as the user acts (copy install, copy snippet), and the first evaluation flips the badge to LIVE and prints the connection receipt. Always dark, since a terminal reads the same in light and dark mode.', + }, + }, + layout: 'padded', + }, + title: 'Pages/Onboarding/OnboardingTerminal', +} +export default meta + +type Story = StoryObj + +export const Listening: Story = {} + +export const InstallCopied: Story = { + args: { installCopied: true }, +} + +export const SnippetsCopied: Story = { + args: { installCopied: true, snippetCopied: true }, +} + +export const Connected: Story = { + args: { connected: true, installCopied: true, snippetCopied: true }, +} diff --git a/frontend/web/components/base/forms/GhostInput/GhostInput.tsx b/frontend/web/components/base/forms/GhostInput/GhostInput.tsx index 2b4b7b381f04..8b1128702a5a 100644 --- a/frontend/web/components/base/forms/GhostInput/GhostInput.tsx +++ b/frontend/web/components/base/forms/GhostInput/GhostInput.tsx @@ -63,6 +63,11 @@ const GhostInput = ({ onKeyDown={onKeyDown} aria-label={ariaLabel} spellCheck={false} + // Opt out of browser autofill + password-manager overlays (1Password, + // LastPass); their icons would overlap the trailing edit pencil. + autoComplete='off' + data-1p-ignore + data-lpignore='true' style={{ width: inputWidth }} /> diff --git a/frontend/web/components/pages/onboarding/OnboardingConnectPanel/CodeCard.tsx b/frontend/web/components/pages/onboarding/OnboardingConnectPanel/CodeCard.tsx index 5dba7a0fe286..276602c213e5 100644 --- a/frontend/web/components/pages/onboarding/OnboardingConnectPanel/CodeCard.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingConnectPanel/CodeCard.tsx @@ -10,11 +10,18 @@ export type CodeCardProps = { language: string // Left side of the card header (e.g. the language label or npm/yarn pills). headerLeft: ReactNode + // Fires when the user copies the code (drives the verify checklist). + onCopy?: () => void } // Owns its own "Copied" feedback so each card is independent. Highlight escapes // the body for display; Copy uses the raw string. -const CodeCard: FC = ({ code, headerLeft, language }) => { +const CodeCard: FC = ({ + code, + headerLeft, + language, + onCopy, +}) => { const { copied, copy } = useCopyFeedback() return ( @@ -25,7 +32,10 @@ const CodeCard: FC = ({ code, headerLeft, language }) => { theme='primary' size='small' className='ms-auto' - onClick={() => copy(code)} + onClick={() => { + copy(code) + onCopy?.() + }} > void + onCopyWire?: () => void } // "Connect your code" tab: pick an SDK, then copy the install + wire snippets, -// pre-filled with the real env key and flag this onboarding created. +// pre-filled with the real env key and flag this onboarding created. The copy +// actions feed the verify checklist. const ConnectYourCodePanel: FC = ({ environmentKey, featureName, + onCopyInstall, + onCopyWire, }) => { const [sdkLang, setSdkLang] = useState(SDK_LANGS[0]) const [installPm, setInstallPm] = useState('npm') @@ -40,6 +45,7 @@ const ConnectYourCodePanel: FC = ({ @@ -74,6 +80,7 @@ const ConnectYourCodePanel: FC = ({ {sdkLang.label} diff --git a/frontend/web/components/pages/onboarding/OnboardingConnectPanel/OnboardingConnectPanel.tsx b/frontend/web/components/pages/onboarding/OnboardingConnectPanel/OnboardingConnectPanel.tsx index d74c815098fe..3e76d28ae58f 100644 --- a/frontend/web/components/pages/onboarding/OnboardingConnectPanel/OnboardingConnectPanel.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingConnectPanel/OnboardingConnectPanel.tsx @@ -28,6 +28,8 @@ const CONNECT_TABS: OnboardingTab[] = [ export type OnboardingConnectPanelProps = { environmentKey: string featureName: string + onCopyInstall?: () => void + onCopyWire?: () => void } // Two ways to connect an app to the pre-created flag: paste an agent-agnostic @@ -37,6 +39,8 @@ export type OnboardingConnectPanelProps = { const OnboardingConnectPanel: FC = ({ environmentKey, featureName, + onCopyInstall, + onCopyWire, }) => { const [tab, setTab] = useState('manual') @@ -68,6 +72,8 @@ const OnboardingConnectPanel: FC = ({ diff --git a/frontend/web/components/pages/onboarding/OnboardingFlagsTable/OnboardingFlagsTable.scss b/frontend/web/components/pages/onboarding/OnboardingFlagsTable/OnboardingFlagsTable.scss new file mode 100644 index 000000000000..3af80a41dbc8 --- /dev/null +++ b/frontend/web/components/pages/onboarding/OnboardingFlagsTable/OnboardingFlagsTable.scss @@ -0,0 +1,92 @@ +// "Your flags" card. A normal light/dark surface (unlike the terminal), so it +// uses the semantic tokens. Connected lifts it with the action/purple border + +// glow to draw the eye; waiting dims it until the first evaluation lands. +.onboarding-flags { + display: flex; + flex-direction: column; + align-items: center; + gap: 14px; + + &__title { + margin: 0; + color: var(--color-text-default); + font-size: 16px; + font-weight: 700; + } + + &__table { + width: 100%; + max-width: 760px; + overflow: hidden; + border-radius: var(--radius-xl); + background: var(--color-surface-default); + // Accent border + purple glow to lift the connected card (matches the mock). + // Derived from the action colour with oklch alpha so it tracks the theme. + border: 1px solid oklch(from var(--color-border-action) l c h / 0.4); + box-shadow: 0 18px 44px 2px oklch(from var(--color-border-action) l c h / 0.2), + var(--shadow-md); + transition: opacity var(--duration-fast) var(--easing-standard), + box-shadow var(--duration-fast) var(--easing-standard), + border-color var(--duration-fast) var(--easing-standard); + + // Until the SDK connects the flag is real but quiet: no accent, dimmed. + &--waiting { + border-color: var(--color-border-default); + box-shadow: none; + opacity: 0.7; + } + } + + &__head { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 20px; + border-bottom: 1px solid var(--color-border-default); + } + + &__col { + color: var(--color-text-secondary); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.5px; + + &--feature { + flex: 1; + } + &--enabled { + width: 96px; + } + } + + &__row { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 20px; + } + + &__feature { + display: flex; + flex: 1; + flex-direction: column; + gap: 4px; + } + + &__name-row { + display: flex; + align-items: center; + gap: 8px; + } + + &__desc { + margin: 0; + color: var(--color-text-secondary); + font-size: 12px; + } + + // Match the ENABLED header column width so the toggle lines up under it. + &__toggle { + width: 96px; + } +} diff --git a/frontend/web/components/pages/onboarding/OnboardingFlagsTable/OnboardingFlagsTable.tsx b/frontend/web/components/pages/onboarding/OnboardingFlagsTable/OnboardingFlagsTable.tsx new file mode 100644 index 000000000000..b10cc80f3821 --- /dev/null +++ b/frontend/web/components/pages/onboarding/OnboardingFlagsTable/OnboardingFlagsTable.tsx @@ -0,0 +1,82 @@ +import React, { FC } from 'react' +import classNames from 'classnames' +import { Tag as TTag } from 'common/types/responses' +import FeatureName from 'components/feature-summary/FeatureName' +import Tag from 'components/tags/Tag' +import Switch from 'components/Switch' +import './OnboardingFlagsTable.scss' + +export type OnboardingFlagsTableStatus = 'waiting' | 'connected' + +export type OnboardingFlagRow = { + name: string + description?: string + tags?: Partial[] + enabled: boolean +} + +export type OnboardingFlagsTableProps = { + status: OnboardingFlagsTableStatus + flags: OnboardingFlagRow[] + onToggle: (flag: OnboardingFlagRow, enabled: boolean) => void + // Name of the flag whose toggle is mid-flight, so its Switch disables. + togglingFlag?: string | null +} + +// The "Your flags" card from the onboarding design: the pre-created flag(s) in a +// real-looking table that reuses the product FeatureName / Tag / Switch. Prop +// driven (the page owns the data and the persisted toggle, see +// useUpdateFeatureStateMutation). `connected` lifts the card with the accent +// border + glow and enables the toggle; `waiting` dims it until the first +// evaluation arrives. +const OnboardingFlagsTable: FC = ({ + flags, + onToggle, + status, + togglingFlag, +}) => { + const waiting = status === 'waiting' + return ( +
+

Your flags

+
+
+ + FEATURE + + + ENABLED + +
+ {flags.map((flag) => ( +
+
+
+ + {flag.tags?.map((tag) => ( + + ))} +
+ {flag.description && ( +

{flag.description}

+ )} +
+
+ onToggle(flag, enabled)} + /> +
+
+ ))} +
+
+ ) +} + +export default OnboardingFlagsTable diff --git a/frontend/web/components/pages/onboarding/OnboardingFlagsTable/index.ts b/frontend/web/components/pages/onboarding/OnboardingFlagsTable/index.ts new file mode 100644 index 000000000000..435ba4d35415 --- /dev/null +++ b/frontend/web/components/pages/onboarding/OnboardingFlagsTable/index.ts @@ -0,0 +1,6 @@ +export { default } from './OnboardingFlagsTable' +export type { + OnboardingFlagRow, + OnboardingFlagsTableProps, + OnboardingFlagsTableStatus, +} from './OnboardingFlagsTable' diff --git a/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx b/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx index ab5c26ea3354..09c7189196aa 100644 --- a/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx @@ -5,8 +5,12 @@ import Icon from 'components/icons/Icon' import OnboardingHeader from 'components/pages/onboarding/OnboardingHeader' import ThemeToggle from 'components/pages/onboarding/ThemeToggle' import OnboardingConnectPanel from 'components/pages/onboarding/OnboardingConnectPanel' +import OnboardingTerminal from 'components/pages/onboarding/OnboardingTerminal' +import OnboardingFlagsTable from 'components/pages/onboarding/OnboardingFlagsTable' import { useEnsureOnboardingResources } from 'components/pages/onboarding/hooks/useEnsureOnboardingResources' import { useOnboardingFlagRename } from 'components/pages/onboarding/hooks/useOnboardingFlagRename' +import { useOnboardingFlag } from 'components/pages/onboarding/hooks/useOnboardingFlag' +import { useOnboardingConnection } from 'components/pages/onboarding/hooks/useOnboardingConnection' import { useUpdateOrganisationMutation } from 'common/services/useOrganisation' import { useUpdateProjectMutation } from 'common/services/useProject' import './OnboardingFlow.scss' @@ -16,7 +20,7 @@ import './OnboardingFlow.scss' // // Resources (org / project / Dev + Prod / first flag) are bootstrapped // idempotently by useEnsureOnboardingResources, and the inline header chips -// persist renames. TODO(#7766): the verify console / flags table land on top. +// persist renames; the verify terminal and flags table render below (#7766). const OnboardingFlow: FC = () => { const { caseSensitive, @@ -59,6 +63,20 @@ const OnboardingFlow: FC = () => { projectId, }) + // Verify terminal + flags table. The checklist ticks as the user copies the + // install / wire snippets; the connection step is stubbed until the + // first-evaluation signal lands (#7767, behind useOnboardingConnection); the + // Dev toggle is real now. + const connection = useOnboardingConnection() + const [installCopied, setInstallCopied] = useState(false) + const [snippetCopied, setSnippetCopied] = useState(false) + const { + enabled: flagEnabled, + isToggling, + tags: flagTags, + toggle: toggleFlag, + } = useOnboardingFlag(environment, projectId, featureName) + // Org/project are single-field PATCHes; the shell nav adopts the new names on // its next load. const renameOrganisation = async (name: string) => { @@ -149,6 +167,27 @@ const OnboardingFlow: FC = () => { setInstallCopied(true)} + onCopyWire={() => setSnippetCopied(true)} + /> + + toggleFlag(next)} + togglingFlag={isToggling ? featureName : null} />