code-coordinator 0.5.46__py3-none-any.whl
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.
- code_coordinator-0.5.46.dist-info/METADATA +625 -0
- code_coordinator-0.5.46.dist-info/RECORD +295 -0
- code_coordinator-0.5.46.dist-info/WHEEL +5 -0
- code_coordinator-0.5.46.dist-info/entry_points.txt +2 -0
- code_coordinator-0.5.46.dist-info/licenses/LICENSE +110 -0
- code_coordinator-0.5.46.dist-info/top_level.txt +1 -0
- coord/__init__.py +176 -0
- coord/_board_mapping.py +229 -0
- coord/acceptance.py +468 -0
- coord/acceptance_drivers.py +632 -0
- coord/agent.py +7517 -0
- coord/agent_app.py +1555 -0
- coord/agent_update.py +417 -0
- coord/agents/opencode/.gitignore +13 -0
- coord/agents/opencode/agents/work.md +129 -0
- coord/agents/opencode/routing.jsonc +49 -0
- coord/audit.py +301 -0
- coord/auto_loop.py +1440 -0
- coord/board_bool_guard.py +72 -0
- coord/board_service.py +141 -0
- coord/board_wire.py +309 -0
- coord/brain.py +581 -0
- coord/branch_model.py +214 -0
- coord/cargo_cache.py +258 -0
- coord/ci_github.py +386 -0
- coord/ci_store.py +560 -0
- coord/claim.py +353 -0
- coord/cli.py +454 -0
- coord/client.py +610 -0
- coord/commands/__init__.py +1 -0
- coord/commands/_common.py +329 -0
- coord/commands/acceptance.py +916 -0
- coord/commands/agent_ops.py +1339 -0
- coord/commands/audit.py +131 -0
- coord/commands/chat.py +320 -0
- coord/commands/dispatch.py +1780 -0
- coord/commands/dispatch_workers.py +4894 -0
- coord/commands/drive.py +616 -0
- coord/commands/drive_queue.py +1203 -0
- coord/commands/gate_a.py +217 -0
- coord/commands/gates.py +89 -0
- coord/commands/issues.py +681 -0
- coord/commands/lifecycle.py +513 -0
- coord/commands/merge.py +1900 -0
- coord/commands/milestone.py +2081 -0
- coord/commands/plan_followup.py +1243 -0
- coord/commands/plans.py +156 -0
- coord/commands/release.py +2232 -0
- coord/commands/report.py +341 -0
- coord/commands/review.py +1523 -0
- coord/commands/scorecard.py +252 -0
- coord/commands/sessions.py +1930 -0
- coord/commands/setup.py +576 -0
- coord/commands/status.py +2089 -0
- coord/commands/terminal.py +385 -0
- coord/commands/test_gate.py +775 -0
- coord/commands/tui.py +288 -0
- coord/comments.py +718 -0
- coord/config.py +3032 -0
- coord/conflict_fix.py +633 -0
- coord/dao.py +483 -0
- coord/dashboard/__init__.py +0 -0
- coord/dashboard/fixture.py +376 -0
- coord/dashboard/index.html +658 -0
- coord/dashboard/server.py +1894 -0
- coord/dashboard/terminal.py +382 -0
- coord/dashboard/webapp/.gitignore +9 -0
- coord/dashboard/webapp/components.json +17 -0
- coord/dashboard/webapp/dist/assets/Gallery-da3qNiIw.js +71 -0
- coord/dashboard/webapp/dist/assets/Terminal-9CEnUXvW.css +32 -0
- coord/dashboard/webapp/dist/assets/Terminal-skVFCxPU.js +63 -0
- coord/dashboard/webapp/dist/assets/index-DltfZR5f.js +184 -0
- coord/dashboard/webapp/dist/assets/index-Dq4kwTdw.css +1 -0
- coord/dashboard/webapp/dist/assets/workbox-window.prod.es5-BqEJf4Xk.js +2 -0
- coord/dashboard/webapp/dist/icons/icon-192.png +0 -0
- coord/dashboard/webapp/dist/icons/icon-512.png +0 -0
- coord/dashboard/webapp/dist/icons/icon.svg +5 -0
- coord/dashboard/webapp/dist/index.html +38 -0
- coord/dashboard/webapp/dist/manifest.webmanifest +1 -0
- coord/dashboard/webapp/dist/sw.js +1 -0
- coord/dashboard/webapp/dist/workbox-e4022e15.js +1 -0
- coord/dashboard/webapp/e2e/available-gates-terminal.spec.ts +75 -0
- coord/dashboard/webapp/e2e/deep-link.spec.ts +172 -0
- coord/dashboard/webapp/e2e/fixtureServer.ts +155 -0
- coord/dashboard/webapp/e2e/live-update-fixture.spec.ts +113 -0
- coord/dashboard/webapp/e2e/realtime.spec.ts +238 -0
- coord/dashboard/webapp/e2e/shell.spec.ts +309 -0
- coord/dashboard/webapp/e2e/smoke.spec.ts +191 -0
- coord/dashboard/webapp/e2e/terminal.spec.ts +420 -0
- coord/dashboard/webapp/e2e/theme.spec.ts +138 -0
- coord/dashboard/webapp/eslint.config.js +20 -0
- coord/dashboard/webapp/index.html +37 -0
- coord/dashboard/webapp/node_modules/flatted/python/flatted.py +144 -0
- coord/dashboard/webapp/package-lock.json +10584 -0
- coord/dashboard/webapp/package.json +63 -0
- coord/dashboard/webapp/playwright.acceptance.config.ts +166 -0
- coord/dashboard/webapp/playwright.config.ts +93 -0
- coord/dashboard/webapp/postcss.config.js +6 -0
- coord/dashboard/webapp/public/icons/icon-192.png +0 -0
- coord/dashboard/webapp/public/icons/icon-512.png +0 -0
- coord/dashboard/webapp/public/icons/icon.svg +5 -0
- coord/dashboard/webapp/src/App.tsx +140 -0
- coord/dashboard/webapp/src/api/client.ts +199 -0
- coord/dashboard/webapp/src/api/generated.ts +176 -0
- coord/dashboard/webapp/src/components/ConnectionBadge.tsx +52 -0
- coord/dashboard/webapp/src/components/Detail.tsx +800 -0
- coord/dashboard/webapp/src/components/Gallery.tsx +341 -0
- coord/dashboard/webapp/src/components/Home.tsx +435 -0
- coord/dashboard/webapp/src/components/MobileKeyBar.tsx +280 -0
- coord/dashboard/webapp/src/components/PanelHeader.tsx +59 -0
- coord/dashboard/webapp/src/components/PipelineCard.tsx +168 -0
- coord/dashboard/webapp/src/components/SessionCard.tsx +99 -0
- coord/dashboard/webapp/src/components/SessionDetail.tsx +140 -0
- coord/dashboard/webapp/src/components/SessionsList.tsx +81 -0
- coord/dashboard/webapp/src/components/Terminal.tsx +376 -0
- coord/dashboard/webapp/src/components/__tests__/ConnectionBadge.test.tsx +81 -0
- coord/dashboard/webapp/src/components/__tests__/Detail.test.tsx +680 -0
- coord/dashboard/webapp/src/components/__tests__/Gallery.test.tsx +83 -0
- coord/dashboard/webapp/src/components/__tests__/Home.test.tsx +271 -0
- coord/dashboard/webapp/src/components/__tests__/MobileKeyBar.test.tsx +197 -0
- coord/dashboard/webapp/src/components/__tests__/PipelineCard.test.tsx +143 -0
- coord/dashboard/webapp/src/components/__tests__/SessionCard.test.tsx +106 -0
- coord/dashboard/webapp/src/components/__tests__/Terminal.test.tsx +504 -0
- coord/dashboard/webapp/src/components/ui/badge.tsx +41 -0
- coord/dashboard/webapp/src/components/ui/button.tsx +54 -0
- coord/dashboard/webapp/src/components/ui/card.tsx +55 -0
- coord/dashboard/webapp/src/components/ui/dialog.tsx +99 -0
- coord/dashboard/webapp/src/components/ui/dropdown-menu.tsx +189 -0
- coord/dashboard/webapp/src/components/ui/empty-state.tsx +35 -0
- coord/dashboard/webapp/src/components/ui/sheet.tsx +123 -0
- coord/dashboard/webapp/src/components/ui/skeleton.tsx +9 -0
- coord/dashboard/webapp/src/components/ui/tabs.tsx +55 -0
- coord/dashboard/webapp/src/components/ui/theme-provider.tsx +78 -0
- coord/dashboard/webapp/src/components/ui/theme-toggle.tsx +20 -0
- coord/dashboard/webapp/src/components/ui/toast.tsx +123 -0
- coord/dashboard/webapp/src/components/ui/toaster.tsx +30 -0
- coord/dashboard/webapp/src/components/ui/tooltip.tsx +26 -0
- coord/dashboard/webapp/src/components/ui/use-toast.ts +134 -0
- coord/dashboard/webapp/src/index.css +210 -0
- coord/dashboard/webapp/src/lib/pipeline.ts +29 -0
- coord/dashboard/webapp/src/lib/utils.ts +6 -0
- coord/dashboard/webapp/src/main.tsx +46 -0
- coord/dashboard/webapp/src/realtime/RealtimeProvider.tsx +112 -0
- coord/dashboard/webapp/src/realtime/__tests__/RealtimeProvider.test.tsx +189 -0
- coord/dashboard/webapp/src/realtime/__tests__/connection.test.ts +255 -0
- coord/dashboard/webapp/src/realtime/connection.ts +227 -0
- coord/dashboard/webapp/src/realtime/events.ts +100 -0
- coord/dashboard/webapp/src/routes/__tests__/paths.test.ts +92 -0
- coord/dashboard/webapp/src/routes/paths.ts +92 -0
- coord/dashboard/webapp/src/shell/ActivityRail.tsx +335 -0
- coord/dashboard/webapp/src/shell/AppShell.tsx +276 -0
- coord/dashboard/webapp/src/shell/ComingSoon.tsx +33 -0
- coord/dashboard/webapp/src/shell/EmptyDetail.tsx +26 -0
- coord/dashboard/webapp/src/shell/RouteNotFound.tsx +33 -0
- coord/dashboard/webapp/src/shell/ShellLayout.tsx +147 -0
- coord/dashboard/webapp/src/shell/StatusBar.tsx +46 -0
- coord/dashboard/webapp/src/shell/__tests__/ShellLayout.test.tsx +520 -0
- coord/dashboard/webapp/src/shell/__tests__/shellState.test.ts +95 -0
- coord/dashboard/webapp/src/shell/__tests__/stubViewport.ts +40 -0
- coord/dashboard/webapp/src/shell/breakpoints.ts +87 -0
- coord/dashboard/webapp/src/shell/railItems.ts +105 -0
- coord/dashboard/webapp/src/shell/shellState.ts +174 -0
- coord/dashboard/webapp/src/shell/useRegionFocus.ts +95 -0
- coord/dashboard/webapp/src/test-setup.ts +41 -0
- coord/dashboard/webapp/src/vite-env.d.ts +2 -0
- coord/dashboard/webapp/tailwind.config.js +140 -0
- coord/dashboard/webapp/tsconfig.json +25 -0
- coord/dashboard/webapp/tsconfig.node.json +11 -0
- coord/dashboard/webapp/vite.config.ts +71 -0
- coord/db.py +1076 -0
- coord/dead_end.py +332 -0
- coord/deploy/README.md +33 -0
- coord/deploy/coord-agent.service +89 -0
- coord/deploy/coord-db-backup.service +60 -0
- coord/deploy/coord-db-backup.sh +74 -0
- coord/deploy/coord-db-backup.timer +18 -0
- coord/deploy/coord-drive-queue.service +117 -0
- coord/deploy/coord-drive-queue.timer +39 -0
- coord/deploy/coord-notify.service +48 -0
- coord/deploy/coord-notify.timer +24 -0
- coord/deploy/coord-release-propagate.service +83 -0
- coord/deploy/coord-release-propagate.timer +38 -0
- coord/deploy/coord-release-window.service +119 -0
- coord/deploy/coord-release-window.timer +36 -0
- coord/deploy/coord-serve.service +82 -0
- coord/deploy/coord-web-dist-build.service +43 -0
- coord/deploy/coord-web-dist-build.timer +36 -0
- coord/deploy/coord-web.service +125 -0
- coord/deploy_manifest.py +80 -0
- coord/deploy_units.py +384 -0
- coord/deps.py +115 -0
- coord/diagnose.py +1623 -0
- coord/dispatch.py +1009 -0
- coord/dist_name.py +123 -0
- coord/drive.py +3101 -0
- coord/drive_queue.py +2298 -0
- coord/drive_state.py +870 -0
- coord/events.py +381 -0
- coord/failure_class.py +914 -0
- coord/filelock.py +168 -0
- coord/fleet_config_health.py +300 -0
- coord/freshness.py +206 -0
- coord/gate_a.py +469 -0
- coord/gate_b.py +411 -0
- coord/gate_snapshot.py +385 -0
- coord/gates.py +582 -0
- coord/github_ops.py +1954 -0
- coord/goal.py +125 -0
- coord/graph_health.py +348 -0
- coord/health/__init__.py +69 -0
- coord/health/aggregate.py +129 -0
- coord/health/checks/__init__.py +13 -0
- coord/health/checks/agent_install.py +280 -0
- coord/health/checks/cargo_targets.py +171 -0
- coord/health/checks/claude_binary.py +65 -0
- coord/health/checks/deploy_lane_facts.py +458 -0
- coord/health/checks/disk.py +99 -0
- coord/health/checks/fleet_board.py +89 -0
- coord/health/checks/fleet_deploy_lanes.py +469 -0
- coord/health/checks/fleet_phantom.py +69 -0
- coord/health/checks/fleet_unit_drift.py +151 -0
- coord/health/checks/graph.py +192 -0
- coord/health/checks/plan_usage.py +88 -0
- coord/health/checks/repo_state.py +161 -0
- coord/health/checks/spawned_coord.py +465 -0
- coord/health/checks/timer_active.py +254 -0
- coord/health/checks/toolchain.py +547 -0
- coord/health/checks/unit_drift.py +648 -0
- coord/health/checks/unit_enablement.py +171 -0
- coord/health/checks/worktrees.py +96 -0
- coord/health/cli.py +121 -0
- coord/health/context.py +106 -0
- coord/health/fleet_snapshot.py +477 -0
- coord/health/models.py +250 -0
- coord/health/pypi.py +231 -0
- coord/health/registry.py +240 -0
- coord/health/render.py +82 -0
- coord/health/units.py +60 -0
- coord/hooks.py +106 -0
- coord/housekeeping.py +204 -0
- coord/interactive.py +4286 -0
- coord/issue_store.py +1496 -0
- coord/liveness_auditor.py +293 -0
- coord/machine_pause.py +755 -0
- coord/merge_queue.py +4681 -0
- coord/milestone_chat.py +600 -0
- coord/milestone_dispatch.py +943 -0
- coord/milestone_gate.py +709 -0
- coord/milestone_order.py +840 -0
- coord/mock_author.py +334 -0
- coord/models.py +891 -0
- coord/network.py +269 -0
- coord/new_issue_chat.py +229 -0
- coord/notify.py +3226 -0
- coord/openapi.py +404 -0
- coord/overlap_fence.py +133 -0
- coord/parentage.py +200 -0
- coord/parentage_github.py +58 -0
- coord/pipeline.py +481 -0
- coord/plan_parser.py +266 -0
- coord/plans.py +543 -0
- coord/platform_paths.py +43 -0
- coord/pr_body_lint.py +67 -0
- coord/prereqs.py +533 -0
- coord/progress.py +425 -0
- coord/providers/__init__.py +683 -0
- coord/providers/base.py +218 -0
- coord/providers/claude.py +284 -0
- coord/providers/claude_pty.py +610 -0
- coord/providers/opencode.py +896 -0
- coord/reconcile.py +2233 -0
- coord/refine_chat.py +485 -0
- coord/release_cordon.py +525 -0
- coord/release_propagate.py +1176 -0
- coord/release_verify.py +777 -0
- coord/release_window.py +322 -0
- coord/reports.py +1643 -0
- coord/revalidate.py +1101 -0
- coord/review.py +3317 -0
- coord/scorecard.py +484 -0
- coord/serve_app.py +7192 -0
- coord/skills/update-issue/SKILL.md +93 -0
- coord/smoke.py +1030 -0
- coord/split_work.py +210 -0
- coord/stage_projection.py +650 -0
- coord/state.py +5720 -0
- coord/test_author.py +1064 -0
- coord/test_chat.py +352 -0
- coord/test_orchestrator.py +494 -0
- coord/test_report.py +178 -0
- coord/tui_release.py +271 -0
- coord/usage.py +753 -0
- coord/usage_limits.py +358 -0
- coord/usage_rollup.py +709 -0
- coord/worker_events.py +954 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity rail (#1547) — the coord-tui `SidebarView` twin, and the *same*
|
|
3
|
+
* component at every viewport.
|
|
4
|
+
*
|
|
5
|
+
* Three presentations, one component and one item list (`railItems.ts`):
|
|
6
|
+
* wide 216px labelled column, grouped, with counts and a rail foot.
|
|
7
|
+
* collapsed 60px icon strip (labels, counts, group headings and the
|
|
8
|
+
* wordmark all drop out; icons centre).
|
|
9
|
+
* narrow a bottom row of icon-over-label tabs. Only `ready` entries
|
|
10
|
+
* appear — a phone's bottom nav is thumb real estate, and a
|
|
11
|
+
* dimmed un-tappable "soon" item there is noise, not a progress
|
|
12
|
+
* bar. On wide there is room for the whole programme, so the
|
|
13
|
+
* whole programme shows.
|
|
14
|
+
*
|
|
15
|
+
* Matches docs/mocks/web/pipeline-wide.html: the selected item takes the
|
|
16
|
+
* accent wash plus a 3px accent bar on its leading edge, `soon` items sit at
|
|
17
|
+
* ~42% opacity behind a `soon` pill, and counts are mono (a value the machine
|
|
18
|
+
* owns) — amber-washed when the count is something needing a human.
|
|
19
|
+
*/
|
|
20
|
+
import {
|
|
21
|
+
ChevronLeft,
|
|
22
|
+
ChevronRight,
|
|
23
|
+
Moon,
|
|
24
|
+
PanelLeftClose,
|
|
25
|
+
PanelLeftOpen,
|
|
26
|
+
Settings2,
|
|
27
|
+
Sun,
|
|
28
|
+
} from 'lucide-react'
|
|
29
|
+
|
|
30
|
+
import { cn } from '@/lib/utils'
|
|
31
|
+
import { useTheme } from '@/components/ui/theme-provider'
|
|
32
|
+
import type { ShellMode } from './breakpoints'
|
|
33
|
+
import { RAIL_GROUPS, type RailItem } from './railItems'
|
|
34
|
+
import type { ShellView } from './shellState'
|
|
35
|
+
|
|
36
|
+
export interface ActivityRailProps {
|
|
37
|
+
mode: ShellMode
|
|
38
|
+
collapsed: boolean
|
|
39
|
+
/** `null` when the current URL doesn't belong to any rail view (#1548) —
|
|
40
|
+
* e.g. a genuinely unknown route. Nothing in the rail lights up rather
|
|
41
|
+
* than falling back to a view that isn't actually selected. */
|
|
42
|
+
view: ShellView | null
|
|
43
|
+
onSelect: (view: ShellView) => void
|
|
44
|
+
onToggleCollapsed: () => void
|
|
45
|
+
/** Wide only: whether the list column is minimised away. */
|
|
46
|
+
listCollapsed?: boolean
|
|
47
|
+
onToggleList?: () => void
|
|
48
|
+
/** Badge counts by view; absent or 0 renders no badge. */
|
|
49
|
+
counts?: Partial<Record<ShellView, number>>
|
|
50
|
+
/** Views whose count should read as "needs a human" (amber, not grey). */
|
|
51
|
+
attentionViews?: ReadonlySet<ShellView>
|
|
52
|
+
/** F6 region registration — see `useRegionFocus`. */
|
|
53
|
+
regionRef?: (el: HTMLElement | null) => void
|
|
54
|
+
/** True while F6 last landed here; draws the region ring. */
|
|
55
|
+
regionFocused?: boolean
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const navItemBase =
|
|
59
|
+
'relative flex w-full items-center rounded-sm text-left text-step-0 ' +
|
|
60
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0'
|
|
61
|
+
|
|
62
|
+
function NavItem({
|
|
63
|
+
item,
|
|
64
|
+
mode,
|
|
65
|
+
collapsed,
|
|
66
|
+
selected,
|
|
67
|
+
count,
|
|
68
|
+
attention,
|
|
69
|
+
onSelect,
|
|
70
|
+
}: {
|
|
71
|
+
item: RailItem
|
|
72
|
+
mode: ShellMode
|
|
73
|
+
collapsed: boolean
|
|
74
|
+
selected: boolean
|
|
75
|
+
count?: number
|
|
76
|
+
attention?: boolean
|
|
77
|
+
onSelect: (view: ShellView) => void
|
|
78
|
+
}) {
|
|
79
|
+
const soon = item.status === 'soon'
|
|
80
|
+
const Icon = item.icon
|
|
81
|
+
const narrow = mode === 'narrow'
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
// A `soon` entry is a signpost, not a control: `aria-disabled` (rather
|
|
87
|
+
// than `disabled`) keeps it reachable by a screen reader reading the
|
|
88
|
+
// rail as a table of contents, while the no-op handler and
|
|
89
|
+
// `cursor-default` keep it inert to a mouse.
|
|
90
|
+
aria-disabled={soon || undefined}
|
|
91
|
+
aria-current={selected ? 'page' : undefined}
|
|
92
|
+
title={item.hint ?? item.label}
|
|
93
|
+
onClick={soon ? undefined : () => onSelect(item.id)}
|
|
94
|
+
className={cn(
|
|
95
|
+
navItemBase,
|
|
96
|
+
narrow
|
|
97
|
+
? 'min-w-[60px] flex-1 flex-col justify-center gap-0.5 px-1 py-1.5'
|
|
98
|
+
: 'gap-2.5 px-2 py-1.5',
|
|
99
|
+
collapsed && !narrow && 'justify-center px-0 py-2',
|
|
100
|
+
soon
|
|
101
|
+
? 'cursor-default text-muted-foreground opacity-40 hover:opacity-70'
|
|
102
|
+
: 'text-muted-foreground hover:bg-surface-2 hover:text-foreground',
|
|
103
|
+
selected && !narrow && 'bg-brand-wash text-brand hover:bg-brand-wash hover:text-brand',
|
|
104
|
+
selected && narrow && 'text-brand',
|
|
105
|
+
)}
|
|
106
|
+
>
|
|
107
|
+
{/* Leading accent bar on the selected row — mock's `.nav-item::before`.
|
|
108
|
+
Suppressed on narrow, where the mock drops it too. */}
|
|
109
|
+
{selected && !narrow && (
|
|
110
|
+
<span
|
|
111
|
+
aria-hidden="true"
|
|
112
|
+
className="absolute -left-2 top-1/2 h-[17px] w-[3px] -translate-y-1/2 rounded-r-[3px] bg-brand"
|
|
113
|
+
/>
|
|
114
|
+
)}
|
|
115
|
+
<Icon className={cn('shrink-0', narrow ? 'h-[19px] w-[19px]' : 'h-[17px] w-[17px]')} aria-hidden="true" />
|
|
116
|
+
{(!collapsed || narrow) && (
|
|
117
|
+
<span
|
|
118
|
+
className={cn(
|
|
119
|
+
'overflow-hidden text-ellipsis whitespace-nowrap',
|
|
120
|
+
narrow ? 'text-[.6rem] leading-tight' : 'min-w-0 flex-1',
|
|
121
|
+
)}
|
|
122
|
+
>
|
|
123
|
+
{item.label}
|
|
124
|
+
</span>
|
|
125
|
+
)}
|
|
126
|
+
{!collapsed && !narrow && soon && (
|
|
127
|
+
<span className="shrink-0 rounded-full border border-line-strong px-1 text-[.58rem] uppercase tracking-[.06em] text-faint">
|
|
128
|
+
soon
|
|
129
|
+
</span>
|
|
130
|
+
)}
|
|
131
|
+
{!collapsed && !narrow && !soon && count != null && count > 0 && (
|
|
132
|
+
<span
|
|
133
|
+
className={cn(
|
|
134
|
+
'rounded-full px-1.5 font-mono text-[.7rem]',
|
|
135
|
+
attention ? 'bg-attn-wash text-attn' : 'bg-surface-2 text-faint',
|
|
136
|
+
)}
|
|
137
|
+
>
|
|
138
|
+
{count}
|
|
139
|
+
</span>
|
|
140
|
+
)}
|
|
141
|
+
</button>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function ActivityRail({
|
|
146
|
+
mode,
|
|
147
|
+
collapsed,
|
|
148
|
+
view,
|
|
149
|
+
onSelect,
|
|
150
|
+
onToggleCollapsed,
|
|
151
|
+
listCollapsed,
|
|
152
|
+
onToggleList,
|
|
153
|
+
counts,
|
|
154
|
+
attentionViews,
|
|
155
|
+
regionRef,
|
|
156
|
+
regionFocused,
|
|
157
|
+
}: ActivityRailProps) {
|
|
158
|
+
const { theme, toggleTheme } = useTheme()
|
|
159
|
+
const narrow = mode === 'narrow'
|
|
160
|
+
// On medium the rail is *always* the icon strip: there isn't room for both
|
|
161
|
+
// a labelled rail and a usable content column, and letting the persisted
|
|
162
|
+
// `collapsed=false` win there is exactly the "snapping between two
|
|
163
|
+
// extremes" the story asks us to avoid.
|
|
164
|
+
const iconOnly = mode === 'medium' || (mode === 'wide' && collapsed)
|
|
165
|
+
|
|
166
|
+
const groups = narrow
|
|
167
|
+
? [{ heading: undefined, items: RAIL_GROUPS.flatMap((g) => g.items).filter((i) => i.status === 'ready') }]
|
|
168
|
+
: RAIL_GROUPS
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<nav
|
|
172
|
+
ref={regionRef}
|
|
173
|
+
tabIndex={-1}
|
|
174
|
+
data-region="rail"
|
|
175
|
+
aria-label="Views"
|
|
176
|
+
className={cn(
|
|
177
|
+
'flex h-full w-full min-w-0 overflow-hidden bg-surface outline-none',
|
|
178
|
+
narrow
|
|
179
|
+
? 'flex-row border-t border-border pb-[env(safe-area-inset-bottom,0px)]'
|
|
180
|
+
: 'flex-col border-r border-border',
|
|
181
|
+
regionFocused && 'ring-1 ring-inset ring-ring',
|
|
182
|
+
)}
|
|
183
|
+
>
|
|
184
|
+
{!narrow && (
|
|
185
|
+
<div
|
|
186
|
+
className={cn(
|
|
187
|
+
'flex items-center gap-[9px] border-b border-border',
|
|
188
|
+
iconOnly ? 'justify-center px-0 py-3' : 'px-3.5 pb-3 pt-3.5',
|
|
189
|
+
)}
|
|
190
|
+
>
|
|
191
|
+
<span
|
|
192
|
+
aria-hidden="true"
|
|
193
|
+
className="grid h-[26px] w-[26px] flex-none place-items-center rounded-[7px] bg-gradient-to-br from-brand to-brand-dim font-mono text-[.72rem] font-bold tracking-[-.02em] text-[#0b1013]"
|
|
194
|
+
>
|
|
195
|
+
co
|
|
196
|
+
</span>
|
|
197
|
+
{!iconOnly && (
|
|
198
|
+
<span className="whitespace-nowrap font-semibold tracking-[-.01em]">
|
|
199
|
+
coord <span className="font-normal text-muted-foreground">web</span>
|
|
200
|
+
</span>
|
|
201
|
+
)}
|
|
202
|
+
</div>
|
|
203
|
+
)}
|
|
204
|
+
|
|
205
|
+
<div
|
|
206
|
+
className={cn(
|
|
207
|
+
'flex',
|
|
208
|
+
narrow
|
|
209
|
+
? 'w-full flex-row justify-around gap-0 overflow-x-auto px-0 py-1'
|
|
210
|
+
: 'flex-1 flex-col gap-px overflow-y-auto px-2 py-2.5',
|
|
211
|
+
)}
|
|
212
|
+
>
|
|
213
|
+
{groups.map((group, groupIndex) => (
|
|
214
|
+
<div
|
|
215
|
+
key={group.heading ?? `group-${groupIndex}`}
|
|
216
|
+
className={cn('flex', narrow ? 'w-full flex-row justify-around' : 'flex-col gap-px')}
|
|
217
|
+
>
|
|
218
|
+
{group.heading && !iconOnly && !narrow && (
|
|
219
|
+
<div className="whitespace-nowrap px-2.5 pb-[5px] pt-3.5 text-[.66rem] font-semibold uppercase tracking-[.09em] text-faint">
|
|
220
|
+
{group.heading}
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
223
|
+
{group.items.map((item) => (
|
|
224
|
+
<NavItem
|
|
225
|
+
key={item.id}
|
|
226
|
+
item={item}
|
|
227
|
+
mode={mode}
|
|
228
|
+
collapsed={iconOnly}
|
|
229
|
+
selected={item.id === view}
|
|
230
|
+
count={counts?.[item.id]}
|
|
231
|
+
attention={attentionViews?.has(item.id)}
|
|
232
|
+
onSelect={onSelect}
|
|
233
|
+
/>
|
|
234
|
+
))}
|
|
235
|
+
</div>
|
|
236
|
+
))}
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
{!narrow && (
|
|
240
|
+
<div className="flex flex-col gap-px border-t border-border px-2 py-2.5">
|
|
241
|
+
<button
|
|
242
|
+
type="button"
|
|
243
|
+
onClick={toggleTheme}
|
|
244
|
+
title={theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme'}
|
|
245
|
+
aria-label={theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme'}
|
|
246
|
+
className={cn(
|
|
247
|
+
navItemBase,
|
|
248
|
+
'gap-[11px] px-2.5 py-1.5 text-muted-foreground hover:bg-surface-2 hover:text-foreground',
|
|
249
|
+
iconOnly && 'justify-center px-0 py-2',
|
|
250
|
+
)}
|
|
251
|
+
>
|
|
252
|
+
{theme === 'dark' ? (
|
|
253
|
+
<Sun className="h-[17px] w-[17px] shrink-0" aria-hidden="true" />
|
|
254
|
+
) : (
|
|
255
|
+
<Moon className="h-[17px] w-[17px] shrink-0" aria-hidden="true" />
|
|
256
|
+
)}
|
|
257
|
+
{!iconOnly && <span className="flex-1 text-left">Theme</span>}
|
|
258
|
+
</button>
|
|
259
|
+
|
|
260
|
+
<button
|
|
261
|
+
type="button"
|
|
262
|
+
aria-disabled="true"
|
|
263
|
+
title="Settings panel — M-W3"
|
|
264
|
+
className={cn(
|
|
265
|
+
navItemBase,
|
|
266
|
+
'cursor-default gap-[11px] px-2.5 py-1.5 text-muted-foreground opacity-40 hover:opacity-70',
|
|
267
|
+
iconOnly && 'justify-center px-0 py-2',
|
|
268
|
+
)}
|
|
269
|
+
>
|
|
270
|
+
<Settings2 className="h-[17px] w-[17px] shrink-0" aria-hidden="true" />
|
|
271
|
+
{!iconOnly && (
|
|
272
|
+
<>
|
|
273
|
+
<span className="flex-1 text-left">Settings</span>
|
|
274
|
+
<span className="rounded-full border border-line-strong px-[5px] text-[.6rem] uppercase tracking-[.06em] text-faint">
|
|
275
|
+
soon
|
|
276
|
+
</span>
|
|
277
|
+
</>
|
|
278
|
+
)}
|
|
279
|
+
</button>
|
|
280
|
+
|
|
281
|
+
{/* The mock puts "minimise list" in the panel head; it lives here
|
|
282
|
+
instead so it can't collide with whatever a panel chooses to put
|
|
283
|
+
in its own header, and so the way back (expand) is in the same
|
|
284
|
+
place as the way out. Wide-only: it's the only mode with a third
|
|
285
|
+
column to give the space to. */}
|
|
286
|
+
{mode === 'wide' && onToggleList && (
|
|
287
|
+
<button
|
|
288
|
+
type="button"
|
|
289
|
+
onClick={onToggleList}
|
|
290
|
+
aria-label={listCollapsed ? 'Show list panel' : 'Minimize list panel'}
|
|
291
|
+
aria-pressed={!!listCollapsed}
|
|
292
|
+
title={listCollapsed ? 'Show list panel' : 'Minimize list panel'}
|
|
293
|
+
className={cn(
|
|
294
|
+
navItemBase,
|
|
295
|
+
'gap-[11px] px-2.5 py-1.5 text-muted-foreground hover:bg-surface-2 hover:text-foreground',
|
|
296
|
+
collapsed && 'justify-center px-0 py-2',
|
|
297
|
+
)}
|
|
298
|
+
>
|
|
299
|
+
{listCollapsed ? (
|
|
300
|
+
<PanelLeftOpen className="h-[17px] w-[17px] shrink-0" aria-hidden="true" />
|
|
301
|
+
) : (
|
|
302
|
+
<PanelLeftClose className="h-[17px] w-[17px] shrink-0" aria-hidden="true" />
|
|
303
|
+
)}
|
|
304
|
+
{!collapsed && <span className="flex-1 text-left">{listCollapsed ? 'Show list' : 'Hide list'}</span>}
|
|
305
|
+
</button>
|
|
306
|
+
)}
|
|
307
|
+
|
|
308
|
+
{/* Medium pins the rail to icons, so offering a toggle there would
|
|
309
|
+
be a button that does nothing. */}
|
|
310
|
+
{mode === 'wide' && (
|
|
311
|
+
<button
|
|
312
|
+
type="button"
|
|
313
|
+
onClick={onToggleCollapsed}
|
|
314
|
+
aria-label={collapsed ? 'Expand rail' : 'Collapse rail'}
|
|
315
|
+
aria-expanded={!collapsed}
|
|
316
|
+
title={collapsed ? 'Expand rail' : 'Collapse rail'}
|
|
317
|
+
className={cn(
|
|
318
|
+
navItemBase,
|
|
319
|
+
'gap-[11px] px-2.5 py-1.5 text-muted-foreground hover:bg-surface-2 hover:text-foreground',
|
|
320
|
+
collapsed && 'justify-center px-0 py-2',
|
|
321
|
+
)}
|
|
322
|
+
>
|
|
323
|
+
{collapsed ? (
|
|
324
|
+
<ChevronRight className="h-[17px] w-[17px] shrink-0" aria-hidden="true" />
|
|
325
|
+
) : (
|
|
326
|
+
<ChevronLeft className="h-[17px] w-[17px] shrink-0" aria-hidden="true" />
|
|
327
|
+
)}
|
|
328
|
+
{!collapsed && <span className="flex-1 text-left">Collapse</span>}
|
|
329
|
+
</button>
|
|
330
|
+
)}
|
|
331
|
+
</div>
|
|
332
|
+
)}
|
|
333
|
+
</nav>
|
|
334
|
+
)
|
|
335
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AppShell (#1547) — the responsive frame every panel hangs off.
|
|
3
|
+
*
|
|
4
|
+
* One CSS grid, three compositions, driven entirely by `mode` (see
|
|
5
|
+
* `breakpoints.ts`). The slots are `ReactNode` props rather than a forked
|
|
6
|
+
* component tree: `ShellLayout` passes the *same* list node and the *same*
|
|
7
|
+
* detail node at every viewport, and this file decides only where they land
|
|
8
|
+
* and which of them is on screen.
|
|
9
|
+
*
|
|
10
|
+
* wide rail | list | detail, three columns, status bar spanning the
|
|
11
|
+
* foot. The list/detail boundary is a draggable separator.
|
|
12
|
+
* medium rail | content. The detail arrives as an overlay sheet in the
|
|
13
|
+
* same grid cell as the list, so a drill-in doesn't cost you the
|
|
14
|
+
* list you drilled in from.
|
|
15
|
+
* narrow one column, status bar, then the rail as a bottom row. Exactly
|
|
16
|
+
* one of list/detail is mounted — this is the phone app's
|
|
17
|
+
* drill-in, and mounting both would duplicate every row's text in
|
|
18
|
+
* the accessibility tree.
|
|
19
|
+
*
|
|
20
|
+
* Mirrors docs/mocks/web/pipeline-wide.html's `.app` grid, including its
|
|
21
|
+
* `data-rail` / `data-panel` state attributes (kept as real DOM attributes:
|
|
22
|
+
* they are what an acceptance spec asserts layout state against without
|
|
23
|
+
* having to measure pixels).
|
|
24
|
+
*/
|
|
25
|
+
import {
|
|
26
|
+
useCallback,
|
|
27
|
+
useEffect,
|
|
28
|
+
useRef,
|
|
29
|
+
type CSSProperties,
|
|
30
|
+
type KeyboardEvent as ReactKeyboardEvent,
|
|
31
|
+
type PointerEvent as ReactPointerEvent,
|
|
32
|
+
type ReactNode,
|
|
33
|
+
} from 'react'
|
|
34
|
+
|
|
35
|
+
import { cn } from '@/lib/utils'
|
|
36
|
+
import type { ShellMode } from './breakpoints'
|
|
37
|
+
import {
|
|
38
|
+
LIST_WIDTH_MAX_PX,
|
|
39
|
+
LIST_WIDTH_MIN_PX,
|
|
40
|
+
RAIL_WIDTH_COLLAPSED_PX,
|
|
41
|
+
RAIL_WIDTH_PX,
|
|
42
|
+
clampListWidth,
|
|
43
|
+
} from './shellState'
|
|
44
|
+
import type { ShellRegion } from './useRegionFocus'
|
|
45
|
+
|
|
46
|
+
/** Keyboard resize step for the separator (Shift = coarse). */
|
|
47
|
+
const RESIZE_STEP_PX = 16
|
|
48
|
+
const RESIZE_STEP_COARSE_PX = 64
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* `inert` isn't in React 18's JSX attribute types (it landed in React 19), but
|
|
52
|
+
* React does forward unknown lowercase string attributes to the DOM, and every
|
|
53
|
+
* browser that ships `inert` honours the empty-string form. This is the only
|
|
54
|
+
* thing that stops Tab from walking into the list hidden *behind* the medium
|
|
55
|
+
* overlay — `aria-hidden` alone hides it from a screen reader while leaving it
|
|
56
|
+
* in the tab order, which is the worse of the two failure modes.
|
|
57
|
+
*/
|
|
58
|
+
const INERT_PROPS = { inert: '' } as unknown as Record<string, string>
|
|
59
|
+
|
|
60
|
+
export interface AppShellProps {
|
|
61
|
+
mode: ShellMode
|
|
62
|
+
railCollapsed: boolean
|
|
63
|
+
listCollapsed: boolean
|
|
64
|
+
listWidthPx: number
|
|
65
|
+
onListWidthChange: (px: number) => void
|
|
66
|
+
/** True when a detail item is selected (on narrow: show detail, hide list). */
|
|
67
|
+
detailActive: boolean
|
|
68
|
+
rail: ReactNode
|
|
69
|
+
list: ReactNode
|
|
70
|
+
detail: ReactNode
|
|
71
|
+
status: ReactNode
|
|
72
|
+
registerRegion: (region: ShellRegion) => (el: HTMLElement | null) => void
|
|
73
|
+
focusedRegion: ShellRegion | null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function AppShell({
|
|
77
|
+
mode,
|
|
78
|
+
railCollapsed,
|
|
79
|
+
listCollapsed,
|
|
80
|
+
listWidthPx,
|
|
81
|
+
onListWidthChange,
|
|
82
|
+
detailActive,
|
|
83
|
+
rail,
|
|
84
|
+
list,
|
|
85
|
+
detail,
|
|
86
|
+
status,
|
|
87
|
+
registerRegion,
|
|
88
|
+
focusedRegion,
|
|
89
|
+
}: AppShellProps) {
|
|
90
|
+
const narrow = mode === 'narrow'
|
|
91
|
+
const wide = mode === 'wide'
|
|
92
|
+
// Medium pins the rail to its icon strip regardless of the persisted flag —
|
|
93
|
+
// see ActivityRail for why.
|
|
94
|
+
const railPx = wide && !railCollapsed ? RAIL_WIDTH_PX : RAIL_WIDTH_COLLAPSED_PX
|
|
95
|
+
|
|
96
|
+
// Wide is the only mode with a third column to give away, so it's the only
|
|
97
|
+
// mode where "minimise the list" means anything.
|
|
98
|
+
const listHidden = wide && listCollapsed
|
|
99
|
+
const showList = narrow ? !detailActive : !listHidden
|
|
100
|
+
// Wide is the only mode with a column reserved for the detail, so it's the
|
|
101
|
+
// only one that mounts the "nothing selected" placeholder. On medium the
|
|
102
|
+
// detail shares the list's grid cell — mounting it unselected would paint an
|
|
103
|
+
// opaque placeholder straight over the list.
|
|
104
|
+
const showDetail = wide || detailActive
|
|
105
|
+
// On medium the detail sheet sits *on top of* the list in the same cell.
|
|
106
|
+
const detailOverlays = mode === 'medium' && detailActive
|
|
107
|
+
|
|
108
|
+
const gridStyle: CSSProperties = narrow
|
|
109
|
+
? { gridTemplateColumns: 'minmax(0,1fr)', gridTemplateRows: 'minmax(0,1fr) auto auto' }
|
|
110
|
+
: {
|
|
111
|
+
gridTemplateColumns: wide
|
|
112
|
+
? `${railPx}px ${listHidden ? 0 : listWidthPx}px minmax(0,1fr)`
|
|
113
|
+
: `${railPx}px minmax(0,1fr)`,
|
|
114
|
+
gridTemplateRows: 'minmax(0,1fr) auto',
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const railStyle: CSSProperties = narrow
|
|
118
|
+
? { gridColumn: 1, gridRow: 3 }
|
|
119
|
+
: { gridColumn: 1, gridRow: 1 }
|
|
120
|
+
const listStyle: CSSProperties = narrow ? { gridColumn: 1, gridRow: 1 } : { gridColumn: 2, gridRow: 1 }
|
|
121
|
+
const detailStyle: CSSProperties = narrow
|
|
122
|
+
? { gridColumn: 1, gridRow: 1 }
|
|
123
|
+
: { gridColumn: wide ? 3 : 2, gridRow: 1 }
|
|
124
|
+
const statusStyle: CSSProperties = narrow
|
|
125
|
+
? { gridColumn: 1, gridRow: 2 }
|
|
126
|
+
: { gridColumn: '1 / -1', gridRow: 2 }
|
|
127
|
+
|
|
128
|
+
// ── separator drag ────────────────────────────────────────────────────────
|
|
129
|
+
const dragRef = useRef<{ startX: number; startWidth: number } | null>(null)
|
|
130
|
+
|
|
131
|
+
const onPointerDown = useCallback(
|
|
132
|
+
(event: ReactPointerEvent<HTMLDivElement>) => {
|
|
133
|
+
// Ignore secondary buttons: a right-click drag would otherwise capture
|
|
134
|
+
// the pointer and never see a matching pointerup.
|
|
135
|
+
if (event.button !== 0) return
|
|
136
|
+
event.preventDefault()
|
|
137
|
+
event.currentTarget.setPointerCapture(event.pointerId)
|
|
138
|
+
dragRef.current = { startX: event.clientX, startWidth: listWidthPx }
|
|
139
|
+
// A col-resize drag that paints half the issue list blue looks broken,
|
|
140
|
+
// and the cursor must not flicker back to `text` when the pointer
|
|
141
|
+
// leaves the 7px handle mid-drag.
|
|
142
|
+
document.body.style.setProperty('user-select', 'none')
|
|
143
|
+
document.body.style.setProperty('cursor', 'col-resize')
|
|
144
|
+
},
|
|
145
|
+
[listWidthPx],
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
const onPointerMove = useCallback(
|
|
149
|
+
(event: ReactPointerEvent<HTMLDivElement>) => {
|
|
150
|
+
const drag = dragRef.current
|
|
151
|
+
if (!drag) return
|
|
152
|
+
onListWidthChange(clampListWidth(drag.startWidth + (event.clientX - drag.startX)))
|
|
153
|
+
},
|
|
154
|
+
[onListWidthChange],
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
const endDrag = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
|
|
158
|
+
if (!dragRef.current) return
|
|
159
|
+
dragRef.current = null
|
|
160
|
+
document.body.style.removeProperty('user-select')
|
|
161
|
+
document.body.style.removeProperty('cursor')
|
|
162
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
|
163
|
+
event.currentTarget.releasePointerCapture(event.pointerId)
|
|
164
|
+
}
|
|
165
|
+
}, [])
|
|
166
|
+
|
|
167
|
+
const onSeparatorKeyDown = useCallback(
|
|
168
|
+
(event: ReactKeyboardEvent<HTMLDivElement>) => {
|
|
169
|
+
const step = event.shiftKey ? RESIZE_STEP_COARSE_PX : RESIZE_STEP_PX
|
|
170
|
+
switch (event.key) {
|
|
171
|
+
case 'ArrowLeft':
|
|
172
|
+
event.preventDefault()
|
|
173
|
+
onListWidthChange(listWidthPx - step)
|
|
174
|
+
break
|
|
175
|
+
case 'ArrowRight':
|
|
176
|
+
event.preventDefault()
|
|
177
|
+
onListWidthChange(listWidthPx + step)
|
|
178
|
+
break
|
|
179
|
+
case 'Home':
|
|
180
|
+
event.preventDefault()
|
|
181
|
+
onListWidthChange(LIST_WIDTH_MIN_PX)
|
|
182
|
+
break
|
|
183
|
+
case 'End':
|
|
184
|
+
event.preventDefault()
|
|
185
|
+
onListWidthChange(LIST_WIDTH_MAX_PX)
|
|
186
|
+
break
|
|
187
|
+
default:
|
|
188
|
+
break
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
[listWidthPx, onListWidthChange],
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
// Unmounting (or dropping out of wide) mid-drag would otherwise strand the
|
|
195
|
+
// document in `user-select: none; cursor: col-resize` forever.
|
|
196
|
+
useEffect(
|
|
197
|
+
() => () => {
|
|
198
|
+
document.body.style.removeProperty('user-select')
|
|
199
|
+
document.body.style.removeProperty('cursor')
|
|
200
|
+
},
|
|
201
|
+
[],
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<div
|
|
206
|
+
className="grid h-screen w-full overflow-hidden bg-background text-foreground [height:100dvh]"
|
|
207
|
+
style={gridStyle}
|
|
208
|
+
data-shell-mode={mode}
|
|
209
|
+
data-rail={narrow ? 'bottom' : !wide || railCollapsed ? 'collapsed' : 'expanded'}
|
|
210
|
+
data-panel={listHidden ? 'collapsed' : 'expanded'}
|
|
211
|
+
data-detail={detailActive ? 'active' : 'empty'}
|
|
212
|
+
>
|
|
213
|
+
<div style={railStyle} className="min-w-0 overflow-hidden">
|
|
214
|
+
{rail}
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
{showList && (
|
|
218
|
+
<section
|
|
219
|
+
ref={registerRegion('list')}
|
|
220
|
+
tabIndex={-1}
|
|
221
|
+
data-region="list"
|
|
222
|
+
aria-label="List"
|
|
223
|
+
aria-hidden={detailOverlays || undefined}
|
|
224
|
+
{...(detailOverlays ? INERT_PROPS : {})}
|
|
225
|
+
style={listStyle}
|
|
226
|
+
className={cn(
|
|
227
|
+
'relative flex min-w-0 flex-col overflow-hidden bg-background outline-none',
|
|
228
|
+
!narrow && 'border-r border-border',
|
|
229
|
+
focusedRegion === 'list' && 'ring-1 ring-inset ring-ring',
|
|
230
|
+
)}
|
|
231
|
+
>
|
|
232
|
+
<div className="min-h-0 flex-1 overflow-y-auto">{list}</div>
|
|
233
|
+
|
|
234
|
+
{wide && (
|
|
235
|
+
<div
|
|
236
|
+
role="separator"
|
|
237
|
+
aria-orientation="vertical"
|
|
238
|
+
aria-label="Resize list panel"
|
|
239
|
+
aria-valuenow={listWidthPx}
|
|
240
|
+
aria-valuemin={LIST_WIDTH_MIN_PX}
|
|
241
|
+
aria-valuemax={LIST_WIDTH_MAX_PX}
|
|
242
|
+
tabIndex={0}
|
|
243
|
+
onPointerDown={onPointerDown}
|
|
244
|
+
onPointerMove={onPointerMove}
|
|
245
|
+
onPointerUp={endDrag}
|
|
246
|
+
onPointerCancel={endDrag}
|
|
247
|
+
onKeyDown={onSeparatorKeyDown}
|
|
248
|
+
className="absolute -right-[3px] bottom-0 top-0 z-20 w-[7px] cursor-col-resize touch-none bg-transparent transition-colors hover:bg-brand/40 focus-visible:bg-brand/60 focus-visible:outline-none"
|
|
249
|
+
/>
|
|
250
|
+
)}
|
|
251
|
+
</section>
|
|
252
|
+
)}
|
|
253
|
+
|
|
254
|
+
{showDetail && (
|
|
255
|
+
<main
|
|
256
|
+
ref={registerRegion('detail')}
|
|
257
|
+
tabIndex={-1}
|
|
258
|
+
data-region="detail"
|
|
259
|
+
aria-label="Detail"
|
|
260
|
+
style={detailStyle}
|
|
261
|
+
className={cn(
|
|
262
|
+
'flex min-w-0 flex-col overflow-hidden bg-background outline-none',
|
|
263
|
+
detailOverlays && 'z-10 border-l border-border shadow-elevation',
|
|
264
|
+
focusedRegion === 'detail' && 'ring-1 ring-inset ring-ring',
|
|
265
|
+
)}
|
|
266
|
+
>
|
|
267
|
+
<div className="min-h-0 flex-1 overflow-y-auto">{detail}</div>
|
|
268
|
+
</main>
|
|
269
|
+
)}
|
|
270
|
+
|
|
271
|
+
<div style={statusStyle} className="min-w-0">
|
|
272
|
+
{status}
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
)
|
|
276
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The list panel's content for a rail-listed view that has a route (#1548
|
|
3
|
+
* asks for "placeholders for the panels M-W5+ will add") but no panel yet —
|
|
4
|
+
* Board, Machines, Merge queue, Milestones, Audit, Spend, Settings.
|
|
5
|
+
*
|
|
6
|
+
* These routes exist for exactly one reason: so a link to `/board` or
|
|
7
|
+
* `/spend` is a real, bookmarkable, not-found-free address today, and
|
|
8
|
+
* swapping this placeholder for the real panel later is a one-line change in
|
|
9
|
+
* `App.tsx`, not a new route. The rail itself already marks these `'soon'`
|
|
10
|
+
* (see `railItems.ts`) and won't navigate here on a click — this is what a
|
|
11
|
+
* *typed or pasted* link to one of them lands on in the meantime.
|
|
12
|
+
*/
|
|
13
|
+
import { RAIL_ITEMS } from './railItems'
|
|
14
|
+
import type { ShellView } from './shellState'
|
|
15
|
+
|
|
16
|
+
export interface ComingSoonProps {
|
|
17
|
+
view: ShellView
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function ComingSoon({ view }: ComingSoonProps) {
|
|
21
|
+
const item = RAIL_ITEMS.find((i) => i.id === view)
|
|
22
|
+
const Icon = item?.icon
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="flex h-full flex-col items-center justify-center gap-3 px-6 py-12 text-center">
|
|
26
|
+
{Icon && <Icon className="h-7 w-7 text-faint" aria-hidden="true" />}
|
|
27
|
+
<p className="text-sm font-medium text-foreground">{item?.label ?? 'Coming soon'}</p>
|
|
28
|
+
<p className="max-w-xs text-xs text-muted-foreground">
|
|
29
|
+
{item?.hint ?? 'This panel is not built yet.'}
|
|
30
|
+
</p>
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the wide layout's detail column shows before anything is selected
|
|
3
|
+
* (#1547).
|
|
4
|
+
*
|
|
5
|
+
* Only ever rendered on wide/medium: on narrow the shell mounts the list
|
|
6
|
+
* *instead of* the detail, so there is no "nothing selected" state to fill.
|
|
7
|
+
* It doubles as the shell's one piece of keyboard-affordance signage — F6 is
|
|
8
|
+
* discoverable nowhere else, and this is the emptiest surface in the app.
|
|
9
|
+
*/
|
|
10
|
+
import { MousePointerClick } from 'lucide-react'
|
|
11
|
+
|
|
12
|
+
export function EmptyDetail() {
|
|
13
|
+
return (
|
|
14
|
+
<div className="flex h-full flex-col items-center justify-center gap-3 px-6 py-12 text-center">
|
|
15
|
+
<MousePointerClick className="h-7 w-7 text-faint" aria-hidden="true" />
|
|
16
|
+
<p className="text-sm font-medium text-foreground">Nothing selected</p>
|
|
17
|
+
<p className="max-w-xs text-xs text-muted-foreground">
|
|
18
|
+
Pick an item from the list to see its stages, worker and actions here.
|
|
19
|
+
</p>
|
|
20
|
+
<p className="text-xs text-faint">
|
|
21
|
+
<kbd className="rounded border border-line-strong px-1 font-mono text-[.7rem]">F6</kbd>{' '}
|
|
22
|
+
cycles rail → list → detail
|
|
23
|
+
</p>
|
|
24
|
+
</div>
|
|
25
|
+
)
|
|
26
|
+
}
|