From e28c7f9a99046a40702bbcb62860bd908387e2c4 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Fri, 28 Aug 2026 11:11:31 -0300 Subject: [PATCH 01/15] refactor(styles): drop the .dark override in the code theme The base rules are tokenised, so the block was restating the same intents in hardcoded white and !important. The three it did carry move onto tokens: .hljs-header takes --color-text-default, its icons follow via currentColor, and the active language label takes the token the icon beside it already used. callout-bar--dark stays. That is a component variant, not the theme. --- frontend/web/styles/3rdParty/_hljs.scss | 30 +++++-------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/frontend/web/styles/3rdParty/_hljs.scss b/frontend/web/styles/3rdParty/_hljs.scss index d7027cdcd371..8cc33d41f454 100644 --- a/frontend/web/styles/3rdParty/_hljs.scss +++ b/frontend/web/styles/3rdParty/_hljs.scss @@ -60,6 +60,7 @@ span { margin: 0; opacity: 1; + color: var(--color-text-default); } .language-icon { color: var(--color-text-default); @@ -161,7 +162,7 @@ font-family: $font-family-header; overflow: hidden; @include transition(all 0.2s ease-in-out); - color: $btn-code-button-color; + color: var(--color-text-default); display: inline-block; line-height: $line-height-xsm; font-weight: bold; @@ -181,6 +182,9 @@ } svg { vertical-align: sub; + path { + fill: currentColor; + } } } @@ -310,30 +314,6 @@ code.txt { } } -.dark { - code.txt { - &:focus, - &:focus.empty { - color: white !important; - } - &.empty { - color: rgba(210, 210, 210, 0.5) !important; - } - border-color: transparent; - background: $input-bg-dark !important; - color: white; - } - .hljs-header { - color: white; - path { - fill: white; - } - } - .value-editor .select-language span.active { - color: white; - } -} - code[contenteditable='false'].hljs { cursor: default; @include customScrollDark(); From 38f4d9954ac769bcf0366316cca6adcd239078ab Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Fri, 28 Aug 2026 11:11:31 -0300 Subject: [PATCH 02/15] refactor(styles): diff view on the shared code tokens Syntax colours now come from --color-code-*, the same tokens as the highlight.js theme, so the diff matches code snippets elsewhere and follows light and dark for free. Four token references become twenty. Row and word backgrounds are derived locally with oklch() from the --red-500 and --green-500 primitives, the same ones the danger and success surfaces are built from. No new tokens or variables: an alpha overlay lightens whatever it sits on, so one value covers both themes. Also drops four rules that match nothing. The library emits hashed line and marker classes, so .react-diff-line--added and friends never applied in either theme. --- frontend/web/styles/3rdParty/_react-diff.scss | 96 ++++++++++--------- 1 file changed, 52 insertions(+), 44 deletions(-) diff --git a/frontend/web/styles/3rdParty/_react-diff.scss b/frontend/web/styles/3rdParty/_react-diff.scss index 36e814cf063a..308c3eedad0d 100644 --- a/frontend/web/styles/3rdParty/_react-diff.scss +++ b/frontend/web/styles/3rdParty/_react-diff.scss @@ -1,23 +1,40 @@ -@import "../variables"; +@import '../variables'; + +// Diff view: react-diff-viewer, with Prism for the JSON. The hashed +// react-diff-* classes need !important because the library injects its +// emotion styles at runtime, after this stylesheet, so it wins any tie. + .react-diff-1n5o7vh-diff-container { background-color: transparent !important; - } + +// Prism's JSON tokens, on the same palette as highlight.js in _hljs.scss. .token.property { - color: $success; + color: var(--color-code-variable); } -.token.number,.token.boolean { - color: $info; +.token.number, +.token.boolean, +.token.null { + color: var(--color-code-literal); } -.react-diff-1igno8l-gutter,.react-diff-1wdmqof-code-fold { - background-color: $black-alpha-8 !important; +.token.string { + color: var(--color-code-string); +} +.token.punctuation, +.token.operator { + color: var(--color-code-text); +} + +.react-diff-1igno8l-gutter, +.react-diff-1wdmqof-code-fold { + background-color: var(--color-surface-subtle) !important; } .react-diff-1989f1g-code-fold-gutter { - background-color: $black-alpha-8 !important; + background-color: var(--color-surface-subtle) !important; } .react-diff-same { font-size: 13px; - background-color: rgba(0, 0, 0, 0.08); + background-color: var(--color-surface-subtle); .react-diff-marker { opacity: 0; } @@ -26,74 +43,65 @@ } } .react-diff-marker { - background-color: $black-alpha-8; + background-color: var(--color-surface-subtle); width: 31px; height: 31px; justify-content: center; display: flex; align-items: center; - >* { + > * { margin: 0; } - &.react-diff-marker--added { - background-color: $success-alfa-16 !important; - - } - &.react-diff-marker--removed { - background-color: $danger-alfa-16 !important; - } } .react-diff-line { line-height: 31px; - &.react-diff-line--added { - background-color: $success-alfa-16 !important; - } - &.react-diff-line--removed { - background-color: $danger-alfa-16 !important; - } .rc-switch { margin-bottom: 2px; } } .react-diff-line pre { - background-color: $black-alpha-8; + background-color: var(--color-surface-subtle); display: flex; - flex:1; + flex: 1; line-height: 28px; font-family: $font-family-code; font-size: 13px !important; } .react-diff-42iver-word-diff { - padding:0 !important; + padding: 0 !important; +} + +// Rows take a 16% overlay, the gutter marker and the changed words 28%, so the +// words read through the row behind them. Flatten these and word-level +// highlighting disappears. +// +// Derived from the palette primitives rather than added as tokens: an alpha +// overlay lightens whatever it sits on, so one value covers both themes, and +// --red-500 / --green-500 are the same primitives the danger and success +// surfaces are built from. +.react-diff-1fxlvce-diff-removed { + background-color: oklch(from var(--red-500) l c h / 0.16) !important; +} +.react-diff-1fqrsd-diff-added { + background-color: oklch(from var(--green-500) l c h / 0.16) !important; } .react-diff-1p2tkx4-marker.react-diff-1fxlvce-diff-removed { - background-color: $danger-alfa-16 !important; + background-color: oklch(from var(--red-500) l c h / 0.28) !important; } .react-diff-1p2tkx4-marker.react-diff-1fqrsd-diff-added { - background-color: $success-alfa-16 !important; + background-color: oklch(from var(--green-500) l c h / 0.28) !important; } .react-diff-hf3w1f-word-removed { - background-color: $danger-alfa-16 !important; + background-color: oklch(from var(--red-500) l c h / 0.28) !important; } .react-diff-1u4zuq6-word-added { - background-color: $success-alfa-16 !important; + background-color: oklch(from var(--green-500) l c h / 0.28) !important; } + .react-diff-1n5o7vh-diff-container pre { font-family: $font-family-monospace; font-size: $font-caption !important; - color: $body-color !important; -} -.react-diff-1fxlvce-diff-removed { - background-color: $danger-alfa-8 !important; -} -.react-diff-1fqrsd-diff-added { - background-color: $success-alfa-8 !important; -} - -.dark { - .react-diff-1n5o7vh-diff-container pre { - color: $body-color-dark !important; - } + color: var(--color-text-default) !important; } // Environment name legend for diff headers From 6e80ddce9d70813db0b4667915927e21b2dae165 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Fri, 28 Aug 2026 11:11:31 -0300 Subject: [PATCH 03/15] test(storybook): add DiffString stories --- .../pages/DiffString.stories.tsx | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 frontend/documentation/pages/DiffString.stories.tsx diff --git a/frontend/documentation/pages/DiffString.stories.tsx b/frontend/documentation/pages/DiffString.stories.tsx new file mode 100644 index 000000000000..11887cfff4ce --- /dev/null +++ b/frontend/documentation/pages/DiffString.stories.tsx @@ -0,0 +1,56 @@ +import type { Meta, StoryObj } from 'storybook' + +import DiffString from 'components/diff/DiffString' + +const meta: Meta = { + component: DiffString, + parameters: { + docs: { + description: { + component: + 'The diff view for flag values (react-diff-viewer + Prism for JSON). ' + + 'Uses the same code colour tokens as the Highlight component, so it ' + + 'follows light/dark mode. Toggle the theme in the toolbar to QA.', + }, + }, + layout: 'padded', + }, + title: 'Components/DiffString', +} + +export default meta + +type Story = StoryObj + +const oldJson = `{ + "id": "london-js", + "title": "Hello London.js!", + "description": "Join us at London.js and get insights on best practices!", + "buttonText": "Register on Meetup", + "isClosable": false +}` + +const newJson = `{ + "id": "london-js", + "title": "Hello London.js!", + "description": "Join us at London.js!", + "buttonText": "Register now", + "isClosable": true +}` + +export const ChangedJson: Story = { + args: { newValue: newJson, oldValue: oldJson }, +} + +export const ChangedString: Story = { + args: { newValue: 'banner_size: large', oldValue: 'banner_size: small' }, +} + +export const SameJson: Story = { + args: { newValue: oldJson, oldValue: oldJson }, +} + +// An empty old value renders as `""`, not a blank row. +export const FromEmpty: Story = { + args: { newValue: 'banner_size: large', oldValue: '' }, +} From 91b88b3fa0d26c6145a9e255d9285dca55d43c5d Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Fri, 28 Aug 2026 11:23:49 -0300 Subject: [PATCH 04/15] fix(storybook): stop docs code styling repainting rendered stories .sbdocs pre and .sbdocs code were painting an opaque background on every
 inside a docs page, including the ones components render. In the
diff viewer that covered the row tint, so on a Docs page a changed row
looked untinted and word-level highlighting vanished, while the same
story rendered correctly on its own page.

Storybook already marks rendered stories .sb-unstyled for exactly this
reason; these two rules predate that and ignored it.
---
 frontend/.storybook/docs-theme.scss | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/frontend/.storybook/docs-theme.scss b/frontend/.storybook/docs-theme.scss
index a684067b05ec..45c50b383c97 100644
--- a/frontend/.storybook/docs-theme.scss
+++ b/frontend/.storybook/docs-theme.scss
@@ -50,6 +50,15 @@
     color: var(--color-text-default, #ffffff);
   }
 
+  // Storybook marks rendered stories .sb-unstyled so docs typography stays out
+  // of them. The two rules above predate that, and were repainting any 
 a
+  // component renders, e.g. every line of the diff viewer.
+  .sbdocs .sb-unstyled code,
+  .sbdocs .sb-unstyled pre {
+    background-color: transparent;
+    color: inherit;
+  }
+
   // Storybook Source/code block container and all its children
   .docblock-source,
   .docblock-source > * {

From 920bf4689634ccd6d21da44fd9779e86fa2191ed Mon Sep 17 00:00:00 2001
From: Talisson Costa 
Date: Fri, 28 Aug 2026 12:48:44 -0300
Subject: [PATCH 05/15] refactor(styles): name the diff's two strengths as
 local custom properties

The oklch expression was repeated six times. It now sits once on the diff
container as --diff-row-* and --diff-word-*, which the rules inherit.

Names carry what a comment was carrying, so that comment goes too.
---
 frontend/web/styles/3rdParty/_react-diff.scss | 28 +++++++++----------
 1 file changed, 14 insertions(+), 14 deletions(-)

diff --git a/frontend/web/styles/3rdParty/_react-diff.scss b/frontend/web/styles/3rdParty/_react-diff.scss
index 308c3eedad0d..92e1dbe0b348 100644
--- a/frontend/web/styles/3rdParty/_react-diff.scss
+++ b/frontend/web/styles/3rdParty/_react-diff.scss
@@ -5,6 +5,14 @@
 // emotion styles at runtime, after this stylesheet, so it wins any tie.
 
 .react-diff-1n5o7vh-diff-container {
+  // The diff's two strengths, in one place. A changed row is faint, the words
+  // that changed inside it are stronger, which is what makes a diff readable.
+  // See-through layers, so one value works on light and dark alike.
+  --diff-row-removed: oklch(from var(--red-500) l c h / 0.16);
+  --diff-row-added: oklch(from var(--green-500) l c h / 0.16);
+  --diff-word-removed: oklch(from var(--red-500) l c h / 0.28);
+  --diff-word-added: oklch(from var(--green-500) l c h / 0.28);
+
   background-color: transparent !important;
 }
 
@@ -71,31 +79,23 @@
   padding: 0 !important;
 }
 
-// Rows take a 16% overlay, the gutter marker and the changed words 28%, so the
-// words read through the row behind them. Flatten these and word-level
-// highlighting disappears.
-//
-// Derived from the palette primitives rather than added as tokens: an alpha
-// overlay lightens whatever it sits on, so one value covers both themes, and
-// --red-500 / --green-500 are the same primitives the danger and success
-// surfaces are built from.
 .react-diff-1fxlvce-diff-removed {
-  background-color: oklch(from var(--red-500) l c h / 0.16) !important;
+  background-color: var(--diff-row-removed) !important;
 }
 .react-diff-1fqrsd-diff-added {
-  background-color: oklch(from var(--green-500) l c h / 0.16) !important;
+  background-color: var(--diff-row-added) !important;
 }
 .react-diff-1p2tkx4-marker.react-diff-1fxlvce-diff-removed {
-  background-color: oklch(from var(--red-500) l c h / 0.28) !important;
+  background-color: var(--diff-word-removed) !important;
 }
 .react-diff-1p2tkx4-marker.react-diff-1fqrsd-diff-added {
-  background-color: oklch(from var(--green-500) l c h / 0.28) !important;
+  background-color: var(--diff-word-added) !important;
 }
 .react-diff-hf3w1f-word-removed {
-  background-color: oklch(from var(--red-500) l c h / 0.28) !important;
+  background-color: var(--diff-word-removed) !important;
 }
 .react-diff-1u4zuq6-word-added {
-  background-color: oklch(from var(--green-500) l c h / 0.28) !important;
+  background-color: var(--diff-word-added) !important;
 }
 
 .react-diff-1n5o7vh-diff-container pre {

From 246719dae9087487bbf7005c8b7b17e993f94ead Mon Sep 17 00:00:00 2001
From: Talisson Costa 
Date: Tue, 1 Sep 2026 10:16:26 -0300
Subject: [PATCH 06/15] fix(styles): put the plain-text value editor on the
 input surface token

Co-Authored-By: Claude Opus 5 (1M context) 
---
 frontend/web/styles/3rdParty/_hljs.scss | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/frontend/web/styles/3rdParty/_hljs.scss b/frontend/web/styles/3rdParty/_hljs.scss
index 8cc33d41f454..ed70b1de7a0d 100644
--- a/frontend/web/styles/3rdParty/_hljs.scss
+++ b/frontend/web/styles/3rdParty/_hljs.scss
@@ -298,7 +298,7 @@
 code.txt {
   font-size: $font-size-base;
   font-weight: 500;
-  background: transparent !important;
+  background: var(--color-surface-default) !important;
   border: 1px solid var(--color-border-default);
   font-family: 'OpenSans', sans-serif;
   color: var(--color-text-default);

From 9fdd24862bad3d6fbfb4207da6e9f862c7d73c40 Mon Sep 17 00:00:00 2001
From: Talisson Costa 
Date: Tue, 1 Sep 2026 10:54:38 -0300
Subject: [PATCH 07/15] feat(diff): add DiffRow for the rows we build ourselves

DiffEnabled and DiffString both hand-build the same thing: a marker cell
beside the content that changed. Both did it by writing react-diff-marker
and react-diff-line themselves, class names that read as the library's, so
their styles were filed in the library's stylesheet. The library never
emits those names.

DiffRow owns that markup and its own class names, with props for what
actually differs between the two callers: the row state and whether long
content scrolls inside the row.

Co-Authored-By: Claude Opus 5 (1M context) 
---
 .../documentation/pages/DiffRow.stories.tsx   | 58 ++++++++++++++
 .../web/components/diff/DiffRow/DiffRow.scss  | 79 +++++++++++++++++++
 .../web/components/diff/DiffRow/DiffRow.tsx   | 39 +++++++++
 frontend/web/components/diff/DiffRow/index.ts |  2 +
 4 files changed, 178 insertions(+)
 create mode 100644 frontend/documentation/pages/DiffRow.stories.tsx
 create mode 100644 frontend/web/components/diff/DiffRow/DiffRow.scss
 create mode 100644 frontend/web/components/diff/DiffRow/DiffRow.tsx
 create mode 100644 frontend/web/components/diff/DiffRow/index.ts

diff --git a/frontend/documentation/pages/DiffRow.stories.tsx b/frontend/documentation/pages/DiffRow.stories.tsx
new file mode 100644
index 000000000000..58c84e705cfd
--- /dev/null
+++ b/frontend/documentation/pages/DiffRow.stories.tsx
@@ -0,0 +1,58 @@
+import type { Meta, StoryObj } from 'storybook'
+
+import DiffRow from 'components/diff/DiffRow'
+
+const meta: Meta = {
+  component: DiffRow,
+  parameters: {
+    // Snapshotted in both themes: these rows carry the added/removed colours
+    // for every diff we render ourselves.
+    chromatic: { disableSnapshot: false },
+    docs: {
+      description: {
+        component:
+          'One row of a diff: a marker cell beside the content that changed. ' +
+          'Used for the rows we build ourselves, in DiffEnabled and ' +
+          'DiffString. The rows react-diff-viewer renders are styled ' +
+          'separately. Toggle the theme in the toolbar to QA.',
+      },
+    },
+    layout: 'padded',
+  },
+  title: 'Components/DiffRow',
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Removed: Story = {
+  args: { children: 'banner_size: small', state: 'removed' },
+}
+
+export const Added: Story = {
+  args: { children: 'banner_size: large', state: 'added' },
+}
+
+// No marker glyph, and the content sits on the code surface.
+export const Unchanged: Story = {
+  args: { children: 'banner_size: large', state: 'unchanged' },
+}
+
+// The pair as DiffEnabled and DiffString render them, one above the other.
+export const Stacked: Story = {
+  render: () => (
+    <>
+      banner_size: small
+      banner_size: large
+    
+  ),
+}
+
+export const LongContentScrolls: Story = {
+  args: {
+    children: 'banner_size: '.repeat(40),
+    scrollable: true,
+    state: 'unchanged',
+  },
+}
diff --git a/frontend/web/components/diff/DiffRow/DiffRow.scss b/frontend/web/components/diff/DiffRow/DiffRow.scss
new file mode 100644
index 000000000000..f3667fe8b775
--- /dev/null
+++ b/frontend/web/components/diff/DiffRow/DiffRow.scss
@@ -0,0 +1,79 @@
+// One row of a diff: a marker cell beside the content that changed.
+// Used for the rows we build ourselves, in DiffEnabled and DiffString. The
+// rows react-diff-viewer renders are styled in web/styles/3rdParty/_react-diff.scss,
+// which repeats the two values below; keep the pairs in step.
+.diff-row {
+  --diff-row-removed: oklch(from var(--red-500) l c h / 0.16);
+  --diff-row-added: oklch(from var(--green-500) l c h / 0.16);
+
+  display: flex;
+  align-items: center;
+  line-height: 31px;
+
+  &__marker {
+    display: flex;
+    width: 31px;
+    height: 31px;
+    flex: none;
+    align-items: center;
+    justify-content: center;
+    background-color: var(--color-surface-subtle);
+
+    > * {
+      margin: 0;
+    }
+  }
+
+  &__content {
+    flex: 1;
+    min-width: 0;
+    padding-right: 0.5rem;
+
+    .rc-switch {
+      margin-bottom: 2px;
+    }
+  }
+
+  &--removed {
+    .diff-row__marker,
+    .diff-row__content {
+      background-color: var(--diff-row-removed);
+    }
+  }
+
+  &--added {
+    .diff-row__marker,
+    .diff-row__content {
+      background-color: var(--diff-row-added);
+    }
+  }
+
+  // Unchanged rows keep the gutter for alignment but show no marker glyph, and
+  // the content sits on the code surface rather than a diff colour.
+  &--unchanged {
+    background-color: var(--color-surface-subtle);
+    font-size: 13px;
+
+    .diff-row__marker {
+      opacity: 0;
+    }
+
+    .diff-row__content {
+      padding-right: 0;
+
+      pre {
+        display: flex;
+        flex: 1;
+        padding-left: 2px;
+        background-color: var(--color-surface-subtle);
+        font-family: var(--bs-font-monospace);
+        font-size: 13px;
+        line-height: 28px;
+      }
+    }
+  }
+
+  &--scrollable {
+    overflow: auto;
+  }
+}
diff --git a/frontend/web/components/diff/DiffRow/DiffRow.tsx b/frontend/web/components/diff/DiffRow/DiffRow.tsx
new file mode 100644
index 000000000000..0b9fd6403c10
--- /dev/null
+++ b/frontend/web/components/diff/DiffRow/DiffRow.tsx
@@ -0,0 +1,39 @@
+import React, { FC, ReactNode } from 'react'
+import classNames from 'classnames'
+import './DiffRow.scss'
+
+export type DiffRowState = 'added' | 'removed' | 'unchanged'
+
+type DiffRowProps = {
+  state: DiffRowState
+  children: ReactNode
+  // Let long content scroll inside the row instead of widening its container.
+  scrollable?: boolean
+  className?: string
+}
+
+const MARKERS: Record = {
+  added: '+',
+  removed: '-',
+  unchanged: '',
+}
+
+const DiffRow: FC = ({
+  children,
+  className,
+  scrollable,
+  state,
+}) => (
+  
+
+
{MARKERS[state]}
+
+
{children}
+
+) + +export default DiffRow diff --git a/frontend/web/components/diff/DiffRow/index.ts b/frontend/web/components/diff/DiffRow/index.ts new file mode 100644 index 000000000000..2db9f1d3b3af --- /dev/null +++ b/frontend/web/components/diff/DiffRow/index.ts @@ -0,0 +1,2 @@ +export { default } from './DiffRow' +export type { DiffRowState } from './DiffRow' From b24fd267fb2bd786da4dafb4905d37c076b890d7 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Tue, 1 Sep 2026 10:54:50 -0300 Subject: [PATCH 08/15] fix(diff): restore the added and removed colours on boolean rows Moving the diff onto shared tokens dropped the rules for react-diff-marker--added/removed and react-diff-line--added/removed as dead code. They were dead for the library's rows, which use hashed class names, but DiffEnabled wrote those four names itself, so every boolean change in the audit log, change-request review and segment overrides lost its red and green. Both callers now render DiffRow, which owns those colours, so the rows can no longer be styled from a stylesheet that does not know about them. What is left in _react-diff.scss is only what tames the library itself. The DiffEnabled story is snapshotted in both themes, which is the coverage that would have caught this. Co-Authored-By: Claude Opus 5 (1M context) --- .../pages/DiffEnabled.stories.tsx | 39 ++++++++++++ frontend/web/components/diff/DiffEnabled.tsx | 24 +++---- frontend/web/components/diff/DiffString.tsx | 8 +-- frontend/web/styles/3rdParty/_react-diff.scss | 63 +------------------ 4 files changed, 52 insertions(+), 82 deletions(-) create mode 100644 frontend/documentation/pages/DiffEnabled.stories.tsx diff --git a/frontend/documentation/pages/DiffEnabled.stories.tsx b/frontend/documentation/pages/DiffEnabled.stories.tsx new file mode 100644 index 000000000000..dc4a22249ad7 --- /dev/null +++ b/frontend/documentation/pages/DiffEnabled.stories.tsx @@ -0,0 +1,39 @@ +import type { Meta, StoryObj } from 'storybook' + +import DiffEnabled from 'components/diff/DiffEnabled' + +const meta: Meta = { + component: DiffEnabled, + parameters: { + // Snapshotted in both themes: these rows carry the added/removed colours + // for every boolean change in the audit log and change-request review. + chromatic: { disableSnapshot: false }, + docs: { + description: { + component: + 'The diff view for a flag or segment override being turned on or ' + + 'off. Hand-built rather than react-diff-viewer, but shares its ' + + 'added/removed row colours. Toggle the theme in the toolbar to QA.', + }, + }, + layout: 'padded', + }, + title: 'Components/DiffEnabled', +} + +export default meta + +type Story = StoryObj + +export const OffToOn: Story = { + args: { newValue: true, oldValue: false }, +} + +export const OnToOff: Story = { + args: { newValue: false, oldValue: true }, +} + +// Unchanged renders a single switch with no diff colours at all. +export const Unchanged: Story = { + args: { newValue: true, oldValue: true }, +} diff --git a/frontend/web/components/diff/DiffEnabled.tsx b/frontend/web/components/diff/DiffEnabled.tsx index bf8767ae322b..9d09c592ae11 100644 --- a/frontend/web/components/diff/DiffEnabled.tsx +++ b/frontend/web/components/diff/DiffEnabled.tsx @@ -1,6 +1,6 @@ import React, { FC } from 'react' -import classNames from 'classnames' import Switch from 'components/Switch' +import DiffRow from './DiffRow' type DiffType = { oldValue: boolean @@ -21,22 +21,12 @@ const DiffEnabled: FC = ({ newValue, oldValue }) => { } return ( <> -
-
-
-
-
-
- -
-
-
-
-
+
-
-
- -
-
+ + + + + + ) } diff --git a/frontend/web/components/diff/DiffString.tsx b/frontend/web/components/diff/DiffString.tsx index be571e63e7a9..0042b10d60d6 100644 --- a/frontend/web/components/diff/DiffString.tsx +++ b/frontend/web/components/diff/DiffString.tsx @@ -3,6 +3,7 @@ import Diff, { DiffMethod } from 'react-diff-viewer-continued' import Prism from 'prismjs' import 'prismjs/components/prism-json' import { FlagsmithValue } from 'common/types/responses' +import DiffRow from './DiffRow' type DiffType = { oldValue: FlagsmithValue @@ -38,10 +39,7 @@ const DiffString: FC = ({ return
No Value.
} return ( -
- -

-        
+
 = ({
             ),
           }}
         />
-      
+ ) } return ( diff --git a/frontend/web/styles/3rdParty/_react-diff.scss b/frontend/web/styles/3rdParty/_react-diff.scss index 92e1dbe0b348..ff93788520e4 100644 --- a/frontend/web/styles/3rdParty/_react-diff.scss +++ b/frontend/web/styles/3rdParty/_react-diff.scss @@ -7,7 +7,9 @@ .react-diff-1n5o7vh-diff-container { // The diff's two strengths, in one place. A changed row is faint, the words // that changed inside it are stronger, which is what makes a diff readable. - // See-through layers, so one value works on light and dark alike. + // See-through layers, so one value works on light and dark alike. The rows + // we build ourselves repeat the two row values in DiffRow.scss, so a change + // to either belongs in both. --diff-row-removed: oklch(from var(--red-500) l c h / 0.16); --diff-row-added: oklch(from var(--green-500) l c h / 0.16); --diff-word-removed: oklch(from var(--red-500) l c h / 0.28); @@ -40,41 +42,6 @@ .react-diff-1989f1g-code-fold-gutter { background-color: var(--color-surface-subtle) !important; } -.react-diff-same { - font-size: 13px; - background-color: var(--color-surface-subtle); - .react-diff-marker { - opacity: 0; - } - pre { - padding-left: 2px; - } -} -.react-diff-marker { - background-color: var(--color-surface-subtle); - width: 31px; - height: 31px; - justify-content: center; - display: flex; - align-items: center; - > * { - margin: 0; - } -} -.react-diff-line { - line-height: 31px; - .rc-switch { - margin-bottom: 2px; - } -} -.react-diff-line pre { - background-color: var(--color-surface-subtle); - display: flex; - flex: 1; - line-height: 28px; - font-family: $font-family-code; - font-size: 13px !important; -} .react-diff-42iver-word-diff { padding: 0 !important; } @@ -103,27 +70,3 @@ font-size: $font-caption !important; color: var(--color-text-default) !important; } - -// Environment name legend for diff headers -.diff-removed-header, -.diff-added-header { - font-weight: 500; - font-size: $font-caption; - padding: 4px 8px; - border-radius: 4px; -} - -.diff-removed-header { - background-color: var(--color-surface-danger); - color: var(--color-text-danger); -} - -.diff-added-header { - background-color: var(--color-surface-success); - color: var(--color-text-success); -} - -.diff-sign { - font-weight: bold; - margin-right: 4px; -} From a16ca0e2405462b81d1750859775e6d89b89877b Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Tue, 1 Sep 2026 10:55:01 -0300 Subject: [PATCH 09/15] refactor(diff): give DiffFeature's header legend its own stylesheet The environment name legend is DiffFeature's alone and has nothing to do with react-diff-viewer, so it sat in a third-party stylesheet where nothing tied it to its only caller. Co-Authored-By: Claude Opus 5 (1M context) --- frontend/web/components/diff/DiffFeature.scss | 23 +++++++++++++++++++ frontend/web/components/diff/DiffFeature.tsx | 1 + 2 files changed, 24 insertions(+) create mode 100644 frontend/web/components/diff/DiffFeature.scss diff --git a/frontend/web/components/diff/DiffFeature.scss b/frontend/web/components/diff/DiffFeature.scss new file mode 100644 index 000000000000..cf5149d500bb --- /dev/null +++ b/frontend/web/components/diff/DiffFeature.scss @@ -0,0 +1,23 @@ +// Environment name legend above a feature diff. +.diff-removed-header, +.diff-added-header { + padding: 4px 8px; + border-radius: 4px; + font-size: 13px; + font-weight: 500; +} + +.diff-removed-header { + background-color: var(--color-surface-danger); + color: var(--color-text-danger); +} + +.diff-added-header { + background-color: var(--color-surface-success); + color: var(--color-text-success); +} + +.diff-sign { + margin-right: 4px; + font-weight: bold; +} diff --git a/frontend/web/components/diff/DiffFeature.tsx b/frontend/web/components/diff/DiffFeature.tsx index 6d265c08a234..5663268981cb 100644 --- a/frontend/web/components/diff/DiffFeature.tsx +++ b/frontend/web/components/diff/DiffFeature.tsx @@ -15,6 +15,7 @@ import { import DiffFeatureStateValues from './DiffFeatureStateValues' import DiffSegmentOverrides from './DiffSegmentOverrides' import DiffVariations from './DiffVariations' +import './DiffFeature.scss' import InfoMessage from 'components/InfoMessage' import Icon from 'components/icons/Icon' import WarningMessage from 'components/WarningMessage' From 0c80b531e49bf41e7f9ff0e69df1aba249883c55 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Tue, 1 Sep 2026 10:55:10 -0300 Subject: [PATCH 10/15] fix(storybook): keep component code surfaces in dark docs The .sb-unstyled reset outranks .hljs, so it cleared the background a Highlight block paints for itself on the Docs tab. Skip the elements highlight.js marks; the diff's own pre rows still get reset. Co-Authored-By: Claude Opus 5 (1M context) --- frontend/.storybook/docs-theme.scss | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/.storybook/docs-theme.scss b/frontend/.storybook/docs-theme.scss index 45c50b383c97..bbd5f4cc6602 100644 --- a/frontend/.storybook/docs-theme.scss +++ b/frontend/.storybook/docs-theme.scss @@ -52,9 +52,10 @@ // Storybook marks rendered stories .sb-unstyled so docs typography stays out // of them. The two rules above predate that, and were repainting any
 a
-  // component renders, e.g. every line of the diff viewer.
-  .sbdocs .sb-unstyled code,
-  .sbdocs .sb-unstyled pre {
+  // component renders, e.g. every line of the diff viewer. Skip .hljs, which
+  // highlight.js stamps on blocks that bring their own surface.
+  .sbdocs .sb-unstyled code:not(.hljs),
+  .sbdocs .sb-unstyled pre:not(.hljs) {
     background-color: transparent;
     color: inherit;
   }

From b0cb8ff3a06ee9f97c9e0d059011344b630e4fa0 Mon Sep 17 00:00:00 2001
From: Talisson Costa 
Date: Tue, 1 Sep 2026 11:01:00 -0300
Subject: [PATCH 11/15] refactor(storybook): group the diff stories under
 Components/Diff

They were the only ungrouped components besides Button, while Charts,
Data Display, Feedback and Forms are all nested.

Co-Authored-By: Claude Opus 5 (1M context) 
---
 frontend/documentation/pages/DiffEnabled.stories.tsx | 2 +-
 frontend/documentation/pages/DiffRow.stories.tsx     | 2 +-
 frontend/documentation/pages/DiffString.stories.tsx  | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/frontend/documentation/pages/DiffEnabled.stories.tsx b/frontend/documentation/pages/DiffEnabled.stories.tsx
index dc4a22249ad7..4be9b07b870b 100644
--- a/frontend/documentation/pages/DiffEnabled.stories.tsx
+++ b/frontend/documentation/pages/DiffEnabled.stories.tsx
@@ -18,7 +18,7 @@ const meta: Meta = {
     },
     layout: 'padded',
   },
-  title: 'Components/DiffEnabled',
+  title: 'Components/Diff/DiffEnabled',
 }
 
 export default meta
diff --git a/frontend/documentation/pages/DiffRow.stories.tsx b/frontend/documentation/pages/DiffRow.stories.tsx
index 58c84e705cfd..bb20ef467d7d 100644
--- a/frontend/documentation/pages/DiffRow.stories.tsx
+++ b/frontend/documentation/pages/DiffRow.stories.tsx
@@ -19,7 +19,7 @@ const meta: Meta = {
     },
     layout: 'padded',
   },
-  title: 'Components/DiffRow',
+  title: 'Components/Diff/DiffRow',
 }
 
 export default meta
diff --git a/frontend/documentation/pages/DiffString.stories.tsx b/frontend/documentation/pages/DiffString.stories.tsx
index 11887cfff4ce..a3e8812f4acd 100644
--- a/frontend/documentation/pages/DiffString.stories.tsx
+++ b/frontend/documentation/pages/DiffString.stories.tsx
@@ -15,7 +15,7 @@ const meta: Meta = {
     },
     layout: 'padded',
   },
-  title: 'Components/DiffString',
+  title: 'Components/Diff/DiffString',
 }
 
 export default meta

From b31cff223e6985d09b0eaa9bed183b156cace54b Mon Sep 17 00:00:00 2001
From: Talisson Costa 
Date: Tue, 1 Sep 2026 11:06:09 -0300
Subject: [PATCH 12/15] refactor(diff): trim the comments and leave the stories
 unsnapshotted

Comments that restated the code are gone; what is left is the cross-file
duplication of the two row values and the .sb-unstyled behaviour.

The diff stories keep Storybook's default of no Chromatic snapshot: the
opt-in list is for components still hand-writing .dark blocks, and these
are tokens-only.

Co-Authored-By: Claude Opus 5 (1M context) 
---
 frontend/.storybook/docs-theme.scss                  | 7 +++----
 frontend/documentation/pages/DiffEnabled.stories.tsx | 8 +-------
 frontend/documentation/pages/DiffRow.stories.tsx     | 9 +--------
 frontend/web/components/diff/DiffFeature.scss        | 1 -
 frontend/web/components/diff/DiffRow/DiffRow.scss    | 7 +------
 frontend/web/components/diff/DiffRow/DiffRow.tsx     | 2 +-
 frontend/web/styles/3rdParty/_react-diff.scss        | 5 ++---
 7 files changed, 9 insertions(+), 30 deletions(-)

diff --git a/frontend/.storybook/docs-theme.scss b/frontend/.storybook/docs-theme.scss
index bbd5f4cc6602..0ae853488a7e 100644
--- a/frontend/.storybook/docs-theme.scss
+++ b/frontend/.storybook/docs-theme.scss
@@ -50,10 +50,9 @@
     color: var(--color-text-default, #ffffff);
   }
 
-  // Storybook marks rendered stories .sb-unstyled so docs typography stays out
-  // of them. The two rules above predate that, and were repainting any 
 a
-  // component renders, e.g. every line of the diff viewer. Skip .hljs, which
-  // highlight.js stamps on blocks that bring their own surface.
+  // Storybook marks rendered stories .sb-unstyled; the two rules above predate
+  // that and were repainting any 
 a component renders. Skip .hljs, which
+  // brings its own surface.
   .sbdocs .sb-unstyled code:not(.hljs),
   .sbdocs .sb-unstyled pre:not(.hljs) {
     background-color: transparent;
diff --git a/frontend/documentation/pages/DiffEnabled.stories.tsx b/frontend/documentation/pages/DiffEnabled.stories.tsx
index 4be9b07b870b..c04d1b053345 100644
--- a/frontend/documentation/pages/DiffEnabled.stories.tsx
+++ b/frontend/documentation/pages/DiffEnabled.stories.tsx
@@ -5,15 +5,10 @@ import DiffEnabled from 'components/diff/DiffEnabled'
 const meta: Meta = {
   component: DiffEnabled,
   parameters: {
-    // Snapshotted in both themes: these rows carry the added/removed colours
-    // for every boolean change in the audit log and change-request review.
-    chromatic: { disableSnapshot: false },
     docs: {
       description: {
         component:
-          'The diff view for a flag or segment override being turned on or ' +
-          'off. Hand-built rather than react-diff-viewer, but shares its ' +
-          'added/removed row colours. Toggle the theme in the toolbar to QA.',
+          'The diff view for a flag or segment override being turned on or off.',
       },
     },
     layout: 'padded',
@@ -33,7 +28,6 @@ export const OnToOff: Story = {
   args: { newValue: false, oldValue: true },
 }
 
-// Unchanged renders a single switch with no diff colours at all.
 export const Unchanged: Story = {
   args: { newValue: true, oldValue: true },
 }
diff --git a/frontend/documentation/pages/DiffRow.stories.tsx b/frontend/documentation/pages/DiffRow.stories.tsx
index bb20ef467d7d..95c3e1c1ca3a 100644
--- a/frontend/documentation/pages/DiffRow.stories.tsx
+++ b/frontend/documentation/pages/DiffRow.stories.tsx
@@ -5,16 +5,11 @@ import DiffRow from 'components/diff/DiffRow'
 const meta: Meta = {
   component: DiffRow,
   parameters: {
-    // Snapshotted in both themes: these rows carry the added/removed colours
-    // for every diff we render ourselves.
-    chromatic: { disableSnapshot: false },
     docs: {
       description: {
         component:
           'One row of a diff: a marker cell beside the content that changed. ' +
-          'Used for the rows we build ourselves, in DiffEnabled and ' +
-          'DiffString. The rows react-diff-viewer renders are styled ' +
-          'separately. Toggle the theme in the toolbar to QA.',
+          'Used by DiffEnabled and DiffString for the rows we build ourselves.',
       },
     },
     layout: 'padded',
@@ -34,12 +29,10 @@ export const Added: Story = {
   args: { children: 'banner_size: large', state: 'added' },
 }
 
-// No marker glyph, and the content sits on the code surface.
 export const Unchanged: Story = {
   args: { children: 'banner_size: large', state: 'unchanged' },
 }
 
-// The pair as DiffEnabled and DiffString render them, one above the other.
 export const Stacked: Story = {
   render: () => (
     <>
diff --git a/frontend/web/components/diff/DiffFeature.scss b/frontend/web/components/diff/DiffFeature.scss
index cf5149d500bb..759aab2a3707 100644
--- a/frontend/web/components/diff/DiffFeature.scss
+++ b/frontend/web/components/diff/DiffFeature.scss
@@ -1,4 +1,3 @@
-// Environment name legend above a feature diff.
 .diff-removed-header,
 .diff-added-header {
   padding: 4px 8px;
diff --git a/frontend/web/components/diff/DiffRow/DiffRow.scss b/frontend/web/components/diff/DiffRow/DiffRow.scss
index f3667fe8b775..e9d7186313c1 100644
--- a/frontend/web/components/diff/DiffRow/DiffRow.scss
+++ b/frontend/web/components/diff/DiffRow/DiffRow.scss
@@ -1,7 +1,4 @@
-// One row of a diff: a marker cell beside the content that changed.
-// Used for the rows we build ourselves, in DiffEnabled and DiffString. The
-// rows react-diff-viewer renders are styled in web/styles/3rdParty/_react-diff.scss,
-// which repeats the two values below; keep the pairs in step.
+// _react-diff.scss repeats these two values for the library's own rows.
 .diff-row {
   --diff-row-removed: oklch(from var(--red-500) l c h / 0.16);
   --diff-row-added: oklch(from var(--green-500) l c h / 0.16);
@@ -48,8 +45,6 @@
     }
   }
 
-  // Unchanged rows keep the gutter for alignment but show no marker glyph, and
-  // the content sits on the code surface rather than a diff colour.
   &--unchanged {
     background-color: var(--color-surface-subtle);
     font-size: 13px;
diff --git a/frontend/web/components/diff/DiffRow/DiffRow.tsx b/frontend/web/components/diff/DiffRow/DiffRow.tsx
index 0b9fd6403c10..7cb7b0879089 100644
--- a/frontend/web/components/diff/DiffRow/DiffRow.tsx
+++ b/frontend/web/components/diff/DiffRow/DiffRow.tsx
@@ -7,7 +7,7 @@ export type DiffRowState = 'added' | 'removed' | 'unchanged'
 type DiffRowProps = {
   state: DiffRowState
   children: ReactNode
-  // Let long content scroll inside the row instead of widening its container.
+  // Scroll long content rather than widening the container.
   scrollable?: boolean
   className?: string
 }
diff --git a/frontend/web/styles/3rdParty/_react-diff.scss b/frontend/web/styles/3rdParty/_react-diff.scss
index ff93788520e4..d7266ddbe961 100644
--- a/frontend/web/styles/3rdParty/_react-diff.scss
+++ b/frontend/web/styles/3rdParty/_react-diff.scss
@@ -7,9 +7,8 @@
 .react-diff-1n5o7vh-diff-container {
   // The diff's two strengths, in one place. A changed row is faint, the words
   // that changed inside it are stronger, which is what makes a diff readable.
-  // See-through layers, so one value works on light and dark alike. The rows
-  // we build ourselves repeat the two row values in DiffRow.scss, so a change
-  // to either belongs in both.
+  // See-through layers, so one value works on light and dark alike.
+  // DiffRow.scss repeats the two row values for the rows we build ourselves.
   --diff-row-removed: oklch(from var(--red-500) l c h / 0.16);
   --diff-row-added: oklch(from var(--green-500) l c h / 0.16);
   --diff-word-removed: oklch(from var(--red-500) l c h / 0.28);

From d526093ce39412ac888dc4c50e8955ac045e2dc2 Mon Sep 17 00:00:00 2001
From: Talisson Costa 
Date: Tue, 1 Sep 2026 11:12:12 -0300
Subject: [PATCH 13/15] refactor(diff): use the token and Bootstrap utilities
 where they exist

Colour, spacing and radius all have utilities, so the header legend keeps
only the two type declarations that have nothing to map to. DiffRow's
unconditional flex layout moves to utilities too.

What stays in SCSS is what a utility cannot express: values a state
modifier overrides, since Bootstrap's utilities are !important, and the
marker's fixed geometry.

Co-Authored-By: Claude Opus 5 (1M context) 
---
 frontend/web/components/diff/DiffFeature.scss | 20 +------------------
 frontend/web/components/diff/DiffFeature.tsx  |  8 ++++----
 .../web/components/diff/DiffRow/DiffRow.scss  | 10 ----------
 .../web/components/diff/DiffRow/DiffRow.tsx   | 11 ++++++----
 4 files changed, 12 insertions(+), 37 deletions(-)

diff --git a/frontend/web/components/diff/DiffFeature.scss b/frontend/web/components/diff/DiffFeature.scss
index 759aab2a3707..c806af33f23a 100644
--- a/frontend/web/components/diff/DiffFeature.scss
+++ b/frontend/web/components/diff/DiffFeature.scss
@@ -1,22 +1,4 @@
-.diff-removed-header,
-.diff-added-header {
-  padding: 4px 8px;
-  border-radius: 4px;
+.diff-header {
   font-size: 13px;
   font-weight: 500;
 }
-
-.diff-removed-header {
-  background-color: var(--color-surface-danger);
-  color: var(--color-text-danger);
-}
-
-.diff-added-header {
-  background-color: var(--color-surface-success);
-  color: var(--color-text-success);
-}
-
-.diff-sign {
-  margin-right: 4px;
-  font-weight: bold;
-}
diff --git a/frontend/web/components/diff/DiffFeature.tsx b/frontend/web/components/diff/DiffFeature.tsx
index 5663268981cb..c27b11c9a67e 100644
--- a/frontend/web/components/diff/DiffFeature.tsx
+++ b/frontend/web/components/diff/DiffFeature.tsx
@@ -105,13 +105,13 @@ const DiffFeature: FC = ({
           {(oldEnvName || newEnvName) && (
             
{oldEnvName && ( - - {oldEnvName} + + {oldEnvName} )} {newEnvName && ( - - + {newEnvName} + + + {newEnvName} )}
diff --git a/frontend/web/components/diff/DiffRow/DiffRow.scss b/frontend/web/components/diff/DiffRow/DiffRow.scss index e9d7186313c1..9867ef14cb86 100644 --- a/frontend/web/components/diff/DiffRow/DiffRow.scss +++ b/frontend/web/components/diff/DiffRow/DiffRow.scss @@ -3,17 +3,11 @@ --diff-row-removed: oklch(from var(--red-500) l c h / 0.16); --diff-row-added: oklch(from var(--green-500) l c h / 0.16); - display: flex; - align-items: center; line-height: 31px; &__marker { - display: flex; width: 31px; height: 31px; - flex: none; - align-items: center; - justify-content: center; background-color: var(--color-surface-subtle); > * { @@ -67,8 +61,4 @@ } } } - - &--scrollable { - overflow: auto; - } } diff --git a/frontend/web/components/diff/DiffRow/DiffRow.tsx b/frontend/web/components/diff/DiffRow/DiffRow.tsx index 7cb7b0879089..24ddc280bbb7 100644 --- a/frontend/web/components/diff/DiffRow/DiffRow.tsx +++ b/frontend/web/components/diff/DiffRow/DiffRow.tsx @@ -25,11 +25,14 @@ const DiffRow: FC = ({ state, }) => (
-
+
{MARKERS[state]}
{children}
From 14cb99a12ef22ca36ee18b3e6a26959f008950f9 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Tue, 1 Sep 2026 13:06:32 -0300 Subject: [PATCH 14/15] refactor(diff): drop two declarations that never applied DiffString passes its pre with d-inline, which is !important, so the display never took effect, and the content cell is not a flex container so the flex value did nothing either. Both were inert on main too. Co-Authored-By: Claude Opus 5 (1M context) --- frontend/web/components/diff/DiffRow/DiffRow.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/web/components/diff/DiffRow/DiffRow.scss b/frontend/web/components/diff/DiffRow/DiffRow.scss index 9867ef14cb86..334a486d1e89 100644 --- a/frontend/web/components/diff/DiffRow/DiffRow.scss +++ b/frontend/web/components/diff/DiffRow/DiffRow.scss @@ -51,8 +51,6 @@ padding-right: 0; pre { - display: flex; - flex: 1; padding-left: 2px; background-color: var(--color-surface-subtle); font-family: var(--bs-font-monospace); From 606de0d612b424acb3ac95f4305587733c3c088b Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Tue, 1 Sep 2026 13:06:33 -0300 Subject: [PATCH 15/15] fix(storybook): keep hljs colours in light docs too The earlier fix only covered the dark block. In light mode `.sbdocs code` still out-specifies `.hljs`, so a Highlight story on the Docs tab showed hardcoded text colour instead of the token. Co-Authored-By: Claude Opus 5 (1M context) --- frontend/.storybook/docs-theme.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/.storybook/docs-theme.scss b/frontend/.storybook/docs-theme.scss index 0ae853488a7e..daf3df0fcee3 100644 --- a/frontend/.storybook/docs-theme.scss +++ b/frontend/.storybook/docs-theme.scss @@ -7,7 +7,7 @@ // ============================================================================= // Light mode — fix invisible inline code -.sbdocs code { +.sbdocs code:not(.hljs) { color: #1a2634; }