Refactor web components

This commit is contained in:
Saoud Rizwan
2024-09-24 11:54:19 -04:00
parent 40f7942801
commit 6fe9ed22b0
18 changed files with 24 additions and 24 deletions

View 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)

View 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

View 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

View 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)

View 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