@korso/shepherd-ui 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -47
- package/dist/ShepherdRoot.js +15 -57
- package/dist/components/Dashboard.d.ts +16 -2
- package/dist/components/Dashboard.js +76 -5
- package/dist/config/ConnectAgent.js +4 -4
- package/dist/config/EmptyState.js +5 -1
- package/dist/config/Members.js +4 -4
- package/dist/config/Workspaces.js +1 -1
- package/dist/lib/{Dashboard-CG6KeFQ3.js → Dashboard-B7nioe5N.js} +529 -457
- package/dist/lib/index.d.ts +15 -1
- package/dist/lib/index.js +300 -359
- package/dist/lib/selfhost.js +1 -1
- package/dist/lib/styles.css +252 -209
- package/dist/selfhost/assets/index-COMbJ-a4.js +40 -0
- package/dist/selfhost/assets/index-CcGvL65Q.css +1 -0
- package/dist/selfhost/index.html +13 -13
- package/dist/useLandscapePolling.d.ts +7 -0
- package/dist/useLandscapePolling.js +6 -1
- package/package.json +67 -67
- package/dist/ShepherdProvider.d.ts +0 -12
- package/dist/ShepherdProvider.js +0 -33
- package/dist/ShepherdProvider.test.d.ts +0 -1
- package/dist/ShepherdProvider.test.js +0 -47
- package/dist/ShepherdRoot.routing.test.d.ts +0 -1
- package/dist/ShepherdRoot.routing.test.js +0 -61
- package/dist/ShepherdRoot.test.d.ts +0 -1
- package/dist/ShepherdRoot.test.js +0 -37
- package/dist/app/assets/index-CDOCIg6s.js +0 -11
- package/dist/app/index.html +0 -12
- package/dist/client.test.d.ts +0 -1
- package/dist/client.test.js +0 -298
- package/dist/config/ConnectAgent.test.d.ts +0 -1
- package/dist/config/ConnectAgent.test.js +0 -92
- package/dist/config/EmptyState.test.d.ts +0 -1
- package/dist/config/EmptyState.test.js +0 -32
- package/dist/config/Members.test.d.ts +0 -1
- package/dist/config/Members.test.js +0 -66
- package/dist/config/Workspaces.test.d.ts +0 -1
- package/dist/config/Workspaces.test.js +0 -90
- package/dist/index.cjs +0 -5289
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -149
- package/dist/index.js.map +0 -1
- package/dist/selfhost/assets/index-BZmImfYZ.css +0 -1
- package/dist/selfhost/assets/index-CS268KSw.js +0 -40
- package/dist/test/setup.d.ts +0 -1
- package/dist/test/setup.js +0 -2
- package/dist/views/Chat.d.ts +0 -5
- package/dist/views/Chat.js +0 -60
- package/dist/views/Chat.test.d.ts +0 -1
- package/dist/views/Chat.test.js +0 -92
- package/dist/views/Config.d.ts +0 -12
- package/dist/views/Config.js +0 -6
- package/dist/views/Tasks.d.ts +0 -5
- package/dist/views/Tasks.js +0 -75
- package/dist/views/Tasks.test.d.ts +0 -1
- package/dist/views/Tasks.test.js +0 -115
- package/dist/views/useLandscape.d.ts +0 -14
- package/dist/views/useLandscape.js +0 -74
- package/dist/views/useLandscape.test.d.ts +0 -1
- package/dist/views/useLandscape.test.js +0 -101
- package/dist/views/wallboard.d.ts +0 -46
- package/dist/views/wallboard.js +0 -209
- package/dist/views/wallboard.test.d.ts +0 -1
- package/dist/views/wallboard.test.js +0 -143
package/README.md
CHANGED
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
# @korso/shepherd-ui
|
|
2
|
-
|
|
3
|
-
The Shepherd dashboard: an **auth-agnostic** React app (Vite) that renders the
|
|
4
|
-
workspace landscape (crew, territory, active/done work, chat) and issues operator
|
|
5
|
-
actions against the hub. "Auth-agnostic" means the core holds no notion of who
|
|
6
|
-
the user is or how they authenticated — it renders coordination state and issues
|
|
7
|
-
operator actions, leaving authentication to the layer in front of it.
|
|
8
|
-
|
|
9
|
-
## Two build outputs
|
|
10
|
-
|
|
11
|
-
One source tree, two Vite builds (`npm run build` runs both):
|
|
12
|
-
|
|
13
|
-
- **`dist/lib`** (`build:lib`) — a component library for **Korso** to embed in
|
|
14
|
-
the hosted product. Exposes `<Dashboard/>`, `createShepherdClient`, the React
|
|
15
|
-
context (`ShepherdClientProvider` / `useShepherdClient`), and types via the `.`
|
|
16
|
-
export. It is **token-blind**: auth lives in the BFF in front of it. The
|
|
17
|
-
stylesheet is opt-in (`@korso/shepherd-ui/styles.css`), not auto-imported.
|
|
18
|
-
- **`dist/selfhost`** (`build:app`) — a self-contained SPA the **hub serves**
|
|
19
|
-
verbatim via `@fastify/static` (the hub resolves `../../ui/dist/selfhost/`).
|
|
20
|
-
It bundles its own CSS and mounts the token-gated root.
|
|
21
|
-
|
|
22
|
-
Both `dist/` outputs are gitignored; `npm run build` (root) and the hub Docker
|
|
23
|
-
image regenerate them.
|
|
24
|
-
|
|
25
|
-
## Two mounts
|
|
26
|
-
|
|
27
|
-
- **Self-host (the Hub).** The Hub serves `<SelfHostApp/>` (the `./selfhost`
|
|
28
|
-
export, mounted by `src/main.tsx`). This is the **one** place a team token is
|
|
29
|
-
handled — the named exception to the auth-agnostic core: it prompts for the
|
|
30
|
-
`TEAM_TOKEN`, persists it, builds a same-origin client that sends
|
|
31
|
-
`Authorization: Bearer <token>`, and re-gates on a 401. The token gate lives
|
|
32
|
-
**only** here, never in the `.` export or `<Dashboard/>`.
|
|
33
|
-
- **Hosted (Korso).** Korso imports `<Dashboard/>` + `createShepherdClient` from
|
|
34
|
-
the `.` export and supplies its own auth layer (the BFF). No token gate.
|
|
35
|
-
|
|
36
|
-
## Development
|
|
37
|
-
|
|
38
|
-
```
|
|
39
|
-
npm -w @korso/shepherd-ui run dev # Vite dev server for the self-host SPA
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
Other scripts: `build` (both outputs), `build:lib`, `build:app`, `type-check`
|
|
43
|
-
(`tsc --noEmit`), `test` (Vitest, jsdom).
|
|
44
|
-
|
|
45
|
-
## Phase note
|
|
46
|
-
|
|
47
|
-
Korso-side consumption of the `dist/lib` library is **finalized in Phase 5**.
|
|
1
|
+
# @korso/shepherd-ui
|
|
2
|
+
|
|
3
|
+
The Shepherd dashboard: an **auth-agnostic** React app (Vite) that renders the
|
|
4
|
+
workspace landscape (crew, territory, active/done work, chat) and issues operator
|
|
5
|
+
actions against the hub. "Auth-agnostic" means the core holds no notion of who
|
|
6
|
+
the user is or how they authenticated — it renders coordination state and issues
|
|
7
|
+
operator actions, leaving authentication to the layer in front of it.
|
|
8
|
+
|
|
9
|
+
## Two build outputs
|
|
10
|
+
|
|
11
|
+
One source tree, two Vite builds (`npm run build` runs both):
|
|
12
|
+
|
|
13
|
+
- **`dist/lib`** (`build:lib`) — a component library for **Korso** to embed in
|
|
14
|
+
the hosted product. Exposes `<Dashboard/>`, `createShepherdClient`, the React
|
|
15
|
+
context (`ShepherdClientProvider` / `useShepherdClient`), and types via the `.`
|
|
16
|
+
export. It is **token-blind**: auth lives in the BFF in front of it. The
|
|
17
|
+
stylesheet is opt-in (`@korso/shepherd-ui/styles.css`), not auto-imported.
|
|
18
|
+
- **`dist/selfhost`** (`build:app`) — a self-contained SPA the **hub serves**
|
|
19
|
+
verbatim via `@fastify/static` (the hub resolves `../../ui/dist/selfhost/`).
|
|
20
|
+
It bundles its own CSS and mounts the token-gated root.
|
|
21
|
+
|
|
22
|
+
Both `dist/` outputs are gitignored; `npm run build` (root) and the hub Docker
|
|
23
|
+
image regenerate them.
|
|
24
|
+
|
|
25
|
+
## Two mounts
|
|
26
|
+
|
|
27
|
+
- **Self-host (the Hub).** The Hub serves `<SelfHostApp/>` (the `./selfhost`
|
|
28
|
+
export, mounted by `src/main.tsx`). This is the **one** place a team token is
|
|
29
|
+
handled — the named exception to the auth-agnostic core: it prompts for the
|
|
30
|
+
`TEAM_TOKEN`, persists it, builds a same-origin client that sends
|
|
31
|
+
`Authorization: Bearer <token>`, and re-gates on a 401. The token gate lives
|
|
32
|
+
**only** here, never in the `.` export or `<Dashboard/>`.
|
|
33
|
+
- **Hosted (Korso).** Korso imports `<Dashboard/>` + `createShepherdClient` from
|
|
34
|
+
the `.` export and supplies its own auth layer (the BFF). No token gate.
|
|
35
|
+
|
|
36
|
+
## Development
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
npm -w @korso/shepherd-ui run dev # Vite dev server for the self-host SPA
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Other scripts: `build` (both outputs), `build:lib`, `build:app`, `type-check`
|
|
43
|
+
(`tsc --noEmit`), `test` (Vitest, jsdom).
|
|
44
|
+
|
|
45
|
+
## Phase note
|
|
46
|
+
|
|
47
|
+
Korso-side consumption of the `dist/lib` library is **finalized in Phase 5**.
|
package/dist/ShepherdRoot.js
CHANGED
|
@@ -1,76 +1,32 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect,
|
|
2
|
+
import { useCallback, useEffect, useState } from "react";
|
|
3
3
|
import { Dashboard } from "./components/Dashboard.js";
|
|
4
4
|
import { Workspaces } from "./config/Workspaces.js";
|
|
5
5
|
import { ConnectAgent } from "./config/ConnectAgent.js";
|
|
6
|
-
import { EmptyState } from "./config/EmptyState.js";
|
|
7
6
|
import { useShepherdClient } from "./context.js";
|
|
8
7
|
import { describeError } from "./client.js";
|
|
9
|
-
const VIEWS = [
|
|
10
|
-
{ id: "board", label: "Board" },
|
|
11
|
-
{ id: "config", label: "Config" },
|
|
12
|
-
];
|
|
13
8
|
export function ShepherdRoot({ hubUrl }) {
|
|
14
9
|
const client = useShepherdClient();
|
|
15
10
|
const [load, setLoad] = useState({ status: "loading" });
|
|
16
11
|
const [selectedId, setSelectedId] = useState(null);
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
const
|
|
21
|
-
// WAI-ARIA "tabs with automatic activation": Arrow keys (wrap-around),
|
|
22
|
-
// Home, End change the active view AND move focus to the matching tab.
|
|
23
|
-
const onTabKeyDown = useCallback((e, index) => {
|
|
24
|
-
const last = VIEWS.length - 1;
|
|
25
|
-
let next = null;
|
|
26
|
-
switch (e.key) {
|
|
27
|
-
case "ArrowRight":
|
|
28
|
-
next = index === last ? 0 : index + 1;
|
|
29
|
-
break;
|
|
30
|
-
case "ArrowLeft":
|
|
31
|
-
next = index === 0 ? last : index - 1;
|
|
32
|
-
break;
|
|
33
|
-
case "Home":
|
|
34
|
-
next = 0;
|
|
35
|
-
break;
|
|
36
|
-
case "End":
|
|
37
|
-
next = last;
|
|
38
|
-
break;
|
|
39
|
-
default:
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
e.preventDefault();
|
|
43
|
-
setView(VIEWS[next].id);
|
|
44
|
-
tabRefs.current[next]?.focus();
|
|
45
|
-
}, []);
|
|
46
|
-
// The landing view (Config vs Board) is decided exactly once, on the first
|
|
47
|
-
// resolved list (isInitial), so a later refresh (after create/join) never
|
|
48
|
-
// re-routes the user off whatever tab they're on.
|
|
49
|
-
const fetchWorkspaces = useCallback(async (isInitial) => {
|
|
12
|
+
// Loads the workspace list and keeps the selection valid: a still-present
|
|
13
|
+
// selection is preserved (so a create/join refresh never yanks the user off
|
|
14
|
+
// their current workspace), otherwise it falls back to the first workspace.
|
|
15
|
+
const fetchWorkspaces = useCallback(async () => {
|
|
50
16
|
try {
|
|
51
17
|
const res = await client.listWorkspaces();
|
|
52
18
|
setLoad({ status: "ready", workspaces: res.workspaces });
|
|
53
|
-
// Keep the current selection if it still exists; else pick the first.
|
|
54
19
|
setSelectedId((prev) => prev && res.workspaces.some((w) => w.id === prev)
|
|
55
20
|
? prev
|
|
56
21
|
: (res.workspaces[0]?.id ?? null));
|
|
57
|
-
if (isInitial) {
|
|
58
|
-
setView(res.workspaces.length === 0 ? "config" : "board");
|
|
59
|
-
}
|
|
60
22
|
}
|
|
61
23
|
catch (err) {
|
|
62
|
-
setLoad({
|
|
63
|
-
status: "error",
|
|
64
|
-
message: describeError(err),
|
|
65
|
-
});
|
|
24
|
+
setLoad({ status: "error", message: describeError(err) });
|
|
66
25
|
}
|
|
67
26
|
}, [client]);
|
|
68
|
-
// Initial load + landing decision (runs once on mount).
|
|
69
27
|
useEffect(() => {
|
|
70
|
-
void fetchWorkspaces(
|
|
71
|
-
|
|
72
|
-
}, []);
|
|
73
|
-
const refresh = useCallback(() => fetchWorkspaces(false), [fetchWorkspaces]);
|
|
28
|
+
void fetchWorkspaces();
|
|
29
|
+
}, [fetchWorkspaces]);
|
|
74
30
|
const workspaces = load.status === "ready" ? load.workspaces : [];
|
|
75
31
|
const selected = workspaces.find((w) => w.id === selectedId) ?? null;
|
|
76
32
|
if (load.status === "loading") {
|
|
@@ -80,9 +36,11 @@ export function ShepherdRoot({ hubUrl }) {
|
|
|
80
36
|
return (_jsx("div", { className: "shepherd-root", children: _jsx("p", { role: "alert", children: load.message }) }));
|
|
81
37
|
}
|
|
82
38
|
const hasWorkspace = workspaces.length > 0;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
39
|
+
// The Config tab's content. Rendered by <Dashboard> in its `config` panel.
|
|
40
|
+
// ConnectAgent only appears once a workspace is selected (nothing to connect
|
|
41
|
+
// an agent to otherwise).
|
|
42
|
+
const configSection = (_jsxs("section", { className: "shepherd-config", "aria-labelledby": "config-heading", children: [_jsx("h2", { id: "config-heading", children: "Configuration" }), _jsx(Workspaces, { workspaces: workspaces, selected: selected, onChanged: () => {
|
|
43
|
+
void fetchWorkspaces();
|
|
44
|
+
}, onSelect: setSelectedId }), selected && _jsx(ConnectAgent, { workspaceId: selected.id, hubUrl: hubUrl })] }));
|
|
45
|
+
return (_jsx("div", { className: "shepherd-root", children: _jsx(Dashboard, { workspaceId: selected?.id, config: configSection, hasWorkspace: hasWorkspace }) }));
|
|
88
46
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ReactElement } from "react";
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
2
|
/** Props for {@link Dashboard}. The client comes from context. */
|
|
3
3
|
export interface DashboardProps {
|
|
4
4
|
/**
|
|
@@ -8,6 +8,20 @@ export interface DashboardProps {
|
|
|
8
8
|
* aliases, so a no-id render is unchanged.
|
|
9
9
|
*/
|
|
10
10
|
workspaceId?: string;
|
|
11
|
+
/**
|
|
12
|
+
* The hosted-shell management view. When supplied, a third `Config` tab is
|
|
13
|
+
* shown beside `Tasks`/`Chat` and this node renders in its panel. Omitting it
|
|
14
|
+
* (the self-host case) keeps the board a plain two-tab Tasks/Chat wallboard.
|
|
15
|
+
*/
|
|
16
|
+
config?: ReactNode;
|
|
17
|
+
/**
|
|
18
|
+
* Whether the account currently belongs to a workspace. Only meaningful in the
|
|
19
|
+
* hosted shell (paired with {@link config}). When `false`, the Tasks/Chat
|
|
20
|
+
* panels show an {@link EmptyState} prompt instead of a board, the board does
|
|
21
|
+
* not poll, and the view lands on Config. Defaults to "yes" (self-host always
|
|
22
|
+
* has its implicit team workspace).
|
|
23
|
+
*/
|
|
24
|
+
hasWorkspace?: boolean;
|
|
11
25
|
}
|
|
12
26
|
/**
|
|
13
27
|
* The Shepherd wallboard shell. Composes {@link useLandscapePolling} (which reads
|
|
@@ -31,4 +45,4 @@ export interface DashboardProps {
|
|
|
31
45
|
*
|
|
32
46
|
* @returns The dashboard element.
|
|
33
47
|
*/
|
|
34
|
-
export declare function Dashboard({ workspaceId }?: DashboardProps): ReactElement;
|
|
48
|
+
export declare function Dashboard({ workspaceId, config, hasWorkspace, }?: DashboardProps): ReactElement;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { useLandscapePolling } from "../useLandscapePolling.js";
|
|
4
4
|
import { defaultRepo, distinctRepos, matchesRepo } from "../logic.js";
|
|
@@ -8,6 +8,7 @@ import { ActiveList } from "./ActiveList.js";
|
|
|
8
8
|
import { DoneList } from "./DoneList.js";
|
|
9
9
|
import { Chat } from "./Chat.js";
|
|
10
10
|
import { Composer } from "./Composer.js";
|
|
11
|
+
import { EmptyState } from "../config/EmptyState.js";
|
|
11
12
|
/**
|
|
12
13
|
* localStorage keys + page size, ported verbatim from the state block of
|
|
13
14
|
* packages/hub/public/app.js (POLL_MS lives in the hook).
|
|
@@ -126,9 +127,73 @@ function computeCounts(snapshot) {
|
|
|
126
127
|
*
|
|
127
128
|
* @returns The dashboard element.
|
|
128
129
|
*/
|
|
129
|
-
export function Dashboard({ workspaceId } = {}) {
|
|
130
|
-
const
|
|
131
|
-
|
|
130
|
+
export function Dashboard({ workspaceId, config, hasWorkspace, } = {}) {
|
|
131
|
+
const hasConfig = config != null;
|
|
132
|
+
// Only the hosted shell with an explicit empty account suppresses the board;
|
|
133
|
+
// self-host (hasWorkspace undefined) always has its implicit team workspace.
|
|
134
|
+
const noWorkspace = hasWorkspace === false;
|
|
135
|
+
const { snapshot, status, lastUpdatedMs, refresh } = useLandscapePolling({
|
|
136
|
+
workspaceId,
|
|
137
|
+
// A no-workspace board has nothing to poll; keep it off the hub.
|
|
138
|
+
enabled: !noWorkspace,
|
|
139
|
+
});
|
|
140
|
+
const [activeTab, setActiveTab] = useState(() => {
|
|
141
|
+
// No workspace yet → land on Config, where create/join lives.
|
|
142
|
+
if (hasConfig && noWorkspace)
|
|
143
|
+
return "config";
|
|
144
|
+
const stored = readStored(TAB_KEY);
|
|
145
|
+
if (stored === "chat")
|
|
146
|
+
return "chat";
|
|
147
|
+
if (stored === "config" && hasConfig)
|
|
148
|
+
return "config";
|
|
149
|
+
return "tasks";
|
|
150
|
+
});
|
|
151
|
+
// The flat tab strip: Tasks/Chat always, Config only when the host supplies it.
|
|
152
|
+
const tabs = hasConfig
|
|
153
|
+
? [
|
|
154
|
+
{ id: "tasks", label: "Tasks" },
|
|
155
|
+
{ id: "chat", label: "Chat" },
|
|
156
|
+
{ id: "config", label: "Config" },
|
|
157
|
+
]
|
|
158
|
+
: [
|
|
159
|
+
{ id: "tasks", label: "Tasks" },
|
|
160
|
+
{ id: "chat", label: "Chat" },
|
|
161
|
+
];
|
|
162
|
+
// Roving-tabindex refs for the tablist: each tab registers by index so
|
|
163
|
+
// arrow/Home/End navigation can move DOM focus to the target tab.
|
|
164
|
+
const tabRefs = useRef([]);
|
|
165
|
+
// WAI-ARIA "tabs with automatic activation": Arrow keys (wrap-around), Home,
|
|
166
|
+
// End change the active view AND move focus to the matching tab.
|
|
167
|
+
const onTabKeyDown = useCallback((e, index) => {
|
|
168
|
+
const last = tabs.length - 1;
|
|
169
|
+
let next = null;
|
|
170
|
+
switch (e.key) {
|
|
171
|
+
case "ArrowRight":
|
|
172
|
+
next = index === last ? 0 : index + 1;
|
|
173
|
+
break;
|
|
174
|
+
case "ArrowLeft":
|
|
175
|
+
next = index === 0 ? last : index - 1;
|
|
176
|
+
break;
|
|
177
|
+
case "Home":
|
|
178
|
+
next = 0;
|
|
179
|
+
break;
|
|
180
|
+
case "End":
|
|
181
|
+
next = last;
|
|
182
|
+
break;
|
|
183
|
+
default:
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
e.preventDefault();
|
|
187
|
+
const target = tabs[next];
|
|
188
|
+
if (!target)
|
|
189
|
+
return;
|
|
190
|
+
onTab(target.id);
|
|
191
|
+
tabRefs.current[next]?.focus();
|
|
192
|
+
},
|
|
193
|
+
// `tabs` is rebuilt each render but its identity only matters by length,
|
|
194
|
+
// which is stable for a given `hasConfig`; `onTab` is stable.
|
|
195
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
196
|
+
[hasConfig]);
|
|
132
197
|
// `null` = not yet chosen (derive a default on first render); "__all__" = All
|
|
133
198
|
// repos; else a specific repo. Seeded from storage exactly like app.js.
|
|
134
199
|
const [selectedRepo, setSelectedRepo] = useState(() => readStored(REPO_KEY));
|
|
@@ -205,5 +270,11 @@ export function Dashboard({ workspaceId } = {}) {
|
|
|
205
270
|
// equals the `active` vital here (same filter), but doneCount is distinct.
|
|
206
271
|
const activeCount = tasks.filter((t) => t.status === "active" && matchesRepo(t, effectiveRepo)).length;
|
|
207
272
|
const doneCount = tasks.filter((t) => t.status !== "active" && matchesRepo(t, effectiveRepo)).length;
|
|
208
|
-
|
|
273
|
+
// The poll-driven header chrome (repo filter, vitals, status, freshness) only
|
|
274
|
+
// makes sense for a live board — hide it on the Config tab and when there is
|
|
275
|
+
// no workspace to poll. The brand and the tab strip stay in every state.
|
|
276
|
+
const showBoardChrome = !noWorkspace && activeTab !== "config";
|
|
277
|
+
return (_jsxs("div", { id: "board", children: [_jsxs("header", { children: [_jsx("h1", { className: "brand", style: { margin: 0, font: "inherit" }, children: "Shepherd" }), showBoardChrome && (_jsxs(_Fragment, { children: [_jsx(RepoSelect, { repos: repos, counts: counts, selected: effectiveRepo, onSelect: onSelectRepo }), _jsxs("span", { className: "vitals", children: [_jsx("b", { id: "vitals-online", children: online }), " online · ", _jsx("b", { id: "vitals-active", children: active }), " active"] })] })), _jsx("span", { className: "grow" }), showBoardChrome && (_jsxs(_Fragment, { children: [_jsx("span", { id: "status", className: "status" + (statusView.kind ? ` status--${statusView.kind}` : ""), children: statusView.text }), _jsx("span", { id: "freshness", className: "freshness", children: freshness })] })), _jsx("nav", { className: "tabs", role: "tablist", "aria-label": "Shepherd views", children: tabs.map(({ id, label }, index) => (_jsx("button", { ref: (el) => {
|
|
278
|
+
tabRefs.current[index] = el;
|
|
279
|
+
}, className: "tab" + (activeTab === id ? " tab--active" : ""), "data-tab": id, type: "button", role: "tab", id: `tab-${id}`, "aria-controls": `panel-${id}`, "aria-selected": activeTab === id, tabIndex: activeTab === id ? 0 : -1, onClick: () => onTab(id), onKeyDown: (e) => onTabKeyDown(e, index), children: label }, id))) })] }), _jsx("section", { id: "panel-tasks", role: "tabpanel", "aria-labelledby": "tab-tasks", hidden: activeTab !== "tasks", children: noWorkspace ? (_jsx(EmptyState, { onGetStarted: () => onTab("config") })) : (_jsxs(_Fragment, { children: [_jsx(Crew, { agents: agents, tasks: tasks, selectedRepo: effectiveRepo }), _jsxs("div", { className: "board", children: [_jsxs("div", { className: "col", children: [_jsxs("div", { className: "colhead", children: [_jsx("h2", { children: "Active" }), _jsx("span", { className: "n", id: "active-count", children: activeCount })] }), _jsx(ActiveList, { tasks: tasks, nowMs: nowMs, selectedRepo: effectiveRepo })] }), _jsx("div", { className: "board__rule" }), _jsxs("div", { className: "col", children: [_jsxs("div", { className: "colhead", children: [_jsx("h2", { children: "Done" }), _jsx("span", { className: "n", id: "done-count", children: doneCount })] }), _jsx(DoneList, { tasks: tasks, nowMs: nowMs, selectedRepo: effectiveRepo, doneShown: doneShown, onLoadMore: onLoadMore })] })] })] })) }), _jsx("section", { id: "panel-chat", role: "tabpanel", "aria-labelledby": "tab-chat", hidden: activeTab !== "chat", children: noWorkspace ? (_jsx(EmptyState, { onGetStarted: () => onTab("config") })) : (_jsxs("div", { className: "chat-wrap", children: [_jsx(Chat, { announcements: announcements, selectedRepo: effectiveRepo, nowMs: nowMs }), _jsx(Composer, { agents: agents, selectedRepo: effectiveRepo, workspaceId: workspaceId, onSent: refresh })] })) }), hasConfig && (_jsx("section", { id: "panel-config", role: "tabpanel", "aria-labelledby": "tab-config", hidden: activeTab !== "config", children: config }))] }));
|
|
209
280
|
}
|
|
@@ -112,8 +112,8 @@ export function ConnectAgent({ workspaceId, hubUrl }) {
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
const command = installCommand(tool, directHubUrl, rawToken ?? TOKEN_PLACEHOLDER);
|
|
115
|
-
return (_jsxs("section", { className: "shepherd-connect-agent", "aria-labelledby": headingId, children: [_jsx("h3", { id: headingId, children: "Connect your agent" }), _jsx("label", { htmlFor: "connect-tool", children: "Tool" }), _jsx("select", { id: "connect-tool", value: tool, onChange: (e) => setTool(e.target.value), children: TOOLS.map((t) => (_jsx("option", { value: t.id, children: t.label }, t.id))) }), _jsxs("div", { className: "generate", children: [_jsx("label", { htmlFor: "token-name", children: "Token name (optional)" }), _jsx("input", { id: "token-name", type: "text", value: name, onChange: (e) => setName(e.target.value), placeholder: "e.g. laptop" }), _jsx("button", { type: "button", onClick: () => void generate(), disabled: busy, children: "Generate token" })] }), error && _jsx("p", { role: "alert", children: error }), status && _jsx("p", { role: "status", children: status }), rawToken && (_jsx("p", { className: "token-once", role: "status", children: "Copy this token now \u2014 it won't be shown again." })), _jsx("pre", { "data-testid": "install-command", children: command }), _jsx("h4", { children: "Existing tokens" }), loading ? (_jsx("p", { role: "status", children: "Loading\u2026" })) : tokens.length === 0 ? (_jsx("p", { children: "No tokens yet." })) : (_jsx("ul", { children: tokens.map((t) => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
return (_jsxs("section", { className: "shepherd-connect-agent", "aria-labelledby": headingId, children: [_jsxs("div", { className: "card-head", children: [_jsx("h3", { id: headingId, children: "Connect your agent" }), _jsx("p", { className: "card-sub", children: "Generate a token, then paste the command into your coding tool." })] }), _jsxs("div", { className: "card-body", children: [_jsxs("div", { className: "field", children: [_jsx("label", { htmlFor: "connect-tool", children: "Tool" }), _jsx("select", { id: "connect-tool", value: tool, onChange: (e) => setTool(e.target.value), children: TOOLS.map((t) => (_jsx("option", { value: t.id, children: t.label }, t.id))) })] }), _jsxs("div", { className: "field generate", children: [_jsx("label", { htmlFor: "token-name", children: "Token name (optional)" }), _jsxs("div", { className: "field__row", children: [_jsx("input", { id: "token-name", type: "text", value: name, onChange: (e) => setName(e.target.value), placeholder: "e.g. laptop" }), _jsx("button", { type: "button", onClick: () => void generate(), disabled: busy, children: "Generate token" })] })] }), error && _jsx("p", { role: "alert", children: error }), status && _jsx("p", { role: "status", children: status }), rawToken && (_jsx("p", { className: "token-once", role: "status", children: "Copy this token now \u2014 it won't be shown again." })), _jsx("pre", { "data-testid": "install-command", children: command }), _jsx("h4", { children: "Existing tokens" }), loading ? (_jsx("p", { role: "status", children: "Loading\u2026" })) : tokens.length === 0 ? (_jsx("p", { children: "No tokens yet." })) : (_jsx("ul", { children: tokens.map((t) => {
|
|
116
|
+
const name = t.name ?? t.id;
|
|
117
|
+
return (_jsxs("li", { children: [_jsx("span", { children: name }), t.revokedAt ? (_jsx("span", { className: "revoked", children: "revoked" })) : (_jsx("button", { type: "button", "aria-label": `Revoke token ${name}`, onClick: () => void revoke(t.id), disabled: revokingId === t.id, children: "Revoke" }))] }, t.id));
|
|
118
|
+
}) }))] })] }));
|
|
119
119
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId } from "react";
|
|
2
3
|
export function EmptyState({ title = "No workspace yet", children, onGetStarted, ctaLabel = "Go to Config", }) {
|
|
3
|
-
|
|
4
|
+
// A per-instance id so two EmptyStates (e.g. the Tasks and Chat panels of a
|
|
5
|
+
// no-workspace board) never collide on a duplicate `empty-state-heading` id.
|
|
6
|
+
const headingId = useId();
|
|
7
|
+
return (_jsxs("section", { className: "shepherd-empty-state", "aria-labelledby": headingId, children: [_jsx("h2", { id: headingId, children: title }), _jsx("p", { children: children ?? "Create a workspace or join one with an invite code to get started." }), onGetStarted && (_jsx("button", { type: "button", onClick: onGetStarted, children: ctaLabel }))] }));
|
|
4
8
|
}
|
package/dist/config/Members.js
CHANGED
|
@@ -76,8 +76,8 @@ export function Members({ workspaceId, refreshKey = 0, canRemove = false, onLeft
|
|
|
76
76
|
setLeaving(false);
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
-
return (_jsxs("section", { className: "shepherd-members", "aria-labelledby": headingId, children: [_jsx("h3", { id: headingId, children: "Members" }), error && _jsx("p", { role: "alert", children: error }), status && _jsx("p", { role: "status", children: status }), loading ? (_jsx("p", { role: "status", children: "Loading\u2026" })) : members.length === 0 ? (_jsx("p", { children: "No members." })) : (_jsx("ul", { children: members.map((m) => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
return (_jsxs("section", { className: "shepherd-members", "aria-labelledby": headingId, children: [_jsx("div", { className: "card-head", children: _jsx("h3", { id: headingId, children: "Members" }) }), _jsxs("div", { className: "card-body", children: [error && _jsx("p", { role: "alert", children: error }), status && _jsx("p", { role: "status", children: status }), loading ? (_jsx("p", { role: "status", children: "Loading\u2026" })) : members.length === 0 ? (_jsx("p", { children: "No members." })) : (_jsx("ul", { children: members.map((m) => {
|
|
80
|
+
const display = m.displayName ?? m.githubLogin ?? m.accountId;
|
|
81
|
+
return (_jsxs("li", { children: [_jsx("span", { children: display }), _jsx("span", { className: "role", children: m.role }), canRemove && (_jsx("button", { type: "button", "aria-label": `Remove ${display}`, onClick: () => void remove(m.accountId, display), disabled: removingId === m.accountId, children: "Remove" }))] }, m.accountId));
|
|
82
|
+
}) })), _jsx("button", { type: "button", onClick: () => void leave(), disabled: leaving, children: "Leave workspace" })] })] }));
|
|
83
83
|
}
|
|
@@ -92,5 +92,5 @@ export function Workspaces({ workspaces, selected, onChanged, onSelect }) {
|
|
|
92
92
|
const joinLink = invite
|
|
93
93
|
? `${window.location.origin}/join/${encodeURIComponent(invite.code)}`
|
|
94
94
|
: null;
|
|
95
|
-
return (_jsxs("section", { className: "shepherd-workspaces", "aria-labelledby": headingId, children: [_jsx("h3", { id: headingId, children: "Workspace management" }), error && _jsx("p", { role: "alert", children: error }), workspaces.length > 1 && onSelect && (_jsxs("div", { className: "switcher", children: [_jsx("label", { htmlFor: "ws-switcher", children: "Active workspace" }), _jsx("select", { id: "ws-switcher", value: selected?.id ?? "", onChange: (e) => onSelect(e.target.value), children: workspaces.map((w) => (_jsx("option", { value: w.id, children: w.name }, w.id))) })] })), _jsxs("div", { className: "create", children: [_jsx("label", { htmlFor: "ws-name", children: "Workspace name" }), _jsx("input", { id: "ws-name", type: "text", value: name, onChange: (e) => setName(e.target.value) }), _jsx("button", { type: "button", onClick: () => void create(), disabled: busy, children: "Create workspace" })] }), _jsxs("div", { className: "join", children: [_jsx("label", { htmlFor: "invite-code", children: "Invite code" }), _jsx("input", { id: "invite-code", type: "text", value: code, onChange: (e) => setCode(e.target.value) }), _jsx("button", { type: "button", onClick: () => void join(), disabled: busy, children: "Join" })] }), isAdmin && selected && (_jsxs("div", { className: "admin", children: [_jsxs("div", { className: "invites", children: [_jsx("button", { type: "button", onClick: () => void createInvite(), disabled: busy, children: "Create invite" }), invite && (_jsxs("div", { className: "invite-result", children: [_jsx("code", { children: invite.code }), _jsx("a", { href: joinLink ?? undefined, children: joinLink }), _jsxs("span", { className: "invite-uses", children: [invite.useCount, " / ", invite.maxUses, " uses"] }), _jsx("button", { type: "button", onClick: () => void revokeInvite(), disabled: busy, children: "Revoke invite" })] }))] }), _jsx(Members, { workspaceId: selected.id, refreshKey: membersRefreshKey, onLeft: onChanged, canRemove: true })] }))] }));
|
|
95
|
+
return (_jsxs("section", { className: "shepherd-workspaces", "aria-labelledby": headingId, children: [_jsxs("div", { className: "card-head", children: [_jsx("h3", { id: headingId, children: "Workspace management" }), _jsx("p", { className: "card-sub", children: "Create a workspace to get started, or join an existing one with an invite code." })] }), _jsxs("div", { className: "card-body", children: [error && _jsx("p", { role: "alert", children: error }), workspaces.length > 1 && onSelect && (_jsxs("div", { className: "field switcher", children: [_jsx("label", { htmlFor: "ws-switcher", children: "Active workspace" }), _jsx("select", { id: "ws-switcher", value: selected?.id ?? "", onChange: (e) => onSelect(e.target.value), children: workspaces.map((w) => (_jsx("option", { value: w.id, children: w.name }, w.id))) })] })), _jsxs("div", { className: "field create", children: [_jsx("label", { htmlFor: "ws-name", children: "Workspace name" }), _jsx("p", { className: "helper", children: "Start a fresh workspace \u2014 you'll be its admin." }), _jsxs("div", { className: "field__row", children: [_jsx("input", { id: "ws-name", type: "text", placeholder: "e.g. Acme Engineering", value: name, onChange: (e) => setName(e.target.value) }), _jsx("button", { type: "button", onClick: () => void create(), disabled: busy, children: "Create workspace" })] })] }), _jsxs("div", { className: "field join", children: [_jsx("label", { htmlFor: "invite-code", children: "Invite code" }), _jsx("p", { className: "helper", children: "Paste an invite code someone shared to join their workspace." }), _jsxs("div", { className: "field__row", children: [_jsx("input", { id: "invite-code", type: "text", placeholder: "Invite code", value: code, onChange: (e) => setCode(e.target.value) }), _jsx("button", { type: "button", onClick: () => void join(), disabled: busy, children: "Join" })] })] }), isAdmin && selected && (_jsxs("div", { className: "admin", children: [_jsxs("div", { className: "invites", children: [_jsx("button", { type: "button", onClick: () => void createInvite(), disabled: busy, children: "Create invite" }), invite && (_jsxs("div", { className: "invite-result", children: [_jsx("code", { children: invite.code }), _jsx("a", { href: joinLink ?? undefined, children: joinLink }), _jsxs("span", { className: "invite-uses", children: [invite.useCount, " / ", invite.maxUses, " uses"] }), _jsx("button", { type: "button", onClick: () => void revokeInvite(), disabled: busy, children: "Revoke invite" })] }))] }), _jsx(Members, { workspaceId: selected.id, refreshKey: membersRefreshKey, onLeft: onChanged, canRemove: true })] }))] })] }));
|
|
96
96
|
}
|