From 4167bb439d67f81579b783500a0a5865232a65c1 Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Thu, 3 Apr 2025 13:40:25 -0600 Subject: [PATCH] diff component v1 --- .../themes/github_light_theme.ts | 16 +- .../AppDiffCodeEditor.stories.tsx | 160 ++++++++++++++++++ .../AppDiffCodeEditor/AppDiffCodeEditor.tsx | 138 +++++++++++++++ .../ui/inputs/AppDiffCodeEditor/index.ts | 1 + web/src/components/ui/inputs/index.ts | 1 + 5 files changed, 313 insertions(+), 3 deletions(-) create mode 100644 web/src/components/ui/inputs/AppDiffCodeEditor/AppDiffCodeEditor.stories.tsx create mode 100644 web/src/components/ui/inputs/AppDiffCodeEditor/AppDiffCodeEditor.tsx create mode 100644 web/src/components/ui/inputs/AppDiffCodeEditor/index.ts diff --git a/web/src/components/ui/inputs/AppCodeEditor/themes/github_light_theme.ts b/web/src/components/ui/inputs/AppCodeEditor/themes/github_light_theme.ts index f6c37bc52..15af44d6e 100644 --- a/web/src/components/ui/inputs/AppCodeEditor/themes/github_light_theme.ts +++ b/web/src/components/ui/inputs/AppCodeEditor/themes/github_light_theme.ts @@ -1,7 +1,13 @@ +'use client'; + import type { editor } from 'monaco-editor'; -const editorBackground = '#ffffff'; -const primaryColor = '#7C3AED'; +const editorBackground = getComputedStyle(document.documentElement).getPropertyValue( + '--color-background' +); +const primaryColor = getComputedStyle(document.documentElement).getPropertyValue('--color-primary'); +const borderColor = getComputedStyle(document.documentElement).getPropertyValue('--color-border'); +const textColor = getComputedStyle(document.documentElement).getPropertyValue('--color-foreground'); const theme: editor.IStandaloneThemeData = { base: 'vs', @@ -356,7 +362,11 @@ const theme: editor.IStandaloneThemeData = { 'editorIndentGuide.background': '#959da5', 'editorIndentGuide.activeBackground': '#24292e', 'editor.selectionHighlightBorder': '#fafbfc', - focusBorder: '#00000000' + focusBorder: '#00000000', + 'editorHoverWidget.background': editorBackground, + 'editorHoverWidget.border': borderColor, + 'inputValidation.infoBorder': borderColor, + 'editorHoverWidget.foreground': textColor } }; diff --git a/web/src/components/ui/inputs/AppDiffCodeEditor/AppDiffCodeEditor.stories.tsx b/web/src/components/ui/inputs/AppDiffCodeEditor/AppDiffCodeEditor.stories.tsx new file mode 100644 index 000000000..b0da46089 --- /dev/null +++ b/web/src/components/ui/inputs/AppDiffCodeEditor/AppDiffCodeEditor.stories.tsx @@ -0,0 +1,160 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { AppDiffCodeEditor } from './AppDiffCodeEditor'; + +const meta: Meta = { + title: 'UI/Inputs/AppDiffCodeEditor', + component: AppDiffCodeEditor, + parameters: { + layout: 'centered' + }, + tags: ['autodocs'], + argTypes: { + viewMode: { + control: 'radio', + options: ['side-by-side', 'inline'], + defaultValue: 'side-by-side', + description: 'Controls whether the diff is displayed side-by-side or inline' + } + }, + decorators: [ + (Story) => ( +
+ +
+ ) + ] +}; + +export default meta; +type Story = StoryObj; + +const originalYaml = `# Original YAML configuration +server: + port: 8080 + host: localhost +database: + url: jdbc:mysql://localhost:3306/mydb + username: admin + password: secret +logging: + level: INFO + path: /var/logs`; + +const modifiedYaml = `# Updated YAML configuration +server: + port: 9090 + host: localhost + timeout: 30s +database: + url: jdbc:mysql://localhost:3306/mydb + username: admin + password: secret + pool: + maxConnections: 20 + minIdle: 5 +logging: + level: DEBUG + path: /var/logs/app`; + +const originalSql = `-- Original SQL query +SELECT + customers.id, + customers.name, + orders.order_date +FROM customers +JOIN orders ON customers.id = orders.customer_id +WHERE orders.status = 'completed' +ORDER BY orders.order_date DESC;`; + +const modifiedSql = `-- Updated SQL query +SELECT + customers.id, + customers.name, + customers.email, + orders.order_date, + orders.total_amount +FROM customers +JOIN orders ON customers.id = orders.customer_id +LEFT JOIN order_items ON orders.id = order_items.order_id +WHERE orders.status = 'completed' + AND orders.total_amount > 100 +GROUP BY customers.id +ORDER BY orders.order_date DESC +LIMIT 100;`; + +export const Default: Story = { + args: { + original: originalYaml, + modified: modifiedYaml, + height: '300px', + language: 'yaml', + variant: 'bordered', + viewMode: 'side-by-side' + } +}; + +export const InlineView: Story = { + args: { + original: originalYaml, + modified: modifiedYaml, + height: '300px', + language: 'yaml', + variant: 'bordered', + viewMode: 'inline' + } +}; + +export const SQL: Story = { + args: { + original: originalSql, + modified: modifiedSql, + height: '300px', + language: 'sql', + variant: 'bordered', + viewMode: 'side-by-side' + } +}; + +export const SQLInline: Story = { + args: { + original: originalSql, + modified: modifiedSql, + height: '300px', + language: 'sql', + variant: 'bordered', + viewMode: 'inline' + } +}; + +export const ReadOnly: Story = { + args: { + original: originalYaml, + modified: modifiedYaml, + height: '300px', + language: 'yaml', + readOnly: true, + variant: 'bordered', + readOnlyMessage: 'This is a read-only view', + viewMode: 'side-by-side' + } +}; + +export const TallerView: Story = { + args: { + original: originalSql, + modified: modifiedSql, + height: '500px', + language: 'sql', + variant: 'bordered', + viewMode: 'side-by-side' + } +}; + +export const EmptyEditor: Story = { + args: { + height: '300px', + language: 'yaml', + variant: 'bordered', + viewMode: 'side-by-side' + } +}; diff --git a/web/src/components/ui/inputs/AppDiffCodeEditor/AppDiffCodeEditor.tsx b/web/src/components/ui/inputs/AppDiffCodeEditor/AppDiffCodeEditor.tsx new file mode 100644 index 000000000..476e98687 --- /dev/null +++ b/web/src/components/ui/inputs/AppDiffCodeEditor/AppDiffCodeEditor.tsx @@ -0,0 +1,138 @@ +'use client'; + +import React, { forwardRef, useRef, useMemo } from 'react'; +import { CircleSpinnerLoaderContainer } from '../../loaders/CircleSpinnerLoaderContainer'; +import { useMemoizedFn } from '@/hooks'; +import { cn } from '@/lib/classMerge'; +import type { editor } from 'monaco-editor/esm/vs/editor/editor.api'; +import { DiffEditor } from '@monaco-editor/react'; +import { useTheme } from 'next-themes'; + +export interface AppDiffCodeEditorProps { + className?: string; + height?: string; + isDarkMode?: boolean; + onMount?: (editor: editor.IStandaloneDiffEditor, monaco: typeof import('monaco-editor')) => void; + original?: string; + modified?: string; + onChange?: (value: string) => void; + style?: React.CSSProperties; + language?: string; + readOnly?: boolean; + readOnlyMessage?: string; + monacoEditorOptions?: editor.IStandaloneDiffEditorConstructionOptions; + variant?: 'bordered' | null; + viewMode?: 'side-by-side' | 'inline'; +} + +export interface AppDiffCodeEditorHandle { + resetCodeEditor: () => void; +} + +export const AppDiffCodeEditor = forwardRef( + ( + { + style, + monacoEditorOptions, + language = 'typescript', + className, + readOnly, + onChange, + height = '100%', + isDarkMode, + onMount, + original = '', + modified = '', + readOnlyMessage = 'Editing code is not allowed', + variant, + viewMode = 'side-by-side' + }, + ref + ) => { + const isDarkModeContext = useTheme()?.theme === 'dark'; + const useDarkMode = isDarkMode ?? isDarkModeContext; + + const memoizedMonacoEditorOptions: editor.IStandaloneDiffEditorConstructionOptions = + useMemo(() => { + return { + originalEditable: false, + automaticLayout: true, + readOnly, + renderSideBySide: viewMode === 'side-by-side', + folding: false, + lineDecorationsWidth: 15, + lineNumbersMinChars: 3, + renderOverviewRuler: false, + wordWrap: 'off', + wordWrapColumn: 999, + wrappingStrategy: 'simple', + scrollBeyondLastLine: false, + minimap: { + enabled: false + }, + contextmenu: false, + readOnlyMessage: { + value: readOnlyMessage + }, + ...monacoEditorOptions + }; + }, [readOnlyMessage, monacoEditorOptions, viewMode]); + + const onMountDiffEditor = useMemoizedFn( + async (editor: editor.IStandaloneDiffEditor, monaco: typeof import('monaco-editor')) => { + const [GithubLightTheme, NightOwlTheme] = await Promise.all([ + (await import('../AppCodeEditor/themes/github_light_theme')).default, + (await import('../AppCodeEditor/themes/tomorrow_night_theme')).default + ]); + + monaco.editor.defineTheme('github-light', GithubLightTheme); + monaco.editor.defineTheme('night-owl', NightOwlTheme); + + // Apply theme to diff editor + const theme = useDarkMode ? 'night-owl' : 'github-light'; + monaco.editor.setTheme(theme); + + console.log('theme', theme, GithubLightTheme); + + // Get the modified editor and add change listener + const modifiedEditor = editor.getModifiedEditor(); + if (!readOnly) { + modifiedEditor.onDidChangeModelContent(() => { + onChange?.(modifiedEditor.getValue() || ''); + }); + } + + onMount?.(editor, monaco); + } + ); + + return ( +
+ } + language={language} + className={className} + original={original} + modified={modified} + theme={useDarkMode ? 'night-owl' : 'github-light'} + onMount={onMountDiffEditor} + options={memoizedMonacoEditorOptions} + /> +
+ ); + } +); +AppDiffCodeEditor.displayName = 'AppDiffCodeEditor'; + +const LoadingContainer = React.memo(() => { + return ; +}); +LoadingContainer.displayName = 'LoadingContainer'; diff --git a/web/src/components/ui/inputs/AppDiffCodeEditor/index.ts b/web/src/components/ui/inputs/AppDiffCodeEditor/index.ts new file mode 100644 index 000000000..0bf2fb406 --- /dev/null +++ b/web/src/components/ui/inputs/AppDiffCodeEditor/index.ts @@ -0,0 +1 @@ +export { AppDiffCodeEditor } from './AppDiffCodeEditor'; diff --git a/web/src/components/ui/inputs/index.ts b/web/src/components/ui/inputs/index.ts index 9efb46543..256c98676 100644 --- a/web/src/components/ui/inputs/index.ts +++ b/web/src/components/ui/inputs/index.ts @@ -1,2 +1,3 @@ export * from './Input'; export * from './InputNumber'; +export * from './AppDiffCodeEditor';