diff --git a/apps/storybook/.storybook/main.ts b/apps/storybook/.storybook/main.ts index 9d3eea82e..18aff4c94 100644 --- a/apps/storybook/.storybook/main.ts +++ b/apps/storybook/.storybook/main.ts @@ -150,6 +150,23 @@ const config: StorybookConfig = { return { ...config, + optimizeDeps: { + ...config.optimizeDeps, + include: [ + ...(config.optimizeDeps?.include ?? []), + '@monaco-editor/react', + '@codemirror/lang-javascript', + '@codemirror/language', + '@codemirror/state', + '@codemirror/view', + '@lezer/highlight', + ], + exclude: [ + ...(config.optimizeDeps?.exclude ?? []), + 'monaco-editor', + ], + }, + // biome-ignore lint/suspicious/noExplicitAny: plugins array typed as unknown[] after flat/filter; cast required plugins: [ ...plugins, react({ babel: { plugins: ['@lingui/babel-plugin-lingui-macro'] } }), @@ -159,7 +176,7 @@ const config: StorybookConfig = { // keep resolving as assets. svgr({ include: '**/apollo-react/src/material/**/*.svg' }), tailwindcss(), - ], + ] as any, resolve: { ...config.resolve, alias: mergeAlias(config.resolve?.alias, [ diff --git a/packages/apollo-wind/package.json b/packages/apollo-wind/package.json index 4b1310847..ff4a22fbb 100644 --- a/packages/apollo-wind/package.json +++ b/packages/apollo-wind/package.json @@ -46,6 +46,11 @@ "./postcss": { "import": "./dist/postcss.config.export.js", "require": "./dist/postcss.config.export.cjs" + }, + "./editor-themes": { + "import": "./dist/editor-themes/index.js", + "require": "./dist/editor-themes/index.cjs", + "types": "./dist/editor-themes/index.d.ts" } }, "files": [ @@ -69,7 +74,23 @@ }, "peerDependencies": { "react": ">=18.0.0", - "react-dom": ">=18.0.0" + "react-dom": ">=18.0.0", + "@monaco-editor/react": ">=4.0.0", + "monaco-editor": ">=0.50.0", + "@codemirror/lang-javascript": ">=6.0.0", + "@codemirror/language": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0", + "@lezer/highlight": ">=1.0.0" + }, + "peerDependenciesMeta": { + "@monaco-editor/react": { "optional": true }, + "monaco-editor": { "optional": true }, + "@codemirror/lang-javascript": { "optional": true }, + "@codemirror/language": { "optional": true }, + "@codemirror/state": { "optional": true }, + "@codemirror/view": { "optional": true }, + "@lezer/highlight": { "optional": true } }, "dependencies": { "@dnd-kit/core": "^6.3.1", @@ -129,6 +150,12 @@ "zod": "^4.3.5" }, "devDependencies": { + "@codemirror/lang-javascript": "^6.2.5", + "@codemirror/language": "^6.12.3", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.43.0", + "@lezer/highlight": "^1.2.3", + "@monaco-editor/react": "^4.7.0", "@rsbuild/plugin-react": "^1.4.1", "@rslib/core": "^0.19.6", "@semantic-release/changelog": "^6.0.3", @@ -156,6 +183,7 @@ "globals": "^16.5.0", "jest-axe": "^10.0.0", "jsdom": "^27.2.0", + "monaco-editor": "^0.55.1", "postcss": "^8.5.6", "postcss-import": "^16.1.1", "react": "19.2.3", diff --git a/packages/apollo-wind/src/components/ui/code-block.stories.tsx b/packages/apollo-wind/src/components/ui/code-block.stories.tsx index d7782c3e2..ed81068ef 100644 --- a/packages/apollo-wind/src/components/ui/code-block.stories.tsx +++ b/packages/apollo-wind/src/components/ui/code-block.stories.tsx @@ -1,58 +1,30 @@ -import type { Meta, StoryObj } from '@storybook/react-vite'; -import type { CodeBlockTheme } from './code-block'; -import { CodeBlock } from './code-block'; +import MonacoEditor from '@monaco-editor/react'; +import { javascript } from '@codemirror/lang-javascript'; +import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'; +import { EditorState } from '@codemirror/state'; +import { EditorView, drawSelection, highlightActiveLine, lineNumbers } from '@codemirror/view'; +import { tags as t } from '@lezer/highlight'; +import type { Meta } from '@storybook/react-vite'; +import { useEffect, useRef } from 'react'; +import type { ApolloFutureCodeMirrorTheme } from '../../editor-themes'; +import { + apolloFutureDarkCodeMirror, + apolloFutureDarkMonaco, + apolloFutureLightCodeMirror, + apolloFutureLightMonaco, +} from '../../editor-themes'; // ============================================================================ // Meta // ============================================================================ const meta = { - title: 'Components/Core/Code Block', - component: CodeBlock, - parameters: { - layout: 'padded', - }, - tags: ['autodocs'], - argTypes: { - language: { - control: 'select', - options: [ - 'tsx', - 'typescript', - 'javascript', - 'json', - 'css', - 'html', - 'python', - 'bash', - 'sql', - 'yaml', - 'markdown', - ], - }, - theme: { - control: 'select', - options: [ - 'dark', - 'light', - 'dark-hc', - 'light-hc', - 'future-dark', - 'future-light', - 'wireframe', - 'vertex', - 'canvas', - ], - }, - showLineNumbers: { control: 'boolean' }, - showCopyButton: { control: 'boolean' }, - wrapLines: { control: 'boolean' }, - fileName: { control: 'text' }, - }, -} satisfies Meta; + title: 'Patterns/Code Editors', + tags: ['!autodocs'], + parameters: { layout: 'padded' }, +} satisfies Meta; export default meta; -type Story = StoryObj; // ============================================================================ // Sample code snippets @@ -348,220 +320,529 @@ export function DataTable({ `.trim(); // ============================================================================ -// Default — no `theme` prop: auto-follows the Apollo page theme +// Live editor components + sample code // ============================================================================ -export const Default: Story = { - args: { - children: tsxSample, - language: 'tsx', - showLineNumbers: true, - showCopyButton: true, - }, -}; +const monacoSample = `interface WorkflowNode { + id: string; + type: 'action' | 'condition' | 'trigger'; + executionStatus: 'NotExecuted' | 'InProgress' | 'Completed' | 'Failed'; +} -// ============================================================================ -// With File Name -// ============================================================================ +function getNextNodes(node: WorkflowNode): string[] { + if (node.executionStatus === 'Completed') { + return node.type === 'condition' + ? ['true-branch', 'false-branch'] + : ['next']; + } + return []; +}`.trim(); -export const WithFileName: Story = { - name: 'With File Name', - args: { - children: tsxSample, - language: 'tsx', - fileName: 'UserCard.tsx', - showLineNumbers: true, - showCopyButton: true, - }, -}; +const codemirrorSample = `workflow.status === "active" && user.role !== "viewer" + ? user.displayName + " — " + workflow.name + : "Access restricted"`.trim(); -// ============================================================================ -// No Line Numbers -// ============================================================================ +function LiveMonacoEditor({ variant }: { variant: 'dark' | 'light' }) { + const themeName = `apollo-future-${variant}`; + return ( + { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + monaco.editor.defineTheme('apollo-future-dark', apolloFutureDarkMonaco as any); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + monaco.editor.defineTheme('apollo-future-light', apolloFutureLightMonaco as any); + }} + options={{ + fontSize: 13, + lineHeight: 20, + minimap: { enabled: false }, + scrollBeyondLastLine: false, + wordWrap: 'on', + fontFamily: + 'ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace', + padding: { top: 16, bottom: 16 }, + lineNumbers: 'on', + glyphMargin: false, + folding: false, + renderLineHighlight: 'line', + hideCursorInOverviewRuler: true, + overviewRulerBorder: false, + scrollbar: { vertical: 'auto', horizontal: 'hidden', alwaysConsumeMouseWheel: false }, + automaticLayout: true, + }} + /> + ); +} -export const NoLineNumbers: Story = { - name: 'No Line Numbers', - args: { - children: jsSample, - language: 'javascript', - showLineNumbers: false, - showCopyButton: true, - }, -}; +function buildCMExtensions(tokens: ApolloFutureCodeMirrorTheme, isDark: boolean) { + const { syntax, ui } = tokens; + const theme = EditorView.theme( + { + '&': { + backgroundColor: ui.background, + color: ui.foreground, + fontFamily: + 'ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace', + fontSize: '13px', + lineHeight: '1.6', + }, + '.cm-cursor, .cm-dropCursor': { borderLeftColor: ui.cursor }, + '&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': { + backgroundColor: ui.selection, + }, + '.cm-activeLine': { backgroundColor: ui.lineHighlight }, + '.cm-gutters': { backgroundColor: ui.background, color: ui.lineNumber, border: 'none' }, + '.cm-gutter.cm-lineNumbers .cm-gutterElement': { + color: ui.lineNumber, + paddingLeft: '12px', + paddingRight: '8px', + }, + '.cm-activeLineGutter': { color: ui.lineNumberActive, backgroundColor: 'transparent' }, + '.cm-matchingBracket': { outline: `1px solid ${ui.matchingBracket}`, borderRadius: '2px' }, + '.cm-content': { padding: '12px 0', caretColor: ui.cursor }, + '.cm-line': { padding: '0 16px' }, + '.cm-scroller': { overflow: 'auto' }, + }, + { dark: isDark } + ); -// ============================================================================ -// No Copy Button -// ============================================================================ + const highlight = HighlightStyle.define([ + { tag: t.comment, color: syntax.comment, fontStyle: 'italic' }, + { tag: t.punctuation, color: syntax.punctuation }, + { tag: [t.keyword, t.operatorKeyword], color: syntax.keyword }, + { tag: t.operator, color: syntax.operator }, + { tag: [t.string, t.regexp, t.special(t.string)], color: syntax.string }, + { tag: [t.number, t.integer, t.float], color: syntax.number }, + { tag: [t.bool, t.null], color: syntax.literal }, + { tag: [t.className, t.typeName, t.definition(t.typeName)], color: syntax.literal }, + { tag: [t.propertyName, t.attributeName], color: syntax.keyword }, + { tag: t.function(t.variableName), color: syntax.keyword }, + { tag: t.meta, color: syntax.meta }, + { tag: t.variableName, color: syntax.rest }, + ]); + + return [ + theme, + syntaxHighlighting(highlight), + javascript({ typescript: true }), + lineNumbers(), + highlightActiveLine(), + drawSelection(), + ]; +} -export const NoCopyButton: Story = { - name: 'No Copy Button', - args: { - children: jsonSample, - language: 'json', - showLineNumbers: true, - showCopyButton: false, - }, -}; +function LiveCodeMirrorEditor({ variant }: { variant: 'dark' | 'light' }) { + const containerRef = useRef(null); + const tokens = variant === 'dark' ? apolloFutureDarkCodeMirror : apolloFutureLightCodeMirror; + const borderColor = variant === 'dark' ? '#3f3f46' : '#d4d4d8'; -// ============================================================================ -// Wrap Long Lines -// ============================================================================ + useEffect(() => { + if (!containerRef.current) return; + const view = new EditorView({ + state: EditorState.create({ + doc: codemirrorSample, + extensions: buildCMExtensions(tokens, variant === 'dark'), + }), + parent: containerRef.current, + }); + return () => view.destroy(); + }, [variant, tokens]); -export const WrapLongLines: Story = { - name: 'Wrap Long Lines', - args: { - children: `const result = await someVeryLongFunctionName({ parameterOne: 'value', parameterTwo: 42, parameterThree: true, parameterFour: 'another long value that pushes the line past the viewport width' });`, - language: 'javascript', - showLineNumbers: false, - wrapLines: true, - }, -}; + return ( +
+ ); +} // ============================================================================ -// Languages +// Code Editors — display vs editing, orchestrator pattern, theme adapters // ============================================================================ -export const Languages = { - name: 'Languages', - parameters: { layout: 'padded' }, +const monacoUsageDark = `import { apolloFutureDarkMonaco } from '@uipath/apollo-wind/editor-themes'; +import * as monaco from 'monaco-editor'; + +// Register once at app startup +monaco.editor.defineTheme('apollo-future-dark', apolloFutureDarkMonaco); +monaco.editor.defineTheme('apollo-future-light', apolloFutureLightMonaco); + +// Use in any editor instance +monaco.editor.create(containerEl, { + theme: 'apollo-future-dark', + language: 'typescript', +});`.trim(); + +const codemirrorUsageDark = `import { EditorView } from '@codemirror/view'; +import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'; +import { tags as t } from '@lezer/highlight'; +import { apolloFutureDarkCodeMirror } from '@uipath/apollo-wind/editor-themes'; + +const { syntax, ui } = apolloFutureDarkCodeMirror; + +const theme = EditorView.theme({ + '&': { backgroundColor: ui.background, color: ui.foreground }, + '.cm-cursor': { borderLeftColor: ui.cursor }, + '&.cm-focused .cm-selectionBackground, .cm-selectionBackground': { + backgroundColor: ui.selection, + }, + '.cm-activeLine': { backgroundColor: ui.lineHighlight }, + '.cm-gutters': { backgroundColor: ui.background, borderRight: 'none' }, + '.cm-lineNumbers .cm-gutterElement': { color: ui.lineNumber }, + '.cm-activeLineGutter': { color: ui.lineNumberActive }, + '.cm-matchingBracket': { outline: \`1px solid \${ui.matchingBracket}\` }, +}, { dark: true }); + +const highlight = HighlightStyle.define([ + { tag: t.comment, color: syntax.comment, fontStyle: 'italic' }, + { tag: t.punctuation, color: syntax.punctuation }, + { tag: [t.keyword, t.operator], color: syntax.keyword }, + { tag: [t.string, t.regexp], color: syntax.string }, + { tag: [t.number, t.integer], color: syntax.number }, + { tag: [t.bool, t.null, t.className, t.typeName], color: syntax.literal }, + { tag: [t.propertyName, t.attributeName], color: syntax.keyword }, + { tag: t.meta, color: syntax.meta }, + { tag: t.name, color: syntax.rest }, +]); + +export const apolloFutureDark = [theme, syntaxHighlighting(highlight)];`.trim(); + +const decisionRows = [ + { + useCase: '{{ variable }} single-line interpolation', + solution: 'CodeMirror', + pkg: '@codemirror/*', + notes: 'Lightweight. Ideal for expression fields.', + }, + { + useCase: 'JavaScript / TypeScript expression editing', + solution: 'Monaco', + pkg: '@monaco-editor/react', + notes: 'Full IntelliSense, diagnostics, multi-line.', + }, + { + useCase: 'Multi-line script or complex expression', + solution: 'Monaco', + pkg: '@monaco-editor/react', + notes: 'Use when CodeMirror is insufficient.', + }, +]; + +export const CodeEditors = { + name: 'Overview', + parameters: { layout: 'fullscreen' }, render: () => ( -
-
-

- TypeScript / TSX +

+
+

Code in Apollo

+

+ Apollo provides two editors for code input, each with a distinct role, package weight, + and interaction model. Pick the one that matches the user's intent.

- - {tsxSample.split('\n').slice(0, 8).join('\n')} - +
-
-

- JavaScript +

+ {/* ── Decision table ── */} +

When to use what

+

+ Start here. Pick the solution that matches the user's intent, then see its dedicated page + for live demos and integration guidance.

- - {jsSample} - -
-
-

- JSON -

- - {jsonSample} - -
+
+ + + + + + + + + + + {decisionRows.map((row) => ( + + + + + + + ))} + +
+ Use case + + Solution + PackageNotes
{row.useCase}{row.solution} + {row.pkg} + {row.notes}
+
-
-

- CSS +

+ + {/* ── Orchestrator pattern ── */} +

+ The orchestrator pattern +

+

+ When building expression or script input fields, encapsulate the Monaco/CodeMirror + decision in a single{' '} + + CodeEditorField + {' '} + component. Feature code passes a{' '} + mode and the + orchestrator picks the right editor — keeping the decision in one place and making it easy + to change.

- - {cssSample} - + +
+
+ Single-line + literal +

+ + mode: literal + {' '} + and line count is 1 → render{' '} + + CodeMirrorEditor + + . Lightweight, optimised for{' '} + + {'{{ variable }}'} + {' '} + interpolation. +

+
+
+ Expression mode +

+ + mode: expression + {' '} + → render{' '} + + MonacoEditor + + . Full IntelliSense, type checking, and diagnostics for JS/TS expressions. +

+
+
+ Multi-line +

+ Any mode where line count exceeds 1 → escalate to{' '} + + MonacoEditor + {' '} + regardless of mode. Prevents CodeMirror being used for complex scripts. +

+
+
+
+ ), +}; -
-

- Python +export const MonacoEditorStory = { + name: 'Editor Monaco', + parameters: { layout: 'fullscreen' }, + render: () => ( +

+
+

Monaco Editor

+

+ Full-featured code editor for multi-line TypeScript / JavaScript expressions and scripts. + Provides IntelliSense, syntax diagnostics, bracket matching, and folding. Import{' '} + + apolloFutureDarkMonaco + {' '} + or{' '} + + apolloFutureLightMonaco + {' '} + from{' '} + + @uipath/apollo-wind/editor-themes + {' '} + and register once at app startup.

- - {pythonSample} - +
-
-

- Bash +

+

Preview

+ +
+
+
+ Future Dark + + apollo-future-dark + +
+
+ +
+
+
+
+ Future Light + + apollo-future-light + +
+
+ +
+
+
+ +
+ +

How to use

+

+ Define both themes once before mounting any editor. The theme name is a plain string + reference — you can switch themes at runtime by updating the{' '} + theme option.

- - {bashSample} - +
+          {monacoUsageDark}
+        
+
+ ), +}; -
-

- SQL +export const CodeMirrorEditorStory = { + name: 'Editor CodeMirror', + parameters: { layout: 'fullscreen' }, + render: () => ( +

+
+

+ CodeMirror Editor +

+

+ Lightweight editor for single-line expressions and{' '} + + {'{{ variable }}'} + {' '} + template literal interpolation. Much smaller than Monaco — use it when you need syntax + highlighting and bracket matching without the full IDE runtime. Consume{' '} + + apolloFutureDarkCodeMirror + {' '} + or{' '} + + apolloFutureLightCodeMirror + {' '} + from{' '} + + @uipath/apollo-wind/editor-themes + + .

- - {sqlSample} - +
-
-

- HTML +

+

Preview

+ +
+
+

Future Dark

+ +
+
+

Future Light

+ +
+
+ +
+ +

How to use

+

+ Destructure{' '} + syntax and{' '} + ui from the + token object. Build a{' '} + + HighlightStyle + {' '} + for syntax colours and an{' '} + + EditorView.theme + {' '} + for editor chrome, then combine them into a single extension array.

- - {htmlSample} - +
+          {codemirrorUsageDark}
+        
), }; -// ============================================================================ -// Long Code -// ============================================================================ - -export const LongCode: Story = { - name: 'Long Code', - args: { - children: longSample, - language: 'tsx', - fileName: 'DataTable.tsx', - showLineNumbers: true, - showCopyButton: true, - }, -}; - // ============================================================================ // All Themes // ============================================================================ -const THEME_LABELS: Record = { - // Standard - dark: 'Dark', - light: 'Light', - 'dark-hc': 'High Contrast Dark', - 'light-hc': 'High Contrast Light', - // Future design language - 'future-dark': 'Future: Dark', - 'future-light': 'Future: Light', - wireframe: 'Wireframe', - vertex: 'Vertex', - canvas: 'Canvas', -}; - -const preview = ` -import { useState } from 'react'; - -export function Counter() { - const [count, setCount] = useState(0); - return ( - - ); -} -`.trim(); - export const AllThemes = { - name: 'All Themes', - parameters: { layout: 'padded' }, + name: 'Themes', + parameters: { layout: 'fullscreen' }, render: () => ( -
- {(Object.keys(THEME_LABELS) as CodeBlockTheme[]).map((t) => ( -
-

- {THEME_LABELS[t]} -

- - {preview} - +
+
+

Editor Themes

+

+ Both Monaco and CodeMirror ship with two Apollo themes:{' '} + future-dark{' '} + and{' '} + future-light. + Import them from{' '} + + @uipath/apollo-wind/editor-themes + + . +

+ +
+

Monaco Editor

+
+
+

Future Dark

+
+ +
+
+
+

Future Light

+
+ +
+
+
+
+ +
+ +
+

CodeMirror Editor

+
+
+

Future Dark

+ +
+
+

Future Light

+ +
+
- ))} +
), }; diff --git a/packages/apollo-wind/src/components/ui/code-block.tsx b/packages/apollo-wind/src/components/ui/code-block.tsx deleted file mode 100644 index 18cbf2267..000000000 --- a/packages/apollo-wind/src/components/ui/code-block.tsx +++ /dev/null @@ -1,334 +0,0 @@ -import { Check, Copy } from 'lucide-react'; -import { useEffect, useRef, useState } from 'react'; -import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; -import { - atomDark, - nightOwl, - nord, - oneDark, - oneLight, - prism, - vs, - vscDarkPlus, -} from 'react-syntax-highlighter/dist/esm/styles/prism/index.js'; - -import { Button } from '@/components/ui/button'; -import { cn } from '@/lib'; - -// ============================================================================ -// Types -// ============================================================================ - -/** - * All Apollo theme variants supported by CodeBlock. - * - * Standard (apollo-core): - * - `'dark'` / `'light'` — Default dark / light - * - `'dark-hc'` / `'light-hc'` — High contrast variants - * - * Future / Demo themes: - * - `'future-dark'` / `'future-light'` — Future zinc palette, cyan brand - * - `'wireframe'` — Greyscale / prototyping - * - `'vertex'` — Deep blue-grey, teal brand - * - `'canvas'` — Apollo MUI dark, orange brand - * - * When no theme is passed the component auto-detects from the Apollo - * `` class and switches live when the theme changes. - */ -export type CodeBlockTheme = - | 'dark' - | 'light' - | 'dark-hc' - | 'light-hc' - | 'future-dark' - | 'future-light' - | 'wireframe' - | 'vertex' - | 'canvas'; - -export interface CodeBlockProps { - /** The code string to display */ - children: string; - /** Programming language for syntax highlighting (e.g. 'tsx', 'python', 'sql') */ - language?: string; - /** Show line numbers on the left */ - showLineNumbers?: boolean; - /** Show copy-to-clipboard button in the header */ - showCopyButton?: boolean; - /** Optional file name displayed in the header */ - fileName?: string; - /** - * Color scheme. When omitted the component auto-follows the active Apollo - * page theme by watching the class on `` — switches live. - */ - theme?: CodeBlockTheme; - /** Wrap long lines instead of scrolling horizontally */ - wrapLines?: boolean; - className?: string; -} - -// ============================================================================ -// Per-theme configuration -// ============================================================================ - -interface ThemeConfig { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - prismStyle: Record; - bg: string; - headerBg: string; - labelColor: string; - iconColor: string; - lineNumberColor: string; -} - -const THEME_CONFIG: Record = { - // ── Dark — Nord for the apollo-core blue-grey palette ──────────────────── - dark: { - prismStyle: nord, - bg: '#182027', - headerBg: '#111920', - labelColor: '#8ea1b1', - iconColor: '#6b8899', - lineNumberColor: '#2e3f4c', - }, - // ── Light — VS Code Light on a clean white surface ─────────────────────── - light: { - prismStyle: vs, - bg: '#ffffff', - headerBg: '#f0f4f8', - labelColor: '#374151', - iconColor: '#6b7280', - lineNumberColor: '#c8d4de', - }, - // ── Future dark — VS Code Dark+ for a modern zinc feel ─────────────────── - 'future-dark': { - prismStyle: vscDarkPlus, - bg: 'var(--surface-raised)', - headerBg: '#09090b', - labelColor: '#a1a1aa', - iconColor: '#71717a', - lineNumberColor: '#3f3f46', - }, - // ── Future light — VS Code Light for a clean zinc-50 feel ──────────────── - 'future-light': { - prismStyle: vs, - bg: 'var(--surface-raised)', - headerBg: '#f4f4f5', - labelColor: '#52525b', - iconColor: '#71717a', - lineNumberColor: '#d4d4d8', - }, - // ── Wireframe — classic Prism on grey-50, minimal ──────────────────────── - wireframe: { - prismStyle: prism, - bg: '#f9fafb', - headerBg: '#f3f4f6', - labelColor: '#6b7280', - iconColor: '#9ca3af', - lineNumberColor: '#d1d5db', - }, - // ── Vertex — Night Owl on deep oklch blue-grey, teal brand ─────────────── - vertex: { - prismStyle: nightOwl, - bg: 'oklch(0.21 0.03 258.5)', - headerBg: 'oklch(0.188 0.028 258.5)', - labelColor: '#a6b5c9', - iconColor: '#7a90a8', - lineNumberColor: 'oklch(0.32 0.03 258.5)', - }, - // ── Canvas — Atom Dark for Apollo MUI dark, UiPath orange brand ────────── - canvas: { - prismStyle: atomDark, - bg: '#182027', - headerBg: '#111920', - labelColor: '#8ea1b1', - iconColor: '#6b8899', - lineNumberColor: '#2e3f4c', - }, - // ── High-contrast dark ─────────────────────────────────────────────────── - 'dark-hc': { - prismStyle: oneDark, - bg: '#0a0a0a', - headerBg: '#141414', - labelColor: '#e4e4e4', - iconColor: '#c8c8c8', - lineNumberColor: '#505050', - }, - // ── High-contrast light ────────────────────────────────────────────────── - 'light-hc': { - prismStyle: oneLight, - bg: '#ffffff', - headerBg: '#e8e8e8', - labelColor: '#111827', - iconColor: '#374151', - lineNumberColor: '#9ca3af', - }, -}; - -// ============================================================================ -// Auto-detect Apollo theme from class -// ============================================================================ - -// Check more specific / longer class names before short ones to avoid -// a class like "dark" matching inside "future-dark" -const BODY_CLASS_PRIORITY: CodeBlockTheme[] = [ - 'future-dark', - 'future-light', - 'dark-hc', - 'light-hc', - 'dark', - 'light', - 'wireframe', - 'vertex', - 'canvas', -]; - -function getBodyTheme(): CodeBlockTheme { - if (typeof document === 'undefined') return 'dark'; - const bodyClasses = document.body.classList; - const htmlClasses = document.documentElement.classList; - return ( - BODY_CLASS_PRIORITY.find((t) => bodyClasses.contains(t) || htmlClasses.contains(t)) ?? - 'future-dark' - ); -} - -function useApolloTheme(): CodeBlockTheme { - const [theme, setTheme] = useState(getBodyTheme); - - useEffect(() => { - if (typeof document === 'undefined') { - // In non-browser environments (e.g. SSR), skip observing - return; - } - - const observer = new MutationObserver(() => setTheme(getBodyTheme())); - - // Observe both body and documentElement to catch theme changes on either - const targets: (HTMLElement | null)[] = [document.body, document.documentElement]; - - targets.forEach((target) => { - if (target) { - observer.observe(target, { attributes: true, attributeFilter: ['class'] }); - } - }); - - return () => observer.disconnect(); - }, []); - - return theme; -} - -// ============================================================================ -// CodeBlock -// ============================================================================ - -/** - * Syntax-highlighted code block built on react-syntax-highlighter (Prism engine). - * - * Supports 200+ languages, optional line numbers, a filename header, and a - * one-click copy button. Automatically follows the active Apollo theme by - * watching the body class — or accept an explicit `theme` prop to override. - * - * Supported themes: dark, light, dark-hc, light-hc, future-dark, - * future-light, wireframe, vertex, canvas. - */ -export function CodeBlock({ - children, - language = 'tsx', - showLineNumbers = true, - showCopyButton = true, - fileName, - theme, - wrapLines = false, - className, -}: CodeBlockProps) { - const [copied, setCopied] = useState(false); - const timeoutRef = useRef | null>(null); - - const detectedTheme = useApolloTheme(); - const activeTheme = theme ?? detectedTheme; - const config = THEME_CONFIG[activeTheme]; - - const code = children.trim(); - - async function handleCopy() { - try { - await navigator.clipboard.writeText(code); - setCopied(true); - timeoutRef.current = setTimeout(() => setCopied(false), 1500); - } catch { - // Clipboard API not available — silent fail - } - } - - useEffect(() => { - return () => { - if (timeoutRef.current) clearTimeout(timeoutRef.current); - }; - }, []); - - const showHeader = !!(fileName || language || showCopyButton); - - return ( -
- {/* ── Header ─────────────────────────────────────────────── */} - {showHeader && ( -
- - {fileName ?? language} - - - {showCopyButton && ( - - )} -
- )} - - {/* ── Code ───────────────────────────────────────────────── */} - - {code} - -
- ); -} diff --git a/packages/apollo-wind/src/components/ui/component-gallery.stories.tsx b/packages/apollo-wind/src/components/ui/component-gallery.stories.tsx index 367fd001e..410d7faa0 100644 --- a/packages/apollo-wind/src/components/ui/component-gallery.stories.tsx +++ b/packages/apollo-wind/src/components/ui/component-gallery.stories.tsx @@ -17,7 +17,6 @@ import { Tabs, TabsList, TabsTrigger } from './tabs'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './table'; import { Calendar } from './calendar'; import { ButtonGroup } from './button-group'; -import { CodeBlock } from './code-block'; import { Textarea } from './textarea'; import { Label } from './label'; import { RadioGroup, RadioGroupItem } from './radio-group'; @@ -203,22 +202,6 @@ const components: ComponentInfo[] = [
), }, - { - name: 'Code Block', - description: 'Syntax-highlighted code display', - storyPath: 'wind-components-core-code-block--docs', - category: Category.Core, - preview: ( - - {'const x =