feat(web): implement new server management for web and desktop (#8513)

This commit is contained in:
OpeOginni
2026-01-24 19:03:36 +01:00
committed by GitHub
parent f4cf3f4976
commit 67ea21b55a
25 changed files with 1104 additions and 360 deletions

View File

@@ -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%;
}
}
}
}
}

View File

@@ -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>