diff options
| author | OpeOginni <[email protected]> | 2026-01-24 19:03:36 +0100 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-01-24 12:03:36 -0600 |
| commit | 67ea21b55a281d6feb00820f1fec94da50165678 (patch) | |
| tree | 44fc751afe3120de59e5e8adac17b1b40d519bb0 /packages/ui/src/components | |
| parent | f4cf3f4976b8e8e60b90098bba57c11ffa115a6a (diff) | |
| download | opencode-67ea21b55a281d6feb00820f1fec94da50165678.tar.gz opencode-67ea21b55a281d6feb00820f1fec94da50165678.zip | |
feat(web): implement new server management for web and desktop (#8513)
Diffstat (limited to 'packages/ui/src/components')
| -rw-r--r-- | packages/ui/src/components/list.css | 36 | ||||
| -rw-r--r-- | packages/ui/src/components/list.tsx | 128 |
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> |
