Skip to content

Commit 0a2d7f5

Browse files
committed
feat(design): shared connection indicator, shown only when disconnected
Add a framework-neutral `connectionIndicator(status)` helper to design/design.ts that returns a dot + label pill for every non-connected state and `null` when connected. Adopt it in each surface's top nav (inspect, messages, data-inspector, git), replacing their bespoke pills/badges, so the connection state renders identically everywhere and only appears while the connection is not live.
1 parent 8513574 commit 0a2d7f5

6 files changed

Lines changed: 75 additions & 60 deletions

File tree

design/design.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,40 @@ export function navBrand(extra?: string): string {
8989
return cx('flex items-center gap-1.5 shrink-0 font-semibold text-sm select-none', extra)
9090
}
9191

92+
// Mirrors devframe's `DevframeConnectionStatus` (kept local so this class-helper
93+
// module stays free of package imports); the two share the same string members.
94+
export type ConnectionStatus = 'connecting' | 'connected' | 'unauthorized' | 'disconnected' | 'error'
95+
96+
export interface ConnectionIndicator {
97+
/** Short status label, e.g. `disconnected`. */
98+
label: string
99+
/** Class chain for the status dot. */
100+
dot: string
101+
/** Class chain for the pill wrapper. */
102+
class: string
103+
}
104+
105+
const CONNECTION_TONE: Record<Exclude<ConnectionStatus, 'connected'>, { label: string, dot: string }> = {
106+
connecting: { label: 'connecting…', dot: 'bg-neutral-400 animate-pulse' },
107+
disconnected: { label: 'disconnected', dot: 'bg-error' },
108+
unauthorized: { label: 'unauthorized', dot: 'bg-warning' },
109+
error: { label: 'error', dot: 'bg-error' },
110+
}
111+
112+
// The shared top-nav connection indicator: a small status dot + label. Returns
113+
// `null` when the client is `connected`, so every surface renders the indicator
114+
// only while the connection is not live.
115+
export function connectionIndicator(status: ConnectionStatus, extra?: string): ConnectionIndicator | null {
116+
if (status === 'connected')
117+
return null
118+
const tone = CONNECTION_TONE[status]
119+
return {
120+
label: tone.label,
121+
dot: cx('inline-block size-1.5 rounded-full shrink-0', tone.dot),
122+
class: cx('flex items-center gap-1.5 shrink-0 text-xs color-muted select-none', extra),
123+
}
124+
}
125+
92126
export function toolbar(extra?: string): string {
93127
return cx('flex items-center gap-2 shrink-0 h-8 px-2.5 border-b border-base bg-secondary text-sm', extra)
94128
}

plugins/data-inspector/src/spa/components/AppHeader.vue

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@
22
import ActionIconButton from '@antfu/design/components/Action/ActionIconButton.vue'
33
import DisplayBadge from '@antfu/design/components/Display/DisplayBadge.vue'
44
import LayoutToolbar from '@antfu/design/components/Layout/LayoutToolbar.vue'
5+
import { computed } from 'vue'
6+
import { connectionIndicator } from '../../../../../design/design'
57
import { connection } from '../composables/rpc'
68
import { isDark } from '../composables/scheme'
9+
10+
// The shared top-nav connection indicator (dot + label), shown only while the
11+
// connection is not live. The `static` mode badge below is a separate concept.
12+
const conn = computed(() => connectionIndicator(connection.status))
713
</script>
814

915
<template>
@@ -20,13 +26,10 @@ import { isDark } from '../composables/scheme'
2026
text="static"
2127
:color="false"
2228
/>
23-
<DisplayBadge
24-
v-else-if="connection.status !== 'connected'"
25-
class="flex items-center gap-1.5 py-1 text-xs select-none capitalize"
26-
:title="connection.error ?? undefined"
27-
:text="connection.status"
28-
:color="connection.connected ? 100 : 200"
29-
/>
29+
<span v-else-if="conn" :class="conn.class" :title="connection.error ?? undefined">
30+
<span :class="conn.dot" />
31+
{{ conn.label }}
32+
</span>
3033
</div>
3134

3235
<template #search>

plugins/git/src/client/components/dashboard.tsx

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@ import type { DevframeRpcClient } from 'devframe/client'
44
import type { PointerEvent as ReactPointerEvent } from 'react'
55
import type { GitBranches } from '../../index'
66
import { useCallback, useEffect, useRef, useState } from 'react'
7-
import { nav as navBar, navBrand, tab as tabClass, tabsList } from '../lib/design'
7+
import { connectionIndicator, nav as navBar, navBrand, tab as tabClass, tabsList } from '../lib/design'
88
import { CommitDetailsPanel } from './commit-details-panel'
99
import { ConnectionState } from './connection-state'
1010
import { LogPanel } from './log-panel'
1111
import { RpcProvider, useRpc } from './rpc-provider'
1212
import { StatusPanel } from './status-panel'
1313
import { useTheme } from './theme'
14-
import { Badge } from './ui/badge'
1514
import { IconButton } from './ui/button'
1615
import { Icon } from './ui/icon'
1716
import { useRpcResource } from './use-rpc-resource'
@@ -91,19 +90,18 @@ function Resizer({ onPointerDown, label }: { onPointerDown: (e: ReactPointerEven
9190
)
9291
}
9392

93+
// The shared top-nav connection indicator (dot + label), shown only while the
94+
// connection is not live; when connected it renders nothing.
9495
function ConnectionBadge() {
95-
const { rpc, status } = useRpc()
96-
if (status === 'error' || status === 'disconnected')
97-
return <Badge variant="destructive">{status}</Badge>
98-
if (status === 'unauthorized')
99-
return <Badge variant="warning">unauthorized</Badge>
100-
if (status === 'connecting' || !rpc)
101-
return <Badge variant="secondary">connecting…</Badge>
102-
const backend = rpc.connectionMeta.backend
96+
const { status } = useRpc()
97+
const conn = connectionIndicator(status)
98+
if (!conn)
99+
return null
103100
return (
104-
<Badge variant={backend === 'websocket' ? 'success' : 'secondary'} className="font-mono">
105-
{backend === 'websocket' ? 'live' : 'static'}
106-
</Badge>
101+
<span className={conn.class}>
102+
<span className={conn.dot} />
103+
{conn.label}
104+
</span>
107105
)
108106
}
109107

plugins/inspect/src/spa/App.vue

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
import ActionIconButton from '@antfu/design/components/Action/ActionIconButton.vue'
33
import LayoutTabs from '@antfu/design/components/Layout/LayoutTabs.vue'
44
import LayoutToolbar from '@antfu/design/components/Layout/LayoutToolbar.vue'
5-
import { onMounted, ref } from 'vue'
5+
import { computed, onMounted, ref } from 'vue'
6+
import { connectionIndicator } from '../../../../design/design'
67
import AgentSmart from './components/AgentSmart.vue'
78
import FunctionsSmart from './components/FunctionsSmart.vue'
89
import HistorySmart from './components/HistorySmart.vue'
@@ -15,6 +16,10 @@ type Tab = 'functions' | 'state' | 'agent' | 'history'
1516
const tab = ref<Tab>('functions')
1617
const { refresh, loading } = useRefresh()
1718
19+
// The shared connection indicator (dot + label) surfaces only while the
20+
// connection is not live; when connected it renders nothing.
21+
const conn = computed(() => connectionIndicator(connection.status))
22+
1823
const tabs: { value: Tab, label: string, icon: string }[] = [
1924
{ value: 'functions', label: 'Functions', icon: 'i-ph-function-duotone' },
2025
{ value: 'state', label: 'State', icon: 'i-ph-database-duotone' },
@@ -45,17 +50,10 @@ function reload(): void {
4550
</template>
4651

4752
<template #end>
48-
<div
49-
class="conn"
50-
:class="{ ok: connection.connected, err: connection.status === 'error' || connection.status === 'disconnected', warn: connection.status === 'unauthorized' }"
51-
>
52-
<span class="led" />
53-
<span v-if="connection.status === 'disconnected'">disconnected</span>
54-
<span v-else-if="connection.status === 'unauthorized'">unauthorized</span>
55-
<span v-else-if="connection.status === 'error'">error</span>
56-
<span v-else-if="connection.connected">{{ connection.backend }}</span>
57-
<span v-else>connecting…</span>
58-
</div>
53+
<span v-if="conn" :class="conn.class">
54+
<span :class="conn.dot" />
55+
{{ conn.label }}
56+
</span>
5957

6058
<ActionIconButton
6159
class="text-sm"

plugins/inspect/src/spa/style.css

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -97,34 +97,7 @@ textarea {
9797
}
9898

9999
/* ---- header: the LayoutToolbar, LayoutTabs and ActionIconButton come from
100-
`@antfu/design`; only the connection status pill below stays bespoke. ---- */
101-
.conn {
102-
display: flex;
103-
align-items: center;
104-
gap: 7px;
105-
font-size: 11.5px;
106-
color: var(--df-fg-dim);
107-
}
108-
109-
.conn .led {
110-
width: 8px;
111-
height: 8px;
112-
border-radius: 50%;
113-
background: var(--df-fg-faint);
114-
}
115-
116-
.conn.ok .led {
117-
background: var(--df-accent);
118-
box-shadow: 0 0 6px var(--df-accent);
119-
}
120-
121-
.conn.err .led {
122-
background: var(--df-danger);
123-
}
124-
125-
.conn.warn .led {
126-
background: var(--df-warning, #d9a441);
127-
}
100+
`@antfu/design`; the connection indicator comes from `design/design.ts`. ---- */
128101

129102
/* ---- body ---- */
130103
.app-body {

plugins/messages/src/client/App.vue

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { DevframeConnectionStatus, DevframeRpcClient } from 'devframe/clien
33
import type { DevframeMessageEntry } from '../types'
44
import LayoutToolbar from '@antfu/design/components/Layout/LayoutToolbar.vue'
55
import { computed, onBeforeUnmount, ref } from 'vue'
6+
import { connectionIndicator } from '../../../../design/design'
67
import MessagesView from './components/MessagesView.vue'
78
import { useMessages } from './state/messages'
89
@@ -29,6 +30,10 @@ const CONNECTION_COPY: Record<Exclude<DevframeConnectionStatus, 'connected'>, {
2930
}
3031
const connectionCopy = computed(() => status.value === 'connected' ? null : CONNECTION_COPY[status.value])
3132
33+
// The shared top-nav connection indicator (dot + label), shown only while the
34+
// connection is not live.
35+
const conn = computed(() => connectionIndicator(status.value))
36+
3237
function reload(): void {
3338
location.reload()
3439
}
@@ -81,6 +86,10 @@ async function onOpenFile(entry: DevframeMessageEntry): Promise<void> {
8186
</template>
8287

8388
<template #end>
89+
<span v-if="conn" :class="conn.class">
90+
<span :class="conn.dot" />
91+
{{ conn.label }}
92+
</span>
8493
<span v-if="state.entries.length > 0" class="badge-muted font-mono">
8594
{{ state.entries.length }}
8695
</span>

0 commit comments

Comments
 (0)