@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.
Files changed (154) hide show
  1. package/.env.example +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +175 -0
  4. package/app/app.vue +14 -0
  5. package/app/assets/css/main.css +129 -0
  6. package/app/components/CommandPalette.vue +182 -0
  7. package/app/components/ShortcutsHelp.vue +85 -0
  8. package/app/components/git/ChangesMinimap.vue +143 -0
  9. package/app/components/git/CommitList.vue +224 -0
  10. package/app/components/git/DiffPanel.vue +402 -0
  11. package/app/components/git/DiffViewer.vue +803 -0
  12. package/app/components/layout/RepoSelector.vue +358 -0
  13. package/app/components/layout/Sidebar.vue +91 -0
  14. package/app/components/prd/Meta.vue +69 -0
  15. package/app/components/prd/Viewer.vue +285 -0
  16. package/app/components/tasks/Board.vue +86 -0
  17. package/app/components/tasks/Card.vue +108 -0
  18. package/app/components/tasks/Column.vue +108 -0
  19. package/app/components/tasks/Detail.vue +291 -0
  20. package/app/components/ui/badge/Badge.vue +26 -0
  21. package/app/components/ui/badge/index.ts +26 -0
  22. package/app/components/ui/button/Button.vue +29 -0
  23. package/app/components/ui/button/index.ts +38 -0
  24. package/app/components/ui/card/Card.vue +22 -0
  25. package/app/components/ui/card/CardAction.vue +17 -0
  26. package/app/components/ui/card/CardContent.vue +17 -0
  27. package/app/components/ui/card/CardDescription.vue +17 -0
  28. package/app/components/ui/card/CardFooter.vue +17 -0
  29. package/app/components/ui/card/CardHeader.vue +17 -0
  30. package/app/components/ui/card/CardTitle.vue +17 -0
  31. package/app/components/ui/card/index.ts +7 -0
  32. package/app/components/ui/combobox/Combobox.vue +19 -0
  33. package/app/components/ui/combobox/ComboboxAnchor.vue +23 -0
  34. package/app/components/ui/combobox/ComboboxEmpty.vue +21 -0
  35. package/app/components/ui/combobox/ComboboxGroup.vue +27 -0
  36. package/app/components/ui/combobox/ComboboxInput.vue +42 -0
  37. package/app/components/ui/combobox/ComboboxItem.vue +24 -0
  38. package/app/components/ui/combobox/ComboboxItemIndicator.vue +23 -0
  39. package/app/components/ui/combobox/ComboboxList.vue +33 -0
  40. package/app/components/ui/combobox/ComboboxSeparator.vue +21 -0
  41. package/app/components/ui/combobox/ComboboxTrigger.vue +24 -0
  42. package/app/components/ui/combobox/ComboboxViewport.vue +23 -0
  43. package/app/components/ui/combobox/index.ts +13 -0
  44. package/app/components/ui/command/Command.vue +103 -0
  45. package/app/components/ui/command/CommandDialog.vue +33 -0
  46. package/app/components/ui/command/CommandEmpty.vue +27 -0
  47. package/app/components/ui/command/CommandGroup.vue +45 -0
  48. package/app/components/ui/command/CommandInput.vue +54 -0
  49. package/app/components/ui/command/CommandItem.vue +76 -0
  50. package/app/components/ui/command/CommandList.vue +25 -0
  51. package/app/components/ui/command/CommandSeparator.vue +21 -0
  52. package/app/components/ui/command/CommandShortcut.vue +17 -0
  53. package/app/components/ui/command/index.ts +25 -0
  54. package/app/components/ui/dialog/Dialog.vue +19 -0
  55. package/app/components/ui/dialog/DialogClose.vue +15 -0
  56. package/app/components/ui/dialog/DialogContent.vue +53 -0
  57. package/app/components/ui/dialog/DialogDescription.vue +23 -0
  58. package/app/components/ui/dialog/DialogFooter.vue +15 -0
  59. package/app/components/ui/dialog/DialogHeader.vue +17 -0
  60. package/app/components/ui/dialog/DialogOverlay.vue +21 -0
  61. package/app/components/ui/dialog/DialogScrollContent.vue +59 -0
  62. package/app/components/ui/dialog/DialogTitle.vue +23 -0
  63. package/app/components/ui/dialog/DialogTrigger.vue +15 -0
  64. package/app/components/ui/dialog/index.ts +10 -0
  65. package/app/components/ui/input/Input.vue +33 -0
  66. package/app/components/ui/input/index.ts +1 -0
  67. package/app/components/ui/scroll-area/ScrollArea.vue +33 -0
  68. package/app/components/ui/scroll-area/ScrollBar.vue +32 -0
  69. package/app/components/ui/scroll-area/index.ts +2 -0
  70. package/app/components/ui/separator/Separator.vue +29 -0
  71. package/app/components/ui/separator/index.ts +1 -0
  72. package/app/components/ui/sheet/Sheet.vue +19 -0
  73. package/app/components/ui/sheet/SheetClose.vue +15 -0
  74. package/app/components/ui/sheet/SheetContent.vue +62 -0
  75. package/app/components/ui/sheet/SheetDescription.vue +21 -0
  76. package/app/components/ui/sheet/SheetFooter.vue +16 -0
  77. package/app/components/ui/sheet/SheetHeader.vue +15 -0
  78. package/app/components/ui/sheet/SheetOverlay.vue +21 -0
  79. package/app/components/ui/sheet/SheetTitle.vue +21 -0
  80. package/app/components/ui/sheet/SheetTrigger.vue +15 -0
  81. package/app/components/ui/sheet/index.ts +8 -0
  82. package/app/components/ui/tabs/Tabs.vue +24 -0
  83. package/app/components/ui/tabs/TabsContent.vue +21 -0
  84. package/app/components/ui/tabs/TabsList.vue +24 -0
  85. package/app/components/ui/tabs/TabsTrigger.vue +26 -0
  86. package/app/components/ui/tabs/index.ts +4 -0
  87. package/app/components/ui/tooltip/Tooltip.vue +19 -0
  88. package/app/components/ui/tooltip/TooltipContent.vue +34 -0
  89. package/app/components/ui/tooltip/TooltipProvider.vue +14 -0
  90. package/app/components/ui/tooltip/TooltipTrigger.vue +15 -0
  91. package/app/components/ui/tooltip/index.ts +4 -0
  92. package/app/composables/useFileWatch.ts +78 -0
  93. package/app/composables/useGit.ts +180 -0
  94. package/app/composables/useKeyboard.ts +180 -0
  95. package/app/composables/usePrd.ts +86 -0
  96. package/app/composables/useRepos.ts +108 -0
  97. package/app/composables/useThemeMode.ts +38 -0
  98. package/app/composables/useToast.ts +31 -0
  99. package/app/layouts/default.vue +197 -0
  100. package/app/lib/utils.ts +7 -0
  101. package/app/pages/[repo]/[prd].vue +263 -0
  102. package/app/pages/index.vue +257 -0
  103. package/app/types/git.ts +81 -0
  104. package/app/types/index.ts +29 -0
  105. package/app/types/prd.ts +49 -0
  106. package/app/types/repo.ts +37 -0
  107. package/app/types/task.ts +134 -0
  108. package/bin/prd +21 -0
  109. package/components.json +21 -0
  110. package/dist/app/types/git.js +1 -0
  111. package/dist/app/types/prd.js +1 -0
  112. package/dist/app/types/repo.js +1 -0
  113. package/dist/app/types/task.js +1 -0
  114. package/dist/host/src/api/git.js +96 -0
  115. package/dist/host/src/api/index.js +4 -0
  116. package/dist/host/src/api/prds.js +195 -0
  117. package/dist/host/src/api/repos.js +47 -0
  118. package/dist/host/src/api/state.js +63 -0
  119. package/dist/host/src/executor.js +109 -0
  120. package/dist/host/src/index.js +95 -0
  121. package/dist/host/src/mcp.js +62 -0
  122. package/dist/host/src/ui.js +64 -0
  123. package/dist/server/utils/db.js +125 -0
  124. package/dist/server/utils/git.js +396 -0
  125. package/dist/server/utils/prd-state.js +229 -0
  126. package/dist/server/utils/repos.js +256 -0
  127. package/docs/MCP.md +180 -0
  128. package/nuxt.config.ts +34 -0
  129. package/package.json +88 -0
  130. package/public/favicon.ico +0 -0
  131. package/public/robots.txt +1 -0
  132. package/server/api/browse.get.ts +52 -0
  133. package/server/api/repos/[repoId]/git/commits.get.ts +103 -0
  134. package/server/api/repos/[repoId]/git/diff.get.ts +77 -0
  135. package/server/api/repos/[repoId]/git/file-content.get.ts +66 -0
  136. package/server/api/repos/[repoId]/git/file-diff.get.ts +109 -0
  137. package/server/api/repos/[repoId]/prd/[prdSlug]/progress.get.ts +36 -0
  138. package/server/api/repos/[repoId]/prd/[prdSlug]/tasks/[taskId]/commits.get.ts +146 -0
  139. package/server/api/repos/[repoId]/prd/[prdSlug]/tasks.get.ts +36 -0
  140. package/server/api/repos/[repoId]/prd/[prdSlug].get.ts +97 -0
  141. package/server/api/repos/[repoId]/prds.get.ts +85 -0
  142. package/server/api/repos/[repoId]/refresh-git-repos.post.ts +42 -0
  143. package/server/api/repos/[repoId].delete.ts +27 -0
  144. package/server/api/repos/index.get.ts +5 -0
  145. package/server/api/repos/index.post.ts +39 -0
  146. package/server/api/watch.get.ts +63 -0
  147. package/server/plugins/migrate-legacy-state.ts +19 -0
  148. package/server/tsconfig.json +3 -0
  149. package/server/utils/db.ts +169 -0
  150. package/server/utils/git.ts +478 -0
  151. package/server/utils/prd-state.ts +335 -0
  152. package/server/utils/repos.ts +322 -0
  153. package/server/utils/watcher.ts +179 -0
  154. 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>