@cat-factory/app 0.6.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/LICENSE +21 -0
- package/README.md +88 -0
- package/app/app.config.ts +8 -0
- package/app/app.vue +11 -0
- package/app/assets/css/main.css +100 -0
- package/app/components/auth/AuthGate.vue +24 -0
- package/app/components/auth/LoginScreen.vue +143 -0
- package/app/components/auth/UserMenu.vue +39 -0
- package/app/components/board/AddTaskModal.vue +444 -0
- package/app/components/board/AgentFailureCard.vue +97 -0
- package/app/components/board/AgentStopButton.vue +61 -0
- package/app/components/board/BoardCanvas.vue +183 -0
- package/app/components/board/ContextPicker.vue +367 -0
- package/app/components/board/RecurringPipelineModal.vue +219 -0
- package/app/components/board/TaskDependencyEdges.vue +132 -0
- package/app/components/board/nodes/AgentChip.vue +59 -0
- package/app/components/board/nodes/BlockNode.vue +433 -0
- package/app/components/board/nodes/DecisionBadge.vue +27 -0
- package/app/components/board/nodes/DraggableTask.vue +48 -0
- package/app/components/board/nodes/ModuleFrame.vue +97 -0
- package/app/components/board/nodes/TaskCard.vue +359 -0
- package/app/components/board/nodes/TaskPipelineMini.vue +159 -0
- package/app/components/bootstrap/BootstrapModal.vue +665 -0
- package/app/components/clarity/ClarityReviewWindow.vue +611 -0
- package/app/components/consensus/ConsensusSessionWindow.vue +210 -0
- package/app/components/documents/DocumentImportModal.vue +161 -0
- package/app/components/documents/DocumentSourceConnectModal.vue +127 -0
- package/app/components/documents/SpawnPreviewModal.vue +161 -0
- package/app/components/documents/TaskContextDocs.vue +83 -0
- package/app/components/focus/BlockFocusView.vue +171 -0
- package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
- package/app/components/gates/GateResultView.vue +282 -0
- package/app/components/github/AddServiceFromRepoModal.vue +354 -0
- package/app/components/github/GitHubConnect.vue +183 -0
- package/app/components/github/GitHubOnboarding.vue +45 -0
- package/app/components/github/GitHubPanel.vue +584 -0
- package/app/components/github/RepoTreeBrowser.vue +171 -0
- package/app/components/layout/AccountTeamSettings.vue +237 -0
- package/app/components/layout/BoardSwitcher.vue +280 -0
- package/app/components/layout/BoardToolbar.vue +156 -0
- package/app/components/layout/CommandBar.vue +336 -0
- package/app/components/layout/GitHubPatBanner.vue +73 -0
- package/app/components/layout/NotificationsInbox.vue +175 -0
- package/app/components/layout/SideBar.vue +314 -0
- package/app/components/layout/SpendWarningBanner.vue +107 -0
- package/app/components/observability/StepMetricsBar.vue +102 -0
- package/app/components/palettes/AgentPalette.vue +86 -0
- package/app/components/panels/AgentStepDetail.vue +737 -0
- package/app/components/panels/DecisionModal.vue +71 -0
- package/app/components/panels/InspectorPanel.vue +465 -0
- package/app/components/panels/ObservabilityPanel.vue +351 -0
- package/app/components/panels/StepMetadataCard.vue +253 -0
- package/app/components/panels/StepRestartControl.vue +90 -0
- package/app/components/panels/StepResultViewHost.vue +40 -0
- package/app/components/panels/StepTestReport.vue +84 -0
- package/app/components/panels/inspector/ContainerSummary.vue +74 -0
- package/app/components/panels/inspector/RecurringScheduleSettings.vue +178 -0
- package/app/components/panels/inspector/ServiceFragments.vue +82 -0
- package/app/components/panels/inspector/ServiceTestConfig.vue +198 -0
- package/app/components/panels/inspector/TaskAgentConfig.vue +81 -0
- package/app/components/panels/inspector/TaskDependencies.vue +70 -0
- package/app/components/panels/inspector/TaskEstimateBadge.vue +56 -0
- package/app/components/panels/inspector/TaskExecution.vue +364 -0
- package/app/components/panels/inspector/TaskRunSettings.vue +187 -0
- package/app/components/panels/inspector/TaskStructure.vue +96 -0
- package/app/components/pipeline/AgentKindIcon.vue +30 -0
- package/app/components/pipeline/IterationCapPrompt.vue +70 -0
- package/app/components/pipeline/PipelineBuilder.vue +817 -0
- package/app/components/pipeline/PipelineProgress.vue +484 -0
- package/app/components/providers/ApiKeysSection.vue +273 -0
- package/app/components/providers/PersonalCredentialModal.vue +128 -0
- package/app/components/providers/PersonalSubscriptionSection.vue +225 -0
- package/app/components/providers/VendorCredentialsModal.vue +197 -0
- package/app/components/recurring/RecurrenceEditor.vue +124 -0
- package/app/components/requirements/RequirementsReviewWindow.vue +620 -0
- package/app/components/settings/DatadogPanel.vue +213 -0
- package/app/components/settings/LocalModelEndpointsPanel.vue +286 -0
- package/app/components/settings/MergeThresholdsPanel.vue +378 -0
- package/app/components/settings/ModelDefaultsPanel.vue +250 -0
- package/app/components/settings/ServiceFragmentDefaultsPanel.vue +124 -0
- package/app/components/settings/WorkspaceSettingsPanel.vue +142 -0
- package/app/components/slack/SlackPanel.vue +299 -0
- package/app/components/tasks/TaskContextIssues.vue +88 -0
- package/app/components/tasks/TaskImportModal.vue +207 -0
- package/app/components/tasks/TaskSourceConnectModal.vue +133 -0
- package/app/components/testing/TestReportWindow.vue +404 -0
- package/app/composables/api/accounts.ts +81 -0
- package/app/composables/api/auth.ts +45 -0
- package/app/composables/api/board.ts +101 -0
- package/app/composables/api/bootstrap.ts +62 -0
- package/app/composables/api/context.ts +25 -0
- package/app/composables/api/documents.ts +74 -0
- package/app/composables/api/execution.ts +127 -0
- package/app/composables/api/fragments.ts +71 -0
- package/app/composables/api/github.ts +131 -0
- package/app/composables/api/models.ts +127 -0
- package/app/composables/api/notifications.ts +23 -0
- package/app/composables/api/presets.ts +29 -0
- package/app/composables/api/recurring.ts +68 -0
- package/app/composables/api/releaseHealth.ts +43 -0
- package/app/composables/api/reviews.ts +146 -0
- package/app/composables/api/slack.ts +54 -0
- package/app/composables/api/tasks.ts +72 -0
- package/app/composables/api/workspaces.ts +36 -0
- package/app/composables/useApi.ts +89 -0
- package/app/composables/useBlockDrag.ts +90 -0
- package/app/composables/useBlockQueries.ts +154 -0
- package/app/composables/useBoardFlow.ts +11 -0
- package/app/composables/useContextLinking.ts +65 -0
- package/app/composables/useDepLabels.ts +26 -0
- package/app/composables/useFrameResize.ts +54 -0
- package/app/composables/useResultView.ts +48 -0
- package/app/composables/useReviewStage.ts +40 -0
- package/app/composables/useSemanticZoom.ts +31 -0
- package/app/composables/useStepApproval.ts +233 -0
- package/app/composables/useStepProse.ts +78 -0
- package/app/composables/useStepTimer.ts +63 -0
- package/app/composables/useTaskExpansion.ts +92 -0
- package/app/composables/useWorkspaceStream.ts +155 -0
- package/app/docs/architecture.md +31 -0
- package/app/pages/index.vue +141 -0
- package/app/stores/accounts.ts +152 -0
- package/app/stores/agentConfig.ts +35 -0
- package/app/stores/agentRuns.ts +122 -0
- package/app/stores/agents.ts +40 -0
- package/app/stores/apiKeys.ts +108 -0
- package/app/stores/auth.ts +166 -0
- package/app/stores/board.spec.ts +205 -0
- package/app/stores/board.ts +286 -0
- package/app/stores/bootstrap.ts +97 -0
- package/app/stores/clarity.ts +196 -0
- package/app/stores/consensus.ts +60 -0
- package/app/stores/documents.ts +176 -0
- package/app/stores/execution.ts +273 -0
- package/app/stores/fragmentLibrary.ts +147 -0
- package/app/stores/fragments.ts +40 -0
- package/app/stores/github.ts +305 -0
- package/app/stores/localModels.ts +51 -0
- package/app/stores/mergePresets.ts +58 -0
- package/app/stores/modelDefaults.ts +76 -0
- package/app/stores/models.ts +134 -0
- package/app/stores/notifications.ts +70 -0
- package/app/stores/observability.ts +144 -0
- package/app/stores/personalSubscriptions.ts +215 -0
- package/app/stores/pipelines.ts +327 -0
- package/app/stores/recurringPipelines.ts +112 -0
- package/app/stores/releaseHealth.ts +75 -0
- package/app/stores/requirements.spec.ts +94 -0
- package/app/stores/requirements.ts +208 -0
- package/app/stores/serviceFragmentDefaults.ts +29 -0
- package/app/stores/services.ts +87 -0
- package/app/stores/slack.ts +142 -0
- package/app/stores/taskExpansion.ts +36 -0
- package/app/stores/tasks.spec.ts +71 -0
- package/app/stores/tasks.ts +176 -0
- package/app/stores/tracker.ts +27 -0
- package/app/stores/ui.ts +434 -0
- package/app/stores/vendorCredentials.ts +54 -0
- package/app/stores/workspace.ts +215 -0
- package/app/stores/workspaceSettings.ts +36 -0
- package/app/types/accounts.ts +77 -0
- package/app/types/bootstrap.ts +83 -0
- package/app/types/clarity.ts +59 -0
- package/app/types/consensus.ts +91 -0
- package/app/types/documents.ts +104 -0
- package/app/types/domain.ts +495 -0
- package/app/types/execution.ts +383 -0
- package/app/types/fragments.ts +72 -0
- package/app/types/github.ts +173 -0
- package/app/types/localModels.ts +73 -0
- package/app/types/merge.ts +71 -0
- package/app/types/models.ts +157 -0
- package/app/types/notifications.ts +74 -0
- package/app/types/recurring.ts +69 -0
- package/app/types/releaseHealth.ts +31 -0
- package/app/types/requirements.ts +61 -0
- package/app/types/services.ts +27 -0
- package/app/types/slack.ts +57 -0
- package/app/types/tasks.ts +82 -0
- package/app/types/tracker.ts +18 -0
- package/app/utils/agentOutput.spec.ts +128 -0
- package/app/utils/agentOutput.ts +173 -0
- package/app/utils/catalog.spec.ts +112 -0
- package/app/utils/catalog.ts +455 -0
- package/app/utils/dnd.ts +29 -0
- package/app/utils/observability.ts +52 -0
- package/app/utils/pipelineRender.ts +151 -0
- package/nuxt.config.ts +55 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Igor Savin
|
|
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,88 @@
|
|
|
1
|
+
# `@cat-factory/app` — Frontend (Nuxt layer)
|
|
2
|
+
|
|
3
|
+
The user-facing app, packaged as a **reusable Nuxt 4 layer**: a single-page app
|
|
4
|
+
that runs entirely in the browser and renders the architecture board, drives agent
|
|
5
|
+
pipelines, and reflects live execution. A deployment consumes it via
|
|
6
|
+
`extends: ['@cat-factory/app']` (see [`deploy/frontend`](../../deploy/frontend)).
|
|
7
|
+
It talks to the [backend Worker](../../backend/README.md) over REST and a single
|
|
8
|
+
WebSocket, sharing wire types from
|
|
9
|
+
[`@cat-factory/contracts`](../../backend/packages/contracts).
|
|
10
|
+
|
|
11
|
+
The SPA source lives under `app/` (the Nuxt srcDir).
|
|
12
|
+
|
|
13
|
+
## Table of contents
|
|
14
|
+
|
|
15
|
+
- [What it is](#what-it-is)
|
|
16
|
+
- [Tech stack](#tech-stack)
|
|
17
|
+
- [Layout](#layout)
|
|
18
|
+
- [Key UI surfaces](#key-ui-surfaces)
|
|
19
|
+
- [Develop & test](#develop--test)
|
|
20
|
+
|
|
21
|
+
## What it is
|
|
22
|
+
|
|
23
|
+
A spatial planning surface. You lay out a system as a **board** of frames
|
|
24
|
+
(services), modules and tasks on a [Vue Flow](https://vueflow.dev) canvas, wire up
|
|
25
|
+
dependencies, attach requirements, and apply **agent pipelines** to blocks.
|
|
26
|
+
Execution streams back in real time — step/subtask progress bars, decision
|
|
27
|
+
prompts, failures with retry — so the canvas doubles as a live dashboard.
|
|
28
|
+
|
|
29
|
+
It is a thin client: there is **no business logic here**. Every mutation calls the
|
|
30
|
+
Worker API and the stores hydrate from server snapshots and live updates pushed
|
|
31
|
+
over the WebSocket. How that sync works is written up in
|
|
32
|
+
[`app/docs/architecture.md`](./app/docs/architecture.md).
|
|
33
|
+
|
|
34
|
+
## Tech stack
|
|
35
|
+
|
|
36
|
+
- **Nuxt 4 / Vue 3** SPA — single route (`pages/index.vue`).
|
|
37
|
+
- **Pinia** (+ `pinia-plugin-persistedstate`) — feature stores.
|
|
38
|
+
- **Vue Flow** (`core`, `background`, `controls`, `minimap`, `node-resizer`) — the canvas.
|
|
39
|
+
- **Nuxt UI** + Tailwind — components and styling.
|
|
40
|
+
- **VueUse** — composable utilities.
|
|
41
|
+
- Lint/format via **oxlint** + **oxfmt**; tests via **vitest** + **happy-dom**.
|
|
42
|
+
|
|
43
|
+
## Layout
|
|
44
|
+
|
|
45
|
+
| Path | Contents |
|
|
46
|
+
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
47
|
+
| `app.vue` | Root; wraps the page in `AuthGate`. |
|
|
48
|
+
| `pages/index.vue` | The only route — mounts the sidebar, canvas, toolbar, inspector, focus view, and all modals. |
|
|
49
|
+
| `components/` | UI grouped by area (see [Key UI surfaces](#key-ui-surfaces)). |
|
|
50
|
+
| `composables/` | `useApi` (typed client), `useWorkspaceStream` (WebSocket sync), `useBlockDrag`, `useBlockQueries`, `useBoardFlow`, `useSemanticZoom`, `useDepLabels`. |
|
|
51
|
+
| `stores/` | Pinia stores, one per feature domain. |
|
|
52
|
+
| `types/` | TypeScript domain unions (`domain.ts`) and wire types mirroring the contracts. |
|
|
53
|
+
| `utils/` | Small pure helpers. |
|
|
54
|
+
|
|
55
|
+
## Key UI surfaces
|
|
56
|
+
|
|
57
|
+
- **Board canvas** (`components/board`) — `BoardCanvas` + `nodes/` (`BlockNode`,
|
|
58
|
+
`ModuleFrame`, `TaskCard`), dependency edges, the per-block `AgentFailureCard` /
|
|
59
|
+
`AgentStopButton`, and a deep-zoom `focus/BlockFocusView`.
|
|
60
|
+
- **Sidebar & chrome** (`components/layout`) — board/account switchers, palettes
|
|
61
|
+
entry points, the `SpendWarningBanner`, and the toolbar (zoom, LOD, decision
|
|
62
|
+
queue).
|
|
63
|
+
- **Palettes** (`components/palettes`) — drag blocks, pipelines and agents onto
|
|
64
|
+
the board.
|
|
65
|
+
- **Inspector** (`components/panels` + `panels/inspector`) — per-block tabs:
|
|
66
|
+
structure, dependencies, model + fragment picker, live execution, and linked
|
|
67
|
+
docs/issues/scenarios. Decisions resolve via `DecisionModal`.
|
|
68
|
+
- **Pipeline builder** (`components/pipeline`) — assemble/edit agent chains and
|
|
69
|
+
watch `PipelineProgress`.
|
|
70
|
+
- **Integrations** — modals/panels for `github`, `bootstrap`, `documents`,
|
|
71
|
+
`tasks`, `requirements` (review), `scenarios` (acceptance), and `fragments`
|
|
72
|
+
(the prompt-fragment library).
|
|
73
|
+
- **Auth** (`components/auth`) — `AuthGate` / `LoginScreen` / `UserMenu`; the app
|
|
74
|
+
is gated when the backend requires sign-in.
|
|
75
|
+
|
|
76
|
+
## Develop & test
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pnpm install
|
|
80
|
+
pnpm dev # Nuxt dev server (expects the Worker running; set NUXT_PUBLIC_API_BASE)
|
|
81
|
+
pnpm test # vitest
|
|
82
|
+
pnpm typecheck # nuxt typecheck
|
|
83
|
+
pnpm lint # oxlint + oxfmt --check
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
> Building/deploying the static site is covered in the deployment docs — see the
|
|
87
|
+
> [top-level README → Deployment](../../README.md#deployment) and
|
|
88
|
+
> [`deploy/frontend/README.md`](../../deploy/frontend/README.md).
|
package/app/app.vue
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
2
|
+
@import '@nuxt/ui';
|
|
3
|
+
|
|
4
|
+
/* Toast text should be selectable. Reka UI sets `user-select: none` inline on
|
|
5
|
+
* the toast root (to support swipe-to-dismiss); since `user-select` is
|
|
6
|
+
* inherited, declaring it directly on the text slots overrides that. */
|
|
7
|
+
[data-slot='viewport'] [data-slot='title'],
|
|
8
|
+
[data-slot='viewport'] [data-slot='description'] {
|
|
9
|
+
user-select: text;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* ---- Agent Architecture Board: shared visual language ---------------------- */
|
|
13
|
+
|
|
14
|
+
:root {
|
|
15
|
+
--board-bg: #0b1020;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
html,
|
|
19
|
+
body,
|
|
20
|
+
#__nuxt {
|
|
21
|
+
height: 100%;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
body {
|
|
25
|
+
margin: 0;
|
|
26
|
+
overflow: hidden;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Vue Flow surface tuned to a dark "design board" look */
|
|
30
|
+
.vue-flow__background {
|
|
31
|
+
background-color: var(--board-bg);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Status-pulse used by blocked / decision-needed blocks */
|
|
35
|
+
@keyframes board-pulse {
|
|
36
|
+
0%,
|
|
37
|
+
100% {
|
|
38
|
+
box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.55);
|
|
39
|
+
}
|
|
40
|
+
50% {
|
|
41
|
+
box-shadow: 0 0 0 10px rgba(245, 158, 11, 0);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.board-pulse {
|
|
46
|
+
animation: board-pulse 1.4s ease-in-out infinite;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Green pulse used when a block reaches the "PR ready" state */
|
|
50
|
+
@keyframes board-pulse-green {
|
|
51
|
+
0%,
|
|
52
|
+
100% {
|
|
53
|
+
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.6);
|
|
54
|
+
}
|
|
55
|
+
50% {
|
|
56
|
+
box-shadow: 0 0 0 12px rgba(34, 197, 94, 0);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.board-pulse-green {
|
|
61
|
+
animation: board-pulse-green 1.2s ease-in-out infinite;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Marching-ants on edges feeding an in-progress block */
|
|
65
|
+
.vue-flow__edge.is-live .vue-flow__edge-path {
|
|
66
|
+
stroke-dasharray: 6 4;
|
|
67
|
+
animation: board-dash 0.6s linear infinite;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* A selected edge: highlight it so the user knows what Delete/Backspace removes */
|
|
71
|
+
.vue-flow__edge.selected .vue-flow__edge-path {
|
|
72
|
+
stroke: #f43f5e !important;
|
|
73
|
+
stroke-width: 2.5 !important;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Fatter invisible hit area so thin edges are easy to click */
|
|
77
|
+
.vue-flow__edge .vue-flow__edge-interaction {
|
|
78
|
+
stroke-width: 18;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@keyframes board-dash {
|
|
82
|
+
to {
|
|
83
|
+
stroke-dashoffset: -20;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* Custom block node should not show the default node chrome */
|
|
88
|
+
.vue-flow__node-block {
|
|
89
|
+
padding: 0;
|
|
90
|
+
border: none;
|
|
91
|
+
background: transparent;
|
|
92
|
+
width: auto;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* Make palette drag images feel intentional. Scoped to a dedicated class so it
|
|
96
|
+
* never collides with Vue Flow's own `dragging` class, which it applies to the
|
|
97
|
+
* pane while panning (a bare `.dragging` rule would dim the whole board). */
|
|
98
|
+
.palette-dragging {
|
|
99
|
+
opacity: 0.5;
|
|
100
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import LoginScreen from '~/components/auth/LoginScreen.vue'
|
|
3
|
+
|
|
4
|
+
// Resolves auth state once on mount, then either renders the app (auth off, or
|
|
5
|
+
// on with a signed-in user) or the login screen. The board's own bootstrap runs
|
|
6
|
+
// inside the default slot, so it only fires once the user is allowed in.
|
|
7
|
+
const auth = useAuthStore()
|
|
8
|
+
|
|
9
|
+
onMounted(() => auth.bootstrap())
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<div
|
|
14
|
+
v-if="!auth.ready"
|
|
15
|
+
class="flex h-screen w-screen flex-col items-center justify-center gap-3 bg-slate-950 text-slate-400"
|
|
16
|
+
>
|
|
17
|
+
<UIcon name="i-lucide-loader" class="h-8 w-8 animate-spin" />
|
|
18
|
+
<span class="text-sm">Loading…</span>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<LoginScreen v-else-if="auth.required && !auth.user" />
|
|
22
|
+
|
|
23
|
+
<slot v-else />
|
|
24
|
+
</template>
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
const auth = useAuthStore()
|
|
5
|
+
|
|
6
|
+
// An invite token may ride in on the URL (?invite=…) — it flows through the OAuth
|
|
7
|
+
// redirect and the password signup so a brand-new user can join the org on first login.
|
|
8
|
+
const invite = computed(() => {
|
|
9
|
+
if (typeof window === 'undefined') return undefined
|
|
10
|
+
return new URLSearchParams(window.location.search).get('invite') || undefined
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
// Password form: signup creates a new user (invite or allowed-email-domain gated),
|
|
14
|
+
// login authenticates an existing one. Default to login; flip to signup when invited.
|
|
15
|
+
const mode = ref<'login' | 'signup'>(invite.value ? 'signup' : 'login')
|
|
16
|
+
const email = ref('')
|
|
17
|
+
const password = ref('')
|
|
18
|
+
const name = ref('')
|
|
19
|
+
const error = ref<string | null>(null)
|
|
20
|
+
const busy = ref(false)
|
|
21
|
+
|
|
22
|
+
async function submitPassword() {
|
|
23
|
+
error.value = null
|
|
24
|
+
busy.value = true
|
|
25
|
+
try {
|
|
26
|
+
if (mode.value === 'signup') {
|
|
27
|
+
await auth.signup({
|
|
28
|
+
email: email.value,
|
|
29
|
+
password: password.value,
|
|
30
|
+
name: name.value || undefined,
|
|
31
|
+
invite: invite.value,
|
|
32
|
+
})
|
|
33
|
+
} else {
|
|
34
|
+
await auth.passwordLogin({ email: email.value, password: password.value })
|
|
35
|
+
}
|
|
36
|
+
// Reload so the app boots with the new session.
|
|
37
|
+
if (typeof window !== 'undefined') window.location.assign(window.location.pathname)
|
|
38
|
+
} catch (e) {
|
|
39
|
+
error.value =
|
|
40
|
+
(e as { data?: { error?: { message?: string } } })?.data?.error?.message ??
|
|
41
|
+
'Sign-in failed. Check your details and try again.'
|
|
42
|
+
} finally {
|
|
43
|
+
busy.value = false
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const showOAuthDivider = computed(
|
|
48
|
+
() => auth.providers.password && (auth.providers.github || auth.providers.google),
|
|
49
|
+
)
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<template>
|
|
53
|
+
<div class="flex h-screen w-screen items-center justify-center bg-slate-950 text-slate-100">
|
|
54
|
+
<div
|
|
55
|
+
class="w-full max-w-sm rounded-xl border border-slate-800 bg-slate-900/80 p-8 backdrop-blur"
|
|
56
|
+
>
|
|
57
|
+
<div class="mb-6 text-center">
|
|
58
|
+
<UIcon name="i-lucide-layout-dashboard" class="mx-auto mb-3 h-10 w-10 text-indigo-400" />
|
|
59
|
+
<h1 class="mb-1 text-lg font-semibold text-white">Architecture Board</h1>
|
|
60
|
+
<p class="text-sm text-slate-400">
|
|
61
|
+
{{ invite ? 'Accept your invitation to continue.' : 'Sign in to continue.' }}
|
|
62
|
+
</p>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<!-- OAuth providers -->
|
|
66
|
+
<div class="space-y-2">
|
|
67
|
+
<UButton
|
|
68
|
+
v-if="auth.providers.github"
|
|
69
|
+
block
|
|
70
|
+
size="lg"
|
|
71
|
+
color="primary"
|
|
72
|
+
icon="i-lucide-github"
|
|
73
|
+
@click="auth.login(invite)"
|
|
74
|
+
>
|
|
75
|
+
Continue with GitHub
|
|
76
|
+
</UButton>
|
|
77
|
+
<UButton
|
|
78
|
+
v-if="auth.providers.google"
|
|
79
|
+
block
|
|
80
|
+
size="lg"
|
|
81
|
+
color="neutral"
|
|
82
|
+
variant="subtle"
|
|
83
|
+
icon="i-lucide-mail"
|
|
84
|
+
@click="auth.loginWithGoogle(invite)"
|
|
85
|
+
>
|
|
86
|
+
Continue with Google
|
|
87
|
+
</UButton>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div v-if="showOAuthDivider" class="my-4 flex items-center gap-3 text-xs text-slate-500">
|
|
91
|
+
<span class="h-px flex-1 bg-slate-800" /> or <span class="h-px flex-1 bg-slate-800" />
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<!-- Email / password -->
|
|
95
|
+
<form v-if="auth.providers.password" class="space-y-3" @submit.prevent="submitPassword">
|
|
96
|
+
<UInput
|
|
97
|
+
v-if="mode === 'signup'"
|
|
98
|
+
v-model="name"
|
|
99
|
+
placeholder="Name (optional)"
|
|
100
|
+
icon="i-lucide-user"
|
|
101
|
+
size="lg"
|
|
102
|
+
class="w-full"
|
|
103
|
+
/>
|
|
104
|
+
<UInput
|
|
105
|
+
v-model="email"
|
|
106
|
+
type="email"
|
|
107
|
+
required
|
|
108
|
+
placeholder="Email"
|
|
109
|
+
icon="i-lucide-at-sign"
|
|
110
|
+
size="lg"
|
|
111
|
+
class="w-full"
|
|
112
|
+
/>
|
|
113
|
+
<UInput
|
|
114
|
+
v-model="password"
|
|
115
|
+
type="password"
|
|
116
|
+
required
|
|
117
|
+
placeholder="Password"
|
|
118
|
+
icon="i-lucide-lock"
|
|
119
|
+
size="lg"
|
|
120
|
+
class="w-full"
|
|
121
|
+
/>
|
|
122
|
+
<p v-if="error" class="text-sm text-rose-400">{{ error }}</p>
|
|
123
|
+
<UButton block size="lg" color="primary" type="submit" :loading="busy">
|
|
124
|
+
{{ mode === 'signup' ? 'Create account' : 'Sign in' }}
|
|
125
|
+
</UButton>
|
|
126
|
+
<p class="text-center text-xs text-slate-400">
|
|
127
|
+
<template v-if="mode === 'login'">
|
|
128
|
+
Need an account?
|
|
129
|
+
<button type="button" class="text-indigo-400 hover:underline" @click="mode = 'signup'">
|
|
130
|
+
Sign up
|
|
131
|
+
</button>
|
|
132
|
+
</template>
|
|
133
|
+
<template v-else>
|
|
134
|
+
Already have an account?
|
|
135
|
+
<button type="button" class="text-indigo-400 hover:underline" @click="mode = 'login'">
|
|
136
|
+
Sign in
|
|
137
|
+
</button>
|
|
138
|
+
</template>
|
|
139
|
+
</p>
|
|
140
|
+
</form>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</template>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
3
|
+
|
|
4
|
+
// Signed-in identity + sign-out, shown in the sidebar when auth is enabled.
|
|
5
|
+
const auth = useAuthStore()
|
|
6
|
+
|
|
7
|
+
const items = computed<DropdownMenuItem[][]>(() => [
|
|
8
|
+
[
|
|
9
|
+
{
|
|
10
|
+
label: 'Sign out',
|
|
11
|
+
icon: 'i-lucide-log-out',
|
|
12
|
+
onSelect: () => auth.logout(),
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
])
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<UDropdownMenu v-if="auth.user" :items="items" :content="{ side: 'top', align: 'start' }">
|
|
20
|
+
<button
|
|
21
|
+
type="button"
|
|
22
|
+
class="flex w-full items-center gap-2 rounded-lg border border-slate-800 bg-slate-900/60 p-2 text-left transition hover:bg-slate-800/60"
|
|
23
|
+
>
|
|
24
|
+
<UAvatar
|
|
25
|
+
:src="auth.user.avatarUrl ?? undefined"
|
|
26
|
+
:alt="auth.user.login"
|
|
27
|
+
size="xs"
|
|
28
|
+
icon="i-lucide-user"
|
|
29
|
+
/>
|
|
30
|
+
<div class="min-w-0 flex-1">
|
|
31
|
+
<div class="truncate text-xs font-medium text-white">
|
|
32
|
+
{{ auth.user.name || auth.user.login }}
|
|
33
|
+
</div>
|
|
34
|
+
<div class="truncate text-[10px] text-slate-500">@{{ auth.user.login }}</div>
|
|
35
|
+
</div>
|
|
36
|
+
<UIcon name="i-lucide-chevron-up" class="h-4 w-4 shrink-0 text-slate-500" />
|
|
37
|
+
</button>
|
|
38
|
+
</UDropdownMenu>
|
|
39
|
+
</template>
|