@thxgg/steward 0.1.0
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/.env.example +7 -0
- package/LICENSE +21 -0
- package/README.md +175 -0
- package/app/app.vue +14 -0
- package/app/assets/css/main.css +129 -0
- package/app/components/CommandPalette.vue +182 -0
- package/app/components/ShortcutsHelp.vue +85 -0
- package/app/components/git/ChangesMinimap.vue +143 -0
- package/app/components/git/CommitList.vue +224 -0
- package/app/components/git/DiffPanel.vue +402 -0
- package/app/components/git/DiffViewer.vue +803 -0
- package/app/components/layout/RepoSelector.vue +358 -0
- package/app/components/layout/Sidebar.vue +91 -0
- package/app/components/prd/Meta.vue +69 -0
- package/app/components/prd/Viewer.vue +285 -0
- package/app/components/tasks/Board.vue +86 -0
- package/app/components/tasks/Card.vue +108 -0
- package/app/components/tasks/Column.vue +108 -0
- package/app/components/tasks/Detail.vue +291 -0
- package/app/components/ui/badge/Badge.vue +26 -0
- package/app/components/ui/badge/index.ts +26 -0
- package/app/components/ui/button/Button.vue +29 -0
- package/app/components/ui/button/index.ts +38 -0
- package/app/components/ui/card/Card.vue +22 -0
- package/app/components/ui/card/CardAction.vue +17 -0
- package/app/components/ui/card/CardContent.vue +17 -0
- package/app/components/ui/card/CardDescription.vue +17 -0
- package/app/components/ui/card/CardFooter.vue +17 -0
- package/app/components/ui/card/CardHeader.vue +17 -0
- package/app/components/ui/card/CardTitle.vue +17 -0
- package/app/components/ui/card/index.ts +7 -0
- package/app/components/ui/combobox/Combobox.vue +19 -0
- package/app/components/ui/combobox/ComboboxAnchor.vue +23 -0
- package/app/components/ui/combobox/ComboboxEmpty.vue +21 -0
- package/app/components/ui/combobox/ComboboxGroup.vue +27 -0
- package/app/components/ui/combobox/ComboboxInput.vue +42 -0
- package/app/components/ui/combobox/ComboboxItem.vue +24 -0
- package/app/components/ui/combobox/ComboboxItemIndicator.vue +23 -0
- package/app/components/ui/combobox/ComboboxList.vue +33 -0
- package/app/components/ui/combobox/ComboboxSeparator.vue +21 -0
- package/app/components/ui/combobox/ComboboxTrigger.vue +24 -0
- package/app/components/ui/combobox/ComboboxViewport.vue +23 -0
- package/app/components/ui/combobox/index.ts +13 -0
- package/app/components/ui/command/Command.vue +103 -0
- package/app/components/ui/command/CommandDialog.vue +33 -0
- package/app/components/ui/command/CommandEmpty.vue +27 -0
- package/app/components/ui/command/CommandGroup.vue +45 -0
- package/app/components/ui/command/CommandInput.vue +54 -0
- package/app/components/ui/command/CommandItem.vue +76 -0
- package/app/components/ui/command/CommandList.vue +25 -0
- package/app/components/ui/command/CommandSeparator.vue +21 -0
- package/app/components/ui/command/CommandShortcut.vue +17 -0
- package/app/components/ui/command/index.ts +25 -0
- package/app/components/ui/dialog/Dialog.vue +19 -0
- package/app/components/ui/dialog/DialogClose.vue +15 -0
- package/app/components/ui/dialog/DialogContent.vue +53 -0
- package/app/components/ui/dialog/DialogDescription.vue +23 -0
- package/app/components/ui/dialog/DialogFooter.vue +15 -0
- package/app/components/ui/dialog/DialogHeader.vue +17 -0
- package/app/components/ui/dialog/DialogOverlay.vue +21 -0
- package/app/components/ui/dialog/DialogScrollContent.vue +59 -0
- package/app/components/ui/dialog/DialogTitle.vue +23 -0
- package/app/components/ui/dialog/DialogTrigger.vue +15 -0
- package/app/components/ui/dialog/index.ts +10 -0
- package/app/components/ui/input/Input.vue +33 -0
- package/app/components/ui/input/index.ts +1 -0
- package/app/components/ui/scroll-area/ScrollArea.vue +33 -0
- package/app/components/ui/scroll-area/ScrollBar.vue +32 -0
- package/app/components/ui/scroll-area/index.ts +2 -0
- package/app/components/ui/separator/Separator.vue +29 -0
- package/app/components/ui/separator/index.ts +1 -0
- package/app/components/ui/sheet/Sheet.vue +19 -0
- package/app/components/ui/sheet/SheetClose.vue +15 -0
- package/app/components/ui/sheet/SheetContent.vue +62 -0
- package/app/components/ui/sheet/SheetDescription.vue +21 -0
- package/app/components/ui/sheet/SheetFooter.vue +16 -0
- package/app/components/ui/sheet/SheetHeader.vue +15 -0
- package/app/components/ui/sheet/SheetOverlay.vue +21 -0
- package/app/components/ui/sheet/SheetTitle.vue +21 -0
- package/app/components/ui/sheet/SheetTrigger.vue +15 -0
- package/app/components/ui/sheet/index.ts +8 -0
- package/app/components/ui/tabs/Tabs.vue +24 -0
- package/app/components/ui/tabs/TabsContent.vue +21 -0
- package/app/components/ui/tabs/TabsList.vue +24 -0
- package/app/components/ui/tabs/TabsTrigger.vue +26 -0
- package/app/components/ui/tabs/index.ts +4 -0
- package/app/components/ui/tooltip/Tooltip.vue +19 -0
- package/app/components/ui/tooltip/TooltipContent.vue +34 -0
- package/app/components/ui/tooltip/TooltipProvider.vue +14 -0
- package/app/components/ui/tooltip/TooltipTrigger.vue +15 -0
- package/app/components/ui/tooltip/index.ts +4 -0
- package/app/composables/useFileWatch.ts +78 -0
- package/app/composables/useGit.ts +180 -0
- package/app/composables/useKeyboard.ts +180 -0
- package/app/composables/usePrd.ts +86 -0
- package/app/composables/useRepos.ts +108 -0
- package/app/composables/useThemeMode.ts +38 -0
- package/app/composables/useToast.ts +31 -0
- package/app/layouts/default.vue +197 -0
- package/app/lib/utils.ts +7 -0
- package/app/pages/[repo]/[prd].vue +263 -0
- package/app/pages/index.vue +257 -0
- package/app/types/git.ts +81 -0
- package/app/types/index.ts +29 -0
- package/app/types/prd.ts +49 -0
- package/app/types/repo.ts +37 -0
- package/app/types/task.ts +134 -0
- package/bin/prd +21 -0
- package/components.json +21 -0
- package/dist/app/types/git.js +1 -0
- package/dist/app/types/prd.js +1 -0
- package/dist/app/types/repo.js +1 -0
- package/dist/app/types/task.js +1 -0
- package/dist/host/src/api/git.js +96 -0
- package/dist/host/src/api/index.js +4 -0
- package/dist/host/src/api/prds.js +195 -0
- package/dist/host/src/api/repos.js +47 -0
- package/dist/host/src/api/state.js +63 -0
- package/dist/host/src/executor.js +109 -0
- package/dist/host/src/index.js +95 -0
- package/dist/host/src/mcp.js +62 -0
- package/dist/host/src/ui.js +64 -0
- package/dist/server/utils/db.js +125 -0
- package/dist/server/utils/git.js +396 -0
- package/dist/server/utils/prd-state.js +229 -0
- package/dist/server/utils/repos.js +256 -0
- package/docs/MCP.md +180 -0
- package/nuxt.config.ts +34 -0
- package/package.json +88 -0
- package/public/favicon.ico +0 -0
- package/public/robots.txt +1 -0
- package/server/api/browse.get.ts +52 -0
- package/server/api/repos/[repoId]/git/commits.get.ts +103 -0
- package/server/api/repos/[repoId]/git/diff.get.ts +77 -0
- package/server/api/repos/[repoId]/git/file-content.get.ts +66 -0
- package/server/api/repos/[repoId]/git/file-diff.get.ts +109 -0
- package/server/api/repos/[repoId]/prd/[prdSlug]/progress.get.ts +36 -0
- package/server/api/repos/[repoId]/prd/[prdSlug]/tasks/[taskId]/commits.get.ts +146 -0
- package/server/api/repos/[repoId]/prd/[prdSlug]/tasks.get.ts +36 -0
- package/server/api/repos/[repoId]/prd/[prdSlug].get.ts +97 -0
- package/server/api/repos/[repoId]/prds.get.ts +85 -0
- package/server/api/repos/[repoId]/refresh-git-repos.post.ts +42 -0
- package/server/api/repos/[repoId].delete.ts +27 -0
- package/server/api/repos/index.get.ts +5 -0
- package/server/api/repos/index.post.ts +39 -0
- package/server/api/watch.get.ts +63 -0
- package/server/plugins/migrate-legacy-state.ts +19 -0
- package/server/tsconfig.json +3 -0
- package/server/utils/db.ts +169 -0
- package/server/utils/git.ts +478 -0
- package/server/utils/prd-state.ts +335 -0
- package/server/utils/repos.ts +322 -0
- package/server/utils/watcher.ts +179 -0
- package/tsconfig.json +4 -0
package/.env.example
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Optional: absolute path to the global PRD SQLite database file.
|
|
2
|
+
# If unset, PRD Viewer uses ${XDG_DATA_HOME:-~/.local/share}/prd/state.db.
|
|
3
|
+
PRD_STATE_DB_PATH=
|
|
4
|
+
|
|
5
|
+
# Optional: base directory for global PRD state.
|
|
6
|
+
# When set, database path resolves to <PRD_STATE_HOME>/state.db.
|
|
7
|
+
PRD_STATE_HOME=
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 thxgg
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Steward
|
|
2
|
+
|
|
3
|
+
Steward is a local-first Nuxt app for browsing Product Requirements Documents and tracking implementation state in a global SQLite database.
|
|
4
|
+
|
|
5
|
+
It is published as `@thxgg/steward` and provides the `prd` CLI command.
|
|
6
|
+
|
|
7
|
+
This project is intended to be downloaded and run locally on your machine. It is not designed for public internet deployment.
|
|
8
|
+
|
|
9
|
+
## What It Does
|
|
10
|
+
|
|
11
|
+
- Scans repositories for `docs/prd/*.md` files and displays them in a focused UI.
|
|
12
|
+
- Reads task state (`tasks.json` and `progress.json`) from a global SQLite database.
|
|
13
|
+
- Supports legacy `.claude/state/*` migration into the global database.
|
|
14
|
+
- Resolves commit references for tasks, including pseudo-monorepo sub-repo contexts.
|
|
15
|
+
|
|
16
|
+
## Local-First Security Model
|
|
17
|
+
|
|
18
|
+
Steward exposes local filesystem and git metadata for convenience. Because of that:
|
|
19
|
+
|
|
20
|
+
- Run it only on trusted local machines.
|
|
21
|
+
- Do not expose it directly to the public internet.
|
|
22
|
+
- Treat it as a developer workstation tool, not a hosted multi-user service.
|
|
23
|
+
|
|
24
|
+
## Requirements
|
|
25
|
+
|
|
26
|
+
- Node.js 22+
|
|
27
|
+
- npm (or pnpm)
|
|
28
|
+
- Git
|
|
29
|
+
- Linux/macOS (or compatible environment)
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install
|
|
35
|
+
npm run dev
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
With pnpm:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pnpm install
|
|
42
|
+
pnpm run dev
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Open `http://localhost:3000`, then add a local repository path that contains a `docs/prd/` directory.
|
|
46
|
+
|
|
47
|
+
## Install CLI
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install -g @thxgg/steward
|
|
51
|
+
prd ui
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
With pnpm:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pnpm add -g @thxgg/steward
|
|
58
|
+
prd ui
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Without global install:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx -y @thxgg/steward ui
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
You can also run the app through the project CLI:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm run ui
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Scripts
|
|
74
|
+
|
|
75
|
+
- `npm run dev` - Start local dev server
|
|
76
|
+
- `npm run typecheck` - Run Nuxt type checking
|
|
77
|
+
- `npm run build` - Build production bundle
|
|
78
|
+
- `npm run preview` - Preview the production build locally
|
|
79
|
+
- `npm run ui` - Launch `prd ui` (dev mode)
|
|
80
|
+
- `npm run mcp` - Start MCP server over stdio
|
|
81
|
+
|
|
82
|
+
## CLI
|
|
83
|
+
|
|
84
|
+
This package ships a `prd` command.
|
|
85
|
+
|
|
86
|
+
- npm package: `@thxgg/steward`
|
|
87
|
+
|
|
88
|
+
- `prd ui` - Launch the web app in dev mode (default)
|
|
89
|
+
- `prd ui --preview` - Launch the production preview server
|
|
90
|
+
- `prd ui --port 3100 --host 127.0.0.1` - Pass Nuxt host/port options
|
|
91
|
+
- `prd mcp` - Run the codemode MCP server over stdio
|
|
92
|
+
|
|
93
|
+
For local shell usage while developing this repo:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npm link
|
|
97
|
+
prd ui
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## MCP Usage
|
|
101
|
+
|
|
102
|
+
The MCP server exposes one codemode tool: `execute`.
|
|
103
|
+
|
|
104
|
+
The codemode runtime has these APIs in scope:
|
|
105
|
+
|
|
106
|
+
- `repos` - list/add/remove/refresh repository registrations
|
|
107
|
+
- `prds` - list/get PRD documents and state-backed task/progress data
|
|
108
|
+
- `git` - commit metadata, diffs, and file content lookups
|
|
109
|
+
- `state` - direct PRD state reads/writes in SQLite
|
|
110
|
+
|
|
111
|
+
Example MCP client configuration:
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"mcpServers": {
|
|
116
|
+
"prd": {
|
|
117
|
+
"command": "prd",
|
|
118
|
+
"args": ["mcp"]
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
For codemode API details and examples, see `docs/MCP.md`.
|
|
125
|
+
|
|
126
|
+
## Global PRD State Storage
|
|
127
|
+
|
|
128
|
+
PRD state is system-global and stored in SQLite:
|
|
129
|
+
|
|
130
|
+
- Default: `${XDG_DATA_HOME:-~/.local/share}/prd/state.db`
|
|
131
|
+
- Override full path: `PRD_STATE_DB_PATH`
|
|
132
|
+
- Override base dir: `PRD_STATE_HOME` (resolved to `<PRD_STATE_HOME>/state.db`)
|
|
133
|
+
|
|
134
|
+
This allows multiple local repos to share one PRD state store.
|
|
135
|
+
|
|
136
|
+
## Environment Variables
|
|
137
|
+
|
|
138
|
+
Copy `.env.example` to `.env` if you want to override defaults.
|
|
139
|
+
|
|
140
|
+
| Variable | Required | Description |
|
|
141
|
+
| --- | --- | --- |
|
|
142
|
+
| `PRD_STATE_DB_PATH` | No | Absolute path to SQLite database file |
|
|
143
|
+
| `PRD_STATE_HOME` | No | Directory used for database home (`state.db` inside it) |
|
|
144
|
+
| `XDG_DATA_HOME` | No | Used for default global state location |
|
|
145
|
+
|
|
146
|
+
## OpenCode Bundle Included
|
|
147
|
+
|
|
148
|
+
This repository includes a curated OpenCode bundle under `opencode/`:
|
|
149
|
+
|
|
150
|
+
- Commands: `prd`, `prd-task`, `complete-next-task`, `commit`
|
|
151
|
+
- Skills: `prd`, `prd-task`, `complete-next-task`, `commit`
|
|
152
|
+
- Script: `prd-db.mjs`
|
|
153
|
+
|
|
154
|
+
`frontend-design` is intentionally not included.
|
|
155
|
+
|
|
156
|
+
To install into a local OpenCode config:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
mkdir -p ~/.config/opencode
|
|
160
|
+
cp -R opencode/commands ~/.config/opencode/
|
|
161
|
+
cp -R opencode/skills ~/.config/opencode/
|
|
162
|
+
cp -R opencode/scripts ~/.config/opencode/
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## CI
|
|
166
|
+
|
|
167
|
+
GitHub Actions runs:
|
|
168
|
+
|
|
169
|
+
- Typecheck + build validation
|
|
170
|
+
- Secret scanning with gitleaks
|
|
171
|
+
|
|
172
|
+
## Roadmap
|
|
173
|
+
|
|
174
|
+
- Continue hardening local-only workflows
|
|
175
|
+
- Expand codemode APIs and automation patterns
|
package/app/app.vue
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Toaster } from 'vue-sonner'
|
|
3
|
+
|
|
4
|
+
const { resolvedTheme } = useThemeMode()
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<NuxtLayout>
|
|
9
|
+
<NuxtPage />
|
|
10
|
+
</NuxtLayout>
|
|
11
|
+
<ClientOnly>
|
|
12
|
+
<Toaster position="bottom-right" :theme="resolvedTheme" rich-colors />
|
|
13
|
+
</ClientOnly>
|
|
14
|
+
</template>
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
3
|
+
|
|
4
|
+
@custom-variant dark (&:is(.dark *));
|
|
5
|
+
|
|
6
|
+
@theme inline {
|
|
7
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
8
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
9
|
+
--radius-lg: var(--radius);
|
|
10
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
11
|
+
--color-background: var(--background);
|
|
12
|
+
--color-foreground: var(--foreground);
|
|
13
|
+
--color-card: var(--card);
|
|
14
|
+
--color-card-foreground: var(--card-foreground);
|
|
15
|
+
--color-popover: var(--popover);
|
|
16
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
17
|
+
--color-primary: var(--primary);
|
|
18
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
19
|
+
--color-secondary: var(--secondary);
|
|
20
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
21
|
+
--color-muted: var(--muted);
|
|
22
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
23
|
+
--color-accent: var(--accent);
|
|
24
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
25
|
+
--color-destructive: var(--destructive);
|
|
26
|
+
--color-border: var(--border);
|
|
27
|
+
--color-input: var(--input);
|
|
28
|
+
--color-ring: var(--ring);
|
|
29
|
+
--color-chart-1: var(--chart-1);
|
|
30
|
+
--color-chart-2: var(--chart-2);
|
|
31
|
+
--color-chart-3: var(--chart-3);
|
|
32
|
+
--color-chart-4: var(--chart-4);
|
|
33
|
+
--color-chart-5: var(--chart-5);
|
|
34
|
+
--color-sidebar: var(--sidebar);
|
|
35
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
36
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
37
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
38
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
39
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
40
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
41
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
:root {
|
|
45
|
+
--radius: 0.625rem;
|
|
46
|
+
--background: oklch(1 0 0);
|
|
47
|
+
--foreground: oklch(0.145 0 0);
|
|
48
|
+
--card: oklch(1 0 0);
|
|
49
|
+
--card-foreground: oklch(0.145 0 0);
|
|
50
|
+
--popover: oklch(1 0 0);
|
|
51
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
52
|
+
--primary: oklch(0.205 0 0);
|
|
53
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
54
|
+
--secondary: oklch(0.97 0 0);
|
|
55
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
56
|
+
--muted: oklch(0.97 0 0);
|
|
57
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
58
|
+
--accent: oklch(0.97 0 0);
|
|
59
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
60
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
61
|
+
--border: oklch(0.922 0 0);
|
|
62
|
+
--input: oklch(0.922 0 0);
|
|
63
|
+
--ring: oklch(0.708 0 0);
|
|
64
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
65
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
66
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
67
|
+
--chart-4: oklch(0.828 0.189 84.429);
|
|
68
|
+
--chart-5: oklch(0.769 0.188 70.08);
|
|
69
|
+
--sidebar: oklch(0.985 0 0);
|
|
70
|
+
--sidebar-foreground: oklch(0.145 0 0);
|
|
71
|
+
--sidebar-primary: oklch(0.205 0 0);
|
|
72
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
73
|
+
--sidebar-accent: oklch(0.97 0 0);
|
|
74
|
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
75
|
+
--sidebar-border: oklch(0.922 0 0);
|
|
76
|
+
--sidebar-ring: oklch(0.708 0 0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.dark {
|
|
80
|
+
--background: oklch(0.145 0 0);
|
|
81
|
+
--foreground: oklch(0.985 0 0);
|
|
82
|
+
--card: oklch(0.205 0 0);
|
|
83
|
+
--card-foreground: oklch(0.985 0 0);
|
|
84
|
+
--popover: oklch(0.205 0 0);
|
|
85
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
86
|
+
--primary: oklch(0.922 0 0);
|
|
87
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
88
|
+
--secondary: oklch(0.269 0 0);
|
|
89
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
90
|
+
--muted: oklch(0.269 0 0);
|
|
91
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
92
|
+
--accent: oklch(0.269 0 0);
|
|
93
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
94
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
95
|
+
--border: oklch(1 0 0 / 10%);
|
|
96
|
+
--input: oklch(1 0 0 / 15%);
|
|
97
|
+
--ring: oklch(0.556 0 0);
|
|
98
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
99
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
100
|
+
--chart-3: oklch(0.769 0.188 70.08);
|
|
101
|
+
--chart-4: oklch(0.627 0.265 303.9);
|
|
102
|
+
--chart-5: oklch(0.645 0.246 16.439);
|
|
103
|
+
--sidebar: oklch(0.205 0 0);
|
|
104
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
105
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
106
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
107
|
+
--sidebar-accent: oklch(0.269 0 0);
|
|
108
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
109
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
110
|
+
--sidebar-ring: oklch(0.556 0 0);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@layer base {
|
|
114
|
+
* {
|
|
115
|
+
@apply border-border outline-ring/50;
|
|
116
|
+
}
|
|
117
|
+
body {
|
|
118
|
+
@apply bg-background text-foreground;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* Hide scrollbar utility */
|
|
123
|
+
.scrollbar-hide {
|
|
124
|
+
-ms-overflow-style: none;
|
|
125
|
+
scrollbar-width: none;
|
|
126
|
+
}
|
|
127
|
+
.scrollbar-hide::-webkit-scrollbar {
|
|
128
|
+
display: none;
|
|
129
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { FileText, Monitor, Moon, Sun, LayoutGrid, Folder, Check, Keyboard, RefreshCw } from 'lucide-vue-next'
|
|
3
|
+
import {
|
|
4
|
+
CommandDialog,
|
|
5
|
+
CommandEmpty,
|
|
6
|
+
CommandGroup,
|
|
7
|
+
CommandInput,
|
|
8
|
+
CommandItem,
|
|
9
|
+
CommandList,
|
|
10
|
+
CommandSeparator
|
|
11
|
+
} from '~/components/ui/command'
|
|
12
|
+
|
|
13
|
+
const open = defineModel<boolean>('open', { default: false })
|
|
14
|
+
const filter = defineModel<string>('filter', { default: '' })
|
|
15
|
+
|
|
16
|
+
const emit = defineEmits<{
|
|
17
|
+
openShortcutsHelp: []
|
|
18
|
+
}>()
|
|
19
|
+
|
|
20
|
+
const router = useRouter()
|
|
21
|
+
const { themeMode, cycleThemeMode } = useThemeMode()
|
|
22
|
+
const { prds } = usePrd()
|
|
23
|
+
const { repos, currentRepoId, selectRepo, refreshGitRepos } = useRepos()
|
|
24
|
+
const { showSuccess, showError } = useToast()
|
|
25
|
+
|
|
26
|
+
// Get current tab from localStorage
|
|
27
|
+
const currentTab = ref<'document' | 'board'>('document')
|
|
28
|
+
|
|
29
|
+
if (import.meta.client) {
|
|
30
|
+
const saved = localStorage.getItem('prd-viewer-tab')
|
|
31
|
+
if (saved === 'document' || saved === 'board') {
|
|
32
|
+
currentTab.value = saved
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function navigateToPrd(slug: string) {
|
|
37
|
+
if (!currentRepoId.value) return
|
|
38
|
+
router.push(`/${currentRepoId.value}/${slug}`)
|
|
39
|
+
open.value = false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function switchRepo(repoId: string) {
|
|
43
|
+
selectRepo(repoId)
|
|
44
|
+
router.push('/')
|
|
45
|
+
open.value = false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function toggleTheme() {
|
|
49
|
+
cycleThemeMode()
|
|
50
|
+
open.value = false
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function toggleTab() {
|
|
54
|
+
const newTab = currentTab.value === 'document' ? 'board' : 'document'
|
|
55
|
+
currentTab.value = newTab
|
|
56
|
+
if (import.meta.client) {
|
|
57
|
+
localStorage.setItem('prd-viewer-tab', newTab)
|
|
58
|
+
// Dispatch a storage event so the page can react
|
|
59
|
+
window.dispatchEvent(new StorageEvent('storage', {
|
|
60
|
+
key: 'prd-viewer-tab',
|
|
61
|
+
newValue: newTab
|
|
62
|
+
}))
|
|
63
|
+
}
|
|
64
|
+
open.value = false
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function openShortcutsHelp() {
|
|
68
|
+
open.value = false
|
|
69
|
+
emit('openShortcutsHelp')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const isRefreshingGitRepos = ref(false)
|
|
73
|
+
|
|
74
|
+
async function handleRefreshGitRepos() {
|
|
75
|
+
if (!currentRepoId.value || isRefreshingGitRepos.value) return
|
|
76
|
+
|
|
77
|
+
isRefreshingGitRepos.value = true
|
|
78
|
+
open.value = false
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const result = await refreshGitRepos(currentRepoId.value)
|
|
82
|
+
showSuccess(`Discovered ${result.discovered} git repositories`)
|
|
83
|
+
} catch {
|
|
84
|
+
showError('Failed to refresh git repos')
|
|
85
|
+
} finally {
|
|
86
|
+
isRefreshingGitRepos.value = false
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Clear the filter when dialog closes
|
|
91
|
+
watch(open, (isOpen) => {
|
|
92
|
+
if (!isOpen) {
|
|
93
|
+
filter.value = ''
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// Detect platform for shortcut hints
|
|
98
|
+
const isMac = computed(() => {
|
|
99
|
+
if (!import.meta.client) return true
|
|
100
|
+
return navigator.platform.toUpperCase().indexOf('MAC') >= 0
|
|
101
|
+
})
|
|
102
|
+
const modKey = computed(() => isMac.value ? '⌘' : 'Ctrl')
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
<template>
|
|
106
|
+
<CommandDialog v-model:open="open" :default-search="filter">
|
|
107
|
+
<CommandInput placeholder="Type a command or search..." />
|
|
108
|
+
<CommandList>
|
|
109
|
+
<CommandEmpty>No results found.</CommandEmpty>
|
|
110
|
+
|
|
111
|
+
<CommandGroup v-if="prds?.length" heading="Documents">
|
|
112
|
+
<CommandItem
|
|
113
|
+
v-for="prd in prds"
|
|
114
|
+
:key="prd.slug"
|
|
115
|
+
:value="`PRD: ${prd.name}`"
|
|
116
|
+
@select="navigateToPrd(prd.slug)"
|
|
117
|
+
>
|
|
118
|
+
<FileText class="size-4" />
|
|
119
|
+
<span>{{ prd.name }}</span>
|
|
120
|
+
</CommandItem>
|
|
121
|
+
</CommandGroup>
|
|
122
|
+
|
|
123
|
+
<CommandSeparator v-if="prds?.length && repos?.length" />
|
|
124
|
+
|
|
125
|
+
<CommandGroup v-if="repos?.length" heading="Repositories">
|
|
126
|
+
<CommandItem
|
|
127
|
+
v-for="repo in repos"
|
|
128
|
+
:key="repo.id"
|
|
129
|
+
:value="`repo-${repo.id} ${repo.name}`"
|
|
130
|
+
@select="switchRepo(repo.id)"
|
|
131
|
+
>
|
|
132
|
+
<Folder class="size-4" />
|
|
133
|
+
<span class="flex-1">{{ repo.name }}</span>
|
|
134
|
+
<Check v-if="repo.id === currentRepoId" class="size-4 text-primary" />
|
|
135
|
+
</CommandItem>
|
|
136
|
+
</CommandGroup>
|
|
137
|
+
|
|
138
|
+
<CommandSeparator v-if="repos?.length" />
|
|
139
|
+
|
|
140
|
+
<CommandGroup heading="Actions">
|
|
141
|
+
<CommandItem value="cycle-theme light dark system" @select="toggleTheme">
|
|
142
|
+
<Monitor v-if="themeMode === 'system'" class="size-4" />
|
|
143
|
+
<Sun v-else-if="themeMode === 'light'" class="size-4" />
|
|
144
|
+
<Moon v-else class="size-4" />
|
|
145
|
+
<span class="flex-1">Cycle theme mode ({{ themeMode }})</span>
|
|
146
|
+
<div class="ml-auto flex items-center gap-1">
|
|
147
|
+
<kbd class="inline-flex h-5 min-w-5 items-center justify-center rounded border border-border bg-muted px-1.5 font-mono text-[10px] text-muted-foreground">{{ modKey }}</kbd>
|
|
148
|
+
<kbd class="inline-flex h-5 min-w-5 items-center justify-center rounded border border-border bg-muted px-1.5 font-mono text-[10px] text-muted-foreground">.</kbd>
|
|
149
|
+
</div>
|
|
150
|
+
</CommandItem>
|
|
151
|
+
<CommandItem
|
|
152
|
+
:value="`switch-tab ${currentTab === 'document' ? 'task board' : 'document'}`"
|
|
153
|
+
@select="toggleTab"
|
|
154
|
+
>
|
|
155
|
+
<LayoutGrid class="size-4" />
|
|
156
|
+
<span class="flex-1">Switch to {{ currentTab === 'document' ? 'Task Board' : 'Document' }}</span>
|
|
157
|
+
<div class="ml-auto flex items-center gap-1">
|
|
158
|
+
<kbd class="inline-flex h-5 min-w-5 items-center justify-center rounded border border-border bg-muted px-1.5 font-mono text-[10px] text-muted-foreground">{{ modKey }}</kbd>
|
|
159
|
+
<kbd class="inline-flex h-5 min-w-5 items-center justify-center rounded border border-border bg-muted px-1.5 font-mono text-[10px] text-muted-foreground">\</kbd>
|
|
160
|
+
</div>
|
|
161
|
+
</CommandItem>
|
|
162
|
+
<CommandItem value="keyboard shortcuts help" @select="openShortcutsHelp">
|
|
163
|
+
<Keyboard class="size-4" />
|
|
164
|
+
<span class="flex-1">Keyboard shortcuts</span>
|
|
165
|
+
<div class="ml-auto flex items-center gap-1">
|
|
166
|
+
<kbd class="inline-flex h-5 min-w-5 items-center justify-center rounded border border-border bg-muted px-1.5 font-mono text-[10px] text-muted-foreground">{{ modKey }}</kbd>
|
|
167
|
+
<kbd class="inline-flex h-5 min-w-5 items-center justify-center rounded border border-border bg-muted px-1.5 font-mono text-[10px] text-muted-foreground">/</kbd>
|
|
168
|
+
</div>
|
|
169
|
+
</CommandItem>
|
|
170
|
+
<CommandItem
|
|
171
|
+
v-if="currentRepoId"
|
|
172
|
+
value="refresh git repos rescan discover"
|
|
173
|
+
:disabled="isRefreshingGitRepos"
|
|
174
|
+
@select="handleRefreshGitRepos"
|
|
175
|
+
>
|
|
176
|
+
<RefreshCw class="size-4" :class="{ 'animate-spin': isRefreshingGitRepos }" />
|
|
177
|
+
<span class="flex-1">Refresh git repos</span>
|
|
178
|
+
</CommandItem>
|
|
179
|
+
</CommandGroup>
|
|
180
|
+
</CommandList>
|
|
181
|
+
</CommandDialog>
|
|
182
|
+
</template>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogContent,
|
|
5
|
+
DialogDescription,
|
|
6
|
+
DialogHeader,
|
|
7
|
+
DialogTitle
|
|
8
|
+
} from '~/components/ui/dialog'
|
|
9
|
+
|
|
10
|
+
const open = defineModel<boolean>('open', { default: false })
|
|
11
|
+
|
|
12
|
+
// Detect if user is on Mac
|
|
13
|
+
const isMac = computed(() => {
|
|
14
|
+
if (!import.meta.client) return true // Default to Mac for SSR
|
|
15
|
+
return navigator.platform.toUpperCase().indexOf('MAC') >= 0
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
// Modifier key display
|
|
19
|
+
const modKey = computed(() => isMac.value ? '⌘' : 'Ctrl')
|
|
20
|
+
|
|
21
|
+
// Shortcut groups
|
|
22
|
+
const shortcutGroups = computed(() => [
|
|
23
|
+
{
|
|
24
|
+
name: 'Navigation',
|
|
25
|
+
shortcuts: [
|
|
26
|
+
{ keys: [modKey.value, 'K'], description: 'Open command palette' },
|
|
27
|
+
{ keys: [modKey.value, 'J'], description: 'Quick jump to document' }
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'Actions',
|
|
32
|
+
shortcuts: [
|
|
33
|
+
{ keys: [modKey.value, '.'], description: 'Cycle theme mode (light/dark/system)' },
|
|
34
|
+
{ keys: [modKey.value, '\\'], description: 'Switch between Document and Task Board' },
|
|
35
|
+
{ keys: [modKey.value, ','], description: 'Add repository' }
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'Help',
|
|
40
|
+
shortcuts: [
|
|
41
|
+
{ keys: [modKey.value, '/'], description: 'Show this help' },
|
|
42
|
+
{ keys: ['Esc'], description: 'Close dialog or palette' }
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
])
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<template>
|
|
49
|
+
<Dialog v-model:open="open">
|
|
50
|
+
<DialogContent class="max-w-md">
|
|
51
|
+
<DialogHeader>
|
|
52
|
+
<DialogTitle>Keyboard Shortcuts</DialogTitle>
|
|
53
|
+
<DialogDescription>
|
|
54
|
+
Quick reference for available keyboard shortcuts.
|
|
55
|
+
</DialogDescription>
|
|
56
|
+
</DialogHeader>
|
|
57
|
+
|
|
58
|
+
<div class="space-y-6">
|
|
59
|
+
<div v-for="group in shortcutGroups" :key="group.name">
|
|
60
|
+
<h3 class="mb-3 text-sm font-medium text-muted-foreground">
|
|
61
|
+
{{ group.name }}
|
|
62
|
+
</h3>
|
|
63
|
+
<div class="space-y-2">
|
|
64
|
+
<div
|
|
65
|
+
v-for="shortcut in group.shortcuts"
|
|
66
|
+
:key="shortcut.description"
|
|
67
|
+
class="flex items-center justify-between"
|
|
68
|
+
>
|
|
69
|
+
<span class="text-sm">{{ shortcut.description }}</span>
|
|
70
|
+
<div class="flex items-center gap-1">
|
|
71
|
+
<kbd
|
|
72
|
+
v-for="(key, index) in shortcut.keys"
|
|
73
|
+
:key="index"
|
|
74
|
+
class="inline-flex h-5 min-w-5 items-center justify-center rounded border border-border bg-muted px-1.5 font-mono text-xs font-medium text-muted-foreground"
|
|
75
|
+
>
|
|
76
|
+
{{ key }}
|
|
77
|
+
</kbd>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</DialogContent>
|
|
84
|
+
</Dialog>
|
|
85
|
+
</template>
|