From c1a079ec498a2f9973941bd2a6f3974c4f21ff39 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Thu, 2 Jul 2026 10:53:46 -0300 Subject: [PATCH 01/19] feat(base): let SelectableCard host arbitrary content Add an optional content slot (children), make selected optional, and pass through className, so SelectableCard can back a navigational card and not just a selection tile. Additive: existing consumers are unaffected. Co-Authored-By: Claude Opus 4.8 --- .../base/SelectableCard/SelectableCard.scss | 2 ++ .../base/SelectableCard/SelectableCard.tsx | 22 +++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/frontend/web/components/base/SelectableCard/SelectableCard.scss b/frontend/web/components/base/SelectableCard/SelectableCard.scss index 0315b3f4a852..72eae4590544 100644 --- a/frontend/web/components/base/SelectableCard/SelectableCard.scss +++ b/frontend/web/components/base/SelectableCard/SelectableCard.scss @@ -36,6 +36,8 @@ display: flex; flex-direction: column; gap: 4px; + flex: 1; + min-width: 0; } &__icon { diff --git a/frontend/web/components/base/SelectableCard/SelectableCard.tsx b/frontend/web/components/base/SelectableCard/SelectableCard.tsx index 3a614f228702..a88d42577312 100644 --- a/frontend/web/components/base/SelectableCard/SelectableCard.tsx +++ b/frontend/web/components/base/SelectableCard/SelectableCard.tsx @@ -1,11 +1,14 @@ import React, { FC, ReactNode } from 'react' +import cn from 'classnames' import BareButton from 'components/base/forms/BareButton' import './SelectableCard.scss' type BadgeVariant = 'primary' | 'secondary' type SelectableCardProps = { - selected: boolean + // Selection state. Omit for a card that acts on click without a chosen state + // (e.g. a card that navigates). + selected?: boolean onClick: () => void icon?: ReactNode title: string @@ -13,10 +16,15 @@ type SelectableCardProps = { badge?: { label: string; variant: BadgeVariant } tags?: string[] disabled?: boolean + className?: string + // Extra content below the description (e.g. an illustrative preview). + children?: ReactNode } const SelectableCard: FC = ({ badge, + children, + className, description, disabled, icon, @@ -27,9 +35,14 @@ const SelectableCard: FC = ({ }) => { return ( @@ -46,6 +59,7 @@ const SelectableCard: FC = ({ ))} )} + {children} {badge && (
From 4581b155748597422b1d91c50137cd14aceae667 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Thu, 2 Jul 2026 10:54:00 -0300 Subject: [PATCH 02/19] feat(onboarding): make the "next quest" cards actionable The three next-quest cards (gradual rollout, experiment, remote config) are built on SelectableCard and each deep-links to the flag's real config: rollout to the segment overrides tab, remote config to the value tab, experiment to the experiments page. The tab param is the slugified tab label. Locked and dimmed (inert) until the app connects. Closes #7739 Co-Authored-By: Claude Opus 4.8 --- .../OnboardingNextSteps.stories.tsx | 30 +++++ .../OnboardingFlow/OnboardingFlow.tsx | 26 ++++ .../OnboardingNextSteps.scss | 90 +++++++++++++ .../OnboardingNextSteps.tsx | 120 ++++++++++++++++++ .../onboarding/OnboardingNextSteps/index.ts | 5 + .../onboarding/hooks/useOnboardingFlag.ts | 2 + 6 files changed, 273 insertions(+) create mode 100644 frontend/documentation/pages/onboarding/OnboardingNextSteps.stories.tsx create mode 100644 frontend/web/components/pages/onboarding/OnboardingNextSteps/OnboardingNextSteps.scss create mode 100644 frontend/web/components/pages/onboarding/OnboardingNextSteps/OnboardingNextSteps.tsx create mode 100644 frontend/web/components/pages/onboarding/OnboardingNextSteps/index.ts diff --git a/frontend/documentation/pages/onboarding/OnboardingNextSteps.stories.tsx b/frontend/documentation/pages/onboarding/OnboardingNextSteps.stories.tsx new file mode 100644 index 000000000000..bbbf3e0802fa --- /dev/null +++ b/frontend/documentation/pages/onboarding/OnboardingNextSteps.stories.tsx @@ -0,0 +1,30 @@ +import type { Meta, StoryObj } from 'storybook' + +import OnboardingNextSteps from 'components/pages/onboarding/OnboardingNextSteps' + +const meta: Meta = { + args: { + locked: false, + onSelect: () => {}, + }, + component: OnboardingNextSteps, + parameters: { + docs: { + description: { + component: + 'The "Choose your next quest" section at the bottom of the onboarding flow: the three ways the demo flag can level up (gradual rollout, experiment, remote config), each linking to its real config. Locked and dimmed until the app connects.', + }, + }, + layout: 'padded', + }, + title: 'Pages/Onboarding/OnboardingNextSteps', +} +export default meta + +type Story = StoryObj + +export const Connected: Story = {} + +export const Locked: Story = { + args: { locked: true }, +} diff --git a/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx b/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx index b3dcecf573bc..ad32dc2ede47 100644 --- a/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx @@ -7,6 +7,9 @@ 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 OnboardingNextSteps, { + OnboardingNextStep, +} from 'components/pages/onboarding/OnboardingNextSteps' import { useEnsureOnboardingResources } from 'components/pages/onboarding/hooks/useEnsureOnboardingResources' import { useOnboardingFlagRename } from 'components/pages/onboarding/hooks/useOnboardingFlagRename' import { useOnboardingFlag } from 'components/pages/onboarding/hooks/useOnboardingFlag' @@ -64,6 +67,7 @@ const OnboardingFlow: FC = () => { const [snippetCopied, setSnippetCopied] = useState(false) const { enabled: flagEnabled, + flagId, isToggling, ready: flagStateReady, tags: flagTags, @@ -117,6 +121,24 @@ const OnboardingFlow: FC = () => { } } + // Each next-step card deep-links to the flag's real config; nothing faked. + const goToNextStep = (step: OnboardingNextStep) => { + if (projectId === null) { + return + } + const base = `/project/${projectId}/environment/${environmentKey}` + if (step === 'experiment') { + history.push(`${base}/experiments`) + return + } + if (flagId === null) { + return + } + // Tab param is the slugified tab label (see TabMenu/Tabs urlParam). + const tab = step === 'rollout' ? 'segment-overrides' : 'value' + history.push(`${base}/features?feature=${flagId}&tab=${tab}`) + } + if (status === 'creating') { return (
@@ -178,6 +200,10 @@ const OnboardingFlow: FC = () => { togglingFlag={isToggling ? featureName : null} togglesReady={flagStateReady} /> +
- What happens next + + What happens next +
  • Detects your stack: language, framework and package manager.
  • Installs the Flagsmith SDK and wires it into your code.
  • diff --git a/frontend/web/components/pages/onboarding/OnboardingConnectPanel/ConnectYourCodePanel.tsx b/frontend/web/components/pages/onboarding/OnboardingConnectPanel/ConnectYourCodePanel.tsx index b73ec452890c..d2477881a1ca 100644 --- a/frontend/web/components/pages/onboarding/OnboardingConnectPanel/ConnectYourCodePanel.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingConnectPanel/ConnectYourCodePanel.tsx @@ -15,7 +15,7 @@ const PACKAGE_MANAGERS: PackageManager[] = ['npm', 'yarn'] const SdkBadge: FC<{ lang: SdkLang }> = ({ lang }) => { const Logo = lang.logo return ( - + {lang.label} @@ -53,7 +53,9 @@ const ConnectYourCodePanel: FC = ({
    1 - Install the SDK + + Install the SDK +
    = ({
    2 - + Wire it in & take instant control of what users see
    diff --git a/frontend/web/components/pages/onboarding/OnboardingConnectPanel/SdkPicker.tsx b/frontend/web/components/pages/onboarding/OnboardingConnectPanel/SdkPicker.tsx index a0c4241e0420..2c0b7e81a4dc 100644 --- a/frontend/web/components/pages/onboarding/OnboardingConnectPanel/SdkPicker.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingConnectPanel/SdkPicker.tsx @@ -69,7 +69,7 @@ const SdkPicker: FC = ({ onSelect, selected }) => { ref={(el) => { refs.current[lang.label] = el }} - className='fw-semibold' + className='font-weight-semibold' role='radio' aria-checked={isSelected} tabIndex={lang.label === tabStopLabel ? 0 : -1} @@ -92,7 +92,7 @@ const SdkPicker: FC = ({ onSelect, selected }) => {
    {popularLangs.map(renderOption)} setMoreOpen((open) => !open)} aria-expanded={moreOpen} > From ae7a277db9637b0e036efccefa8908753b8f58f6 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Wed, 15 Jul 2026 09:46:15 -0300 Subject: [PATCH 16/19] style(onboarding): soften inline-edit radius and spacing Review (Kyle): the resting-fill fields felt cramped. Bump the radius to --radius-sm and the padding to 1px 8px, matching the flag pill. Co-Authored-By: Claude Opus 4.8 --- .../components/pages/onboarding/InlineInput/InlineInput.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/web/components/pages/onboarding/InlineInput/InlineInput.scss b/frontend/web/components/pages/onboarding/InlineInput/InlineInput.scss index 1032d4ffac12..e1dec31ac08b 100644 --- a/frontend/web/components/pages/onboarding/InlineInput/InlineInput.scss +++ b/frontend/web/components/pages/onboarding/InlineInput/InlineInput.scss @@ -3,8 +3,8 @@ align-items: center; vertical-align: baseline; gap: 4px; - padding: 0 4px; - border-radius: var(--radius-xs); + padding: 1px 8px; + border-radius: var(--radius-sm); border-bottom: 1px solid var(--color-border-action); // Resting fill so it reads as editable, not only on hover. background-color: var(--color-surface-subtle); From 8767248170d313d2536982abada7a50a30a915f5 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Wed, 15 Jul 2026 09:49:02 -0300 Subject: [PATCH 17/19] style(onboarding): round only the top of the inline-edit fields Rounded bottom corners fought the action underline. Square the bottom so it sits flush; the flag pill (no underline) keeps its full radius. Co-Authored-By: Claude Opus 4.8 --- .../components/pages/onboarding/InlineInput/InlineInput.scss | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/web/components/pages/onboarding/InlineInput/InlineInput.scss b/frontend/web/components/pages/onboarding/InlineInput/InlineInput.scss index e1dec31ac08b..68285be954db 100644 --- a/frontend/web/components/pages/onboarding/InlineInput/InlineInput.scss +++ b/frontend/web/components/pages/onboarding/InlineInput/InlineInput.scss @@ -4,7 +4,9 @@ vertical-align: baseline; gap: 4px; padding: 1px 8px; - border-radius: var(--radius-sm); + // Round the top only; the bottom stays square so it sits flush with the + // action underline (rounded bottom corners fight the border). + border-radius: var(--radius-sm) var(--radius-sm) 0 0; border-bottom: 1px solid var(--color-border-action); // Resting fill so it reads as editable, not only on hover. background-color: var(--color-surface-subtle); From 765339e068323e047378abe8e36833ec89d77d0c Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Wed, 15 Jul 2026 09:52:03 -0300 Subject: [PATCH 18/19] style(onboarding): medium weight on the SDK badges Semibold read too heavy on the chips; use font-weight-medium for the SDK badge and picker chips. Section headings stay semibold. Co-Authored-By: Claude Opus 4.8 --- .../OnboardingConnectPanel/ConnectYourCodePanel.tsx | 2 +- .../pages/onboarding/OnboardingConnectPanel/SdkPicker.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/web/components/pages/onboarding/OnboardingConnectPanel/ConnectYourCodePanel.tsx b/frontend/web/components/pages/onboarding/OnboardingConnectPanel/ConnectYourCodePanel.tsx index d2477881a1ca..a32a58387b42 100644 --- a/frontend/web/components/pages/onboarding/OnboardingConnectPanel/ConnectYourCodePanel.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingConnectPanel/ConnectYourCodePanel.tsx @@ -15,7 +15,7 @@ const PACKAGE_MANAGERS: PackageManager[] = ['npm', 'yarn'] const SdkBadge: FC<{ lang: SdkLang }> = ({ lang }) => { const Logo = lang.logo return ( - + {lang.label} diff --git a/frontend/web/components/pages/onboarding/OnboardingConnectPanel/SdkPicker.tsx b/frontend/web/components/pages/onboarding/OnboardingConnectPanel/SdkPicker.tsx index 2c0b7e81a4dc..fb477210edc1 100644 --- a/frontend/web/components/pages/onboarding/OnboardingConnectPanel/SdkPicker.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingConnectPanel/SdkPicker.tsx @@ -69,7 +69,7 @@ const SdkPicker: FC = ({ onSelect, selected }) => { ref={(el) => { refs.current[lang.label] = el }} - className='font-weight-semibold' + className='font-weight-medium' role='radio' aria-checked={isSelected} tabIndex={lang.label === tabStopLabel ? 0 : -1} @@ -92,7 +92,7 @@ const SdkPicker: FC = ({ onSelect, selected }) => {
    {popularLangs.map(renderOption)} setMoreOpen((open) => !open)} aria-expanded={moreOpen} > From 2e1fdc23428fb7fbc766003d9992d4a5d1280501 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Wed, 15 Jul 2026 10:13:16 -0300 Subject: [PATCH 19/19] style(onboarding): tighten inline-edit radius to 2px Co-Authored-By: Claude Opus 4.8 --- .../components/pages/onboarding/InlineInput/InlineInput.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/web/components/pages/onboarding/InlineInput/InlineInput.scss b/frontend/web/components/pages/onboarding/InlineInput/InlineInput.scss index 68285be954db..045c13b15612 100644 --- a/frontend/web/components/pages/onboarding/InlineInput/InlineInput.scss +++ b/frontend/web/components/pages/onboarding/InlineInput/InlineInput.scss @@ -6,7 +6,7 @@ padding: 1px 8px; // Round the top only; the bottom stays square so it sits flush with the // action underline (rounded bottom corners fight the border). - border-radius: var(--radius-sm) var(--radius-sm) 0 0; + border-radius: var(--radius-xs) var(--radius-xs) 0 0; border-bottom: 1px solid var(--color-border-action); // Resting fill so it reads as editable, not only on hover. background-color: var(--color-surface-subtle);