@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.
Files changed (189) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -0
  3. package/app/app.config.ts +8 -0
  4. package/app/app.vue +11 -0
  5. package/app/assets/css/main.css +100 -0
  6. package/app/components/auth/AuthGate.vue +24 -0
  7. package/app/components/auth/LoginScreen.vue +143 -0
  8. package/app/components/auth/UserMenu.vue +39 -0
  9. package/app/components/board/AddTaskModal.vue +444 -0
  10. package/app/components/board/AgentFailureCard.vue +97 -0
  11. package/app/components/board/AgentStopButton.vue +61 -0
  12. package/app/components/board/BoardCanvas.vue +183 -0
  13. package/app/components/board/ContextPicker.vue +367 -0
  14. package/app/components/board/RecurringPipelineModal.vue +219 -0
  15. package/app/components/board/TaskDependencyEdges.vue +132 -0
  16. package/app/components/board/nodes/AgentChip.vue +59 -0
  17. package/app/components/board/nodes/BlockNode.vue +433 -0
  18. package/app/components/board/nodes/DecisionBadge.vue +27 -0
  19. package/app/components/board/nodes/DraggableTask.vue +48 -0
  20. package/app/components/board/nodes/ModuleFrame.vue +97 -0
  21. package/app/components/board/nodes/TaskCard.vue +359 -0
  22. package/app/components/board/nodes/TaskPipelineMini.vue +159 -0
  23. package/app/components/bootstrap/BootstrapModal.vue +665 -0
  24. package/app/components/clarity/ClarityReviewWindow.vue +611 -0
  25. package/app/components/consensus/ConsensusSessionWindow.vue +210 -0
  26. package/app/components/documents/DocumentImportModal.vue +161 -0
  27. package/app/components/documents/DocumentSourceConnectModal.vue +127 -0
  28. package/app/components/documents/SpawnPreviewModal.vue +161 -0
  29. package/app/components/documents/TaskContextDocs.vue +83 -0
  30. package/app/components/focus/BlockFocusView.vue +171 -0
  31. package/app/components/fragments/FragmentLibraryPanel.vue +340 -0
  32. package/app/components/gates/GateResultView.vue +282 -0
  33. package/app/components/github/AddServiceFromRepoModal.vue +354 -0
  34. package/app/components/github/GitHubConnect.vue +183 -0
  35. package/app/components/github/GitHubOnboarding.vue +45 -0
  36. package/app/components/github/GitHubPanel.vue +584 -0
  37. package/app/components/github/RepoTreeBrowser.vue +171 -0
  38. package/app/components/layout/AccountTeamSettings.vue +237 -0
  39. package/app/components/layout/BoardSwitcher.vue +280 -0
  40. package/app/components/layout/BoardToolbar.vue +156 -0
  41. package/app/components/layout/CommandBar.vue +336 -0
  42. package/app/components/layout/GitHubPatBanner.vue +73 -0
  43. package/app/components/layout/NotificationsInbox.vue +175 -0
  44. package/app/components/layout/SideBar.vue +314 -0
  45. package/app/components/layout/SpendWarningBanner.vue +107 -0
  46. package/app/components/observability/StepMetricsBar.vue +102 -0
  47. package/app/components/palettes/AgentPalette.vue +86 -0
  48. package/app/components/panels/AgentStepDetail.vue +737 -0
  49. package/app/components/panels/DecisionModal.vue +71 -0
  50. package/app/components/panels/InspectorPanel.vue +465 -0
  51. package/app/components/panels/ObservabilityPanel.vue +351 -0
  52. package/app/components/panels/StepMetadataCard.vue +253 -0
  53. package/app/components/panels/StepRestartControl.vue +90 -0
  54. package/app/components/panels/StepResultViewHost.vue +40 -0
  55. package/app/components/panels/StepTestReport.vue +84 -0
  56. package/app/components/panels/inspector/ContainerSummary.vue +74 -0
  57. package/app/components/panels/inspector/RecurringScheduleSettings.vue +178 -0
  58. package/app/components/panels/inspector/ServiceFragments.vue +82 -0
  59. package/app/components/panels/inspector/ServiceTestConfig.vue +198 -0
  60. package/app/components/panels/inspector/TaskAgentConfig.vue +81 -0
  61. package/app/components/panels/inspector/TaskDependencies.vue +70 -0
  62. package/app/components/panels/inspector/TaskEstimateBadge.vue +56 -0
  63. package/app/components/panels/inspector/TaskExecution.vue +364 -0
  64. package/app/components/panels/inspector/TaskRunSettings.vue +187 -0
  65. package/app/components/panels/inspector/TaskStructure.vue +96 -0
  66. package/app/components/pipeline/AgentKindIcon.vue +30 -0
  67. package/app/components/pipeline/IterationCapPrompt.vue +70 -0
  68. package/app/components/pipeline/PipelineBuilder.vue +817 -0
  69. package/app/components/pipeline/PipelineProgress.vue +484 -0
  70. package/app/components/providers/ApiKeysSection.vue +273 -0
  71. package/app/components/providers/PersonalCredentialModal.vue +128 -0
  72. package/app/components/providers/PersonalSubscriptionSection.vue +225 -0
  73. package/app/components/providers/VendorCredentialsModal.vue +197 -0
  74. package/app/components/recurring/RecurrenceEditor.vue +124 -0
  75. package/app/components/requirements/RequirementsReviewWindow.vue +620 -0
  76. package/app/components/settings/DatadogPanel.vue +213 -0
  77. package/app/components/settings/LocalModelEndpointsPanel.vue +286 -0
  78. package/app/components/settings/MergeThresholdsPanel.vue +378 -0
  79. package/app/components/settings/ModelDefaultsPanel.vue +250 -0
  80. package/app/components/settings/ServiceFragmentDefaultsPanel.vue +124 -0
  81. package/app/components/settings/WorkspaceSettingsPanel.vue +142 -0
  82. package/app/components/slack/SlackPanel.vue +299 -0
  83. package/app/components/tasks/TaskContextIssues.vue +88 -0
  84. package/app/components/tasks/TaskImportModal.vue +207 -0
  85. package/app/components/tasks/TaskSourceConnectModal.vue +133 -0
  86. package/app/components/testing/TestReportWindow.vue +404 -0
  87. package/app/composables/api/accounts.ts +81 -0
  88. package/app/composables/api/auth.ts +45 -0
  89. package/app/composables/api/board.ts +101 -0
  90. package/app/composables/api/bootstrap.ts +62 -0
  91. package/app/composables/api/context.ts +25 -0
  92. package/app/composables/api/documents.ts +74 -0
  93. package/app/composables/api/execution.ts +127 -0
  94. package/app/composables/api/fragments.ts +71 -0
  95. package/app/composables/api/github.ts +131 -0
  96. package/app/composables/api/models.ts +127 -0
  97. package/app/composables/api/notifications.ts +23 -0
  98. package/app/composables/api/presets.ts +29 -0
  99. package/app/composables/api/recurring.ts +68 -0
  100. package/app/composables/api/releaseHealth.ts +43 -0
  101. package/app/composables/api/reviews.ts +146 -0
  102. package/app/composables/api/slack.ts +54 -0
  103. package/app/composables/api/tasks.ts +72 -0
  104. package/app/composables/api/workspaces.ts +36 -0
  105. package/app/composables/useApi.ts +89 -0
  106. package/app/composables/useBlockDrag.ts +90 -0
  107. package/app/composables/useBlockQueries.ts +154 -0
  108. package/app/composables/useBoardFlow.ts +11 -0
  109. package/app/composables/useContextLinking.ts +65 -0
  110. package/app/composables/useDepLabels.ts +26 -0
  111. package/app/composables/useFrameResize.ts +54 -0
  112. package/app/composables/useResultView.ts +48 -0
  113. package/app/composables/useReviewStage.ts +40 -0
  114. package/app/composables/useSemanticZoom.ts +31 -0
  115. package/app/composables/useStepApproval.ts +233 -0
  116. package/app/composables/useStepProse.ts +78 -0
  117. package/app/composables/useStepTimer.ts +63 -0
  118. package/app/composables/useTaskExpansion.ts +92 -0
  119. package/app/composables/useWorkspaceStream.ts +155 -0
  120. package/app/docs/architecture.md +31 -0
  121. package/app/pages/index.vue +141 -0
  122. package/app/stores/accounts.ts +152 -0
  123. package/app/stores/agentConfig.ts +35 -0
  124. package/app/stores/agentRuns.ts +122 -0
  125. package/app/stores/agents.ts +40 -0
  126. package/app/stores/apiKeys.ts +108 -0
  127. package/app/stores/auth.ts +166 -0
  128. package/app/stores/board.spec.ts +205 -0
  129. package/app/stores/board.ts +286 -0
  130. package/app/stores/bootstrap.ts +97 -0
  131. package/app/stores/clarity.ts +196 -0
  132. package/app/stores/consensus.ts +60 -0
  133. package/app/stores/documents.ts +176 -0
  134. package/app/stores/execution.ts +273 -0
  135. package/app/stores/fragmentLibrary.ts +147 -0
  136. package/app/stores/fragments.ts +40 -0
  137. package/app/stores/github.ts +305 -0
  138. package/app/stores/localModels.ts +51 -0
  139. package/app/stores/mergePresets.ts +58 -0
  140. package/app/stores/modelDefaults.ts +76 -0
  141. package/app/stores/models.ts +134 -0
  142. package/app/stores/notifications.ts +70 -0
  143. package/app/stores/observability.ts +144 -0
  144. package/app/stores/personalSubscriptions.ts +215 -0
  145. package/app/stores/pipelines.ts +327 -0
  146. package/app/stores/recurringPipelines.ts +112 -0
  147. package/app/stores/releaseHealth.ts +75 -0
  148. package/app/stores/requirements.spec.ts +94 -0
  149. package/app/stores/requirements.ts +208 -0
  150. package/app/stores/serviceFragmentDefaults.ts +29 -0
  151. package/app/stores/services.ts +87 -0
  152. package/app/stores/slack.ts +142 -0
  153. package/app/stores/taskExpansion.ts +36 -0
  154. package/app/stores/tasks.spec.ts +71 -0
  155. package/app/stores/tasks.ts +176 -0
  156. package/app/stores/tracker.ts +27 -0
  157. package/app/stores/ui.ts +434 -0
  158. package/app/stores/vendorCredentials.ts +54 -0
  159. package/app/stores/workspace.ts +215 -0
  160. package/app/stores/workspaceSettings.ts +36 -0
  161. package/app/types/accounts.ts +77 -0
  162. package/app/types/bootstrap.ts +83 -0
  163. package/app/types/clarity.ts +59 -0
  164. package/app/types/consensus.ts +91 -0
  165. package/app/types/documents.ts +104 -0
  166. package/app/types/domain.ts +495 -0
  167. package/app/types/execution.ts +383 -0
  168. package/app/types/fragments.ts +72 -0
  169. package/app/types/github.ts +173 -0
  170. package/app/types/localModels.ts +73 -0
  171. package/app/types/merge.ts +71 -0
  172. package/app/types/models.ts +157 -0
  173. package/app/types/notifications.ts +74 -0
  174. package/app/types/recurring.ts +69 -0
  175. package/app/types/releaseHealth.ts +31 -0
  176. package/app/types/requirements.ts +61 -0
  177. package/app/types/services.ts +27 -0
  178. package/app/types/slack.ts +57 -0
  179. package/app/types/tasks.ts +82 -0
  180. package/app/types/tracker.ts +18 -0
  181. package/app/utils/agentOutput.spec.ts +128 -0
  182. package/app/utils/agentOutput.ts +173 -0
  183. package/app/utils/catalog.spec.ts +112 -0
  184. package/app/utils/catalog.ts +455 -0
  185. package/app/utils/dnd.ts +29 -0
  186. package/app/utils/observability.ts +52 -0
  187. package/app/utils/pipelineRender.ts +151 -0
  188. package/nuxt.config.ts +55 -0
  189. 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).
@@ -0,0 +1,8 @@
1
+ export default defineAppConfig({
2
+ ui: {
3
+ colors: {
4
+ primary: 'indigo',
5
+ neutral: 'slate',
6
+ },
7
+ },
8
+ })
package/app/app.vue ADDED
@@ -0,0 +1,11 @@
1
+ <script setup lang="ts">
2
+ import AuthGate from '~/components/auth/AuthGate.vue'
3
+ </script>
4
+
5
+ <template>
6
+ <UApp>
7
+ <AuthGate>
8
+ <NuxtPage />
9
+ </AuthGate>
10
+ </UApp>
11
+ </template>
@@ -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>