From edcf4f04085d0b90f0602ff8a7d95594fd1c8d5c Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Fri, 17 Jul 2026 11:15:41 -0300 Subject: [PATCH 1/9] feat(tokens): add code syntax colour tokens New --color-code-* semantic family for highlight.js syntax, mapped to the nearest palette primitives (light in :root, lighter in .dark). Added to tokens.json (source of truth) and regenerated; registered in the Semantic Colour Tokens story so it shows the group. Co-Authored-By: Claude Opus 4.8 --- frontend/common/theme/tokens.json | 11 +++ frontend/common/theme/tokens.ts | 11 +++ .../documentation/SemanticTokens.stories.tsx | 2 + .../TokenReference.generated.stories.tsx | 83 +++++++++++++++++++ frontend/web/styles/_tokens.scss | 20 +++++ 5 files changed, 127 insertions(+) diff --git a/frontend/common/theme/tokens.json b/frontend/common/theme/tokens.json index 7004ce34a887..fcad1cb37f7d 100644 --- a/frontend/common/theme/tokens.json +++ b/frontend/common/theme/tokens.json @@ -110,6 +110,17 @@ "warning": { "cssVar": "--color-text-warning", "light": "#ff9f43", "dark": "#ff9f43" }, "info": { "cssVar": "--color-text-info", "light": "#0aaddf", "dark": "#0aaddf" } }, + "code": { + "text": { "cssVar": "--color-code-text", "light": "#2d3443", "dark": "#9da4ae" }, + "comment": { "cssVar": "--color-code-comment", "light": "#9da4ae", "dark": "#656d7b" }, + "keyword": { "cssVar": "--color-code-keyword", "light": "#6837fc", "dark": "#906af6" }, + "name": { "cssVar": "--color-code-name", "light": "#ef4d56", "dark": "#f57c78" }, + "literal": { "cssVar": "--color-code-literal", "light": "#0b8bb2", "dark": "#45bce0" }, + "string": { "cssVar": "--color-code-string", "light": "#27ab95", "dark": "#56ccad" }, + "variable": { "cssVar": "--color-code-variable", "light": "#d06907", "dark": "#efb47c" }, + "title": { "cssVar": "--color-code-title", "light": "#0aaddf", "dark": "#7ecde2" }, + "builtin": { "cssVar": "--color-code-builtin", "light": "#d4b050", "dark": "#f9dc80" } + }, "border": { "default": { "cssVar": "--color-border-default", "light": "rgba(101, 109, 123, 0.16)", "dark": "rgba(255, 255, 255, 0.16)" }, "strong": { "cssVar": "--color-border-strong", "light": "rgba(101, 109, 123, 0.24)", "dark": "rgba(255, 255, 255, 0.24)" }, diff --git a/frontend/common/theme/tokens.ts b/frontend/common/theme/tokens.ts index 2b8839e6feaa..7397d1972cfa 100644 --- a/frontend/common/theme/tokens.ts +++ b/frontend/common/theme/tokens.ts @@ -148,6 +148,17 @@ export const colorBorderStrong = export const colorBorderSuccess = 'var(--color-border-success, #27ab95)' export const colorBorderWarning = 'var(--color-border-warning, #ff9f43)' +// Code +export const colorCodeBuiltin = 'var(--color-code-builtin, #d4b050)' +export const colorCodeComment = 'var(--color-code-comment, #9da4ae)' +export const colorCodeKeyword = 'var(--color-code-keyword, #6837fc)' +export const colorCodeLiteral = 'var(--color-code-literal, #0b8bb2)' +export const colorCodeName = 'var(--color-code-name, #ef4d56)' +export const colorCodeString = 'var(--color-code-string, #27ab95)' +export const colorCodeText = 'var(--color-code-text, #2d3443)' +export const colorCodeTitle = 'var(--color-code-title, #0aaddf)' +export const colorCodeVariable = 'var(--color-code-variable, #d06907)' + // Icon export const colorIconAction = 'var(--color-icon-action, #6837fc)' export const colorIconDanger = 'var(--color-icon-danger, #ef4d56)' diff --git a/frontend/documentation/SemanticTokens.stories.tsx b/frontend/documentation/SemanticTokens.stories.tsx index 825481d4eaf6..d378dc4e52d1 100644 --- a/frontend/documentation/SemanticTokens.stories.tsx +++ b/frontend/documentation/SemanticTokens.stories.tsx @@ -22,6 +22,7 @@ const TOKEN_PREFIX = '--color-' const GROUP_LABELS: Record = { border: 'Border', brand: 'Brand', + code: 'Code', danger: 'Danger', icon: 'Icon', info: 'Info', @@ -83,6 +84,7 @@ function readTokens(): TokenGroupData[] { 'brand', 'surface', 'text', + 'code', 'border', 'icon', 'danger', diff --git a/frontend/documentation/TokenReference.generated.stories.tsx b/frontend/documentation/TokenReference.generated.stories.tsx index 15927ca8228f..97c711495103 100644 --- a/frontend/documentation/TokenReference.generated.stories.tsx +++ b/frontend/documentation/TokenReference.generated.stories.tsx @@ -234,6 +234,89 @@ export const AllTokens: StoryObj = { +

Colour: code

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TokenValue
+ --color-code-text + + var(--slate-700) +
+ --color-code-comment + + var(--slate-300) +
+ --color-code-keyword + + var(--purple-600) +
+ --color-code-name + + var(--red-500) +
+ --color-code-literal + + var(--blue-600) +
+ --color-code-string + + var(--green-500) +
+ --color-code-variable + + var(--orange-700) +
+ --color-code-title + + var(--blue-500) +
+ --color-code-builtin + + var(--gold-700) +

Colour: border

diff --git a/frontend/web/styles/_tokens.scss b/frontend/web/styles/_tokens.scss index a7546c72afa5..c5b653e2c50b 100644 --- a/frontend/web/styles/_tokens.scss +++ b/frontend/web/styles/_tokens.scss @@ -114,6 +114,17 @@ --color-text-tertiary: var(--slate-300); --color-text-warning: var(--orange-500); + // Code + --color-code-builtin: var(--gold-700); + --color-code-comment: var(--slate-300); + --color-code-keyword: var(--purple-600); + --color-code-literal: var(--blue-600); + --color-code-name: var(--red-500); + --color-code-string: var(--green-500); + --color-code-text: var(--slate-700); + --color-code-title: var(--blue-500); + --color-code-variable: var(--orange-700); + // Border --color-border-action: var(--purple-600); --color-border-danger: var(--red-500); @@ -201,6 +212,15 @@ --color-text-disabled: oklch(from var(--slate-0) l c h / 0.32); --color-text-secondary: var(--slate-300); --color-text-tertiary: oklch(from var(--slate-0) l c h / 0.48); + --color-code-builtin: var(--gold-400); + --color-code-comment: var(--slate-500); + --color-code-keyword: var(--purple-400); + --color-code-literal: var(--blue-400); + --color-code-name: var(--red-400); + --color-code-string: var(--green-400); + --color-code-text: var(--slate-300); + --color-code-title: var(--blue-300); + --color-code-variable: var(--orange-400); --color-border-action: var(--purple-400); --color-border-default: oklch(from var(--slate-0) l c h / 0.16); --color-border-disabled: oklch(from var(--slate-0) l c h / 0.08); From c0035f94ff5f8e06348ce10fcf3399229ba73006 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Fri, 17 Jul 2026 11:15:46 -0300 Subject: [PATCH 2/9] refactor(styles): theme-adaptive shared code theme Point the shared .hljs palette (and the Pylon --hljs-* var-feed) at the --color-code-* tokens so code blocks follow light/dark. Add .hljs--embedded for containers that own their surface (onboarding's CodeCard opts in via Highlight's embedded prop and drops its bespoke palette) and a theme-adaptive scrollbar. Nest the palette under .hljs to out-specify Pylon's injected rules. Drop the dead $code-color/$code-bg vars and the legacy .yaml override. Header chrome untouched. Co-Authored-By: Claude Opus 4.8 --- .../OnboardingConnectPanel/CodeCard.tsx | 2 +- .../OnboardingConnectPanel.scss | 60 +---- frontend/web/styles/3rdParty/_hljs.scss | 224 +++++++++++------- frontend/web/styles/_variables.scss | 2 - 4 files changed, 142 insertions(+), 146 deletions(-) diff --git a/frontend/web/components/pages/onboarding/OnboardingConnectPanel/CodeCard.tsx b/frontend/web/components/pages/onboarding/OnboardingConnectPanel/CodeCard.tsx index 270897ec8742..c0faddc9e182 100644 --- a/frontend/web/components/pages/onboarding/OnboardingConnectPanel/CodeCard.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingConnectPanel/CodeCard.tsx @@ -47,7 +47,7 @@ const CodeCard: FC = ({ - + {code} diff --git a/frontend/web/components/pages/onboarding/OnboardingConnectPanel/OnboardingConnectPanel.scss b/frontend/web/components/pages/onboarding/OnboardingConnectPanel/OnboardingConnectPanel.scss index 55e5df84ecd0..eb5bd7b02c30 100644 --- a/frontend/web/components/pages/onboarding/OnboardingConnectPanel/OnboardingConnectPanel.scss +++ b/frontend/web/components/pages/onboarding/OnboardingConnectPanel/OnboardingConnectPanel.scss @@ -32,67 +32,11 @@ } } - // Code cards - theme-adaptive via semantic tokens (light editor in light mode, - // dark in dark mode). Surface, base text and every syntax colour map to - // semantic tokens that flip with the theme, so there's no hardcoded hex and no - // per-theme override. TODO(#6606): lift this palette into the shared `.hljs` - // theme so all code blocks app-wide adapt the same way. + // The card owns the surface + rounding; the code block opts into the shared + // embedded modifier (Highlight's `embedded` prop) for its transparent variant. &__codecard { border-radius: 10px; overflow: hidden; - - .hljs { - // Inherit the card surface; base text adapts with the theme. - background: transparent; - border: 0; - border-radius: 0; - color: var(--color-text-default); - // Reserve the scrollbar gutter permanently so hover doesn't shove the - // layout (the global dark-scroll mixin toggles display:none on a - // space-reserving webkit bar otherwise). - overflow-x: scroll; - overflow-y: hidden; - - &::-webkit-scrollbar, - &:hover::-webkit-scrollbar { - display: block !important; - } - - // Unmapped tokens (tags, attributes, params, punctuation) inherit the - // readable base instead of the global hljs greys; the semantic palette - // below re-colours the specific token types. - * { - color: inherit; - } - } - - .hljs-keyword, - .hljs-built_in, - .hljs-literal, - .hljs-selector-tag { - color: var(--color-text-action); - } - .hljs-string, - .hljs-regexp { - color: var(--color-text-success); - } - .hljs-attr, - .hljs-attribute, - .hljs-property, - .hljs-number, - .hljs-type { - color: var(--color-text-info); - } - .hljs-title, - .hljs-name, - .hljs-section { - color: var(--color-text-default); - } - .hljs-comment, - .hljs-quote, - .hljs-meta { - color: var(--color-text-secondary); - } } &__codecard-head { diff --git a/frontend/web/styles/3rdParty/_hljs.scss b/frontend/web/styles/3rdParty/_hljs.scss index 2b564e813435..447b4d82328c 100644 --- a/frontend/web/styles/3rdParty/_hljs.scss +++ b/frontend/web/styles/3rdParty/_hljs.scss @@ -8,8 +8,26 @@ -webkit-transition: $transition; transition: $transition; } + +// Recolour customScrollDark's fixed dark thumb with tokens (it reads as a +// black bar on light surfaces). Include after customScrollDark so these win. +@mixin hljsScrollbar { + &::-webkit-scrollbar-track { + background-color: transparent; + } + &::-webkit-scrollbar-thumb { + background-color: var(--color-border-strong); + &:hover { + background-color: var(--color-text-tertiary); + } + } +} + +// highlight.js and Pylon's chat widget both use .hljs. We win two ways: feed +// our tokens into Pylon's --hljs-* vars, and nest our palette under .hljs to +// out-specify its global rules. Also paints Pylon's chat code, which is fine. .invalid .value-editor .hljs { - border: 2px solid $danger; + border: 2px solid var(--color-border-danger); } .value-editor { position: relative; @@ -20,10 +38,8 @@ } &.code-medium { .hljs { - // Align the single-line editor height with adjacent inputs ($input-height). - // Reduced vertical padding so border + line-height + padding == $input-height, - // while min-height keeps the empty/placeholder state aligned too. The editor - // still grows for multi-line values. + // Match adjacent inputs' height ($input-height); min-height holds the + // empty state, and the editor still grows for multi-line values. padding: 9px 12px 9px 16px; min-height: $input-height; } @@ -46,34 +62,34 @@ opacity: 1; } .language-icon { - color: $body-color; + color: var(--color-text-default); } } span { display: flex; align-items: center; gap: 2px; - color: $text-icon-light-grey; + color: var(--color-text-secondary); cursor: pointer; font-size: $font-caption-sm; line-height: $line-height-xxsm; &.active { - color: $body-color; + color: var(--color-text-default); font-weight: 500; } &.primary { - color: $primary !important; + color: var(--color-text-action); } } } } .hljs { a { - color: white !important; + color: var(--color-text-action) !important; text-decoration: underline; .hljs-regexp, .hljs-comment { - color: white !important; + color: var(--color-text-action) !important; } } cursor: text; @@ -82,10 +98,23 @@ display: block; overflow-x: auto; @include customScrollDark(); + @include hljsScrollbar(); padding: 0.5em; - background: $code-bg; + // Redefine Pylon's --hljs-* vars to our code tokens so its injected rules + // paint our theme (belt-and-braces with the palette specificity below). + --hljs-bg: var(--color-surface-subtle); + --hljs-text: var(--color-code-text); + --hljs-comment: var(--color-code-comment); + --hljs-keyword: var(--color-code-keyword); + --hljs-literal: var(--color-code-literal); + --hljs-string: var(--color-code-string); + --hljs-attr: var(--color-code-variable); + --hljs-section: var(--color-code-name); + --hljs-symbol: var(--color-code-title); + --hljs-class: var(--color-code-builtin); + background: var(--color-surface-subtle); border-radius: $border-radius; - border: 1px solid $basic-alpha-8; + border: 1px solid var(--color-border-default); &.no-pad { margin-bottom: 0; } @@ -93,21 +122,10 @@ font-size: $font-sm; line-height: $line-height-sm; padding-left: 12px; - color: $text-icon-light; + color: var(--color-text-secondary); margin-bottom: 0; } } -.yaml { - .hljs-bullet { - color: #eee; - } - .hljs-attr { - color: #64c77c; - } - .hljs-string { - color: #4c96ec; - } -} .hljs-container { position: relative; &:hover { @@ -177,71 +195,88 @@ } .hljs { - color: $code-color; + color: var(--color-code-text); padding: 18px; -} -.json { - border: none; -} -.hljs-strong, -.hljs-emphasis { - color: #a8a8a2; -} + // Nested under .hljs (0,2,0) to out-specify Pylon's global token rules + // (0,1,0). The catch-all sends unmapped tokens to base; groups re-colour. + [class*='hljs-'] { + color: inherit; + } -.hljs-bullet, -.hljs-quote, -.hljs-link, -.hljs-number, -.hljs-regexp, -.hljs-literal { - color: $info; -} + .hljs-comment, + .hljs-quote { + color: var(--color-code-comment); + font-style: italic; + } -.hljs-code, -.hljs-selector-class { - color: #a6e22e; -} + .hljs-doctag, + .hljs-keyword, + .hljs-formula { + color: var(--color-code-keyword); + } -.hljs-emphasis { - font-style: italic; -} + .hljs-section, + .hljs-name, + .hljs-selector-tag, + .hljs-deletion, + .hljs-subst { + color: var(--color-code-name); + } -.hljs-keyword, -.hljs-selector-tag, -.hljs-section, -.hljs-attribute, -.hljs-name, -.hljs-variable { - color: $warning; -} + .hljs-literal { + color: var(--color-code-literal); + } -.hljs-params { - color: #b9b9b9; -} + .hljs-string, + .hljs-regexp, + .hljs-addition, + .hljs-attribute, + .hljs-meta .hljs-string { + color: var(--color-code-string); + } -.hljs-string { - color: $success; -} + .hljs-attr, + .hljs-variable, + .hljs-template-variable, + .hljs-type, + .hljs-selector-class, + .hljs-selector-attr, + .hljs-selector-pseudo, + .hljs-number { + color: var(--color-code-variable); + } + + .hljs-symbol, + .hljs-bullet, + .hljs-link, + .hljs-meta, + .hljs-selector-id, + .hljs-title { + color: var(--color-code-title); + } + + .hljs-built_in, + .hljs-title.class_, + .hljs-class .hljs-title { + color: var(--color-code-builtin); + } + + .hljs-emphasis { + font-style: italic; + } + + .hljs-strong { + font-weight: bold; + } -.hljs-subst, -.hljs-type, -.hljs-built_in, -.hljs-builtin-name, -.hljs-symbol, -.hljs-selector-id, -.hljs-selector-attr, -.hljs-selector-pseudo, -.hljs-template-tag, -.hljs-template-variable, -.hljs-addition { - color: $secondary400; + .hljs-link { + text-decoration: underline; + } } -.hljs-comment, -.hljs-deletion, -.hljs-meta { - color: #7f7f7f; +.json { + border: none; } .value-editor pre { overflow: hidden; @@ -256,18 +291,18 @@ code.txt { font-size: $font-size-base; font-weight: 500; background: transparent !important; - border: 1px solid $input-border-color; + border: 1px solid var(--color-border-default); font-family: 'OpenSans', sans-serif; - color: $body-color; + color: var(--color-text-default); &.empty { font-weight: normal; - color: $input-placeholder-color; + color: var(--color-text-tertiary); } &:focus, &:focus.empty { font-weight: 500; - color: $body-color; - border-color: $primary; + color: var(--color-text-default); + border-color: var(--color-border-action); } } @@ -296,6 +331,25 @@ code.txt { } code[contenteditable='false'].hljs { - cursor: default !important; + cursor: default; @include customScrollDark(); + @include hljsScrollbar(); +} + +// For code inside a container that already provides the surface: transparent, +// no border/radius; the container owns the frame. +.hljs--embedded { + --hljs-bg: transparent; + background: transparent; + border: 0; + border-radius: 0; + // Reserve the scrollbar gutter so hover doesn't shift layout; !important + // beats customScrollDark's display:none on non-editable code. + overflow-x: scroll; + overflow-y: hidden; + + &::-webkit-scrollbar, + &:hover::-webkit-scrollbar { + display: block !important; + } } diff --git a/frontend/web/styles/_variables.scss b/frontend/web/styles/_variables.scss index 2bde5714f42b..46efbd7ef2da 100644 --- a/frontend/web/styles/_variables.scss +++ b/frontend/web/styles/_variables.scss @@ -312,8 +312,6 @@ $tab-line-color: $primary; //CodeHelp $code-btn-bg: $secondary-alfa-16; $code-btn-description-color: $secondary600; -$code-color: $text-icon-light; -$code-bg: $bg-dark200; //Panels $panel-border-color: $basic-alpha-16; From 9e257238aa6f41452caa8c95e876a440526a7dfd Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Fri, 17 Jul 2026 11:15:48 -0300 Subject: [PATCH 3/9] refactor(components): add embedded prop, explicit Button import, getRawHtml Add an `embedded` boolean prop that applies the .hljs--embedded modifier, instead of callers concatenating the class into the language string. Import Button (via the components/ alias) instead of the global, since the Highlight story renders it and Storybook doesn't stub the global. Extract the nested raw-html ternary into a getRawHtml method. No behaviour change. Co-Authored-By: Claude Opus 4.8 --- frontend/web/components/Highlight.js | 32 +++++++++++----------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/frontend/web/components/Highlight.js b/frontend/web/components/Highlight.js index b0323c96b117..f7cf376a4781 100644 --- a/frontend/web/components/Highlight.js +++ b/frontend/web/components/Highlight.js @@ -1,5 +1,6 @@ import React from 'react' import hljs from 'highlight.js' +import Button from './base/forms/Button' function escapeHtml(unsafe) { if (!unsafe || !unsafe.__html) return unsafe @@ -103,12 +104,20 @@ class Highlight extends React.Component { this.props.onBlur?.() } + // The value to render before escaping: the live edit while focused, the + // current value when there's content, otherwise a disabled/empty placeholder. + getRawHtml = () => { + if (this.state.focus) return this.state.value + if (this.props.children) return { ...this.state.value } + return this.props.disabled ? defaultDisabledValue : defaultValue + } + render() { const { children, className, - disabled, element: Element, + embedded, innerHTML, } = this.props const props = { className, ref: this.setEl } @@ -125,23 +134,8 @@ class Highlight extends React.Component { return {children} } - const html = this.props.preventEscape - ? this.state.focus - ? this.state.value - : this.props.children - ? { ...this.state.value } - : disabled - ? defaultDisabledValue - : defaultValue - : escapeHtml( - this.state.focus - ? this.state.value - : this.props.children - ? { ...this.state.value } - : disabled - ? defaultDisabledValue - : defaultValue, - ) + const raw = this.getRawHtml() + const html = this.props.preventEscape ? raw : escapeHtml(raw) return (
Date: Fri, 17 Jul 2026 11:15:50 -0300
Subject: [PATCH 4/9] test(storybook): add Highlight stories

Cover the code wrapper across languages plus the embedded variant. Stub the
isMobile global so components that read it render in Storybook.

Co-Authored-By: Claude Opus 4.8 
---
 frontend/.storybook/preview.js                |  2 +
 .../documentation/pages/Highlight.stories.tsx | 79 +++++++++++++++++++
 2 files changed, 81 insertions(+)
 create mode 100644 frontend/documentation/pages/Highlight.stories.tsx

diff --git a/frontend/.storybook/preview.js b/frontend/.storybook/preview.js
index 54f8d7f7c031..29065d615a96 100644
--- a/frontend/.storybook/preview.js
+++ b/frontend/.storybook/preview.js
@@ -55,6 +55,8 @@ global.Select = (props) =>
 window.Tooltip = Tooltip
 window.Row = Row
 window.FormGroup = FormGroup
+// isMobile is set at app boot; stub it so components that read it render in Storybook.
+window.isMobile = false
 
 /** @type { import('storybook').Preview } */
 const preview = {
diff --git a/frontend/documentation/pages/Highlight.stories.tsx b/frontend/documentation/pages/Highlight.stories.tsx
new file mode 100644
index 000000000000..801db9d80fdf
--- /dev/null
+++ b/frontend/documentation/pages/Highlight.stories.tsx
@@ -0,0 +1,79 @@
+import type { Meta, StoryObj } from 'storybook'
+
+import Highlight from 'components/Highlight'
+
+const meta: Meta = {
+  component: Highlight,
+  parameters: {
+    docs: {
+      description: {
+        component:
+          'The code wrapper: renders a `
` block and runs ' +
+          'highlight.js over it. This is what the shared code theme styles (surface, ' +
+          'border, syntax palette). Toggle the theme in the toolbar to QA light vs dark; ' +
+          '`className` is the highlight.js language.',
+      },
+    },
+    layout: 'padded',
+  },
+  title: 'Components/Highlight',
+}
+
+export default meta
+
+type Story = StoryObj
+
+const js = `// Initialise the client
+import flagsmith from 'flagsmith'
+
+flagsmith.init({
+  environmentID: 'YOUR_ENVIRONMENT_KEY',
+  onChange: () => {
+    const bannerSize = flagsmith.getValue('banner_size')
+  },
+})`
+
+const ts = `import { useFlags } from 'flagsmith/react'
+
+type FlagName = 'banner_size' | 'my_feature'
+
+const flags = useFlags(['banner_size'])
+const size: number = flags.banner_size.value ?? 0`
+
+const json = `{
+  "flags": [
+    { "feature": "banner_size", "enabled": true, "value": 42 },
+    { "feature": "my_feature", "enabled": false, "value": null }
+  ]
+}`
+
+const yaml = `flags:
+  - feature: banner_size
+    enabled: true
+    value: 42`
+
+export const JavaScript: Story = {
+  args: { children: js, className: 'javascript', forceExpanded: true },
+}
+
+export const TypeScript: Story = {
+  args: { children: ts, className: 'typescript', forceExpanded: true },
+}
+
+export const Json: Story = {
+  args: { children: json, className: 'json', forceExpanded: true },
+}
+
+export const Yaml: Story = {
+  args: { children: yaml, className: 'yaml', forceExpanded: true },
+}
+
+// Embedded variant: transparent, for code whose container owns the surface.
+export const Embedded: Story = {
+  args: {
+    children: js,
+    className: 'javascript',
+    embedded: true,
+    forceExpanded: true,
+  },
+}

From 7a4c0818f6b6624c34608ee0c16bbfa67c5f5894 Mon Sep 17 00:00:00 2001
From: Talisson Costa 
Date: Fri, 17 Jul 2026 13:10:57 -0300
Subject: [PATCH 5/9] fix(styles): markup tag names use the title colour, not
 red

Review (Kyle): red tag names read as errors. Tags now render like an IDE:
grey angle brackets (base text via the catch-all), blue tag name.

Co-Authored-By: Claude Fable 5 
---
 frontend/web/styles/3rdParty/_hljs.scss | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/frontend/web/styles/3rdParty/_hljs.scss b/frontend/web/styles/3rdParty/_hljs.scss
index 447b4d82328c..2b266995cf51 100644
--- a/frontend/web/styles/3rdParty/_hljs.scss
+++ b/frontend/web/styles/3rdParty/_hljs.scss
@@ -217,13 +217,19 @@
   }
 
   .hljs-section,
-  .hljs-name,
-  .hljs-selector-tag,
   .hljs-deletion,
   .hljs-subst {
     color: var(--color-code-name);
   }
 
+  // Markup tag names are deliberately not red (red reads as an error or a
+  // deletion). The < > punctuation (.hljs-tag) stays base text via the
+  // catch-all above, so tags render like an IDE: grey brackets, blue name.
+  .hljs-name,
+  .hljs-selector-tag {
+    color: var(--color-code-title);
+  }
+
   .hljs-literal {
     color: var(--color-code-literal);
   }

From 904981b49fa24b439a2ac49fd3a1a55270b1f1bc Mon Sep 17 00:00:00 2001
From: Talisson Costa 
Date: Fri, 17 Jul 2026 13:10:58 -0300
Subject: [PATCH 6/9] test(storybook): add JSX Highlight story

Covers markup tags inside JavaScript, mirroring the React code-help
snippet, so the tag colour renders in Chromatic.

Co-Authored-By: Claude Fable 5 
---
 frontend/documentation/pages/Highlight.stories.tsx | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/frontend/documentation/pages/Highlight.stories.tsx b/frontend/documentation/pages/Highlight.stories.tsx
index 801db9d80fdf..9d1d5866d76b 100644
--- a/frontend/documentation/pages/Highlight.stories.tsx
+++ b/frontend/documentation/pages/Highlight.stories.tsx
@@ -52,6 +52,16 @@ const yaml = `flags:
     enabled: true
     value: 42`
 
+// Mirrors the React SDK snippet in code-help: markup tags inside JavaScript.
+const jsx = `export function HomePage() {
+  const flags = useFlags(['banner_size'])
+  return (
+    
+      
+    
+  )
+}`
+
 export const JavaScript: Story = {
   args: { children: js, className: 'javascript', forceExpanded: true },
 }
@@ -68,6 +78,10 @@ export const Yaml: Story = {
   args: { children: yaml, className: 'yaml', forceExpanded: true },
 }
 
+export const Jsx: Story = {
+  args: { children: jsx, className: 'javascript', forceExpanded: true },
+}
+
 // Embedded variant: transparent, for code whose container owns the surface.
 export const Embedded: Story = {
   args: {

From dd347ec4a647e79ba3019ac169d417f0d2958736 Mon Sep 17 00:00:00 2001
From: Talisson Costa 
Date: Wed, 8 Jul 2026 15:57:08 -0300
Subject: [PATCH 7/9] fix(code-help): correct the React init snippet

The shared React init snippet was two files mashed into one - two 'export
default function' (App and HomePage, invalid JS), duplicate imports, an unused
useFlagsmith import, and an App that never rendered HomePage. Make it one
coherent example: a named HomePage using useFlags, rendered inside the default
App wrapped in FlagsmithProvider. Shared via codeHelp.INIT, so this fixes the
snippet app-wide, including the onboarding wire step.

Co-Authored-By: Claude Opus 4.8 
---
 frontend/common/code-help/init/init-react.js | 36 +++++++++-----------
 1 file changed, 17 insertions(+), 19 deletions(-)

diff --git a/frontend/common/code-help/init/init-react.js b/frontend/common/code-help/init/init-react.js
index 6b1a3412cf0e..2791237c6b1f 100644
--- a/frontend/common/code-help/init/init-react.js
+++ b/frontend/common/code-help/init/init-react.js
@@ -3,9 +3,20 @@ import Constants from 'common/constants'
 export default (
   envId,
   { FEATURE_NAME, FEATURE_NAME_ALT, LIB_NAME, NPM_CLIENT },
-) => `// App root
-import ${LIB_NAME} from "${NPM_CLIENT}";
-import { FlagsmithProvider } from '@flagsmith/flagsmith/react';
+) => `import ${LIB_NAME} from "${NPM_CLIENT}";
+import { FlagsmithProvider, useFlags } from '${NPM_CLIENT}/react';
+
+export function HomePage() {
+  const flags = useFlags(['${FEATURE_NAME}','${FEATURE_NAME_ALT}']); // only causes re-render if specified flag values / traits change
+  const ${FEATURE_NAME} = flags.${FEATURE_NAME}.enabled
+  const ${FEATURE_NAME_ALT} = flags.${FEATURE_NAME_ALT}.value
+  return (
+    <>
+      {\`${FEATURE_NAME}: \${${FEATURE_NAME}}\`}
+      {\`${FEATURE_NAME_ALT}: \${${FEATURE_NAME_ALT}}\`}
+    </>
+  );
+}
 
 export default function App() {
   return (
@@ -17,21 +28,8 @@ export default function App() {
     : ''
 }
       }}
-      flagsmith={flagsmith}>
-      {...Your app}
-    </FlagsmithProvider>
-  );
-}
-
-// Home Page
-import ${LIB_NAME} from '${NPM_CLIENT}';
-import { useFlags, useFlagsmith } from '${NPM_CLIENT}/react';
-
-export default function HomePage() {
-  const flags = useFlags(['${FEATURE_NAME}','${FEATURE_NAME_ALT}']); // only causes re-render if specified flag values / traits change
-  const ${FEATURE_NAME} = flags.${FEATURE_NAME}.enabled
-  const ${FEATURE_NAME_ALT} = flags.${FEATURE_NAME_ALT}.value
-  return (
-    <>{...}</>
+      flagsmith={${LIB_NAME}}>
+      <HomePage />
+    </FlagsmithProvider>
   );
 }`

From a353029b5a194b905572ac0730cc01c5222b6fa3 Mon Sep 17 00:00:00 2001
From: Talisson Costa 
Date: Fri, 17 Jul 2026 13:30:01 -0300
Subject: [PATCH 8/9] fix(styles): only show the embedded code scrollbar on
 overflow

Ports the onboarding code-card fix (was on #7989) to the shared embedded
variant: overflow-x auto instead of a permanently reserved gutter.

Co-Authored-By: Claude Fable 5 
---
 frontend/web/styles/3rdParty/_hljs.scss | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/frontend/web/styles/3rdParty/_hljs.scss b/frontend/web/styles/3rdParty/_hljs.scss
index 2b266995cf51..2cda10cb75c7 100644
--- a/frontend/web/styles/3rdParty/_hljs.scss
+++ b/frontend/web/styles/3rdParty/_hljs.scss
@@ -349,9 +349,10 @@ code[contenteditable='false'].hljs {
   background: transparent;
   border: 0;
   border-radius: 0;
-  // Reserve the scrollbar gutter so hover doesn't shift layout; !important
-  // beats customScrollDark's display:none on non-editable code.
-  overflow-x: scroll;
+  // Scroll only when the code overflows. When it does, the webkit rule below
+  // forces the bar visible over customScrollDark (which display:none-s it on
+  // hover), so a long snippet's bar doesn't flicker.
+  overflow-x: auto;
   overflow-y: hidden;
 
   &::-webkit-scrollbar,

From a1f42d28f761fa608dd6791ea5a3e2562bcfbe3a Mon Sep 17 00:00:00 2001
From: Talisson Costa 
Date: Fri, 17 Jul 2026 13:32:31 -0300
Subject: [PATCH 9/9] fix(code-help): copy real brackets, not HTML entities

Snippet sources were pre-escaped (< >) because CodeHelp rendered
them as raw HTML via preventEscape, so Copy Code shipped the entities
to the clipboard. Store plain < > and let Highlight's default escaping
handle rendering, like the onboarding code card already does.
preventEscape now has no users, so the prop is gone.

Co-Authored-By: Claude Fable 5 
---
 .../code-help/create-user/create-user-next.js |  8 ++---
 .../create-user/create-user-react.js          |  6 ++--
 .../code-help/init/init-next-app-router.js    | 32 +++++++++----------
 .../code-help/init/init-next-pages-router.js  | 10 +++---
 frontend/common/code-help/init/init-react.js  | 12 +++----
 .../common/code-help/traits/traits-next.js    | 10 +++---
 .../common/code-help/traits/traits-react.js   |  8 ++---
 frontend/web/components/CodeHelp.tsx          |  1 -
 frontend/web/components/Highlight.js          |  2 +-
 .../integrations/mcp/MCPSnippet.tsx           |  2 +-
 10 files changed, 45 insertions(+), 46 deletions(-)

diff --git a/frontend/common/code-help/create-user/create-user-next.js b/frontend/common/code-help/create-user/create-user-next.js
index fcd4b1c5c6e8..cfd0890775f8 100644
--- a/frontend/common/code-help/create-user/create-user-next.js
+++ b/frontend/common/code-help/create-user/create-user-next.js
@@ -23,7 +23,7 @@ export default function HomePage() {
 
 export default function App({ Component, pageProps, flagsmithState } {
   return (
-    <FlagsmithProvider
+    
-    </FlagsmithProvider>
+      flagsmith={flagsmith}>
+        
+    
   );
 }
 
diff --git a/frontend/common/code-help/create-user/create-user-react.js b/frontend/common/code-help/create-user/create-user-react.js
index 4de611b66d58..e44396b5e1a0 100644
--- a/frontend/common/code-help/create-user/create-user-react.js
+++ b/frontend/common/code-help/create-user/create-user-react.js
@@ -10,7 +10,7 @@ import { FlagsmithProvider } from '${NPM_CLIENT}/react';
 
 export default function App() {
   return (
-    <FlagsmithProvider
+    
       {...Your app}
-    </FlagsmithProvider>
+    
   );
 }
 
diff --git a/frontend/common/code-help/init/init-next-app-router.js b/frontend/common/code-help/init/init-next-app-router.js
index 3cc974fa91f0..c912e46d5665 100644
--- a/frontend/common/code-help/init/init-next-app-router.js
+++ b/frontend/common/code-help/init/init-next-app-router.js
@@ -9,9 +9,9 @@ import { FeatureFlagProvider } from "./components/FeatureFlagProvider";
 
 export default async function RootLayout({
   children,
-}: Readonly<{
+}: Readonly<{
   children: React.ReactNode;
-}>) {
+}>) {
   await flagsmith.init({
     environmentID: "${envId}",${
   Constants.isCustomFlagsmithUrl()
@@ -21,16 +21,16 @@ export default async function RootLayout({
   const serverState = flagsmith.getState();
 
   return (
-    <html lang="en">
-      <head>
-        <meta name="viewport" content="initial-scale=1, width=device-width" />
-      </head>
-      <body>
-        <FeatureFlagProvider serverState={serverState}>
+    
+      
+        
+      
+      
+        
           {children}
-        </FeatureFlagProvider>
-      </body>
-    </html>
+        
+      
+    
   );
 }
 
@@ -48,13 +48,13 @@ export const FeatureFlagProvider = ({
 }: {
   serverState: IState;
   children: ReactNode;
-}) => {
+}) => {
   const flagsmithInstance = useRef(createFlagsmithInstance());
   
   return (
-    <FlagsmithProvider flagsmith={flagsmithInstance.current} serverState={serverState}>
-      <>{children}</>
-    </FlagsmithProvider>
+    
+      <>{children}
+    
   );
 };
 
@@ -69,6 +69,6 @@ export default function HomePage() {
   const ${FEATURE_NAME_ALT} = flags.${FEATURE_NAME_ALT}.value
   
   return (
-    <>{...}</>
+    <>{...}
   );
 }`
diff --git a/frontend/common/code-help/init/init-next-pages-router.js b/frontend/common/code-help/init/init-next-pages-router.js
index 1e60dc327167..87dcf864b5eb 100644
--- a/frontend/common/code-help/init/init-next-pages-router.js
+++ b/frontend/common/code-help/init/init-next-pages-router.js
@@ -8,7 +8,7 @@ import { FlagsmithProvider } from '@flagsmith/flagsmith/react';
 
 export default function App({ Component, pageProps, flagsmithState } {
   return (
-    <FlagsmithProvider
+    
-    </FlagsmithProvider>
+      flagsmith={flagsmith}>
+        
+    
   );
 }
 
@@ -43,6 +43,6 @@ export default function HomePage() {
   const ${FEATURE_NAME} = flags.${FEATURE_NAME}.enabled
   const ${FEATURE_NAME_ALT} = flags.${FEATURE_NAME_ALT}.value
   return (
-    <>{...}</>
+    <>{...}
   );
 }`
diff --git a/frontend/common/code-help/init/init-react.js b/frontend/common/code-help/init/init-react.js
index 2791237c6b1f..8b143701b529 100644
--- a/frontend/common/code-help/init/init-react.js
+++ b/frontend/common/code-help/init/init-react.js
@@ -11,16 +11,16 @@ export function HomePage() {
   const ${FEATURE_NAME} = flags.${FEATURE_NAME}.enabled
   const ${FEATURE_NAME_ALT} = flags.${FEATURE_NAME_ALT}.value
   return (
-    <>
+    <>
       {\`${FEATURE_NAME}: \${${FEATURE_NAME}}\`}
       {\`${FEATURE_NAME_ALT}: \${${FEATURE_NAME_ALT}}\`}
-    </>
+    
   );
 }
 
 export default function App() {
   return (
-    <FlagsmithProvider
+    
+      
+    
   );
 }`
diff --git a/frontend/common/code-help/traits/traits-next.js b/frontend/common/code-help/traits/traits-next.js
index 1a096ed39dda..d396cb7e2983 100644
--- a/frontend/common/code-help/traits/traits-next.js
+++ b/frontend/common/code-help/traits/traits-next.js
@@ -23,7 +23,7 @@ export default function HomePage() {
   };
 
   return (
-    <>{...}</>
+    <>{...}
   );
 }
 
@@ -31,7 +31,7 @@ export default function HomePage() {
 
 export default function App({ Component, pageProps, flagsmithState } {
   return (
-    <FlagsmithProvider
+    
-    </FlagsmithProvider>
+      flagsmith={flagsmith}>
+        
+    
   );
 }
 
diff --git a/frontend/common/code-help/traits/traits-react.js b/frontend/common/code-help/traits/traits-react.js
index c9bf65a6bff2..05df85d95c20 100644
--- a/frontend/common/code-help/traits/traits-react.js
+++ b/frontend/common/code-help/traits/traits-react.js
@@ -10,7 +10,7 @@ import { FlagsmithProvider } from '${NPM_CLIENT}/react';
 
 export default function App() {
   return (
-    <FlagsmithProvider
+    
       {...Your app}
-    </FlagsmithProvider>
+    
   );
 }
 
@@ -46,6 +46,6 @@ export default function HomePage() {
   };
 
   return (
-    <>{...}</>
+    <>{...}
   );
 }`
diff --git a/frontend/web/components/CodeHelp.tsx b/frontend/web/components/CodeHelp.tsx
index ad2dbb87e052..47959a888382 100644
--- a/frontend/web/components/CodeHelp.tsx
+++ b/frontend/web/components/CodeHelp.tsx
@@ -144,7 +144,6 @@ const SnippetItem: FC = ({
       />
       
         
 = ({ code, language = 'bash' }) => (
   
- + {code}