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,99 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
|
3
|
+
import { X } from 'lucide-react'
|
|
4
|
+
import { cn } from '@/lib/utils'
|
|
5
|
+
|
|
6
|
+
const Dialog = DialogPrimitive.Root
|
|
7
|
+
const DialogTrigger = DialogPrimitive.Trigger
|
|
8
|
+
const DialogPortal = DialogPrimitive.Portal
|
|
9
|
+
const DialogClose = DialogPrimitive.Close
|
|
10
|
+
|
|
11
|
+
const DialogOverlay = React.forwardRef<
|
|
12
|
+
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
|
13
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
14
|
+
>(({ className, ...props }, ref) => (
|
|
15
|
+
<DialogPrimitive.Overlay
|
|
16
|
+
ref={ref}
|
|
17
|
+
className={cn(
|
|
18
|
+
'fixed inset-0 z-50 bg-black/60 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
|
19
|
+
className,
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
))
|
|
24
|
+
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
|
25
|
+
|
|
26
|
+
const DialogContent = React.forwardRef<
|
|
27
|
+
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
28
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
|
29
|
+
>(({ className, children, ...props }, ref) => (
|
|
30
|
+
<DialogPortal>
|
|
31
|
+
<DialogOverlay />
|
|
32
|
+
<DialogPrimitive.Content
|
|
33
|
+
ref={ref}
|
|
34
|
+
className={cn(
|
|
35
|
+
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 rounded-lg border border-border bg-card p-6 text-card-foreground shadow-elevation duration-200',
|
|
36
|
+
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
|
37
|
+
className,
|
|
38
|
+
)}
|
|
39
|
+
{...props}
|
|
40
|
+
>
|
|
41
|
+
{children}
|
|
42
|
+
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm text-muted-foreground opacity-70 transition-opacity hover:opacity-100 hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none">
|
|
43
|
+
<X className="h-4 w-4" />
|
|
44
|
+
<span className="sr-only">Close</span>
|
|
45
|
+
</DialogPrimitive.Close>
|
|
46
|
+
</DialogPrimitive.Content>
|
|
47
|
+
</DialogPortal>
|
|
48
|
+
))
|
|
49
|
+
DialogContent.displayName = DialogPrimitive.Content.displayName
|
|
50
|
+
|
|
51
|
+
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
52
|
+
<div className={cn('flex flex-col gap-1.5 text-left', className)} {...props} />
|
|
53
|
+
)
|
|
54
|
+
DialogHeader.displayName = 'DialogHeader'
|
|
55
|
+
|
|
56
|
+
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
57
|
+
<div
|
|
58
|
+
className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
|
|
59
|
+
{...props}
|
|
60
|
+
/>
|
|
61
|
+
)
|
|
62
|
+
DialogFooter.displayName = 'DialogFooter'
|
|
63
|
+
|
|
64
|
+
const DialogTitle = React.forwardRef<
|
|
65
|
+
React.ElementRef<typeof DialogPrimitive.Title>,
|
|
66
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
67
|
+
>(({ className, ...props }, ref) => (
|
|
68
|
+
<DialogPrimitive.Title
|
|
69
|
+
ref={ref}
|
|
70
|
+
className={cn('text-base font-semibold leading-none tracking-tight', className)}
|
|
71
|
+
{...props}
|
|
72
|
+
/>
|
|
73
|
+
))
|
|
74
|
+
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
|
75
|
+
|
|
76
|
+
const DialogDescription = React.forwardRef<
|
|
77
|
+
React.ElementRef<typeof DialogPrimitive.Description>,
|
|
78
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
79
|
+
>(({ className, ...props }, ref) => (
|
|
80
|
+
<DialogPrimitive.Description
|
|
81
|
+
ref={ref}
|
|
82
|
+
className={cn('text-sm text-muted-foreground', className)}
|
|
83
|
+
{...props}
|
|
84
|
+
/>
|
|
85
|
+
))
|
|
86
|
+
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
|
87
|
+
|
|
88
|
+
export {
|
|
89
|
+
Dialog,
|
|
90
|
+
DialogPortal,
|
|
91
|
+
DialogOverlay,
|
|
92
|
+
DialogClose,
|
|
93
|
+
DialogTrigger,
|
|
94
|
+
DialogContent,
|
|
95
|
+
DialogHeader,
|
|
96
|
+
DialogFooter,
|
|
97
|
+
DialogTitle,
|
|
98
|
+
DialogDescription,
|
|
99
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
|
|
3
|
+
import { Check, ChevronRight, Circle } from 'lucide-react'
|
|
4
|
+
import { cn } from '@/lib/utils'
|
|
5
|
+
|
|
6
|
+
const DropdownMenu = DropdownMenuPrimitive.Root
|
|
7
|
+
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
|
8
|
+
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
|
9
|
+
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
|
10
|
+
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
|
11
|
+
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
|
12
|
+
|
|
13
|
+
const DropdownMenuSubTrigger = React.forwardRef<
|
|
14
|
+
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
|
15
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { inset?: boolean }
|
|
16
|
+
>(({ className, inset, children, ...props }, ref) => (
|
|
17
|
+
<DropdownMenuPrimitive.SubTrigger
|
|
18
|
+
ref={ref}
|
|
19
|
+
className={cn(
|
|
20
|
+
'flex cursor-default select-none items-center gap-2 rounded-sm px-2.5 py-1.5 text-sm text-muted-foreground outline-none hover:bg-secondary hover:text-foreground data-[state=open]:bg-secondary data-[state=open]:text-foreground',
|
|
21
|
+
inset && 'pl-8',
|
|
22
|
+
className,
|
|
23
|
+
)}
|
|
24
|
+
{...props}
|
|
25
|
+
>
|
|
26
|
+
{children}
|
|
27
|
+
<ChevronRight className="ml-auto h-4 w-4" />
|
|
28
|
+
</DropdownMenuPrimitive.SubTrigger>
|
|
29
|
+
))
|
|
30
|
+
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
|
|
31
|
+
|
|
32
|
+
const DropdownMenuSubContent = React.forwardRef<
|
|
33
|
+
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
|
34
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
|
35
|
+
>(({ className, ...props }, ref) => (
|
|
36
|
+
<DropdownMenuPrimitive.SubContent
|
|
37
|
+
ref={ref}
|
|
38
|
+
className={cn(
|
|
39
|
+
'z-50 min-w-[10rem] overflow-hidden rounded-md border border-line-strong bg-popover p-1 text-popover-foreground shadow-elevation',
|
|
40
|
+
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
|
41
|
+
className,
|
|
42
|
+
)}
|
|
43
|
+
{...props}
|
|
44
|
+
/>
|
|
45
|
+
))
|
|
46
|
+
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
|
|
47
|
+
|
|
48
|
+
// Matches docs/mocks/web/issue-detail.html's `.menu` — the ~20-action
|
|
49
|
+
// surface for M-W3, 246px wide, dark surface, subtle border, elevation.
|
|
50
|
+
const DropdownMenuContent = React.forwardRef<
|
|
51
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
52
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
|
53
|
+
>(({ className, sideOffset = 7, ...props }, ref) => (
|
|
54
|
+
<DropdownMenuPrimitive.Portal>
|
|
55
|
+
<DropdownMenuPrimitive.Content
|
|
56
|
+
ref={ref}
|
|
57
|
+
sideOffset={sideOffset}
|
|
58
|
+
className={cn(
|
|
59
|
+
'z-50 min-w-[246px] overflow-hidden rounded-md border border-line-strong bg-popover p-[5px] text-popover-foreground shadow-elevation',
|
|
60
|
+
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
|
61
|
+
className,
|
|
62
|
+
)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
</DropdownMenuPrimitive.Portal>
|
|
66
|
+
))
|
|
67
|
+
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
|
68
|
+
|
|
69
|
+
const DropdownMenuItem = React.forwardRef<
|
|
70
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
71
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
72
|
+
inset?: boolean
|
|
73
|
+
/** Matches `.menu button.hot` — the review-findings escalation item. */
|
|
74
|
+
variant?: 'default' | 'hot' | 'danger'
|
|
75
|
+
}
|
|
76
|
+
>(({ className, inset, variant = 'default', ...props }, ref) => (
|
|
77
|
+
<DropdownMenuPrimitive.Item
|
|
78
|
+
ref={ref}
|
|
79
|
+
className={cn(
|
|
80
|
+
'relative flex cursor-default select-none items-center gap-2.5 rounded-sm px-2.5 py-1.5 text-sm text-muted-foreground outline-none transition-colors',
|
|
81
|
+
'hover:bg-secondary hover:text-foreground focus:bg-secondary focus:text-foreground',
|
|
82
|
+
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
83
|
+
'[&_svg]:h-3.5 [&_svg]:w-3.5 [&_svg]:shrink-0 [&_svg]:opacity-80',
|
|
84
|
+
inset && 'pl-8',
|
|
85
|
+
variant === 'hot' && 'text-attn hover:text-attn focus:text-attn',
|
|
86
|
+
variant === 'danger' && 'text-fail hover:text-fail focus:text-fail',
|
|
87
|
+
className,
|
|
88
|
+
)}
|
|
89
|
+
{...props}
|
|
90
|
+
/>
|
|
91
|
+
))
|
|
92
|
+
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
|
93
|
+
|
|
94
|
+
const DropdownMenuCheckboxItem = React.forwardRef<
|
|
95
|
+
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
|
96
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
|
97
|
+
>(({ className, children, checked, ...props }, ref) => (
|
|
98
|
+
<DropdownMenuPrimitive.CheckboxItem
|
|
99
|
+
ref={ref}
|
|
100
|
+
className={cn(
|
|
101
|
+
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2.5 text-sm text-muted-foreground outline-none transition-colors hover:bg-secondary hover:text-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
102
|
+
className,
|
|
103
|
+
)}
|
|
104
|
+
checked={checked}
|
|
105
|
+
{...props}
|
|
106
|
+
>
|
|
107
|
+
<span className="absolute left-2.5 flex h-3.5 w-3.5 items-center justify-center">
|
|
108
|
+
<DropdownMenuPrimitive.ItemIndicator>
|
|
109
|
+
<Check className="h-4 w-4" />
|
|
110
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
111
|
+
</span>
|
|
112
|
+
{children}
|
|
113
|
+
</DropdownMenuPrimitive.CheckboxItem>
|
|
114
|
+
))
|
|
115
|
+
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
|
|
116
|
+
|
|
117
|
+
const DropdownMenuRadioItem = React.forwardRef<
|
|
118
|
+
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
|
119
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
|
120
|
+
>(({ className, children, ...props }, ref) => (
|
|
121
|
+
<DropdownMenuPrimitive.RadioItem
|
|
122
|
+
ref={ref}
|
|
123
|
+
className={cn(
|
|
124
|
+
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2.5 text-sm text-muted-foreground outline-none transition-colors hover:bg-secondary hover:text-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
125
|
+
className,
|
|
126
|
+
)}
|
|
127
|
+
{...props}
|
|
128
|
+
>
|
|
129
|
+
<span className="absolute left-2.5 flex h-3.5 w-3.5 items-center justify-center">
|
|
130
|
+
<DropdownMenuPrimitive.ItemIndicator>
|
|
131
|
+
<Circle className="h-2 w-2 fill-current" />
|
|
132
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
133
|
+
</span>
|
|
134
|
+
{children}
|
|
135
|
+
</DropdownMenuPrimitive.RadioItem>
|
|
136
|
+
))
|
|
137
|
+
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
|
138
|
+
|
|
139
|
+
// Matches `.menu-label` — the "Start — interactive" / "Start — automated" section headers.
|
|
140
|
+
const DropdownMenuLabel = React.forwardRef<
|
|
141
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
|
142
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { inset?: boolean }
|
|
143
|
+
>(({ className, inset, ...props }, ref) => (
|
|
144
|
+
<DropdownMenuPrimitive.Label
|
|
145
|
+
ref={ref}
|
|
146
|
+
className={cn(
|
|
147
|
+
'px-2.5 pb-1 pt-2 text-[0.63rem] font-semibold uppercase tracking-wider text-faint',
|
|
148
|
+
inset && 'pl-8',
|
|
149
|
+
className,
|
|
150
|
+
)}
|
|
151
|
+
{...props}
|
|
152
|
+
/>
|
|
153
|
+
))
|
|
154
|
+
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
|
155
|
+
|
|
156
|
+
const DropdownMenuSeparator = React.forwardRef<
|
|
157
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
|
158
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
159
|
+
>(({ className, ...props }, ref) => (
|
|
160
|
+
<DropdownMenuPrimitive.Separator
|
|
161
|
+
ref={ref}
|
|
162
|
+
className={cn('-mx-[3px] my-1 h-px bg-border', className)}
|
|
163
|
+
{...props}
|
|
164
|
+
/>
|
|
165
|
+
))
|
|
166
|
+
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
|
167
|
+
|
|
168
|
+
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => (
|
|
169
|
+
<span className={cn('ml-auto font-mono text-xs text-faint', className)} {...props} />
|
|
170
|
+
)
|
|
171
|
+
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
|
|
172
|
+
|
|
173
|
+
export {
|
|
174
|
+
DropdownMenu,
|
|
175
|
+
DropdownMenuTrigger,
|
|
176
|
+
DropdownMenuContent,
|
|
177
|
+
DropdownMenuItem,
|
|
178
|
+
DropdownMenuCheckboxItem,
|
|
179
|
+
DropdownMenuRadioItem,
|
|
180
|
+
DropdownMenuLabel,
|
|
181
|
+
DropdownMenuSeparator,
|
|
182
|
+
DropdownMenuShortcut,
|
|
183
|
+
DropdownMenuGroup,
|
|
184
|
+
DropdownMenuPortal,
|
|
185
|
+
DropdownMenuSub,
|
|
186
|
+
DropdownMenuSubContent,
|
|
187
|
+
DropdownMenuSubTrigger,
|
|
188
|
+
DropdownMenuRadioGroup,
|
|
189
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { cn } from '@/lib/utils'
|
|
3
|
+
|
|
4
|
+
export interface EmptyStateProps {
|
|
5
|
+
icon?: ReactNode
|
|
6
|
+
title: string
|
|
7
|
+
description?: string
|
|
8
|
+
action?: ReactNode
|
|
9
|
+
className?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A blank/zero-result state — not in a mock verbatim (none of the three
|
|
14
|
+
* Gate-A screens hit one), but built from the same vocabulary: faint icon,
|
|
15
|
+
* ui-sans title, text-dim description, generous padding, no card chrome of
|
|
16
|
+
* its own since it usually sits inside one (e.g. an empty `.list` or
|
|
17
|
+
* `.card-body`).
|
|
18
|
+
*/
|
|
19
|
+
export function EmptyState({ icon, title, description, action, className }: EmptyStateProps) {
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
className={cn(
|
|
23
|
+
'flex flex-col items-center gap-3 px-6 py-12 text-center',
|
|
24
|
+
className,
|
|
25
|
+
)}
|
|
26
|
+
>
|
|
27
|
+
{icon && <div className="text-faint [&_svg]:h-8 [&_svg]:w-8">{icon}</div>}
|
|
28
|
+
<div className="flex flex-col gap-1">
|
|
29
|
+
<p className="text-sm font-medium text-foreground">{title}</p>
|
|
30
|
+
{description && <p className="text-sm text-muted-foreground">{description}</p>}
|
|
31
|
+
</div>
|
|
32
|
+
{action && <div className="mt-1">{action}</div>}
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
|
3
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
4
|
+
import { X } from 'lucide-react'
|
|
5
|
+
import { cn } from '@/lib/utils'
|
|
6
|
+
|
|
7
|
+
const Sheet = DialogPrimitive.Root
|
|
8
|
+
const SheetTrigger = DialogPrimitive.Trigger
|
|
9
|
+
const SheetClose = DialogPrimitive.Close
|
|
10
|
+
const SheetPortal = DialogPrimitive.Portal
|
|
11
|
+
|
|
12
|
+
const SheetOverlay = React.forwardRef<
|
|
13
|
+
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
|
14
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
15
|
+
>(({ className, ...props }, ref) => (
|
|
16
|
+
<DialogPrimitive.Overlay
|
|
17
|
+
ref={ref}
|
|
18
|
+
className={cn(
|
|
19
|
+
'fixed inset-0 z-50 bg-black/60 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
|
20
|
+
className,
|
|
21
|
+
)}
|
|
22
|
+
{...props}
|
|
23
|
+
/>
|
|
24
|
+
))
|
|
25
|
+
SheetOverlay.displayName = DialogPrimitive.Overlay.displayName
|
|
26
|
+
|
|
27
|
+
// "bottom" is the default: this is the mobile action sheet the mocks'
|
|
28
|
+
// narrow layout (docs/mocks/web/pipeline-narrow.html) needs for the
|
|
29
|
+
// per-issue action list on phone widths, where a bottom sheet reads as a
|
|
30
|
+
// native affordance and a centered dialog doesn't.
|
|
31
|
+
const sheetVariants = cva(
|
|
32
|
+
'fixed z-50 gap-4 bg-card p-6 text-card-foreground shadow-elevation transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-200 data-[state=open]:duration-300',
|
|
33
|
+
{
|
|
34
|
+
variants: {
|
|
35
|
+
side: {
|
|
36
|
+
top: 'inset-x-0 top-0 border-b border-border data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
|
|
37
|
+
bottom:
|
|
38
|
+
'inset-x-0 bottom-0 rounded-t-lg border-t border-border data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
|
|
39
|
+
left: 'inset-y-0 left-0 h-full w-3/4 border-r border-border data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
|
|
40
|
+
right:
|
|
41
|
+
'inset-y-0 right-0 h-full w-3/4 border-l border-border data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
defaultVariants: {
|
|
45
|
+
side: 'bottom',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
interface SheetContentProps
|
|
51
|
+
extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>,
|
|
52
|
+
VariantProps<typeof sheetVariants> {}
|
|
53
|
+
|
|
54
|
+
const SheetContent = React.forwardRef<
|
|
55
|
+
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
56
|
+
SheetContentProps
|
|
57
|
+
>(({ side = 'bottom', className, children, ...props }, ref) => (
|
|
58
|
+
<SheetPortal>
|
|
59
|
+
<SheetOverlay />
|
|
60
|
+
<DialogPrimitive.Content
|
|
61
|
+
ref={ref}
|
|
62
|
+
className={cn(sheetVariants({ side }), className)}
|
|
63
|
+
{...props}
|
|
64
|
+
>
|
|
65
|
+
{children}
|
|
66
|
+
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm text-muted-foreground opacity-70 transition-opacity hover:opacity-100 hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none">
|
|
67
|
+
<X className="h-4 w-4" />
|
|
68
|
+
<span className="sr-only">Close</span>
|
|
69
|
+
</DialogPrimitive.Close>
|
|
70
|
+
</DialogPrimitive.Content>
|
|
71
|
+
</SheetPortal>
|
|
72
|
+
))
|
|
73
|
+
SheetContent.displayName = DialogPrimitive.Content.displayName
|
|
74
|
+
|
|
75
|
+
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
76
|
+
<div className={cn('flex flex-col gap-1.5 text-left', className)} {...props} />
|
|
77
|
+
)
|
|
78
|
+
SheetHeader.displayName = 'SheetHeader'
|
|
79
|
+
|
|
80
|
+
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
81
|
+
<div
|
|
82
|
+
className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
|
|
83
|
+
{...props}
|
|
84
|
+
/>
|
|
85
|
+
)
|
|
86
|
+
SheetFooter.displayName = 'SheetFooter'
|
|
87
|
+
|
|
88
|
+
const SheetTitle = React.forwardRef<
|
|
89
|
+
React.ElementRef<typeof DialogPrimitive.Title>,
|
|
90
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
91
|
+
>(({ className, ...props }, ref) => (
|
|
92
|
+
<DialogPrimitive.Title
|
|
93
|
+
ref={ref}
|
|
94
|
+
className={cn('text-base font-semibold text-foreground', className)}
|
|
95
|
+
{...props}
|
|
96
|
+
/>
|
|
97
|
+
))
|
|
98
|
+
SheetTitle.displayName = DialogPrimitive.Title.displayName
|
|
99
|
+
|
|
100
|
+
const SheetDescription = React.forwardRef<
|
|
101
|
+
React.ElementRef<typeof DialogPrimitive.Description>,
|
|
102
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
103
|
+
>(({ className, ...props }, ref) => (
|
|
104
|
+
<DialogPrimitive.Description
|
|
105
|
+
ref={ref}
|
|
106
|
+
className={cn('text-sm text-muted-foreground', className)}
|
|
107
|
+
{...props}
|
|
108
|
+
/>
|
|
109
|
+
))
|
|
110
|
+
SheetDescription.displayName = DialogPrimitive.Description.displayName
|
|
111
|
+
|
|
112
|
+
export {
|
|
113
|
+
Sheet,
|
|
114
|
+
SheetPortal,
|
|
115
|
+
SheetOverlay,
|
|
116
|
+
SheetTrigger,
|
|
117
|
+
SheetClose,
|
|
118
|
+
SheetContent,
|
|
119
|
+
SheetHeader,
|
|
120
|
+
SheetFooter,
|
|
121
|
+
SheetTitle,
|
|
122
|
+
SheetDescription,
|
|
123
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'react'
|
|
2
|
+
import { cn } from '@/lib/utils'
|
|
3
|
+
|
|
4
|
+
/** Loading placeholder — a pulsing block the size of the content it stands in for. */
|
|
5
|
+
function Skeleton({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
|
|
6
|
+
return <div className={cn('animate-pulse rounded-md bg-surface-2', className)} {...props} />
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export { Skeleton }
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import * as TabsPrimitive from '@radix-ui/react-tabs'
|
|
3
|
+
import { cn } from '@/lib/utils'
|
|
4
|
+
|
|
5
|
+
const Tabs = TabsPrimitive.Root
|
|
6
|
+
|
|
7
|
+
// Underline style, matching docs/mocks/web/issue-detail.html's `.tabs`/`.tab`
|
|
8
|
+
// (border-bottom on the active tab, not a pill/segmented look).
|
|
9
|
+
const TabsList = React.forwardRef<
|
|
10
|
+
React.ElementRef<typeof TabsPrimitive.List>,
|
|
11
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
|
12
|
+
>(({ className, ...props }, ref) => (
|
|
13
|
+
<TabsPrimitive.List
|
|
14
|
+
ref={ref}
|
|
15
|
+
className={cn('flex items-center gap-2 border-b border-border', className)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
))
|
|
19
|
+
TabsList.displayName = TabsPrimitive.List.displayName
|
|
20
|
+
|
|
21
|
+
const TabsTrigger = React.forwardRef<
|
|
22
|
+
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
|
23
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
|
24
|
+
>(({ className, ...props }, ref) => (
|
|
25
|
+
<TabsPrimitive.Trigger
|
|
26
|
+
ref={ref}
|
|
27
|
+
className={cn(
|
|
28
|
+
'flex items-center gap-1.5 whitespace-nowrap border-b-2 border-transparent px-3 py-2.5 text-sm text-muted-foreground transition-colors',
|
|
29
|
+
'hover:text-foreground',
|
|
30
|
+
'data-[state=active]:border-primary data-[state=active]:text-foreground',
|
|
31
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',
|
|
32
|
+
'disabled:pointer-events-none disabled:opacity-50',
|
|
33
|
+
className,
|
|
34
|
+
)}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
))
|
|
38
|
+
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
|
39
|
+
|
|
40
|
+
const TabsContent = React.forwardRef<
|
|
41
|
+
React.ElementRef<typeof TabsPrimitive.Content>,
|
|
42
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
|
43
|
+
>(({ className, ...props }, ref) => (
|
|
44
|
+
<TabsPrimitive.Content
|
|
45
|
+
ref={ref}
|
|
46
|
+
className={cn(
|
|
47
|
+
'mt-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
|
48
|
+
className,
|
|
49
|
+
)}
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
))
|
|
53
|
+
TabsContent.displayName = TabsPrimitive.Content.displayName
|
|
54
|
+
|
|
55
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Dark/light theme (#1546). Binary by design -- the mocks' rail has one
|
|
5
|
+
* "Theme" button that flips dark <-> light (docs/mocks/web/pipeline-wide.html),
|
|
6
|
+
* not a three-way light/dark/system picker, so that's the surface this
|
|
7
|
+
* exposes too. "System" still matters as the *source* of the initial value
|
|
8
|
+
* (see `systemTheme` below); it just isn't a mode a user can select back to
|
|
9
|
+
* once they've picked explicitly, matching the mock.
|
|
10
|
+
*/
|
|
11
|
+
export type Theme = 'light' | 'dark'
|
|
12
|
+
|
|
13
|
+
export const THEME_STORAGE_KEY = 'coord-web-theme'
|
|
14
|
+
|
|
15
|
+
function systemTheme(): Theme {
|
|
16
|
+
if (typeof window === 'undefined' || !window.matchMedia) return 'dark'
|
|
17
|
+
// Dark is the default per the design (docs/mocks/web/README.md): only an
|
|
18
|
+
// explicit OS preference for light should override it.
|
|
19
|
+
return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readStoredTheme(): Theme | null {
|
|
23
|
+
if (typeof window === 'undefined') return null
|
|
24
|
+
try {
|
|
25
|
+
const stored = window.localStorage.getItem(THEME_STORAGE_KEY)
|
|
26
|
+
return stored === 'light' || stored === 'dark' ? stored : null
|
|
27
|
+
} catch {
|
|
28
|
+
// localStorage can throw in private-browsing / lockdown modes.
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Mirrors the blocking script in index.html so React's first render already agrees with it -- no flash. */
|
|
34
|
+
function initialTheme(): Theme {
|
|
35
|
+
return readStoredTheme() ?? systemTheme()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface ThemeContextValue {
|
|
39
|
+
theme: Theme
|
|
40
|
+
setTheme: (theme: Theme) => void
|
|
41
|
+
toggleTheme: () => void
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined)
|
|
45
|
+
|
|
46
|
+
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
47
|
+
const [theme, setThemeState] = useState<Theme>(initialTheme)
|
|
48
|
+
|
|
49
|
+
// The inline script in index.html already set data-theme before paint;
|
|
50
|
+
// this keeps it in sync whenever `theme` changes from here on.
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
document.documentElement.setAttribute('data-theme', theme)
|
|
53
|
+
}, [theme])
|
|
54
|
+
|
|
55
|
+
const setTheme = (next: Theme) => {
|
|
56
|
+
setThemeState(next)
|
|
57
|
+
try {
|
|
58
|
+
window.localStorage.setItem(THEME_STORAGE_KEY, next)
|
|
59
|
+
} catch {
|
|
60
|
+
// Persistence is a nice-to-have; the theme still applies this session.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const toggleTheme = () => setTheme(theme === 'dark' ? 'light' : 'dark')
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
|
|
68
|
+
{children}
|
|
69
|
+
</ThemeContext.Provider>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// eslint-disable-next-line react-refresh/only-export-components -- a hook alongside its provider component is the standard context pattern (see components/ui/badge.tsx for the same precedent).
|
|
74
|
+
export function useTheme(): ThemeContextValue {
|
|
75
|
+
const ctx = useContext(ThemeContext)
|
|
76
|
+
if (!ctx) throw new Error('useTheme must be used within a ThemeProvider')
|
|
77
|
+
return ctx
|
|
78
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Moon, Sun } from 'lucide-react'
|
|
2
|
+
import { Button } from '@/components/ui/button'
|
|
3
|
+
import { useTheme } from '@/components/ui/theme-provider'
|
|
4
|
+
|
|
5
|
+
/** The rail's "Theme" button (docs/mocks/web/pipeline-wide.html) — flips dark/light, persists. */
|
|
6
|
+
export function ThemeToggle() {
|
|
7
|
+
const { theme, toggleTheme } = useTheme()
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<Button
|
|
11
|
+
variant="ghost"
|
|
12
|
+
size="icon"
|
|
13
|
+
onClick={toggleTheme}
|
|
14
|
+
aria-label={theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme'}
|
|
15
|
+
title="Theme"
|
|
16
|
+
>
|
|
17
|
+
{theme === 'dark' ? <Sun /> : <Moon />}
|
|
18
|
+
</Button>
|
|
19
|
+
)
|
|
20
|
+
}
|