diff options
| author | Adam <[email protected]> | 2025-10-24 11:23:32 -0500 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-10-24 12:16:33 -0500 |
| commit | 86447b576490ee4cde0825418015652d8ed26794 (patch) | |
| tree | 02f0a7780eb7c96d1fa7a801088743a29e265a9d | |
| parent | fe8f6d7a3eef34e932bd43d244460d417865de88 (diff) | |
| download | opencode-86447b576490ee4cde0825418015652d8ed26794.tar.gz opencode-86447b576490ee4cde0825418015652d8ed26794.zip | |
wip: desktop work
| -rw-r--r-- | packages/desktop/src/components/diff.tsx | 256 | ||||
| -rw-r--r-- | packages/desktop/src/components/prompt-input.tsx | 90 | ||||
| -rw-r--r-- | packages/desktop/src/pages/index.tsx | 69 | ||||
| -rw-r--r-- | packages/ui/src/components/accordion.css | 102 | ||||
| -rw-r--r-- | packages/ui/src/components/accordion.tsx | 92 | ||||
| -rw-r--r-- | packages/ui/src/components/collapsible.tsx | 1 | ||||
| -rw-r--r-- | packages/ui/src/components/icon.tsx | 1 | ||||
| -rw-r--r-- | packages/ui/src/components/index.ts | 1 | ||||
| -rw-r--r-- | packages/ui/src/demo.tsx | 36 | ||||
| -rw-r--r-- | packages/ui/src/styles/index.css | 2 |
10 files changed, 477 insertions, 173 deletions
diff --git a/packages/desktop/src/components/diff.tsx b/packages/desktop/src/components/diff.tsx index 4667bbb3a..0facaba47 100644 --- a/packages/desktop/src/components/diff.tsx +++ b/packages/desktop/src/components/diff.tsx @@ -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 after: FileContents + annotations?: DiffLineAnnotation<T>[] + class?: string + classList?: ComponentProps<"div">["classList"] } -export function Diff(props: DiffProps) { - let container!: HTMLDivElement - - console.log(props) +// interface ThreadMetadata { +// threadId: string +// } - interface ThreadMetadata { - threadId: string - } +export function Diff<T>(props: DiffProps<T>) { + let container!: HTMLDivElement + const [local, others] = splitProps(props, ["before", "after", "class", "classList", "annotations"]) - const lineAnnotations: DiffLineAnnotation<ThreadMetadata>[] = [ - { - 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 - }, - }) + // const lineAnnotations: DiffLineAnnotation<ThreadMetadata>[] = [ + // { + // 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" }, + // }, + // ] // 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 // existing properties, it's a full replace - instance.setOptions({ - ...instance.options, - theme: "pierre-dark", - themes: undefined, - }) + // instance.setOptions({ + // ...instance.options, + // theme: "pierre-dark", + // themes: undefined, + // }) // When ready to render, simply call .render with old/new file, optional // annotations and a container element to hold the diff - instance.render({ - oldFile: props.before, - newFile: props.after, - lineAnnotations, - containerWrapper: container, + createEffect(() => { + const instance = new FileDiff<T>({ + theme: "pierre-light", + // 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: "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} + /> + ) } diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx index 47893f44c..3838d19ba 100644 --- a/packages/desktop/src/components/prompt-input.tsx +++ b/packages/desktop/src/components/prompt-input.tsx @@ -1,6 +1,6 @@ import { Button, Icon, IconButton, Select, SelectDialog } from "@opencode-ai/ui" 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 { FileIcon } from "@/ui" import { getDirectory, getFilename } from "@/utils" @@ -46,6 +46,21 @@ export const PromptInput: Component<PromptInputProps> = (props) => { const isEmpty = createMemo(() => isEqual(store.contentParts, defaultParts)) 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(() => { if (isFocused()) { handleInput() @@ -144,16 +159,27 @@ export const PromptInput: Component<PromptInputProps> = (props) => { const rawText = store.contentParts.map((p) => p.content).join("") const textBeforeCursor = rawText.substring(0, cursorPosition) const atMatch = textBeforeCursor.match(/@(\S*)$/) - if (!atMatch) return - const startIndex = atMatch.index! - const endIndex = cursorPosition + const startIndex = atMatch ? atMatch.index! : 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 { parts: nextParts, - cursorIndex, - cursorOffset, inserted, + cursorPositionAfter, } = store.contentParts.reduce( (acc, item) => { if (acc.inserted) { @@ -180,17 +206,23 @@ export const PromptInput: Component<PromptInputProps> = (props) => { const head = item.content.slice(0, headLength) const tail = item.content.slice(tailLength) - if (head) acc.parts.push({ type: "text", content: head }) + pushText(acc, head) - acc.parts.push(part) - - 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") { + pushText(acc, part.content) + } + 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.runningIndex = nextIndex return acc @@ -199,29 +231,27 @@ export const PromptInput: Component<PromptInputProps> = (props) => { parts: [] as ContentPart[], runningIndex: 0, inserted: false, - cursorIndex: null as number | null, - cursorOffset: 0, + cursorPositionAfter: cursorPosition + part.content.length, }, ) - 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("popoverIsOpen", false) - queueMicrotask(() => { - 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) - } - }) + queueMicrotask(() => setCursorPosition(editorRef, cursorPositionAfter)) } const handleKeyDown = (event: KeyboardEvent) => { diff --git a/packages/desktop/src/pages/index.tsx b/packages/desktop/src/pages/index.tsx index f6ea4cb90..f61e36549 100644 --- a/packages/desktop/src/pages/index.tsx +++ b/packages/desktop/src/pages/index.tsx @@ -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 FileTree from "@/components/file-tree" 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) => { if (event.getModifierState(MOD) && event.shiftKey && event.key.toLowerCase() === "p") { event.preventDefault() - // TODO: command palette return } 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]"> <For each={local.session.userMessages()}> {(message) => { - console.log(message) return ( <div data-message={message.id} @@ -583,22 +581,55 @@ export default function Page() { </div> <div class="text-14-regular text-text-base">{message.summary?.text}</div> </div> - <div class=""> - <For each={message.summary?.diffs}> - {(diff) => ( - <Diff - before={{ - name: diff.file!, - contents: diff.before!, - }} - after={{ - name: diff.file!, - contents: diff.after!, - }} - /> - )} - </For> - </div> + <Show when={message.summary?.diffs.length}> + <Accordion class="w-full" multiple> + <For each={message.summary?.diffs || []}> + {(diff) => ( + <Accordion.Item value={diff.file}> + <Accordion.Header> + <Accordion.Trigger> + <div class="flex items-center justify-between w-full"> + <div class="flex items-center gap-5"> + <FileIcon + node={{ path: diff.file, type: "file" }} + class="shrink-0 size-4" + /> + <div class="flex"> + <Show when={diff.file.includes("/")}> + <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> ) }} diff --git a/packages/ui/src/components/accordion.css b/packages/ui/src/components/accordion.css new file mode 100644 index 000000000..c8dfdfab1 --- /dev/null +++ b/packages/ui/src/components/accordion.css @@ -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; + } +} diff --git a/packages/ui/src/components/accordion.tsx b/packages/ui/src/components/accordion.tsx new file mode 100644 index 000000000..535d38e3d --- /dev/null +++ b/packages/ui/src/components/accordion.tsx @@ -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, +}) diff --git a/packages/ui/src/components/collapsible.tsx b/packages/ui/src/components/collapsible.tsx index 011d103c1..f926192e8 100644 --- a/packages/ui/src/components/collapsible.tsx +++ b/packages/ui/src/components/collapsible.tsx @@ -8,7 +8,6 @@ export interface CollapsibleProps extends ParentProps<CollapsibleRootProps> { function CollapsibleRoot(props: CollapsibleProps) { const [local, others] = splitProps(props, ["class", "classList"]) - return ( <Kobalte data-component="collapsible" diff --git a/packages/ui/src/components/icon.tsx b/packages/ui/src/components/icon.tsx index 0c42083de..0011a9676 100644 --- a/packages/ui/src/components/icon.tsx +++ b/packages/ui/src/components/icon.tsx @@ -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"/>`, 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"/>`, + "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"> { diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index 3691363cd..31672001b 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -1,3 +1,4 @@ +export * from "./accordion" export * from "./button" export * from "./collapsible" export * from "./dialog" diff --git a/packages/ui/src/demo.tsx b/packages/ui/src/demo.tsx index 7c507d7d9..791281815 100644 --- a/packages/ui/src/demo.tsx +++ b/packages/ui/src/demo.tsx @@ -1,6 +1,7 @@ import type { Component } from "solid-js" import { createSignal } from "solid-js" import { + Accordion, Button, Select, Tabs, @@ -214,6 +215,41 @@ const Demo: Component = () => { </Collapsible.Content> </Collapsible> </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> ) diff --git a/packages/ui/src/styles/index.css b/packages/ui/src/styles/index.css index dc5335c43..7d426a83e 100644 --- a/packages/ui/src/styles/index.css +++ b/packages/ui/src/styles/index.css @@ -5,7 +5,9 @@ @import "./base.css" layer(base); +@import "../components/accordion.css" layer(components); @import "../components/button.css" layer(components); +@import "../components/collapsible.css" layer(components); @import "../components/dialog.css" layer(components); @import "../components/icon.css" layer(components); @import "../components/icon-button.css" layer(components); |
