= {
+ 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`
+
+// 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 },
+}
+
+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 },
+}
+
+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: {
+ children: js,
+ className: 'javascript',
+ embedded: true,
+ forceExpanded: true,
+ },
+}
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 = ({
/>
{
+ 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 = escapeHtml(raw)
return (
= ({ code, language = 'bash' }) => (
-
+
{code}
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..2cda10cb75c7 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,94 @@
}
.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-deletion,
+ .hljs-subst {
+ color: var(--color-code-name);
+ }
-.hljs-keyword,
-.hljs-selector-tag,
-.hljs-section,
-.hljs-attribute,
-.hljs-name,
-.hljs-variable {
- color: $warning;
-}
+ // 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-params {
- color: #b9b9b9;
-}
+ .hljs-literal {
+ color: var(--color-code-literal);
+ }
-.hljs-string {
- color: $success;
-}
+ .hljs-string,
+ .hljs-regexp,
+ .hljs-addition,
+ .hljs-attribute,
+ .hljs-meta .hljs-string {
+ color: var(--color-code-string);
+ }
+
+ .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 +297,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 +337,26 @@ 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;
+ // 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,
+ &:hover::-webkit-scrollbar {
+ display: block !important;
+ }
}
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);
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;