diff --git a/frontend/.storybook/preview.js b/frontend/.storybook/preview.js index 54f8d7f7c031..29065d615a96 100644 --- a/frontend/.storybook/preview.js +++ b/frontend/.storybook/preview.js @@ -55,6 +55,8 @@ global.Select = (props) => window.Tooltip = Tooltip window.Row = Row window.FormGroup = FormGroup +// isMobile is set at app boot; stub it so components that read it render in Storybook. +window.isMobile = false /** @type { import('storybook').Preview } */ const preview = { diff --git a/frontend/common/theme/tokens.json b/frontend/common/theme/tokens.json index 7004ce34a887..fcad1cb37f7d 100644 --- a/frontend/common/theme/tokens.json +++ b/frontend/common/theme/tokens.json @@ -110,6 +110,17 @@ "warning": { "cssVar": "--color-text-warning", "light": "#ff9f43", "dark": "#ff9f43" }, "info": { "cssVar": "--color-text-info", "light": "#0aaddf", "dark": "#0aaddf" } }, + "code": { + "text": { "cssVar": "--color-code-text", "light": "#2d3443", "dark": "#9da4ae" }, + "comment": { "cssVar": "--color-code-comment", "light": "#9da4ae", "dark": "#656d7b" }, + "keyword": { "cssVar": "--color-code-keyword", "light": "#6837fc", "dark": "#906af6" }, + "name": { "cssVar": "--color-code-name", "light": "#ef4d56", "dark": "#f57c78" }, + "literal": { "cssVar": "--color-code-literal", "light": "#0b8bb2", "dark": "#45bce0" }, + "string": { "cssVar": "--color-code-string", "light": "#27ab95", "dark": "#56ccad" }, + "variable": { "cssVar": "--color-code-variable", "light": "#d06907", "dark": "#efb47c" }, + "title": { "cssVar": "--color-code-title", "light": "#0aaddf", "dark": "#7ecde2" }, + "builtin": { "cssVar": "--color-code-builtin", "light": "#d4b050", "dark": "#f9dc80" } + }, "border": { "default": { "cssVar": "--color-border-default", "light": "rgba(101, 109, 123, 0.16)", "dark": "rgba(255, 255, 255, 0.16)" }, "strong": { "cssVar": "--color-border-strong", "light": "rgba(101, 109, 123, 0.24)", "dark": "rgba(255, 255, 255, 0.24)" }, diff --git a/frontend/common/theme/tokens.ts b/frontend/common/theme/tokens.ts index 2b8839e6feaa..7397d1972cfa 100644 --- a/frontend/common/theme/tokens.ts +++ b/frontend/common/theme/tokens.ts @@ -148,6 +148,17 @@ export const colorBorderStrong = export const colorBorderSuccess = 'var(--color-border-success, #27ab95)' export const colorBorderWarning = 'var(--color-border-warning, #ff9f43)' +// Code +export const colorCodeBuiltin = 'var(--color-code-builtin, #d4b050)' +export const colorCodeComment = 'var(--color-code-comment, #9da4ae)' +export const colorCodeKeyword = 'var(--color-code-keyword, #6837fc)' +export const colorCodeLiteral = 'var(--color-code-literal, #0b8bb2)' +export const colorCodeName = 'var(--color-code-name, #ef4d56)' +export const colorCodeString = 'var(--color-code-string, #27ab95)' +export const colorCodeText = 'var(--color-code-text, #2d3443)' +export const colorCodeTitle = 'var(--color-code-title, #0aaddf)' +export const colorCodeVariable = 'var(--color-code-variable, #d06907)' + // Icon export const colorIconAction = 'var(--color-icon-action, #6837fc)' export const colorIconDanger = 'var(--color-icon-danger, #ef4d56)' diff --git a/frontend/documentation/SemanticTokens.stories.tsx b/frontend/documentation/SemanticTokens.stories.tsx index 825481d4eaf6..d378dc4e52d1 100644 --- a/frontend/documentation/SemanticTokens.stories.tsx +++ b/frontend/documentation/SemanticTokens.stories.tsx @@ -22,6 +22,7 @@ const TOKEN_PREFIX = '--color-' const GROUP_LABELS: Record = { border: 'Border', brand: 'Brand', + code: 'Code', danger: 'Danger', icon: 'Icon', info: 'Info', @@ -83,6 +84,7 @@ function readTokens(): TokenGroupData[] { 'brand', 'surface', 'text', + 'code', 'border', 'icon', 'danger', diff --git a/frontend/documentation/TokenReference.generated.stories.tsx b/frontend/documentation/TokenReference.generated.stories.tsx index 15927ca8228f..97c711495103 100644 --- a/frontend/documentation/TokenReference.generated.stories.tsx +++ b/frontend/documentation/TokenReference.generated.stories.tsx @@ -234,6 +234,89 @@ export const AllTokens: StoryObj = { +

Colour: code

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

Colour: border

diff --git a/frontend/documentation/pages/Highlight.stories.tsx b/frontend/documentation/pages/Highlight.stories.tsx new file mode 100644 index 000000000000..9d1d5866d76b --- /dev/null +++ b/frontend/documentation/pages/Highlight.stories.tsx @@ -0,0 +1,93 @@ +import type { Meta, StoryObj } from 'storybook' + +import Highlight from 'components/Highlight' + +const meta: Meta = { + component: Highlight, + parameters: { + docs: { + description: { + component: + 'The code wrapper: renders a `
` block and runs ' +
+          'highlight.js over it. This is what the shared code theme styles (surface, ' +
+          'border, syntax palette). Toggle the theme in the toolbar to QA light vs dark; ' +
+          '`className` is the highlight.js language.',
+      },
+    },
+    layout: 'padded',
+  },
+  title: 'Components/Highlight',
+}
+
+export default meta
+
+type Story = StoryObj
+
+const js = `// Initialise the client
+import flagsmith from 'flagsmith'
+
+flagsmith.init({
+  environmentID: 'YOUR_ENVIRONMENT_KEY',
+  onChange: () => {
+    const bannerSize = flagsmith.getValue('banner_size')
+  },
+})`
+
+const ts = `import { useFlags } from 'flagsmith/react'
+
+type FlagName = 'banner_size' | 'my_feature'
+
+const flags = useFlags(['banner_size'])
+const size: number = flags.banner_size.value ?? 0`
+
+const json = `{
+  "flags": [
+    { "feature": "banner_size", "enabled": true, "value": 42 },
+    { "feature": "my_feature", "enabled": false, "value": null }
+  ]
+}`
+
+const yaml = `flags:
+  - feature: banner_size
+    enabled: true
+    value: 42`
+
+// 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/Highlight.js b/frontend/web/components/Highlight.js
index b0323c96b117..0473eb163003 100644
--- a/frontend/web/components/Highlight.js
+++ b/frontend/web/components/Highlight.js
@@ -103,12 +103,20 @@ class Highlight extends React.Component {
     this.props.onBlur?.()
   }
 
+  // The value to render before escaping: the live edit while focused, the
+  // current value when there's content, otherwise a disabled/empty placeholder.
+  getRawHtml = () => {
+    if (this.state.focus) return this.state.value
+    if (this.props.children) return { ...this.state.value }
+    return this.props.disabled ? defaultDisabledValue : defaultValue
+  }
+
   render() {
     const {
       children,
       className,
-      disabled,
       element: Element,
+      embedded,
       innerHTML,
     } = this.props
     const props = { className, ref: this.setEl }
@@ -125,23 +133,8 @@ class Highlight extends React.Component {
       return {children}
     }
 
-    const html = this.props.preventEscape
-      ? this.state.focus
-        ? this.state.value
-        : this.props.children
-        ? { ...this.state.value }
-        : disabled
-        ? defaultDisabledValue
-        : defaultValue
-      : escapeHtml(
-          this.state.focus
-            ? this.state.value
-            : this.props.children
-            ? { ...this.state.value }
-            : disabled
-            ? defaultDisabledValue
-            : defaultValue,
-        )
+    const raw = this.getRawHtml()
+    const html = this.props.preventEscape ? raw : escapeHtml(raw)
     return (
       
 = ({
           
         
       
- + {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;