diff options
| author | Adam <[email protected]> | 2025-10-17 12:05:52 -0500 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-10-17 12:06:36 -0500 |
| commit | 887a819f2444c8454a43049983d831194883c6cd (patch) | |
| tree | 7247e5d619c6065a4b1c7d02c74366d43e7e3c05 /packages/ui/src/components | |
| parent | fe8b3a25155c0aaad20b506d0ba6fc6b8f2d0e5b (diff) | |
| download | opencode-887a819f2444c8454a43049983d831194883c6cd.tar.gz opencode-887a819f2444c8454a43049983d831194883c6cd.zip | |
wip: desktop work
Diffstat (limited to 'packages/ui/src/components')
| -rw-r--r-- | packages/ui/src/components/button.css | 106 | ||||
| -rw-r--r-- | packages/ui/src/components/fonts.tsx | 12 | ||||
| -rw-r--r-- | packages/ui/src/components/icon.css | 6 | ||||
| -rw-r--r-- | packages/ui/src/components/index.ts | 1 | ||||
| -rw-r--r-- | packages/ui/src/components/list.css | 30 | ||||
| -rw-r--r-- | packages/ui/src/components/list.tsx | 76 | ||||
| -rw-r--r-- | packages/ui/src/components/select.css | 124 | ||||
| -rw-r--r-- | packages/ui/src/components/style.css | 2 | ||||
| -rw-r--r-- | packages/ui/src/components/tabs.css | 95 | ||||
| -rw-r--r-- | packages/ui/src/components/tooltip.css | 59 | ||||
| -rw-r--r-- | packages/ui/src/components/tooltip.tsx | 2 |
11 files changed, 504 insertions, 9 deletions
diff --git a/packages/ui/src/components/button.css b/packages/ui/src/components/button.css new file mode 100644 index 000000000..c9ccf4ecb --- /dev/null +++ b/packages/ui/src/components/button.css @@ -0,0 +1,106 @@ +[data-component="button"] { + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + border-style: solid; + border-width: 1px; + border-radius: 6px; + text-decoration: none; + user-select: none; + + &[data-variant="primary"] { + border-color: var(--border-base); + background-color: var(--surface-brand-base); + color: var(--text-on-brand-strong); + + &:hover:not(:disabled) { + border-color: var(--border-hover); + background-color: var(--surface-brand-hover); + } + &:active:not(:disabled) { + border-color: var(--border-active); + background-color: var(--surface-brand-active); + } + &:focus:not(:disabled) { + border-color: var(--border-focus); + background-color: var(--surface-brand-focus); + } + } + + &[data-variant="secondary"] { + border-color: var(--border-weak-base); + background-color: var(--button-secondary-base); + color: var(--text-strong); + + /* shadow-xs */ + box-shadow: + 0 1px 2px -1px rgba(19, 16, 16, 0.04), + 0 1px 2px 0 rgba(19, 16, 16, 0.06), + 0 1px 3px 0 rgba(19, 16, 16, 0.08); + + &:hover:not(:disabled) { + border-color: var(--border-hover); + background-color: var(--surface-hover); + } + &:active:not(:disabled) { + border-color: var(--border-active); + background-color: var(--surface-active); + } + &:focus:not(:disabled) { + border-color: var(--border-focus); + background-color: var(--surface-focus); + } + } + + &[data-variant="ghost"] { + border-color: transparent; + background-color: transparent; + color: var(--text-strong); + + &:hover:not(:disabled) { + background-color: var(--surface-hover); + } + &:active:not(:disabled) { + border-color: var(--border-active); + background-color: var(--surface-active); + } + &:focus:not(:disabled) { + border-color: var(--border-focus); + background-color: var(--surface-focus); + } + } + + &[data-size="normal"] { + padding: 0 6px 0 6px; + + font-size: var(--font-size-small); + line-height: var(--line-height-large); + gap: calc(var(--spacing) * 0.5); + } + + &[data-size="large"] { + height: 32px; + padding: 0 8px 0 6px; + gap: 8px; + + /* text-12-medium */ + font-family: var(--font-family-sans); + font-size: var(--font-size-small); + font-style: normal; + font-weight: var(--font-weight-medium); + line-height: var(--line-height-large); /* 166.667% */ + letter-spacing: var(--letter-spacing-normal); + } + + &:disabled { + border-color: var(--border-disabled); + background-color: var(--surface-disabled); + color: var(--text-weak); + cursor: not-allowed; + } + + &:focus { + outline: none; + } +} diff --git a/packages/ui/src/components/fonts.tsx b/packages/ui/src/components/fonts.tsx index 447c486e4..ff4fb7588 100644 --- a/packages/ui/src/components/fonts.tsx +++ b/packages/ui/src/components/fonts.tsx @@ -1,20 +1,20 @@ import { Style, Link } from "@solidjs/meta" -import geist from "@opencode-ai/css/fonts/geist.woff2" -import geistMono from "@opencode-ai/css/fonts/geist-mono.woff2" +import geist from "../assets/fonts/geist.woff2" +import geistMono from "../assets/fonts/geist-mono.woff2" export const Fonts = () => { return ( <> <Style>{` @font-face { - font-family: "geist"; + font-family: "Geist"; src: url("${geist}") format("woff2-variations"); font-display: swap; font-style: normal; font-weight: 100 900; } @font-face { - font-family: "geist-fallback"; + font-family: "Geist Fallback"; src: local("Arial"); size-adjust: 100%; ascent-override: 97%; @@ -22,14 +22,14 @@ export const Fonts = () => { line-gap-override: 1%; } @font-face { - font-family: "geist-mono"; + font-family: "Geist Mono"; src: url("${geistMono}") format("woff2-variations"); font-display: swap; font-style: normal; font-weight: 100 900; } @font-face { - font-family: "geist-mono-fallback"; + font-family: "Geist Mono Fallback"; src: local("Courier New"); size-adjust: 100%; ascent-override: 97%; diff --git a/packages/ui/src/components/icon.css b/packages/ui/src/components/icon.css new file mode 100644 index 000000000..abc193220 --- /dev/null +++ b/packages/ui/src/components/icon.css @@ -0,0 +1,6 @@ +[data-component="icon"] { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index 2048ec639..d6ddc3ec0 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -1,6 +1,7 @@ export * from "./button" export * from "./icon" export * from "./fonts" +export * from "./list" export * from "./select" export * from "./tabs" export * from "./tooltip" diff --git a/packages/ui/src/components/list.css b/packages/ui/src/components/list.css new file mode 100644 index 000000000..b98cae07c --- /dev/null +++ b/packages/ui/src/components/list.css @@ -0,0 +1,30 @@ +[data-component="list"] { + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 6px; + + /* Hide scrollbar */ + &::-webkit-scrollbar { + display: none; + } + -ms-overflow-style: none; + scrollbar-width: none; + + [data-slot="item"] { + cursor: pointer; + width: 100%; + padding: 4px 12px; + text-align: left; + + border-radius: 6px; + transition: background-color 0.2s ease-in-out; + + &[data-active="true"] { + background-color: var(--surface-raised-base-hover); + } + &:focus { + outline: none; + } + } +} diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx new file mode 100644 index 000000000..9704e4554 --- /dev/null +++ b/packages/ui/src/components/list.tsx @@ -0,0 +1,76 @@ +import { ComponentProps, createEffect, createSignal, type JSX } from "solid-js" +import { VirtualizerHandle, VList } from "virtua/solid" +import { createList } from "solid-list" +import { createStore } from "solid-js/store" + +export interface ListProps<T> { + data: T[] + children: (x: T) => JSX.Element + key: (x: T) => string + current?: T + onSelect?: (value: T | undefined) => void + class?: ComponentProps<"div">["class"] +} + +export function List<T>(props: ListProps<T>) { + const [virtualizer, setVirtualizer] = createSignal<VirtualizerHandle | undefined>(undefined) + const [store, setStore] = createStore({ + mouseActive: false, + }) + const list = createList({ + items: () => props.data.map(props.key), + initialActive: props.current ? props.key(props.current) : undefined, + loop: true, + }) + // const resetSelection = () => { + // if (props.data.length === 0) return + // list.setActive(props.key(props.data[0])) + // } + const handleSelect = (item: T) => { + props.onSelect?.(item) + } + + const handleKey = (e: KeyboardEvent) => { + setStore("mouseActive", false) + + if (e.key === "Enter") { + e.preventDefault() + const selected = props.data.find((x) => props.key(x) === list.active()) + if (selected) handleSelect(selected) + } else { + list.onKeyDown(e) + } + } + + createEffect(() => { + if (store.mouseActive || props.data.length === 0) return + const index = props.data.findIndex((x) => props.key(x) === list.active()) + if (index === 0) { + virtualizer()?.scrollTo(0) + return + } + // virtualizer()?.scrollTo(list.active()) + // const element = virtualizer()?.querySelector(`[data-key="${list.active()}"]`) + // element?.scrollIntoView({ block: "nearest", behavior: "smooth" }) + }) + + return ( + <VList data-component="list" ref={setVirtualizer} data={props.data} onKeyDown={handleKey} class={props.class}> + {(item) => ( + <button + data-slot="item" + data-key={props.key(item)} + data-active={props.key(item) === list.active()} + onClick={() => handleSelect(item)} + onMouseMove={(e) => { + e.currentTarget.focus() + setStore("mouseActive", true) + list.setActive(props.key(item)) + }} + > + {props.children(item)} + </button> + )} + </VList> + ) +} diff --git a/packages/ui/src/components/select.css b/packages/ui/src/components/select.css new file mode 100644 index 000000000..b6b884a1f --- /dev/null +++ b/packages/ui/src/components/select.css @@ -0,0 +1,124 @@ +[data-component="select"] { + [data-slot="trigger"] { + padding: 0 4px 0 8px; + + [data-slot="value"] { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + [data-slot="icon"] { + width: fit-content; + height: fit-content; + flex-shrink: 0; + color: var(--text-weak); + transition: transform 0.1s ease-in-out; + } + } +} + +[data-component="select-content"] { + min-width: 8rem; + overflow: hidden; + border-radius: var(--radius-md); + border-width: 1px; + border-style: solid; + border-color: var(--border-weak-base); + background-color: var(--surface-raised-base); + padding: calc(var(--spacing) * 1); + box-shadow: var(--shadow-md); + z-index: 50; + + &[data-closed] { + animation: select-close 0.15s ease-out; + } + + &[data-expanded] { + animation: select-open 0.15s ease-out; + } + + [data-slot="list"] { + overflow-y: auto; + max-height: 12rem; + white-space: nowrap; + overflow-x: hidden; + + &:focus { + outline: none; + } + } + + [data-slot="section"] { + font-size: var(--text-xs); + line-height: var(--text-xs--line-height); + font-weight: var(--font-weight-light); + text-transform: uppercase; + color: var(--text-weak); + opacity: 0.6; + margin-top: calc(var(--spacing) * 3); + margin-left: calc(var(--spacing) * 2); + &:first-child { + margin-top: 0; + } + } + + [data-slot="item"] { + position: relative; + display: flex; + align-items: center; + padding: calc(var(--spacing) * 2) calc(var(--spacing) * 2); + border-radius: var(--radius-sm); + font-size: var(--text-xs); + line-height: var(--text-xs--line-height); + color: var(--text-base); + cursor: pointer; + transition: + background-color 0.2s ease-in-out, + color 0.2s ease-in-out; + outline: none; + user-select: none; + + &[data-highlighted] { + background-color: var(--surface-base); + } + + &[data-disabled] { + background-color: var(--surface-disabled); + pointer-events: none; + } + + [data-slot="item-indicator"] { + margin-left: auto; + } + + &:focus { + outline: none; + } + + &:hover { + background-color: var(--surface-hover); + } + } +} + +@keyframes select-open { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes select-close { + from { + opacity: 1; + transform: scale(1); + } + to { + opacity: 0; + transform: scale(0.95); + } +} diff --git a/packages/ui/src/components/style.css b/packages/ui/src/components/style.css deleted file mode 100644 index 52539dc8f..000000000 --- a/packages/ui/src/components/style.css +++ /dev/null @@ -1,2 +0,0 @@ -/* re-exporting for convenience */ -@import "@opencode-ai/css"; diff --git a/packages/ui/src/components/tabs.css b/packages/ui/src/components/tabs.css new file mode 100644 index 000000000..c6d09c656 --- /dev/null +++ b/packages/ui/src/components/tabs.css @@ -0,0 +1,95 @@ +[data-component="tabs"] { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + border-width: 1px; + border-style: solid; + border-radius: var(--radius-sm); + border-color: var(--border-weak-base); + background-color: var(--background-stronger); + overflow: clip; + + & [data-slot="list"] { + width: 100%; + position: relative; + display: flex; + align-items: center; + overflow-x: auto; + + /* Hide scrollbar */ + scrollbar-width: none; + -ms-overflow-style: none; + &::-webkit-scrollbar { + display: none; + } + + /* After element to fill remaining space */ + &::after { + content: ""; + display: block; + flex-grow: 1; + height: 100%; + border-bottom: 1px solid var(--border-weak-base); + background-color: var(--background-base); + border-top-right-radius: var(--radius-sm); + } + + &:empty::after { + display: none; + } + } + + & [data-slot="trigger"] { + position: relative; + height: 36px; + padding: 8px 12px; + display: flex; + align-items: center; + font-size: var(--text-sm); + font-weight: var(--font-weight-medium); + color: var(--text-weak); + cursor: pointer; + white-space: nowrap; + flex-shrink: 0; + border-bottom: 1px solid var(--border-weak-base); + border-right: 1px solid var(--border-weak-base); + background-color: var(--background-weak); + transition: + background-color 0.15s ease, + color 0.15s ease; + + &:disabled { + pointer-events: none; + color: var(--text-weaker); + } + &:focus-visible { + outline: none; + box-shadow: 0 0 0 2px var(--border-focus); + } + &[data-selected] { + color: var(--text-base); + background-color: transparent; + border-bottom-color: transparent; + } + &:hover:not(:disabled):not([data-selected]) { + color: var(--text-strong); + } + } + + & [data-slot="content"] { + overflow-y: auto; + flex: 1; + + /* Hide scrollbar */ + scrollbar-width: none; + -ms-overflow-style: none; + &::-webkit-scrollbar { + display: none; + } + + &:focus-visible { + outline: none; + } + } +} diff --git a/packages/ui/src/components/tooltip.css b/packages/ui/src/components/tooltip.css new file mode 100644 index 000000000..0577365d6 --- /dev/null +++ b/packages/ui/src/components/tooltip.css @@ -0,0 +1,59 @@ +/* [data-component="tooltip-trigger"] { */ +/* display: flex; */ +/* align-items: center; */ +/* } */ + +[data-component="tooltip"] { + z-index: 1000; + max-width: 320px; + border-radius: 12px; + background-color: var(--surface-float-base); + color: var(--white); + padding: 2px 12px 2px 12px; + box-shadow: var(--shadow-md); + pointer-events: none !important; + transition: all 150ms ease-out; + transform: translate3d(0, 0, 0); + transform-origin: var(--kb-tooltip-content-transform-origin); + + /* text-14-regular */ + font-family: var(--font-family-sans); + font-size: var(--font-size-base); + font-style: normal; + font-weight: var(--font-weight-regular); + line-height: var(--line-height-large); /* 171.429% */ + letter-spacing: var(--letter-spacing-normal); + + &[data-expanded] { + opacity: 1; + transform: translate3d(0, 0, 0); + } + + &[data-closed] { + opacity: 0; + } + + &[data-placement="top"] { + &[data-closed] { + transform: translate3d(0, 4px, 0); + } + } + + &[data-placement="bottom"] { + &[data-closed] { + transform: translate3d(0, -4px, 0); + } + } + + &[data-placement="left"] { + &[data-closed] { + transform: translate3d(4px, 0, 0); + } + } + + &[data-placement="right"] { + &[data-closed] { + transform: translate3d(-4px, 0, 0); + } + } +} diff --git a/packages/ui/src/components/tooltip.tsx b/packages/ui/src/components/tooltip.tsx index 7cb22b290..b975099fb 100644 --- a/packages/ui/src/components/tooltip.tsx +++ b/packages/ui/src/components/tooltip.tsx @@ -36,7 +36,7 @@ export function Tooltip(props: TooltipProps) { <KobalteTooltip.Portal> <KobalteTooltip.Content data-component="tooltip" data-placement={props.placement} class={local.class}> {typeof others.value === "function" ? others.value() : others.value} - <KobalteTooltip.Arrow data-slot="arrow" size={18} /> + {/* <KobalteTooltip.Arrow data-slot="arrow" size={18} /> */} </KobalteTooltip.Content> </KobalteTooltip.Portal> </KobalteTooltip> |
