summaryrefslogtreecommitdiffhomepage
path: root/packages/ui/src
diff options
context:
space:
mode:
authorOpeOginni <[email protected]>2026-01-24 19:03:36 +0100
committerGitHub <[email protected]>2026-01-24 12:03:36 -0600
commit67ea21b55a281d6feb00820f1fec94da50165678 (patch)
tree44fc751afe3120de59e5e8adac17b1b40d519bb0 /packages/ui/src
parentf4cf3f4976b8e8e60b90098bba57c11ffa115a6a (diff)
downloadopencode-67ea21b55a281d6feb00820f1fec94da50165678.tar.gz
opencode-67ea21b55a281d6feb00820f1fec94da50165678.zip
feat(web): implement new server management for web and desktop (#8513)
Diffstat (limited to 'packages/ui/src')
-rw-r--r--packages/ui/src/components/list.css36
-rw-r--r--packages/ui/src/components/list.tsx128
2 files changed, 118 insertions, 46 deletions
diff --git a/packages/ui/src/components/list.css b/packages/ui/src/components/list.css
index 95641bb20..b2b8a2262 100644
--- a/packages/ui/src/components/list.css
+++ b/packages/ui/src/components/list.css
@@ -214,6 +214,7 @@
[data-slot="list-item"] {
display: flex;
+ position: relative;
width: 100%;
padding: 6px 8px 6px 8px;
align-items: center;
@@ -254,6 +255,20 @@
margin-left: -4px;
}
+ [data-slot="list-item-divider"] {
+ position: absolute;
+ bottom: 0;
+ left: var(--list-divider-inset, 16px);
+ right: var(--list-divider-inset, 16px);
+ height: 1px;
+ background: var(--border-weak-base);
+ pointer-events: none;
+ }
+
+ [data-slot="list-item"]:last-child [data-slot="list-item-divider"] {
+ display: none;
+ }
+
&[data-active="true"] {
border-radius: var(--radius-md);
background: var(--surface-raised-base-hover);
@@ -272,6 +287,27 @@
outline: none;
}
}
+
+ [data-slot="list-item-add"] {
+ display: flex;
+ position: relative;
+ width: 100%;
+ padding: 6px 8px 6px 8px;
+ align-items: center;
+ color: var(--text-strong);
+
+ /* text-14-medium */
+ font-family: var(--font-family-sans);
+ font-size: 14px;
+ font-style: normal;
+ font-weight: var(--font-weight-medium);
+ line-height: var(--line-height-large); /* 142.857% */
+ letter-spacing: var(--letter-spacing-normal);
+
+ [data-component="input"] {
+ width: 100%;
+ }
+ }
}
}
}
diff --git a/packages/ui/src/components/list.tsx b/packages/ui/src/components/list.tsx
index 6a7f3a029..5f585f90c 100644
--- a/packages/ui/src/components/list.tsx
+++ b/packages/ui/src/components/list.tsx
@@ -21,6 +21,16 @@ export interface ListSearchProps {
action?: JSX.Element
}
+export interface ListAddProps {
+ class?: string
+ render: () => JSX.Element
+}
+
+export interface ListAddProps {
+ class?: string
+ render: () => JSX.Element
+}
+
export interface ListProps<T> extends FilteredListProps<T> {
class?: string
children: (item: T) => JSX.Element
@@ -32,6 +42,8 @@ export interface ListProps<T> extends FilteredListProps<T> {
filter?: string
search?: ListSearchProps | boolean
itemWrapper?: (item: T, node: JSX.Element) => JSX.Element
+ divider?: boolean
+ add?: ListAddProps
}
export interface ListRef {
@@ -70,6 +82,8 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
const searchProps = () => (typeof props.search === "object" ? props.search : {})
const searchAction = () => searchProps().action
+ const addProps = () => props.add
+ const showAdd = () => !!addProps()
const moved = (event: MouseEvent) => event.movementX !== 0 || event.movementY !== 0
@@ -159,6 +173,16 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
setScrollRef,
})
+ const renderAdd = () => {
+ const add = addProps()
+ if (!add) return null
+ return (
+ <div data-slot="list-item-add" classList={{ [add.class ?? ""]: !!add.class }}>
+ {add.render()}
+ </div>
+ )
+ }
+
function GroupHeader(groupProps: { category: string }): JSX.Element {
const [stuck, setStuck] = createSignal(false)
const [header, setHeader] = createSignal<HTMLDivElement | undefined>(undefined)
@@ -243,7 +267,7 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
</Show>
<div ref={setScrollRef} data-slot="list-scroll">
<Show
- when={flat().length > 0}
+ when={flat().length > 0 || showAdd()}
fallback={
<div data-slot="list-empty-state">
<div data-slot="list-message">{emptyMessage()}</div>
@@ -251,55 +275,67 @@ export function List<T>(props: ListProps<T> & { ref?: (ref: ListRef) => void })
}
>
<For each={grouped.latest}>
- {(group) => (
- <div data-slot="list-group">
- <Show when={group.category}>
- <GroupHeader category={group.category} />
- </Show>
- <div data-slot="list-items">
- <For each={group.items}>
- {(item, i) => {
- const node = (
- <button
- data-slot="list-item"
- data-key={props.key(item)}
- data-active={props.key(item) === active()}
- data-selected={item === props.current}
- onClick={() => handleSelect(item, i())}
- type="button"
- onMouseMove={(event) => {
- if (!moved(event)) return
- setStore("mouseActive", true)
- setActive(props.key(item))
- }}
- onMouseLeave={() => {
- if (!store.mouseActive) return
- setActive(null)
- }}
- >
- {props.children(item)}
- <Show when={item === props.current}>
- <span data-slot="list-item-selected-icon">
- <Icon name="check-small" />
- </span>
- </Show>
- <Show when={props.activeIcon}>
- {(icon) => (
- <span data-slot="list-item-active-icon">
- <Icon name={icon()} />
+ {(group, groupIndex) => {
+ const isLastGroup = () => groupIndex() === grouped.latest.length - 1
+ return (
+ <div data-slot="list-group">
+ <Show when={group.category}>
+ <GroupHeader category={group.category} />
+ </Show>
+ <div data-slot="list-items">
+ <For each={group.items}>
+ {(item, i) => {
+ const node = (
+ <button
+ data-slot="list-item"
+ data-key={props.key(item)}
+ data-active={props.key(item) === active()}
+ data-selected={item === props.current}
+ onClick={() => handleSelect(item, i())}
+ type="button"
+ onMouseMove={(event) => {
+ if (!moved(event)) return
+ setStore("mouseActive", true)
+ setActive(props.key(item))
+ }}
+ onMouseLeave={() => {
+ if (!store.mouseActive) return
+ setActive(null)
+ }}
+ >
+ {props.children(item)}
+ <Show when={item === props.current}>
+ <span data-slot="list-item-selected-icon">
+ <Icon name="check-small" />
</span>
+ </Show>
+ <Show when={props.activeIcon}>
+ {(icon) => (
+ <span data-slot="list-item-active-icon">
+ <Icon name={icon()} />
+ </span>
+ )}
+ </Show>
+ {props.divider && (i() !== group.items.length - 1 || (showAdd() && isLastGroup())) && (
+ <span data-slot="list-item-divider" />
)}
- </Show>
- </button>
- )
- if (props.itemWrapper) return props.itemWrapper(item, node)
- return node
- }}
- </For>
+ </button>
+ )
+ if (props.itemWrapper) return props.itemWrapper(item, node)
+ return node
+ }}
+ </For>
+ <Show when={showAdd() && isLastGroup()}>{renderAdd()}</Show>
+ </div>
</div>
- </div>
- )}
+ )
+ }}
</For>
+ <Show when={grouped.latest.length === 0 && showAdd()}>
+ <div data-slot="list-group">
+ <div data-slot="list-items">{renderAdd()}</div>
+ </div>
+ </Show>
</Show>
</div>
</div>