diff component v1

This commit is contained in:
Nate Kelley 2025-04-03 13:40:25 -06:00
parent c76bd59622
commit 4167bb439d
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
5 changed files with 313 additions and 3 deletions

View File

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

View File

@ -0,0 +1,160 @@
import type { Meta, StoryObj } from '@storybook/react';
import { AppDiffCodeEditor } from './AppDiffCodeEditor';
const meta: Meta<typeof AppDiffCodeEditor> = {
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) => (
<div className="min-h-[500px] min-w-[1000px]">
<Story />
</div>
)
]
};
export default meta;
type Story = StoryObj<typeof AppDiffCodeEditor>;
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'
}
};

View File

@ -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<AppDiffCodeEditorHandle, AppDiffCodeEditorProps>(
(
{
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 (
<div
className={cn(
'app-diff-code-editor relative h-full w-full',
variant === 'bordered' && 'overflow-hidden border',
className
)}
style={style}>
<DiffEditor
key={`${useDarkMode ? 'dark' : 'light'}-${viewMode}`}
height={height}
loading={<LoadingContainer />}
language={language}
className={className}
original={original}
modified={modified}
theme={useDarkMode ? 'night-owl' : 'github-light'}
onMount={onMountDiffEditor}
options={memoizedMonacoEditorOptions}
/>
</div>
);
}
);
AppDiffCodeEditor.displayName = 'AppDiffCodeEditor';
const LoadingContainer = React.memo(() => {
return <CircleSpinnerLoaderContainer className="animate-in fade-in-0 duration-300" />;
});
LoadingContainer.displayName = 'LoadingContainer';

View File

@ -0,0 +1 @@
export { AppDiffCodeEditor } from './AppDiffCodeEditor';

View File

@ -1,2 +1,3 @@
export * from './Input';
export * from './InputNumber';
export * from './AppDiffCodeEditor';