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;
}