mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-21 09:44:21 +01:00
wip: desktop work
This commit is contained in:
@@ -1,140 +1,150 @@
|
|||||||
import { type FileContents, FileDiff, type DiffLineAnnotation } from "@pierre/precision-diffs"
|
import { type FileContents, FileDiff, type DiffLineAnnotation, DiffFileRendererOptions } from "@pierre/precision-diffs"
|
||||||
|
import { ComponentProps, createEffect, splitProps } from "solid-js"
|
||||||
|
|
||||||
export interface DiffProps {
|
export type DiffProps<T = {}> = Omit<DiffFileRendererOptions<T>, "themes"> & {
|
||||||
before: FileContents
|
before: FileContents
|
||||||
after: FileContents
|
after: FileContents
|
||||||
|
annotations?: DiffLineAnnotation<T>[]
|
||||||
|
class?: string
|
||||||
|
classList?: ComponentProps<"div">["classList"]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Diff(props: DiffProps) {
|
// interface ThreadMetadata {
|
||||||
|
// threadId: string
|
||||||
|
// }
|
||||||
|
|
||||||
|
export function Diff<T>(props: DiffProps<T>) {
|
||||||
let container!: HTMLDivElement
|
let container!: HTMLDivElement
|
||||||
|
const [local, others] = splitProps(props, ["before", "after", "class", "classList", "annotations"])
|
||||||
|
|
||||||
console.log(props)
|
// const lineAnnotations: DiffLineAnnotation<ThreadMetadata>[] = [
|
||||||
|
// {
|
||||||
interface ThreadMetadata {
|
// side: "additions",
|
||||||
threadId: string
|
// // The line number specified for an annotation is the visual line number
|
||||||
}
|
// // you see in the number column of a diff
|
||||||
|
// lineNumber: 16,
|
||||||
const lineAnnotations: DiffLineAnnotation<ThreadMetadata>[] = [
|
// metadata: { threadId: "68b329da9893e34099c7d8ad5cb9c940" },
|
||||||
{
|
// },
|
||||||
side: "additions",
|
// ]
|
||||||
// The line number specified for an annotation is the visual line number
|
|
||||||
// you see in the number column of a diff
|
|
||||||
lineNumber: 16,
|
|
||||||
metadata: { threadId: "68b329da9893e34099c7d8ad5cb9c940" },
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const instance = new FileDiff<ThreadMetadata>({
|
|
||||||
// You can provide a 'theme' prop that maps to any
|
|
||||||
// built in shiki theme or you can register a custom
|
|
||||||
// theme. We also include 2 custom themes
|
|
||||||
//
|
|
||||||
// 'pierre-night' and 'pierre-light
|
|
||||||
//
|
|
||||||
// For the rest of the available shiki themes, check out:
|
|
||||||
// https://shiki.style/themes
|
|
||||||
theme: "none",
|
|
||||||
// Or can also provide a 'themes' prop, which allows the code to adapt
|
|
||||||
// to your OS light or dark theme
|
|
||||||
// themes: { dark: 'pierre-night', light: 'pierre-light' },
|
|
||||||
// When using the 'themes' prop, 'themeType' allows you to force 'dark'
|
|
||||||
// or 'light' theme, or inherit from the OS ('system') theme.
|
|
||||||
themeType: "system",
|
|
||||||
// Disable the line numbers for your diffs, generally not recommended
|
|
||||||
disableLineNumbers: false,
|
|
||||||
// Whether code should 'wrap' with long lines or 'scroll'.
|
|
||||||
overflow: "scroll",
|
|
||||||
// Normally you shouldn't need this prop, but if you don't provide a
|
|
||||||
// valid filename or your file doesn't have an extension you may want to
|
|
||||||
// override the automatic detection. You can specify that language here:
|
|
||||||
// https://shiki.style/languages
|
|
||||||
// lang?: SupportedLanguages;
|
|
||||||
// 'diffStyle' controls whether the diff is presented side by side or
|
|
||||||
// in a unified (single column) view
|
|
||||||
diffStyle: "split",
|
|
||||||
// Line decorators to help highlight changes.
|
|
||||||
// 'bars' (default):
|
|
||||||
// Shows some red-ish or green-ish (theme dependent) bars on the left
|
|
||||||
// edge of relevant lines
|
|
||||||
//
|
|
||||||
// 'classic':
|
|
||||||
// shows '+' characters on additions and '-' characters on deletions
|
|
||||||
//
|
|
||||||
// 'none':
|
|
||||||
// No special diff indicators are shown
|
|
||||||
diffIndicators: "bars",
|
|
||||||
// By default green-ish or red-ish background are shown on added and
|
|
||||||
// deleted lines respectively. Disable that feature here
|
|
||||||
disableBackground: false,
|
|
||||||
// Diffs are split up into hunks, this setting customizes what to show
|
|
||||||
// between each hunk.
|
|
||||||
//
|
|
||||||
// 'line-info' (default):
|
|
||||||
// Shows a bar that tells you how many lines are collapsed. If you are
|
|
||||||
// using the oldFile/newFile API then you can click those bars to
|
|
||||||
// expand the content between them
|
|
||||||
//
|
|
||||||
// 'metadata':
|
|
||||||
// Shows the content you'd see in a normal patch file, usually in some
|
|
||||||
// format like '@@ -60,6 +60,22 @@'. You cannot use these to expand
|
|
||||||
// hidden content
|
|
||||||
//
|
|
||||||
// 'simple':
|
|
||||||
// Just a subtle bar separator between each hunk
|
|
||||||
hunkSeparators: "line-info",
|
|
||||||
// On lines that have both additions and deletions, we can run a
|
|
||||||
// separate diff check to mark parts of the lines that change.
|
|
||||||
// 'none':
|
|
||||||
// Do not show these secondary highlights
|
|
||||||
//
|
|
||||||
// 'char':
|
|
||||||
// Show changes at a per character granularity
|
|
||||||
//
|
|
||||||
// 'word':
|
|
||||||
// Show changes but rounded up to word boundaries
|
|
||||||
//
|
|
||||||
// 'word-alt' (default):
|
|
||||||
// Similar to 'word', however we attempt to minimize single character
|
|
||||||
// gaps between highlighted changes
|
|
||||||
lineDiffType: "word-alt",
|
|
||||||
// If lines exceed these character lengths then we won't perform the
|
|
||||||
// line lineDiffType check
|
|
||||||
maxLineDiffLength: 1000,
|
|
||||||
// If any line in the diff exceeds this value then we won't attempt to
|
|
||||||
// syntax highlight the diff
|
|
||||||
maxLineLengthForHighlighting: 1000,
|
|
||||||
// Enabling this property will hide the file header with file name and
|
|
||||||
// diff stats.
|
|
||||||
disableFileHeader: false,
|
|
||||||
// You can optionally pass a render function for rendering out line
|
|
||||||
// annotations. Just return the dom node to render
|
|
||||||
renderAnnotation(annotation: DiffLineAnnotation<ThreadMetadata>): HTMLElement {
|
|
||||||
// Despite the diff itself being rendered in the shadow dom,
|
|
||||||
// annotations are inserted via the web components 'slots' api and you
|
|
||||||
// can use all your normal normal css and styling for them
|
|
||||||
const element = document.createElement("div")
|
|
||||||
element.innerText = annotation.metadata.threadId
|
|
||||||
return element
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// If you ever want to update the options for an instance, simple call
|
// If you ever want to update the options for an instance, simple call
|
||||||
// 'setOptions' with the new options. Bear in mind, this does NOT merge
|
// 'setOptions' with the new options. Bear in mind, this does NOT merge
|
||||||
// existing properties, it's a full replace
|
// existing properties, it's a full replace
|
||||||
instance.setOptions({
|
// instance.setOptions({
|
||||||
...instance.options,
|
// ...instance.options,
|
||||||
theme: "pierre-dark",
|
// theme: "pierre-dark",
|
||||||
themes: undefined,
|
// themes: undefined,
|
||||||
})
|
// })
|
||||||
|
|
||||||
// When ready to render, simply call .render with old/new file, optional
|
// When ready to render, simply call .render with old/new file, optional
|
||||||
// annotations and a container element to hold the diff
|
// annotations and a container element to hold the diff
|
||||||
instance.render({
|
createEffect(() => {
|
||||||
oldFile: props.before,
|
const instance = new FileDiff<T>({
|
||||||
newFile: props.after,
|
theme: "pierre-light",
|
||||||
lineAnnotations,
|
// Or can also provide a 'themes' prop, which allows the code to adapt
|
||||||
containerWrapper: container,
|
// to your OS light or dark theme
|
||||||
|
// themes: { dark: 'pierre-night', light: 'pierre-light' },
|
||||||
|
// When using the 'themes' prop, 'themeType' allows you to force 'dark'
|
||||||
|
// or 'light' theme, or inherit from the OS ('system') theme.
|
||||||
|
themeType: "system",
|
||||||
|
// Disable the line numbers for your diffs, generally not recommended
|
||||||
|
disableLineNumbers: false,
|
||||||
|
// Whether code should 'wrap' with long lines or 'scroll'.
|
||||||
|
overflow: "scroll",
|
||||||
|
// Normally you shouldn't need this prop, but if you don't provide a
|
||||||
|
// valid filename or your file doesn't have an extension you may want to
|
||||||
|
// override the automatic detection. You can specify that language here:
|
||||||
|
// https://shiki.style/languages
|
||||||
|
// lang?: SupportedLanguages;
|
||||||
|
// 'diffStyle' controls whether the diff is presented side by side or
|
||||||
|
// in a unified (single column) view
|
||||||
|
diffStyle: "unified",
|
||||||
|
// Line decorators to help highlight changes.
|
||||||
|
// 'bars' (default):
|
||||||
|
// Shows some red-ish or green-ish (theme dependent) bars on the left
|
||||||
|
// edge of relevant lines
|
||||||
|
//
|
||||||
|
// 'classic':
|
||||||
|
// shows '+' characters on additions and '-' characters on deletions
|
||||||
|
//
|
||||||
|
// 'none':
|
||||||
|
// No special diff indicators are shown
|
||||||
|
diffIndicators: "bars",
|
||||||
|
// By default green-ish or red-ish background are shown on added and
|
||||||
|
// deleted lines respectively. Disable that feature here
|
||||||
|
disableBackground: false,
|
||||||
|
// Diffs are split up into hunks, this setting customizes what to show
|
||||||
|
// between each hunk.
|
||||||
|
//
|
||||||
|
// 'line-info' (default):
|
||||||
|
// Shows a bar that tells you how many lines are collapsed. If you are
|
||||||
|
// using the oldFile/newFile API then you can click those bars to
|
||||||
|
// expand the content between them
|
||||||
|
//
|
||||||
|
// 'metadata':
|
||||||
|
// Shows the content you'd see in a normal patch file, usually in some
|
||||||
|
// format like '@@ -60,6 +60,22 @@'. You cannot use these to expand
|
||||||
|
// hidden content
|
||||||
|
//
|
||||||
|
// 'simple':
|
||||||
|
// Just a subtle bar separator between each hunk
|
||||||
|
hunkSeparators: "line-info",
|
||||||
|
// On lines that have both additions and deletions, we can run a
|
||||||
|
// separate diff check to mark parts of the lines that change.
|
||||||
|
// 'none':
|
||||||
|
// Do not show these secondary highlights
|
||||||
|
//
|
||||||
|
// 'char':
|
||||||
|
// Show changes at a per character granularity
|
||||||
|
//
|
||||||
|
// 'word':
|
||||||
|
// Show changes but rounded up to word boundaries
|
||||||
|
//
|
||||||
|
// 'word-alt' (default):
|
||||||
|
// Similar to 'word', however we attempt to minimize single character
|
||||||
|
// gaps between highlighted changes
|
||||||
|
lineDiffType: "word-alt",
|
||||||
|
// If lines exceed these character lengths then we won't perform the
|
||||||
|
// line lineDiffType check
|
||||||
|
maxLineDiffLength: 1000,
|
||||||
|
// If any line in the diff exceeds this value then we won't attempt to
|
||||||
|
// syntax highlight the diff
|
||||||
|
maxLineLengthForHighlighting: 1000,
|
||||||
|
// Enabling this property will hide the file header with file name and
|
||||||
|
// diff stats.
|
||||||
|
disableFileHeader: true,
|
||||||
|
// You can optionally pass a render function for rendering out line
|
||||||
|
// annotations. Just return the dom node to render
|
||||||
|
// renderAnnotation(annotation: DiffLineAnnotation<T>): HTMLElement {
|
||||||
|
// // Despite the diff itself being rendered in the shadow dom,
|
||||||
|
// // annotations are inserted via the web components 'slots' api and you
|
||||||
|
// // can use all your normal normal css and styling for them
|
||||||
|
// const element = document.createElement("div")
|
||||||
|
// element.innerText = annotation.metadata.threadId
|
||||||
|
// return element
|
||||||
|
// },
|
||||||
|
...others,
|
||||||
|
})
|
||||||
|
|
||||||
|
instance.render({
|
||||||
|
oldFile: local.before,
|
||||||
|
newFile: local.after,
|
||||||
|
lineAnnotations: local.annotations,
|
||||||
|
containerWrapper: container,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return <div ref={container} />
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
"--pjs-font-family": "var(--font-family-mono)",
|
||||||
|
"--pjs-font-size": "var(--font-size-small)",
|
||||||
|
"--pjs-line-height": "24px",
|
||||||
|
"--pjs-tab-size": 4,
|
||||||
|
"--pjs-font-features": "var(--font-family-mono--font-feature-settings)",
|
||||||
|
"--pjs-header-font-family": "var(--font-family-sans)",
|
||||||
|
}}
|
||||||
|
ref={container}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Button, Icon, IconButton, Select, SelectDialog } from "@opencode-ai/ui"
|
import { Button, Icon, IconButton, Select, SelectDialog } from "@opencode-ai/ui"
|
||||||
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
||||||
import { createEffect, on, Component, createMemo, Show, For } from "solid-js"
|
import { createEffect, on, Component, createMemo, Show, For, onMount, onCleanup } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { FileIcon } from "@/ui"
|
import { FileIcon } from "@/ui"
|
||||||
import { getDirectory, getFilename } from "@/utils"
|
import { getDirectory, getFilename } from "@/utils"
|
||||||
@@ -46,6 +46,21 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const isEmpty = createMemo(() => isEqual(store.contentParts, defaultParts))
|
const isEmpty = createMemo(() => isEqual(store.contentParts, defaultParts))
|
||||||
const isFocused = createFocusSignal(() => editorRef)
|
const isFocused = createFocusSignal(() => editorRef)
|
||||||
|
|
||||||
|
const handlePaste = (event: ClipboardEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
// @ts-expect-error
|
||||||
|
const plainText = (event.clipboardData || window.clipboardData)?.getData("text/plain") ?? ""
|
||||||
|
addPart({ type: "text", content: plainText })
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
editorRef.addEventListener("paste", handlePaste)
|
||||||
|
})
|
||||||
|
onCleanup(() => {
|
||||||
|
editorRef.removeEventListener("paste", handlePaste)
|
||||||
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (isFocused()) {
|
if (isFocused()) {
|
||||||
handleInput()
|
handleInput()
|
||||||
@@ -144,16 +159,27 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const rawText = store.contentParts.map((p) => p.content).join("")
|
const rawText = store.contentParts.map((p) => p.content).join("")
|
||||||
const textBeforeCursor = rawText.substring(0, cursorPosition)
|
const textBeforeCursor = rawText.substring(0, cursorPosition)
|
||||||
const atMatch = textBeforeCursor.match(/@(\S*)$/)
|
const atMatch = textBeforeCursor.match(/@(\S*)$/)
|
||||||
if (!atMatch) return
|
|
||||||
|
|
||||||
const startIndex = atMatch.index!
|
const startIndex = atMatch ? atMatch.index! : cursorPosition
|
||||||
const endIndex = cursorPosition
|
const endIndex = atMatch ? cursorPosition : cursorPosition
|
||||||
|
|
||||||
|
const pushText = (acc: { parts: ContentPart[] }, value: string) => {
|
||||||
|
if (!value) return
|
||||||
|
const last = acc.parts[acc.parts.length - 1]
|
||||||
|
if (last && last.type === "text") {
|
||||||
|
acc.parts[acc.parts.length - 1] = {
|
||||||
|
type: "text",
|
||||||
|
content: last.content + value,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
acc.parts.push({ type: "text", content: value })
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
parts: nextParts,
|
parts: nextParts,
|
||||||
cursorIndex,
|
|
||||||
cursorOffset,
|
|
||||||
inserted,
|
inserted,
|
||||||
|
cursorPositionAfter,
|
||||||
} = store.contentParts.reduce(
|
} = store.contentParts.reduce(
|
||||||
(acc, item) => {
|
(acc, item) => {
|
||||||
if (acc.inserted) {
|
if (acc.inserted) {
|
||||||
@@ -180,16 +206,22 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const head = item.content.slice(0, headLength)
|
const head = item.content.slice(0, headLength)
|
||||||
const tail = item.content.slice(tailLength)
|
const tail = item.content.slice(tailLength)
|
||||||
|
|
||||||
if (head) acc.parts.push({ type: "text", content: head })
|
pushText(acc, head)
|
||||||
|
|
||||||
acc.parts.push(part)
|
if (part.type === "text") {
|
||||||
|
pushText(acc, part.content)
|
||||||
const rest = /^\s/.test(tail) ? tail : ` ${tail}`
|
|
||||||
if (rest) {
|
|
||||||
acc.cursorIndex = acc.parts.length
|
|
||||||
acc.cursorOffset = Math.min(1, rest.length)
|
|
||||||
acc.parts.push({ type: "text", content: rest })
|
|
||||||
}
|
}
|
||||||
|
if (part.type !== "text") {
|
||||||
|
acc.parts.push({ ...part })
|
||||||
|
}
|
||||||
|
|
||||||
|
const needsGap = Boolean(atMatch)
|
||||||
|
const rest = needsGap ? (tail ? (/^\s/.test(tail) ? tail : ` ${tail}`) : " ") : tail
|
||||||
|
pushText(acc, rest)
|
||||||
|
|
||||||
|
const baseCursor = startIndex + part.content.length
|
||||||
|
const cursorAddition = needsGap && rest.length > 0 ? 1 : 0
|
||||||
|
acc.cursorPositionAfter = baseCursor + cursorAddition
|
||||||
|
|
||||||
acc.inserted = true
|
acc.inserted = true
|
||||||
acc.runningIndex = nextIndex
|
acc.runningIndex = nextIndex
|
||||||
@@ -199,29 +231,27 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
parts: [] as ContentPart[],
|
parts: [] as ContentPart[],
|
||||||
runningIndex: 0,
|
runningIndex: 0,
|
||||||
inserted: false,
|
inserted: false,
|
||||||
cursorIndex: null as number | null,
|
cursorPositionAfter: cursorPosition + part.content.length,
|
||||||
cursorOffset: 0,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!inserted || cursorIndex === null) return
|
if (!inserted) {
|
||||||
|
const baseParts = store.contentParts.filter((item) => !(item.type === "text" && item.content === ""))
|
||||||
|
const appendedAcc = { parts: [...baseParts] as ContentPart[] }
|
||||||
|
if (part.type === "text") pushText(appendedAcc, part.content)
|
||||||
|
if (part.type !== "text") appendedAcc.parts.push({ ...part })
|
||||||
|
const next = appendedAcc.parts.length > 0 ? appendedAcc.parts : defaultParts
|
||||||
|
setStore("contentParts", next)
|
||||||
|
setStore("popoverIsOpen", false)
|
||||||
|
const nextCursor = rawText.length + part.content.length
|
||||||
|
queueMicrotask(() => setCursorPosition(editorRef, nextCursor))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setStore("contentParts", nextParts)
|
setStore("contentParts", nextParts)
|
||||||
setStore("popoverIsOpen", false)
|
setStore("popoverIsOpen", false)
|
||||||
|
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => setCursorPosition(editorRef, cursorPositionAfter))
|
||||||
const node = editorRef.childNodes[cursorIndex]
|
|
||||||
if (node && node.nodeType === Node.TEXT_NODE) {
|
|
||||||
const range = document.createRange()
|
|
||||||
const selection = window.getSelection()
|
|
||||||
const length = node.textContent ? node.textContent.length : 0
|
|
||||||
const offset = cursorOffset > length ? length : cursorOffset
|
|
||||||
range.setStart(node, offset)
|
|
||||||
range.collapse(true)
|
|
||||||
selection?.removeAllRanges()
|
|
||||||
selection?.addRange(range)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Button, List, SelectDialog, Tooltip, IconButton, Tabs, Icon } from "@opencode-ai/ui"
|
import { Button, List, SelectDialog, Tooltip, IconButton, Tabs, Icon, Accordion } from "@opencode-ai/ui"
|
||||||
import { FileIcon } from "@/ui"
|
import { FileIcon } from "@/ui"
|
||||||
import FileTree from "@/components/file-tree"
|
import FileTree from "@/components/file-tree"
|
||||||
import { For, onCleanup, onMount, Show, Match, Switch, createSignal, createEffect, createMemo } from "solid-js"
|
import { For, onCleanup, onMount, Show, Match, Switch, createSignal, createEffect, createMemo } from "solid-js"
|
||||||
@@ -55,7 +55,6 @@ export default function Page() {
|
|||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.getModifierState(MOD) && event.shiftKey && event.key.toLowerCase() === "p") {
|
if (event.getModifierState(MOD) && event.shiftKey && event.key.toLowerCase() === "p") {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
// TODO: command palette
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event.getModifierState(MOD) && event.key.toLowerCase() === "p") {
|
if (event.getModifierState(MOD) && event.key.toLowerCase() === "p") {
|
||||||
@@ -571,7 +570,6 @@ export default function Page() {
|
|||||||
<div class="flex flex-col items-start gap-50 pb-[800px]">
|
<div class="flex flex-col items-start gap-50 pb-[800px]">
|
||||||
<For each={local.session.userMessages()}>
|
<For each={local.session.userMessages()}>
|
||||||
{(message) => {
|
{(message) => {
|
||||||
console.log(message)
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-message={message.id}
|
data-message={message.id}
|
||||||
@@ -583,22 +581,55 @@ export default function Page() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="text-14-regular text-text-base">{message.summary?.text}</div>
|
<div class="text-14-regular text-text-base">{message.summary?.text}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="">
|
<Show when={message.summary?.diffs.length}>
|
||||||
<For each={message.summary?.diffs}>
|
<Accordion class="w-full" multiple>
|
||||||
{(diff) => (
|
<For each={message.summary?.diffs || []}>
|
||||||
<Diff
|
{(diff) => (
|
||||||
before={{
|
<Accordion.Item value={diff.file}>
|
||||||
name: diff.file!,
|
<Accordion.Header>
|
||||||
contents: diff.before!,
|
<Accordion.Trigger>
|
||||||
}}
|
<div class="flex items-center justify-between w-full">
|
||||||
after={{
|
<div class="flex items-center gap-5">
|
||||||
name: diff.file!,
|
<FileIcon
|
||||||
contents: diff.after!,
|
node={{ path: diff.file, type: "file" }}
|
||||||
}}
|
class="shrink-0 size-4"
|
||||||
/>
|
/>
|
||||||
)}
|
<div class="flex">
|
||||||
</For>
|
<Show when={diff.file.includes("/")}>
|
||||||
</div>
|
<span class="text-text-base">
|
||||||
|
{getDirectory(diff.file)}/
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
<span class="text-text-strong">{getFilename(diff.file)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-4 items-center justify-end">
|
||||||
|
<div class="flex gap-2 justify-end items-center">
|
||||||
|
<span class="text-12-mono text-right text-text-diff-add-base">{`+${diff.additions}`}</span>
|
||||||
|
<span class="text-12-mono text-right text-text-diff-delete-base">{`-${diff.deletions}`}</span>
|
||||||
|
</div>
|
||||||
|
<Icon name="chevron-grabber-vertical" size="small" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Accordion.Trigger>
|
||||||
|
</Accordion.Header>
|
||||||
|
<Accordion.Content>
|
||||||
|
<Diff
|
||||||
|
before={{
|
||||||
|
name: diff.file!,
|
||||||
|
contents: diff.before!,
|
||||||
|
}}
|
||||||
|
after={{
|
||||||
|
name: diff.file!,
|
||||||
|
contents: diff.after!,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Accordion.Content>
|
||||||
|
</Accordion.Item>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</Accordion>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
|||||||
102
packages/ui/src/components/accordion.css
Normal file
102
packages/ui/src/components/accordion.css
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
[data-component="accordion"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0px;
|
||||||
|
align-self: stretch;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border-weak-base);
|
||||||
|
|
||||||
|
[data-slot="accordion-item"] {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0px;
|
||||||
|
align-self: stretch;
|
||||||
|
|
||||||
|
[data-slot="accordion-header"] {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
[data-slot="accordion-trigger"] {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
height: 40px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
align-self: stretch;
|
||||||
|
cursor: default;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
background-color: var(--surface-base);
|
||||||
|
border-bottom: 1px solid var(--border-weak-base);
|
||||||
|
color: var(--text-strong);
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
|
||||||
|
/* text-12-regular */
|
||||||
|
font-family: var(--font-family-sans);
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
|
line-height: var(--line-height-large); /* 166.667% */
|
||||||
|
letter-spacing: var(--letter-spacing-normal);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--surface-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-disabled] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
[data-slot="accordion-trigger"] {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-expanded] {
|
||||||
|
border-bottom: 1px solid var(--border-weak-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="accordion-content"] {
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
/* animation: slideUp 250ms cubic-bezier(0.87, 0, 0.13, 1); */
|
||||||
|
/**/
|
||||||
|
/* &[data-expanded] { */
|
||||||
|
/* animation: slideDown 250ms cubic-bezier(0.87, 0, 0.13, 1); */
|
||||||
|
/* } */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
height: var(--kb-accordion-content-height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
height: var(--kb-accordion-content-height);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
92
packages/ui/src/components/accordion.tsx
Normal file
92
packages/ui/src/components/accordion.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { Accordion as Kobalte } from "@kobalte/core/accordion"
|
||||||
|
import { splitProps } from "solid-js"
|
||||||
|
import type { ComponentProps, ParentProps } from "solid-js"
|
||||||
|
|
||||||
|
export interface AccordionProps extends ComponentProps<typeof Kobalte> {}
|
||||||
|
export interface AccordionItemProps extends ComponentProps<typeof Kobalte.Item> {}
|
||||||
|
export interface AccordionHeaderProps extends ComponentProps<typeof Kobalte.Header> {}
|
||||||
|
export interface AccordionTriggerProps extends ComponentProps<typeof Kobalte.Trigger> {}
|
||||||
|
export interface AccordionContentProps extends ComponentProps<typeof Kobalte.Content> {}
|
||||||
|
|
||||||
|
function AccordionRoot(props: AccordionProps) {
|
||||||
|
const [split, rest] = splitProps(props, ["class", "classList"])
|
||||||
|
return (
|
||||||
|
<Kobalte
|
||||||
|
{...rest}
|
||||||
|
data-component="accordion"
|
||||||
|
classList={{
|
||||||
|
...(split.classList ?? {}),
|
||||||
|
[split.class ?? ""]: !!split.class,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionItem(props: AccordionItemProps) {
|
||||||
|
const [split, rest] = splitProps(props, ["class", "classList"])
|
||||||
|
return (
|
||||||
|
<Kobalte.Item
|
||||||
|
{...rest}
|
||||||
|
data-slot="accordion-item"
|
||||||
|
classList={{
|
||||||
|
...(split.classList ?? {}),
|
||||||
|
[split.class ?? ""]: !!split.class,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionHeader(props: ParentProps<AccordionHeaderProps>) {
|
||||||
|
const [split, rest] = splitProps(props, ["class", "classList", "children"])
|
||||||
|
return (
|
||||||
|
<Kobalte.Header
|
||||||
|
{...rest}
|
||||||
|
data-slot="accordion-header"
|
||||||
|
classList={{
|
||||||
|
...(split.classList ?? {}),
|
||||||
|
[split.class ?? ""]: !!split.class,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{split.children}
|
||||||
|
</Kobalte.Header>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionTrigger(props: ParentProps<AccordionTriggerProps>) {
|
||||||
|
const [split, rest] = splitProps(props, ["class", "classList", "children"])
|
||||||
|
return (
|
||||||
|
<Kobalte.Trigger
|
||||||
|
{...rest}
|
||||||
|
data-slot="accordion-trigger"
|
||||||
|
classList={{
|
||||||
|
...(split.classList ?? {}),
|
||||||
|
[split.class ?? ""]: !!split.class,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{split.children}
|
||||||
|
</Kobalte.Trigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionContent(props: ParentProps<AccordionContentProps>) {
|
||||||
|
const [split, rest] = splitProps(props, ["class", "classList", "children"])
|
||||||
|
return (
|
||||||
|
<Kobalte.Content
|
||||||
|
{...rest}
|
||||||
|
data-slot="accordion-content"
|
||||||
|
classList={{
|
||||||
|
...(split.classList ?? {}),
|
||||||
|
[split.class ?? ""]: !!split.class,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{split.children}
|
||||||
|
</Kobalte.Content>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Accordion = Object.assign(AccordionRoot, {
|
||||||
|
Item: AccordionItem,
|
||||||
|
Header: AccordionHeader,
|
||||||
|
Trigger: AccordionTrigger,
|
||||||
|
Content: AccordionContent,
|
||||||
|
})
|
||||||
@@ -8,7 +8,6 @@ export interface CollapsibleProps extends ParentProps<CollapsibleRootProps> {
|
|||||||
|
|
||||||
function CollapsibleRoot(props: CollapsibleProps) {
|
function CollapsibleRoot(props: CollapsibleProps) {
|
||||||
const [local, others] = splitProps(props, ["class", "classList"])
|
const [local, others] = splitProps(props, ["class", "classList"])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Kobalte
|
<Kobalte
|
||||||
data-component="collapsible"
|
data-component="collapsible"
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ const newIcons = {
|
|||||||
"edit-small-2": `<path d="M17.0834 17.0833V17.5833H17.5834V17.0833H17.0834ZM2.91675 17.0833H2.41675V17.5833H2.91675V17.0833ZM2.91675 2.91659V2.41659H2.41675V2.91659H2.91675ZM9.58341 3.41659H10.0834V2.41659H9.58341V2.91659V3.41659ZM17.5834 10.4166V9.91659H16.5834V10.4166H17.0834H17.5834ZM10.4167 7.08325L10.0632 6.7297L9.91675 6.87615V7.08325H10.4167ZM10.4167 9.58325H9.91675V10.0833H10.4167V9.58325ZM12.9167 9.58325V10.0833H13.1239L13.2703 9.93681L12.9167 9.58325ZM15.4167 2.08325L15.7703 1.7297L15.4167 1.37615L15.0632 1.7297L15.4167 2.08325ZM17.9167 4.58325L18.2703 4.93681L18.6239 4.58325L18.2703 4.2297L17.9167 4.58325ZM17.0834 17.0833V16.5833H2.91675V17.0833V17.5833H17.0834V17.0833ZM2.91675 17.0833H3.41675V2.91659H2.91675H2.41675V17.0833H2.91675ZM2.91675 2.91659V3.41659H9.58341V2.91659V2.41659H2.91675V2.91659ZM17.0834 10.4166H16.5834V17.0833H17.0834H17.5834V10.4166H17.0834ZM10.4167 7.08325H9.91675V9.58325H10.4167H10.9167V7.08325H10.4167ZM10.4167 9.58325V10.0833H12.9167V9.58325V9.08325H10.4167V9.58325ZM10.4167 7.08325L10.7703 7.43681L15.7703 2.43681L15.4167 2.08325L15.0632 1.7297L10.0632 6.7297L10.4167 7.08325ZM15.4167 2.08325L15.0632 2.43681L17.5632 4.93681L17.9167 4.58325L18.2703 4.2297L15.7703 1.7297L15.4167 2.08325ZM17.9167 4.58325L17.5632 4.2297L12.5632 9.2297L12.9167 9.58325L13.2703 9.93681L18.2703 4.93681L17.9167 4.58325Z" fill="currentColor"/>`,
|
"edit-small-2": `<path d="M17.0834 17.0833V17.5833H17.5834V17.0833H17.0834ZM2.91675 17.0833H2.41675V17.5833H2.91675V17.0833ZM2.91675 2.91659V2.41659H2.41675V2.91659H2.91675ZM9.58341 3.41659H10.0834V2.41659H9.58341V2.91659V3.41659ZM17.5834 10.4166V9.91659H16.5834V10.4166H17.0834H17.5834ZM10.4167 7.08325L10.0632 6.7297L9.91675 6.87615V7.08325H10.4167ZM10.4167 9.58325H9.91675V10.0833H10.4167V9.58325ZM12.9167 9.58325V10.0833H13.1239L13.2703 9.93681L12.9167 9.58325ZM15.4167 2.08325L15.7703 1.7297L15.4167 1.37615L15.0632 1.7297L15.4167 2.08325ZM17.9167 4.58325L18.2703 4.93681L18.6239 4.58325L18.2703 4.2297L17.9167 4.58325ZM17.0834 17.0833V16.5833H2.91675V17.0833V17.5833H17.0834V17.0833ZM2.91675 17.0833H3.41675V2.91659H2.91675H2.41675V17.0833H2.91675ZM2.91675 2.91659V3.41659H9.58341V2.91659V2.41659H2.91675V2.91659ZM17.0834 10.4166H16.5834V17.0833H17.0834H17.5834V10.4166H17.0834ZM10.4167 7.08325H9.91675V9.58325H10.4167H10.9167V7.08325H10.4167ZM10.4167 9.58325V10.0833H12.9167V9.58325V9.08325H10.4167V9.58325ZM10.4167 7.08325L10.7703 7.43681L15.7703 2.43681L15.4167 2.08325L15.0632 1.7297L10.0632 6.7297L10.4167 7.08325ZM15.4167 2.08325L15.0632 2.43681L17.5632 4.93681L17.9167 4.58325L18.2703 4.2297L15.7703 1.7297L15.4167 2.08325ZM17.9167 4.58325L17.5632 4.2297L12.5632 9.2297L12.9167 9.58325L13.2703 9.93681L18.2703 4.93681L17.9167 4.58325Z" fill="currentColor"/>`,
|
||||||
folder: `<path d="M2.08301 2.91675V16.2501H17.9163V5.41675H9.99967L8.33301 2.91675H2.08301Z" stroke="currentColor" stroke-linecap="round"/>`,
|
folder: `<path d="M2.08301 2.91675V16.2501H17.9163V5.41675H9.99967L8.33301 2.91675H2.08301Z" stroke="currentColor" stroke-linecap="round"/>`,
|
||||||
"pencil-line": `<path d="M9.58301 17.9166H17.9163M17.9163 5.83325L14.1663 2.08325L2.08301 14.1666V17.9166H5.83301L17.9163 5.83325Z" stroke="currentColor" stroke-linecap="square"/>`,
|
"pencil-line": `<path d="M9.58301 17.9166H17.9163M17.9163 5.83325L14.1663 2.08325L2.08301 14.1666V17.9166H5.83301L17.9163 5.83325Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||||
|
"chevron-grabber-vertical": `<path d="M6.66675 12.4998L10.0001 15.8332L13.3334 12.4998M6.66675 7.49984L10.0001 4.1665L13.3334 7.49984" stroke="currentColor" stroke-linecap="square"/>`,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IconProps extends ComponentProps<"svg"> {
|
export interface IconProps extends ComponentProps<"svg"> {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export * from "./accordion"
|
||||||
export * from "./button"
|
export * from "./button"
|
||||||
export * from "./collapsible"
|
export * from "./collapsible"
|
||||||
export * from "./dialog"
|
export * from "./dialog"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Component } from "solid-js"
|
import type { Component } from "solid-js"
|
||||||
import { createSignal } from "solid-js"
|
import { createSignal } from "solid-js"
|
||||||
import {
|
import {
|
||||||
|
Accordion,
|
||||||
Button,
|
Button,
|
||||||
Select,
|
Select,
|
||||||
Tabs,
|
Tabs,
|
||||||
@@ -214,6 +215,41 @@ const Demo: Component = () => {
|
|||||||
</Collapsible.Content>
|
</Collapsible.Content>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</section>
|
</section>
|
||||||
|
<h3>Accordion</h3>
|
||||||
|
<section>
|
||||||
|
<Accordion collapsible>
|
||||||
|
<Accordion.Item value="item-1">
|
||||||
|
<Accordion.Header>
|
||||||
|
<Accordion.Trigger>What is Kobalte?</Accordion.Trigger>
|
||||||
|
</Accordion.Header>
|
||||||
|
<Accordion.Content>
|
||||||
|
<div style={{ padding: "16px" }}>
|
||||||
|
<p>Kobalte is a UI toolkit for building accessible web apps and design systems with SolidJS.</p>
|
||||||
|
</div>
|
||||||
|
</Accordion.Content>
|
||||||
|
</Accordion.Item>
|
||||||
|
<Accordion.Item value="item-2">
|
||||||
|
<Accordion.Header>
|
||||||
|
<Accordion.Trigger>Is it accessible?</Accordion.Trigger>
|
||||||
|
</Accordion.Header>
|
||||||
|
<Accordion.Content>
|
||||||
|
<div style={{ padding: "16px" }}>
|
||||||
|
<p>Yes. It adheres to the WAI-ARIA design patterns.</p>
|
||||||
|
</div>
|
||||||
|
</Accordion.Content>
|
||||||
|
</Accordion.Item>
|
||||||
|
<Accordion.Item value="item-3">
|
||||||
|
<Accordion.Header>
|
||||||
|
<Accordion.Trigger>Can it be animated?</Accordion.Trigger>
|
||||||
|
</Accordion.Header>
|
||||||
|
<Accordion.Content>
|
||||||
|
<div style={{ padding: "16px" }}>
|
||||||
|
<p>Yes! You can animate the content height using CSS animations.</p>
|
||||||
|
</div>
|
||||||
|
</Accordion.Content>
|
||||||
|
</Accordion.Item>
|
||||||
|
</Accordion>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
|
|
||||||
@import "./base.css" layer(base);
|
@import "./base.css" layer(base);
|
||||||
|
|
||||||
|
@import "../components/accordion.css" layer(components);
|
||||||
@import "../components/button.css" layer(components);
|
@import "../components/button.css" layer(components);
|
||||||
|
@import "../components/collapsible.css" layer(components);
|
||||||
@import "../components/dialog.css" layer(components);
|
@import "../components/dialog.css" layer(components);
|
||||||
@import "../components/icon.css" layer(components);
|
@import "../components/icon.css" layer(components);
|
||||||
@import "../components/icon-button.css" layer(components);
|
@import "../components/icon-button.css" layer(components);
|
||||||
|
|||||||
Reference in New Issue
Block a user