diff --git a/frontend/.storybook/docs-theme.scss b/frontend/.storybook/docs-theme.scss index a684067b05ec..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; } @@ -50,6 +50,15 @@ color: var(--color-text-default, #ffffff); } + // 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;
+    color: inherit;
+  }
+
   // Storybook Source/code block container and all its children
   .docblock-source,
   .docblock-source > * {
diff --git a/frontend/documentation/pages/DiffEnabled.stories.tsx b/frontend/documentation/pages/DiffEnabled.stories.tsx
new file mode 100644
index 000000000000..c04d1b053345
--- /dev/null
+++ b/frontend/documentation/pages/DiffEnabled.stories.tsx
@@ -0,0 +1,33 @@
+import type { Meta, StoryObj } from 'storybook'
+
+import DiffEnabled from 'components/diff/DiffEnabled'
+
+const meta: Meta = {
+  component: DiffEnabled,
+  parameters: {
+    docs: {
+      description: {
+        component:
+          'The diff view for a flag or segment override being turned on or off.',
+      },
+    },
+    layout: 'padded',
+  },
+  title: 'Components/Diff/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 },
+}
+
+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
new file mode 100644
index 000000000000..95c3e1c1ca3a
--- /dev/null
+++ b/frontend/documentation/pages/DiffRow.stories.tsx
@@ -0,0 +1,51 @@
+import type { Meta, StoryObj } from 'storybook'
+
+import DiffRow from 'components/diff/DiffRow'
+
+const meta: Meta = {
+  component: DiffRow,
+  parameters: {
+    docs: {
+      description: {
+        component:
+          'One row of a diff: a marker cell beside the content that changed. ' +
+          'Used by DiffEnabled and DiffString for the rows we build ourselves.',
+      },
+    },
+    layout: 'padded',
+  },
+  title: 'Components/Diff/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' },
+}
+
+export const Unchanged: Story = {
+  args: { children: 'banner_size: large', state: 'unchanged' },
+}
+
+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/documentation/pages/DiffString.stories.tsx b/frontend/documentation/pages/DiffString.stories.tsx
new file mode 100644
index 000000000000..a3e8812f4acd
--- /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/Diff/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: '' },
+}
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/DiffFeature.scss b/frontend/web/components/diff/DiffFeature.scss new file mode 100644 index 000000000000..c806af33f23a --- /dev/null +++ b/frontend/web/components/diff/DiffFeature.scss @@ -0,0 +1,4 @@ +.diff-header { + font-size: 13px; + font-weight: 500; +} diff --git a/frontend/web/components/diff/DiffFeature.tsx b/frontend/web/components/diff/DiffFeature.tsx index 6d265c08a234..c27b11c9a67e 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' @@ -104,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 new file mode 100644 index 000000000000..334a486d1e89 --- /dev/null +++ b/frontend/web/components/diff/DiffRow/DiffRow.scss @@ -0,0 +1,62 @@ +// _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); + + line-height: 31px; + + &__marker { + width: 31px; + height: 31px; + 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 { + background-color: var(--color-surface-subtle); + font-size: 13px; + + .diff-row__marker { + opacity: 0; + } + + .diff-row__content { + padding-right: 0; + + pre { + padding-left: 2px; + background-color: var(--color-surface-subtle); + font-family: var(--bs-font-monospace); + font-size: 13px; + line-height: 28px; + } + } + } +} diff --git a/frontend/web/components/diff/DiffRow/DiffRow.tsx b/frontend/web/components/diff/DiffRow/DiffRow.tsx new file mode 100644 index 000000000000..24ddc280bbb7 --- /dev/null +++ b/frontend/web/components/diff/DiffRow/DiffRow.tsx @@ -0,0 +1,42 @@ +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 + // Scroll long content rather than widening the 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' 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/_hljs.scss b/frontend/web/styles/3rdParty/_hljs.scss index d7027cdcd371..ed70b1de7a0d 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; + } } } @@ -294,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); @@ -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(); diff --git a/frontend/web/styles/3rdParty/_react-diff.scss b/frontend/web/styles/3rdParty/_react-diff.scss index 36e814cf063a..d7266ddbe961 100644 --- a/frontend/web/styles/3rdParty/_react-diff.scss +++ b/frontend/web/styles/3rdParty/_react-diff.scss @@ -1,121 +1,71 @@ -@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; + // 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. + // 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); + --diff-word-added: oklch(from var(--green-500) l c h / 0.28); + background-color: transparent !important; } + +// Prism's JSON tokens, on the same palette as highlight.js in _hljs.scss. .token.property { - color: $success; -} -.token.number,.token.boolean { - color: $info; + color: var(--color-code-variable); } -.react-diff-1igno8l-gutter,.react-diff-1wdmqof-code-fold { - background-color: $black-alpha-8 !important; +.token.number, +.token.boolean, +.token.null { + color: var(--color-code-literal); } -.react-diff-1989f1g-code-fold-gutter { - background-color: $black-alpha-8 !important; +.token.string { + color: var(--color-code-string); } -.react-diff-same { - font-size: 13px; - background-color: rgba(0, 0, 0, 0.08); - .react-diff-marker { - opacity: 0; - } - pre { - padding-left: 2px; - } +.token.punctuation, +.token.operator { + color: var(--color-code-text); } -.react-diff-marker { - background-color: $black-alpha-8; - 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-1igno8l-gutter, +.react-diff-1wdmqof-code-fold { + background-color: var(--color-surface-subtle) !important; } -.react-diff-line pre { - background-color: $black-alpha-8; - display: flex; - flex:1; - line-height: 28px; - font-family: $font-family-code; - font-size: 13px !important; +.react-diff-1989f1g-code-fold-gutter { + background-color: var(--color-surface-subtle) !important; } .react-diff-42iver-word-diff { - padding:0 !important; + padding: 0 !important; +} + +.react-diff-1fxlvce-diff-removed { + background-color: var(--diff-row-removed) !important; +} +.react-diff-1fqrsd-diff-added { + background-color: var(--diff-row-added) !important; } .react-diff-1p2tkx4-marker.react-diff-1fxlvce-diff-removed { - background-color: $danger-alfa-16 !important; + background-color: var(--diff-word-removed) !important; } .react-diff-1p2tkx4-marker.react-diff-1fqrsd-diff-added { - background-color: $success-alfa-16 !important; + background-color: var(--diff-word-added) !important; } .react-diff-hf3w1f-word-removed { - background-color: $danger-alfa-16 !important; + background-color: var(--diff-word-removed) !important; } .react-diff-1u4zuq6-word-added { - background-color: $success-alfa-16 !important; + background-color: var(--diff-word-added) !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; - } -} - -// 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; + color: var(--color-text-default) !important; }