mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-21 04:41:16 -05:00
Refactor web components
This commit is contained in:
119
webview-ui/src/components/common/CodeAccordian.tsx
Normal file
119
webview-ui/src/components/common/CodeAccordian.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { memo, useMemo } from "react"
|
||||
import { getLanguageFromPath } from "../../utils/getLanguageFromPath"
|
||||
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "./CodeBlock"
|
||||
|
||||
interface CodeAccordianProps {
|
||||
code?: string
|
||||
diff?: string
|
||||
language?: string | undefined
|
||||
path?: string
|
||||
isFeedback?: boolean
|
||||
isConsoleLogs?: boolean
|
||||
isExpanded: boolean
|
||||
onToggleExpand: () => void
|
||||
}
|
||||
|
||||
/*
|
||||
We need to remove leading non-alphanumeric characters from the path in order for our leading ellipses trick to work.
|
||||
^: Anchors the match to the start of the string.
|
||||
[^a-zA-Z0-9]+: Matches one or more characters that are not alphanumeric.
|
||||
The replace method removes these matched characters, effectively trimming the string up to the first alphanumeric character.
|
||||
*/
|
||||
export const removeLeadingNonAlphanumeric = (path: string): string => path.replace(/^[^a-zA-Z0-9]+/, "")
|
||||
|
||||
const CodeAccordian = ({
|
||||
code,
|
||||
diff,
|
||||
language,
|
||||
path,
|
||||
isFeedback,
|
||||
isConsoleLogs,
|
||||
isExpanded,
|
||||
onToggleExpand,
|
||||
}: CodeAccordianProps) => {
|
||||
const inferredLanguage = useMemo(
|
||||
() => code && (language ?? (path ? getLanguageFromPath(path) : undefined)),
|
||||
[path, language, code]
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 3,
|
||||
backgroundColor: CODE_BLOCK_BG_COLOR,
|
||||
overflow: "hidden", // This ensures the inner scrollable area doesn't overflow the rounded corners
|
||||
border: "1px solid var(--vscode-editorGroup-border)",
|
||||
}}>
|
||||
{(path || isFeedback || isConsoleLogs) && (
|
||||
<div
|
||||
style={{
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: "9px 10px",
|
||||
cursor: "pointer",
|
||||
userSelect: "none",
|
||||
WebkitUserSelect: "none",
|
||||
MozUserSelect: "none",
|
||||
msUserSelect: "none",
|
||||
}}
|
||||
onClick={onToggleExpand}>
|
||||
{isFeedback || isConsoleLogs ? (
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<span
|
||||
className={`codicon codicon-${isFeedback ? "feedback" : "output"}`}
|
||||
style={{ marginRight: "6px" }}></span>
|
||||
<span
|
||||
style={{
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
marginRight: "8px",
|
||||
}}>
|
||||
{isFeedback ? "User Edits" : "Console Logs"}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{path?.startsWith(".") && <span>.</span>}
|
||||
<span
|
||||
style={{
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
marginRight: "8px",
|
||||
// trick to get ellipsis at beginning of string
|
||||
direction: "rtl",
|
||||
textAlign: "left",
|
||||
}}>
|
||||
{removeLeadingNonAlphanumeric(path ?? "") + "\u200E"}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<div style={{ flexGrow: 1 }}></div>
|
||||
<span className={`codicon codicon-chevron-${isExpanded ? "up" : "down"}`}></span>
|
||||
</div>
|
||||
)}
|
||||
{(!(path || isFeedback || isConsoleLogs) || isExpanded) && (
|
||||
<div
|
||||
//className="code-block-scrollable" this doesn't seem to be necessary anymore, on silicon macs it shows the native mac scrollbar instead of the vscode styled one
|
||||
style={{
|
||||
overflowX: "auto",
|
||||
overflowY: "hidden",
|
||||
maxWidth: "100%",
|
||||
}}>
|
||||
<CodeBlock
|
||||
source={`${"```"}${diff !== undefined ? "diff" : inferredLanguage}\n${(
|
||||
code ??
|
||||
diff ??
|
||||
""
|
||||
).trim()}\n${"```"}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// memo does shallow comparison of props, so if you need it to re-render when a nested object changes, you need to pass a custom comparison function
|
||||
export default memo(CodeAccordian)
|
||||
148
webview-ui/src/components/common/CodeBlock.tsx
Normal file
148
webview-ui/src/components/common/CodeBlock.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import { memo, useEffect } from "react"
|
||||
import { useRemark } from "react-remark"
|
||||
import rehypeHighlight, { Options } from "rehype-highlight"
|
||||
import styled from "styled-components"
|
||||
import { visit } from "unist-util-visit"
|
||||
import { useExtensionState } from "../context/ExtensionStateContext"
|
||||
|
||||
export const CODE_BLOCK_BG_COLOR = "var(--vscode-editor-background, --vscode-sideBar-background, rgb(30 30 30))"
|
||||
|
||||
/*
|
||||
overflowX: auto + inner div with padding results in an issue where the top/left/bottom padding renders but the right padding inside does not count as overflow as the width of the element is not exceeded. Once the inner div is outside the boundaries of the parent it counts as overflow.
|
||||
https://stackoverflow.com/questions/60778406/why-is-padding-right-clipped-with-overflowscroll/77292459#77292459
|
||||
this fixes the issue of right padding clipped off
|
||||
“ideal” size in a given axis when given infinite available space--allows the syntax highlighter to grow to largest possible width including its padding
|
||||
minWidth: "max-content",
|
||||
*/
|
||||
|
||||
interface CodeBlockProps {
|
||||
source?: string
|
||||
forceWrap?: boolean
|
||||
}
|
||||
|
||||
const StyledMarkdown = styled.div<{ forceWrap: boolean }>`
|
||||
${({ forceWrap }) =>
|
||||
forceWrap &&
|
||||
`
|
||||
pre, code {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
`}
|
||||
|
||||
pre {
|
||||
background-color: ${CODE_BLOCK_BG_COLOR};
|
||||
border-radius: 5px;
|
||||
margin: 0;
|
||||
min-width: ${({ forceWrap }) => (forceWrap ? "auto" : "max-content")};
|
||||
padding: 10px 10px;
|
||||
}
|
||||
|
||||
pre > code {
|
||||
.hljs-deletion {
|
||||
background-color: var(--vscode-diffEditor-removedTextBackground);
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
.hljs-addition {
|
||||
background-color: var(--vscode-diffEditor-insertedTextBackground);
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
span.line:empty {
|
||||
display: none;
|
||||
}
|
||||
word-wrap: break-word;
|
||||
border-radius: 5px;
|
||||
background-color: ${CODE_BLOCK_BG_COLOR};
|
||||
font-size: var(--vscode-editor-font-size, var(--vscode-font-size, 12px));
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
}
|
||||
|
||||
code:not(pre > code) {
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
color: #f78383;
|
||||
}
|
||||
|
||||
background-color: ${CODE_BLOCK_BG_COLOR};
|
||||
font-family: var(--vscode-font-family), system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||
font-size: var(--vscode-editor-font-size, var(--vscode-font-size, 12px));
|
||||
color: var(--vscode-editor-foreground, #fff);
|
||||
|
||||
p,
|
||||
li,
|
||||
ol,
|
||||
ul {
|
||||
line-height: 1.5;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledPre = styled.pre<{ theme: any }>`
|
||||
& .hljs {
|
||||
color: var(--vscode-editor-foreground, #fff);
|
||||
}
|
||||
|
||||
${(props) =>
|
||||
Object.keys(props.theme)
|
||||
.map((key, index) => {
|
||||
return `
|
||||
& ${key} {
|
||||
color: ${props.theme[key]};
|
||||
}
|
||||
`
|
||||
})
|
||||
.join("")}
|
||||
`
|
||||
|
||||
const CodeBlock = memo(({ source, forceWrap = false }: CodeBlockProps) => {
|
||||
const { theme } = useExtensionState()
|
||||
const [reactContent, setMarkdownSource] = useRemark({
|
||||
remarkPlugins: [
|
||||
() => {
|
||||
return (tree) => {
|
||||
visit(tree, "code", (node: any) => {
|
||||
if (!node.lang) {
|
||||
node.lang = "javascript"
|
||||
} else if (node.lang.includes(".")) {
|
||||
// if the langauge is a file, get the extension
|
||||
node.lang = node.lang.split(".").slice(-1)[0]
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
],
|
||||
rehypePlugins: [
|
||||
rehypeHighlight as any,
|
||||
{
|
||||
// languages: {},
|
||||
} as Options,
|
||||
],
|
||||
rehypeReactOptions: {
|
||||
components: {
|
||||
pre: ({ node, ...preProps }: any) => <StyledPre {...preProps} theme={theme} />,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setMarkdownSource(source || "")
|
||||
}, [source, setMarkdownSource, theme])
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
overflowY: forceWrap ? "visible" : "auto",
|
||||
maxHeight: forceWrap ? "none" : "100%",
|
||||
backgroundColor: CODE_BLOCK_BG_COLOR,
|
||||
}}>
|
||||
<StyledMarkdown forceWrap={forceWrap}>{reactContent}</StyledMarkdown>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default CodeBlock
|
||||
131
webview-ui/src/components/common/Demo.tsx
Normal file
131
webview-ui/src/components/common/Demo.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
|
||||
import {
|
||||
VSCodeBadge,
|
||||
VSCodeButton,
|
||||
VSCodeCheckbox,
|
||||
VSCodeDataGrid,
|
||||
VSCodeDataGridCell,
|
||||
VSCodeDataGridRow,
|
||||
VSCodeDivider,
|
||||
VSCodeDropdown,
|
||||
VSCodeLink,
|
||||
VSCodeOption,
|
||||
VSCodePanels,
|
||||
VSCodePanelTab,
|
||||
VSCodePanelView,
|
||||
VSCodeProgressRing,
|
||||
VSCodeRadio,
|
||||
VSCodeRadioGroup,
|
||||
VSCodeTag,
|
||||
VSCodeTextArea,
|
||||
VSCodeTextField,
|
||||
} from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
function Demo() {
|
||||
// function handleHowdyClick() {
|
||||
// vscode.postMessage({
|
||||
// command: "hello",
|
||||
// text: "Hey there partner! 🤠",
|
||||
// })
|
||||
// }
|
||||
|
||||
const rowData = [
|
||||
{
|
||||
cell1: "Cell Data",
|
||||
cell2: "Cell Data",
|
||||
cell3: "Cell Data",
|
||||
cell4: "Cell Data",
|
||||
},
|
||||
{
|
||||
cell1: "Cell Data",
|
||||
cell2: "Cell Data",
|
||||
cell3: "Cell Data",
|
||||
cell4: "Cell Data",
|
||||
},
|
||||
{
|
||||
cell1: "Cell Data",
|
||||
cell2: "Cell Data",
|
||||
cell3: "Cell Data",
|
||||
cell4: "Cell Data",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<main>
|
||||
<h1>Hello World!</h1>
|
||||
<VSCodeButton>Howdy!</VSCodeButton>
|
||||
|
||||
<div className="grid gap-3 p-2 place-items-start">
|
||||
<VSCodeDataGrid>
|
||||
<VSCodeDataGridRow row-type="header">
|
||||
<VSCodeDataGridCell cell-type="columnheader" grid-column="1">
|
||||
A Custom Header Title
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell cell-type="columnheader" grid-column="2">
|
||||
Another Custom Title
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell cell-type="columnheader" grid-column="3">
|
||||
Title Is Custom
|
||||
</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell cell-type="columnheader" grid-column="4">
|
||||
Custom Title
|
||||
</VSCodeDataGridCell>
|
||||
</VSCodeDataGridRow>
|
||||
{rowData.map((row, index) => (
|
||||
<VSCodeDataGridRow key={index}>
|
||||
<VSCodeDataGridCell grid-column="1">{row.cell1}</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell grid-column="2">{row.cell2}</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell grid-column="3">{row.cell3}</VSCodeDataGridCell>
|
||||
<VSCodeDataGridCell grid-column="4">{row.cell4}</VSCodeDataGridCell>
|
||||
</VSCodeDataGridRow>
|
||||
))}
|
||||
</VSCodeDataGrid>
|
||||
|
||||
<VSCodeTextField>
|
||||
<section slot="end" style={{ display: "flex", alignItems: "center" }}>
|
||||
<VSCodeButton appearance="icon" aria-label="Match Case">
|
||||
<span className="codicon codicon-case-sensitive"></span>
|
||||
</VSCodeButton>
|
||||
<VSCodeButton appearance="icon" aria-label="Match Whole Word">
|
||||
<span className="codicon codicon-whole-word"></span>
|
||||
</VSCodeButton>
|
||||
<VSCodeButton appearance="icon" aria-label="Use Regular Expression">
|
||||
<span className="codicon codicon-regex"></span>
|
||||
</VSCodeButton>
|
||||
</section>
|
||||
</VSCodeTextField>
|
||||
<span slot="end" className="codicon codicon-chevron-right"></span>
|
||||
|
||||
<span className="flex gap-3">
|
||||
<VSCodeProgressRing />
|
||||
<VSCodeTextField />
|
||||
<VSCodeButton>Add</VSCodeButton>
|
||||
<VSCodeButton appearance="secondary">Remove</VSCodeButton>
|
||||
</span>
|
||||
|
||||
<VSCodeBadge>Badge</VSCodeBadge>
|
||||
<VSCodeCheckbox>Checkbox</VSCodeCheckbox>
|
||||
<VSCodeDivider />
|
||||
<VSCodeDropdown>
|
||||
<VSCodeOption>Option 1</VSCodeOption>
|
||||
<VSCodeOption>Option 2</VSCodeOption>
|
||||
</VSCodeDropdown>
|
||||
<VSCodeLink href="#">Link</VSCodeLink>
|
||||
<VSCodePanels>
|
||||
<VSCodePanelTab id="tab-1">Tab 1</VSCodePanelTab>
|
||||
<VSCodePanelTab id="tab-2">Tab 2</VSCodePanelTab>
|
||||
<VSCodePanelView id="view-1">Panel View 1</VSCodePanelView>
|
||||
<VSCodePanelView id="view-2">Panel View 2</VSCodePanelView>
|
||||
</VSCodePanels>
|
||||
<VSCodeRadioGroup>
|
||||
<VSCodeRadio>Radio 1</VSCodeRadio>
|
||||
<VSCodeRadio>Radio 2</VSCodeRadio>
|
||||
</VSCodeRadioGroup>
|
||||
<VSCodeTag>Tag</VSCodeTag>
|
||||
<VSCodeTextArea placeholder="Text Area" />
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default Demo
|
||||
98
webview-ui/src/components/common/Thumbnails.tsx
Normal file
98
webview-ui/src/components/common/Thumbnails.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import React, { useState, useRef, useLayoutEffect, memo } from "react"
|
||||
import { useWindowSize } from "react-use"
|
||||
import { vscode } from "../utils/vscode"
|
||||
|
||||
interface ThumbnailsProps {
|
||||
images: string[]
|
||||
style?: React.CSSProperties
|
||||
setImages?: React.Dispatch<React.SetStateAction<string[]>>
|
||||
onHeightChange?: (height: number) => void
|
||||
}
|
||||
|
||||
const Thumbnails = ({ images, style, setImages, onHeightChange }: ThumbnailsProps) => {
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const { width } = useWindowSize()
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (containerRef.current) {
|
||||
let height = containerRef.current.clientHeight
|
||||
// some browsers return 0 for clientHeight
|
||||
if (!height) {
|
||||
height = containerRef.current.getBoundingClientRect().height
|
||||
}
|
||||
onHeightChange?.(height)
|
||||
}
|
||||
setHoveredIndex(null)
|
||||
}, [images, width, onHeightChange])
|
||||
|
||||
const handleDelete = (index: number) => {
|
||||
setImages?.((prevImages) => prevImages.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
const isDeletable = setImages !== undefined
|
||||
|
||||
const handleImageClick = (image: string) => {
|
||||
vscode.postMessage({ type: "openImage", text: image })
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: 5,
|
||||
rowGap: 3,
|
||||
...style,
|
||||
}}>
|
||||
{images.map((image, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{ position: "relative" }}
|
||||
onMouseEnter={() => setHoveredIndex(index)}
|
||||
onMouseLeave={() => setHoveredIndex(null)}>
|
||||
<img
|
||||
src={image}
|
||||
alt={`Thumbnail ${index + 1}`}
|
||||
style={{
|
||||
width: 34,
|
||||
height: 34,
|
||||
objectFit: "cover",
|
||||
borderRadius: 4,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => handleImageClick(image)}
|
||||
/>
|
||||
{isDeletable && hoveredIndex === index && (
|
||||
<div
|
||||
onClick={() => handleDelete(index)}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: -4,
|
||||
right: -4,
|
||||
width: 13,
|
||||
height: 13,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "var(--vscode-badge-background)",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
cursor: "pointer",
|
||||
}}>
|
||||
<span
|
||||
className="codicon codicon-close"
|
||||
style={{
|
||||
color: "var(--vscode-foreground)",
|
||||
fontSize: 10,
|
||||
fontWeight: "bold",
|
||||
}}></span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Thumbnails)
|
||||
23
webview-ui/src/components/common/VSCodeButtonLink.tsx
Normal file
23
webview-ui/src/components/common/VSCodeButtonLink.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from "react"
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
interface VSCodeButtonLinkProps {
|
||||
href: string
|
||||
children: React.ReactNode
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
const VSCodeButtonLink: React.FC<VSCodeButtonLinkProps> = ({ href, children, ...props }) => {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
style={{
|
||||
textDecoration: "none",
|
||||
color: "inherit",
|
||||
}}>
|
||||
<VSCodeButton {...props}>{children}</VSCodeButton>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
export default VSCodeButtonLink
|
||||
Reference in New Issue
Block a user