@cat-factory/app 0.311.2 → 0.312.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/README.md +92 -4
- package/app/app.config.ts +7 -0
- package/app/components/assistant/AssistantModal.vue +2 -5
- package/app/components/auth/AuthGate.vue +2 -1
- package/app/components/binaryCandidates/BinaryCandidatesWindow.vue +14 -15
- package/app/components/binaryOutput/StoredAssetView.vue +1 -3
- package/app/components/board/AddTaskModal.vue +3 -10
- package/app/components/board/AgentFailureCard.vue +4 -5
- package/app/components/board/AgentStopButton.vue +5 -7
- package/app/components/board/BoardCanvas.vue +31 -30
- package/app/components/board/CreateInitiativeModal.vue +2 -4
- package/app/components/board/RecurringPipelineModal.vue +2 -7
- package/app/components/board/nodes/AgentChip.vue +3 -2
- package/app/components/board/nodes/BlockNode.vue +14 -11
- package/app/components/board/nodes/InitiativeCard.vue +6 -3
- package/app/components/board/nodes/TaskCard.vue +45 -212
- package/app/components/bootstrap/AdoptionReviewModal.vue +2 -5
- package/app/components/bootstrap/BootstrapModal.vue +6 -15
- package/app/components/bootstrap/BootstrapRunSteps.vue +3 -1
- package/app/components/brainstorm/BrainstormWindow.vue +32 -26
- package/app/components/bugFishing/BugFishingWindow.vue +15 -13
- package/app/components/clarity/ClarityReviewWindow.vue +31 -26
- package/app/components/common/ClarificationItem.vue +3 -1
- package/app/components/common/EmptyState.vue +41 -19
- package/app/components/common/InterviewGateNotice.vue +3 -1
- package/app/components/common/Spinner.vue +28 -0
- package/app/components/consensus/ConsensusSessionWindow.vue +5 -5
- package/app/components/docs/DocInterviewWindow.vue +6 -6
- package/app/components/documents/ContextDocumentPicker.vue +2 -2
- package/app/components/documents/DocumentImportModal.vue +9 -8
- package/app/components/documents/DocumentSourceConnectModal.vue +10 -10
- package/app/components/documents/DocumentTemplatesModal.vue +13 -7
- package/app/components/documents/RepoContextDocPicker.vue +2 -2
- package/app/components/documents/SpawnPreviewModal.vue +4 -6
- package/app/components/documents/StartFromDesignModal.vue +2 -1
- package/app/components/documents/TaskContextDocs.vue +4 -5
- package/app/components/environments/EnvironmentStatusPanel.vue +11 -8
- package/app/components/environments/steps/EnvPickStep.vue +7 -3
- package/app/components/environments/steps/EnvPreflightStep.vue +6 -5
- package/app/components/focus/BlockFocusView.vue +6 -5
- package/app/components/followUp/FollowUpWindow.vue +7 -9
- package/app/components/forkDecision/ForkDecisionWindow.vue +9 -8
- package/app/components/foundational/FoundationalServiceCatalogList.vue +9 -5
- package/app/components/foundational/FoundationalServiceRegistry.vue +11 -7
- package/app/components/foundational/FoundationalServiceSources.vue +20 -22
- package/app/components/foundational/FoundationalSuppressions.vue +2 -2
- package/app/components/foundational/ServiceCatalogConnection.vue +15 -15
- package/app/components/fragments/FragmentLibraryManager.vue +36 -34
- package/app/components/fragments/FragmentSelector.vue +7 -3
- package/app/components/gates/GateResultView.vue +6 -9
- package/app/components/github/AddServiceFromRepoModal.vue +6 -10
- package/app/components/github/GitHubConnect.vue +10 -17
- package/app/components/github/GitHubPanel.vue +44 -47
- package/app/components/github/RepoTreeBrowser.vue +9 -5
- package/app/components/guidedReview/GuidedReviewModal.vue +16 -6
- package/app/components/guidedReview/GuidedReviewOverview.vue +2 -1
- package/app/components/guidedReview/GuidedReviewPicker.vue +10 -7
- package/app/components/guidedReview/GuidedReviewThread.vue +9 -4
- package/app/components/humanTest/HumanTestWindow.vue +2 -1
- package/app/components/initiative/InitiativePlanningWindow.vue +6 -6
- package/app/components/initiative/InitiativeTrackerWindow.vue +11 -11
- package/app/components/judge/JudgeResultView.vue +2 -7
- package/app/components/kaizen/KaizenPanel.vue +20 -5
- package/app/components/kaizen/KaizenStepStatus.vue +2 -1
- package/app/components/layout/AccountAuditLog.vue +11 -3
- package/app/components/layout/AccountDeploymentSettings.vue +17 -62
- package/app/components/layout/AccountFailureKindRules.vue +6 -5
- package/app/components/layout/AccountModelPolicySettings.vue +2 -6
- package/app/components/layout/AccountPlatformAlertSettings.vue +5 -13
- package/app/components/layout/AccountRiskPolicySettings.vue +13 -14
- package/app/components/layout/AccountRunCredentialSettings.vue +2 -2
- package/app/components/layout/AccountTeamSettings.vue +12 -9
- package/app/components/layout/BoardSwitcher.vue +2 -2
- package/app/components/layout/BoardToolbar.vue +27 -37
- package/app/components/layout/BoardTopOverlays.vue +8 -2
- package/app/components/layout/CommandBar.vue +7 -3
- package/app/components/layout/ConnectionStatusBanner.vue +2 -1
- package/app/components/layout/IntegrationsHub.vue +7 -3
- package/app/components/layout/ModelProvidersHub.vue +7 -3
- package/app/components/layout/NotificationsInbox.vue +3 -11
- package/app/components/layout/SideBar.vue +5 -0
- package/app/components/layout/WorkViewSwitcher.vue +64 -0
- package/app/components/layout/WorkspaceMembersSettings.vue +12 -5
- package/app/components/media/ArtifactLightbox.vue +4 -3
- package/app/components/notifications/NotificationSettingsPanel.vue +4 -11
- package/app/components/observability/ToolCallList.vue +12 -17
- package/app/components/panels/AgentStepDetail.vue +5 -5
- package/app/components/panels/GenericStructuredResultView.vue +6 -9
- package/app/components/panels/InspectorPanel.vue +2 -1
- package/app/components/panels/MergerResultView.vue +5 -6
- package/app/components/panels/ObservabilityPanel.vue +26 -41
- package/app/components/panels/OperatorDashboardPanel.vue +29 -17
- package/app/components/panels/ReportsPanel.vue +17 -11
- package/app/components/panels/ReportsSpendBreakdown.vue +7 -3
- package/app/components/panels/RunDetailLoadState.vue +4 -3
- package/app/components/panels/StepContainerStatus.vue +4 -3
- package/app/components/panels/StepDelegatedStatus.vue +3 -2
- package/app/components/panels/StepMetadataCard.vue +6 -8
- package/app/components/panels/StepRunMeta.vue +2 -5
- package/app/components/panels/inspector/ContainerSummary.vue +7 -1
- package/app/components/panels/inspector/EpicChildren.vue +7 -3
- package/app/components/panels/inspector/FrontendConfig.vue +7 -3
- package/app/components/panels/inspector/InitiativeInspector.vue +5 -2
- package/app/components/panels/inspector/ServiceConnections.vue +7 -3
- package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +2 -6
- package/app/components/panels/inspector/ServiceTestSecrets.vue +2 -6
- package/app/components/panels/inspector/ServiceTestingContext.vue +2 -6
- package/app/components/panels/inspector/ServiceValidationConfig.vue +38 -39
- package/app/components/panels/inspector/TaskExecution.vue +5 -6
- package/app/components/panels/inspector/TaskRunSettings.vue +7 -3
- package/app/components/panels/inspector/TaskStructure.vue +0 -1
- package/app/components/pipeline/AgentPromptEditor.vue +25 -18
- package/app/components/pipeline/PipelineBuilder.vue +14 -24
- package/app/components/pipeline/PipelineProgress.vue +16 -17
- package/app/components/prReview/PrReviewPhaseBadge.vue +3 -2
- package/app/components/prReview/PrReviewWindow.vue +16 -20
- package/app/components/providers/ApiKeysSection.vue +2 -6
- package/app/components/providers/PersonalCredentialModal.vue +2 -6
- package/app/components/providers/PersonalSubscriptionSection.vue +3 -7
- package/app/components/providers/VendorCredentialsModal.vue +2 -6
- package/app/components/provisioning/ProvisioningLogsDrawer.vue +10 -6
- package/app/components/queue/QueueCard.vue +269 -0
- package/app/components/queue/QueueView.vue +192 -0
- package/app/components/ralph/RalphLoopResultView.vue +2 -7
- package/app/components/requirements/RequirementsReviewWindow.vue +44 -39
- package/app/components/sandbox/SandboxPanel.vue +37 -25
- package/app/components/settings/AccountSettingsPanel.vue +6 -3
- package/app/components/settings/ApiTokensPanel.vue +2 -6
- package/app/components/settings/BudgetSettings.vue +2 -2
- package/app/components/settings/CapabilityCredentialsPanel.vue +2 -6
- package/app/components/settings/CloudflareHandlerSection.vue +3 -7
- package/app/components/settings/ConsensusGroupsSection.vue +7 -3
- package/app/components/settings/CustomManifestTypeEditor.vue +7 -3
- package/app/components/settings/DefaultProvisionTypeSection.vue +2 -2
- package/app/components/settings/InfraHandlersConfigurator.vue +8 -11
- package/app/components/settings/IssueTrackerPanel.vue +9 -9
- package/app/components/settings/LocalContainerPoolSettings.vue +2 -6
- package/app/components/settings/LocalModelEndpointsPanel.vue +5 -7
- package/app/components/settings/McpAuthorizeScreen.vue +8 -3
- package/app/components/settings/McpOAuthCallbackScreen.vue +2 -1
- package/app/components/settings/ModelConfigurationPanel.vue +45 -17
- package/app/components/settings/ObservabilityConnectionPanel.vue +4 -15
- package/app/components/settings/OpenRouterCatalogPanel.vue +8 -24
- package/app/components/settings/PackageRegistriesPanel.vue +2 -6
- package/app/components/settings/ProviderConnectionTab.vue +3 -7
- package/app/components/settings/RiskPolicyPanel.vue +4 -16
- package/app/components/settings/SharedStacksPanel.vue +8 -18
- package/app/components/settings/TaskTypeSuppressionsPanel.vue +10 -6
- package/app/components/settings/UsageSettings.vue +11 -2
- package/app/components/settings/UserSecretsSection.vue +5 -7
- package/app/components/settings/WorkspaceMetadataSettings.vue +9 -9
- package/app/components/settings/WorkspaceSettingsPanel.vue +3 -11
- package/app/components/skills/ReviewSkillQueue.vue +7 -3
- package/app/components/skills/SkillLibraryManager.vue +20 -18
- package/app/components/slack/SlackPanel.vue +5 -8
- package/app/components/spec/ServiceSpecWindow.vue +32 -29
- package/app/components/tasks/BugHuntModal.vue +23 -19
- package/app/components/tasks/ContextIssuePicker.vue +2 -2
- package/app/components/tasks/TaskContextIssues.vue +4 -5
- package/app/components/tasks/TaskImportModal.vue +10 -8
- package/app/components/tasks/TaskSourceConnectModal.vue +11 -11
- package/app/components/testing/TestReportWindow.vue +6 -12
- package/app/components/tutorial/TutorialCatalogue.vue +7 -3
- package/app/components/tutorial/TutorialOverlay.vue +2 -1
- package/app/components/tutorial/TutorialPrompt.vue +7 -3
- package/app/components/vcs/GitLabConnect.vue +2 -6
- package/app/components/visualConfirm/VisualConfirmationWindow.vue +9 -4
- package/app/composables/toastTitleKeys.spec.ts +143 -0
- package/app/composables/useActionToast.spec.ts +71 -0
- package/app/composables/useActionToast.ts +101 -0
- package/app/composables/useBlockDeletion.ts +3 -6
- package/app/composables/useConfirmAction.ts +5 -3
- package/app/composables/useContextLinking.spec.ts +3 -0
- package/app/composables/useContextLinking.ts +11 -16
- package/app/composables/useCopyToClipboard.ts +4 -3
- package/app/composables/useFrameLanes.ts +6 -82
- package/app/composables/useNavContributions.ts +6 -5
- package/app/composables/usePipelineErrorToast.spec.ts +24 -0
- package/app/composables/usePipelineErrorToast.ts +26 -2
- package/app/composables/usePipelineLibraryActions.ts +8 -9
- package/app/composables/useTaskActions.ts +254 -0
- package/app/composables/useTaskLaneClassifier.ts +88 -0
- package/app/modular/nav-contributions.spec.ts +4 -0
- package/app/modular/nav-contributions.ts +31 -0
- package/app/modular/tutorial-tours.ts +10 -0
- package/app/pages/index.vue +42 -8
- package/app/stores/board/context.ts +4 -3
- package/app/stores/board/placement.ts +8 -20
- package/app/stores/board/removal.ts +4 -13
- package/app/stores/board.spec.ts +10 -1
- package/app/stores/board.ts +3 -2
- package/app/stores/workView.ts +51 -0
- package/app/stores/workspaceQueue.ts +148 -0
- package/app/utils/bootstrapSteps.ts +11 -2
- package/app/utils/pipelineRender.ts +21 -8
- package/app/utils/queueSections.spec.ts +111 -0
- package/app/utils/queueSections.ts +117 -0
- package/app/utils/workView.spec.ts +16 -0
- package/app/utils/workView.ts +25 -0
- package/i18n/locales/de.json +73 -32
- package/i18n/locales/en.json +73 -32
- package/i18n/locales/es.json +73 -32
- package/i18n/locales/fr.json +73 -32
- package/i18n/locales/he.json +73 -32
- package/i18n/locales/it.json +73 -32
- package/i18n/locales/ja.json +73 -32
- package/i18n/locales/pl.json +73 -32
- package/i18n/locales/tr.json +73 -32
- package/i18n/locales/uk.json +73 -32
- package/package.json +1 -1
|
@@ -20,11 +20,13 @@ import type {
|
|
|
20
20
|
ReviewItemStatus,
|
|
21
21
|
} from '~/types/clarity'
|
|
22
22
|
import SectionLabel from '~/components/common/SectionLabel.vue'
|
|
23
|
+
import Spinner from '~/components/common/Spinner.vue'
|
|
24
|
+
import EmptyState from '~/components/common/EmptyState.vue'
|
|
23
25
|
|
|
24
26
|
const board = useBoardStore()
|
|
25
27
|
const clarity = useClarityStore()
|
|
26
28
|
const models = useModelsStore()
|
|
27
|
-
const
|
|
29
|
+
const actionToast = useActionToast()
|
|
28
30
|
const { present } = usePipelineErrorToast()
|
|
29
31
|
const { t } = useI18n()
|
|
30
32
|
const access = useWorkspaceAccess()
|
|
@@ -233,10 +235,8 @@ async function incorporate(feedback?: string) {
|
|
|
233
235
|
showRedo.value = false
|
|
234
236
|
// The fold + re-review now run in the durable driver. Hand the user back to the board;
|
|
235
237
|
// a notification calls them back only if the re-review needs more input.
|
|
236
|
-
toast.
|
|
237
|
-
title: t('clarity.toast.clarifyingTitle'),
|
|
238
|
+
actionToast.info('clarity.toast.clarifyingTitle', {
|
|
238
239
|
description: t('clarity.toast.clarifyingDescription'),
|
|
239
|
-
icon: 'i-lucide-wand-sparkles',
|
|
240
240
|
})
|
|
241
241
|
close()
|
|
242
242
|
}
|
|
@@ -246,15 +246,16 @@ async function reReview() {
|
|
|
246
246
|
try {
|
|
247
247
|
const updated = await clarity.reReview(blockId.value)
|
|
248
248
|
const newFindings = clarity.openCount(updated)
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
249
|
+
if (updated.status === 'incorporated') {
|
|
250
|
+
actionToast.success('clarity.toast.reReviewSatisfied')
|
|
251
|
+
} else if (updated.status === 'exceeded') {
|
|
252
|
+
actionToast.warning('clarity.toast.reReviewExceeded')
|
|
253
|
+
} else {
|
|
254
|
+
actionToast.info('clarity.toast.reReviewNewFindings', {
|
|
255
|
+
params: { count: newFindings },
|
|
256
|
+
plural: newFindings,
|
|
257
|
+
})
|
|
258
|
+
}
|
|
258
259
|
} catch (e) {
|
|
259
260
|
present(e, 'clarity.error.reReview')
|
|
260
261
|
}
|
|
@@ -266,7 +267,7 @@ async function proceed() {
|
|
|
266
267
|
try {
|
|
267
268
|
await flushDrafts()
|
|
268
269
|
await clarity.proceed(blockId.value)
|
|
269
|
-
|
|
270
|
+
actionToast.info('clarity.toast.proceeding')
|
|
270
271
|
} catch (e) {
|
|
271
272
|
present(e, 'clarity.error.proceed')
|
|
272
273
|
} finally {
|
|
@@ -280,12 +281,12 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
|
|
|
280
281
|
try {
|
|
281
282
|
await clarity.resolveExceeded(blockId.value, choice)
|
|
282
283
|
if (choice === 'stop-reset') {
|
|
283
|
-
|
|
284
|
+
actionToast.info('clarity.toast.taskReset')
|
|
284
285
|
close()
|
|
285
286
|
} else if (choice === 'proceed') {
|
|
286
|
-
|
|
287
|
+
actionToast.info('clarity.toast.proceeding')
|
|
287
288
|
} else {
|
|
288
|
-
|
|
289
|
+
actionToast.info('clarity.toast.extraRoundGranted')
|
|
289
290
|
}
|
|
290
291
|
} catch (e) {
|
|
291
292
|
present(e, 'clarity.error.resolve')
|
|
@@ -329,20 +330,24 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
|
|
|
329
330
|
|
|
330
331
|
<!-- empty state — the reviewer runs automatically as the first pipeline
|
|
331
332
|
gate step, so there's nothing to do here until then -->
|
|
332
|
-
<
|
|
333
|
+
<EmptyState
|
|
333
334
|
v-if="!review && !busy && !loading"
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
335
|
+
icon="i-lucide-list-checks"
|
|
336
|
+
:title="t('clarity.empty')"
|
|
337
|
+
/>
|
|
338
|
+
|
|
339
|
+
<!-- initial fetch on open -->
|
|
340
|
+
<div v-else-if="loading && !busy && !review" class="flex flex-col gap-3">
|
|
341
|
+
<USkeleton v-for="n in 3" :key="n" class="h-20 w-full rounded-lg" />
|
|
337
342
|
</div>
|
|
338
343
|
|
|
339
|
-
<!--
|
|
344
|
+
<!-- a reviewer pass running -->
|
|
340
345
|
<div
|
|
341
|
-
v-else-if="
|
|
346
|
+
v-else-if="busy && !review"
|
|
342
347
|
class="flex items-center justify-center gap-2 p-8 text-sm text-muted"
|
|
343
348
|
>
|
|
344
|
-
<
|
|
345
|
-
{{
|
|
349
|
+
<Spinner class="h-4 w-4" />
|
|
350
|
+
{{ t('clarity.triaging') }}
|
|
346
351
|
</div>
|
|
347
352
|
|
|
348
353
|
<template v-else-if="review">
|
|
@@ -371,7 +376,7 @@ async function resolveExceeded(choice: 'extra-round' | 'proceed' | 'stop-reset')
|
|
|
371
376
|
v-else-if="working"
|
|
372
377
|
class="mb-4 flex items-center gap-2 rounded-lg border border-primary/60 bg-primary/10 p-4 text-sm text-primary"
|
|
373
378
|
>
|
|
374
|
-
<
|
|
379
|
+
<Spinner class="h-5 w-5 shrink-0" />
|
|
375
380
|
<span v-if="incorporating">
|
|
376
381
|
{{ t('clarity.incorporatingStage') }}
|
|
377
382
|
</span>
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
// -specific extras (severity/category badges, a window's own recommendation section) ride the
|
|
9
9
|
// `badges` / `actions` slots; the recommend button only EMITS, so each window wires its own
|
|
10
10
|
// recommend mechanism. `dismissed`/`requested` hide the textarea (nothing to answer right now).
|
|
11
|
+
import Spinner from '~/components/common/Spinner.vue'
|
|
12
|
+
|
|
11
13
|
const props = defineProps<{
|
|
12
14
|
/** The question / finding headline. */
|
|
13
15
|
prompt: string
|
|
@@ -82,7 +84,7 @@ const draft = computed({
|
|
|
82
84
|
class="mt-2 inline-flex items-center gap-1 text-2xs text-primary"
|
|
83
85
|
data-testid="clarification-requested"
|
|
84
86
|
>
|
|
85
|
-
<
|
|
87
|
+
<Spinner class="h-3.5 w-3.5" />
|
|
86
88
|
{{ t('clarification.generating') }}
|
|
87
89
|
</div>
|
|
88
90
|
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
2
|
+
// The SPA's one "nothing here yet" render. Every list, panel section and picker that can be empty
|
|
3
|
+
// shows this, so an empty state reads the same everywhere (frontend/app/README.md, "Empty states:
|
|
4
|
+
// `EmptyState`, always").
|
|
5
|
+
//
|
|
6
|
+
// It renders Nuxt UI's `UEmpty`, which owns the structure, the call-to-action row and the theme
|
|
7
|
+
// tokens. This file decides what `UEmpty` leaves to each caller, once:
|
|
8
|
+
//
|
|
9
|
+
// - The `naked` variant: an empty state sits INSIDE a panel that already draws its own border, so
|
|
10
|
+
// `UEmpty`'s default outline would draw a second box.
|
|
11
|
+
// - The spacing. `UEmpty`'s root pads `p-4 sm:p-6 lg:p-8` with a `gap-4`, sized for a page body.
|
|
12
|
+
// Every responsive step is reset here, because overriding the base `p-4` alone leaves 32px of
|
|
13
|
+
// padding inside a narrow picker at desktop widths.
|
|
14
|
+
// - The header markup. `UEmpty` renders its title as an `<h2>`, which would put a level-2 heading
|
|
15
|
+
// into every picker, dropdown and inspector section. The header slot renders the copy as plain
|
|
16
|
+
// text instead.
|
|
17
|
+
//
|
|
18
|
+
// The default slot is the call-to-action: a site with a button to fill the empty list passes it
|
|
19
|
+
// here. Dumb by design: it takes already-resolved copy and an icon.
|
|
7
20
|
withDefaults(
|
|
8
21
|
defineProps<{
|
|
9
22
|
icon?: string
|
|
@@ -11,25 +24,34 @@ withDefaults(
|
|
|
11
24
|
description?: string
|
|
12
25
|
compact?: boolean
|
|
13
26
|
}>(),
|
|
14
|
-
{ icon: 'i-lucide-inbox', compact: false },
|
|
27
|
+
{ icon: 'i-lucide-inbox', description: undefined, compact: false },
|
|
15
28
|
)
|
|
16
29
|
</script>
|
|
17
30
|
|
|
18
31
|
<template>
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
:
|
|
32
|
+
<UEmpty
|
|
33
|
+
variant="naked"
|
|
34
|
+
:ui="{
|
|
35
|
+
root: compact
|
|
36
|
+
? 'gap-1 p-0 py-4 sm:p-0 sm:py-4 lg:p-0 lg:py-4'
|
|
37
|
+
: 'gap-2 p-0 py-10 sm:p-0 sm:py-10 lg:p-0 lg:py-10',
|
|
38
|
+
header: compact ? 'gap-1' : 'gap-2',
|
|
39
|
+
}"
|
|
22
40
|
data-testid="empty-state"
|
|
23
41
|
>
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
{{ title }}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
42
|
+
<template #header>
|
|
43
|
+
<UIcon :name="icon" :class="compact ? 'size-5' : 'size-8'" class="text-app-600" />
|
|
44
|
+
<p :class="compact ? 'text-xs' : 'text-sm'" class="font-medium text-muted">{{ title }}</p>
|
|
45
|
+
<p
|
|
46
|
+
v-if="description"
|
|
47
|
+
:class="compact ? 'text-2xs' : 'text-xs'"
|
|
48
|
+
class="text-balance text-dimmed"
|
|
49
|
+
>
|
|
50
|
+
{{ description }}
|
|
51
|
+
</p>
|
|
52
|
+
</template>
|
|
53
|
+
<template v-if="$slots.default" #actions>
|
|
32
54
|
<slot />
|
|
33
|
-
</
|
|
34
|
-
</
|
|
55
|
+
</template>
|
|
56
|
+
</UEmpty>
|
|
35
57
|
</template>
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// proceed read as dead buttons. Copy is per-feature (passed in); the treatment is shared, so the
|
|
7
7
|
// two windows can't drift on how a wait or a failure looks. See
|
|
8
8
|
// `docs/initiatives/clarification-items.md`.
|
|
9
|
+
import Spinner from '~/components/common/Spinner.vue'
|
|
10
|
+
|
|
9
11
|
defineProps<{
|
|
10
12
|
/** `working` = a pass is in flight (spinner); `failed` = the run stopped (error treatment). */
|
|
11
13
|
variant: 'working' | 'failed'
|
|
@@ -24,7 +26,7 @@ defineProps<{
|
|
|
24
26
|
class="flex flex-col items-center gap-2 rounded-lg border border-default bg-app-950/40 p-6 text-center"
|
|
25
27
|
:data-testid="testid"
|
|
26
28
|
>
|
|
27
|
-
<
|
|
29
|
+
<Spinner class="h-5 w-5 text-primary" />
|
|
28
30
|
<p class="text-sm text-default">{{ title }}</p>
|
|
29
31
|
<p class="text-xs text-muted">{{ hint }}</p>
|
|
30
32
|
</div>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The one place `animate-spin` is written (`scripts/check-frontend-feedback.mjs` enforces it).
|
|
3
|
+
//
|
|
4
|
+
// Use it only where no Nuxt UI component carries the state: `UButton`, `UInput`, `USelectMenu` and
|
|
5
|
+
// the rest take `:loading`, and that prop is the answer there. This is for a bare icon that shows
|
|
6
|
+
// work in progress: a step-status glyph, a "loading…" line, a full-panel wait.
|
|
7
|
+
//
|
|
8
|
+
// `spinning` lets a status icon keep ONE element across its states (a step glyph that spins while
|
|
9
|
+
// it runs and stops when it settles) instead of two branches. `name` defaults to Nuxt UI's own
|
|
10
|
+
// loading icon, so a plain spinner matches the one `:loading` draws inside a button. Size and
|
|
11
|
+
// colour are the caller's classes, which land on the icon.
|
|
12
|
+
//
|
|
13
|
+
// `motion-safe:` so a reader who asked the OS for reduced motion sees the icon still; the state
|
|
14
|
+
// is carried by the icon itself and its surrounding copy, never by the motion alone.
|
|
15
|
+
withDefaults(defineProps<{ name?: string; spinning?: boolean }>(), {
|
|
16
|
+
name: undefined,
|
|
17
|
+
spinning: true,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const appConfig = useAppConfig()
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<UIcon
|
|
25
|
+
:name="name ?? appConfig.ui.icons.loading"
|
|
26
|
+
:class="spinning ? 'motion-safe:animate-spin' : ''"
|
|
27
|
+
/>
|
|
28
|
+
</template>
|
|
@@ -13,6 +13,7 @@ import MarkdownProse from '~/components/common/MarkdownProse.vue'
|
|
|
13
13
|
import ResultWindowShell from '~/components/panels/ResultWindowShell.vue'
|
|
14
14
|
import { agentKindMeta } from '~/utils/catalog'
|
|
15
15
|
import SectionLabel from '~/components/common/SectionLabel.vue'
|
|
16
|
+
import EmptyState from '~/components/common/EmptyState.vue'
|
|
16
17
|
|
|
17
18
|
const { t, n } = useI18n()
|
|
18
19
|
|
|
@@ -145,12 +146,11 @@ function topScore(c: ConsensusContribution): { label: string; value: number } |
|
|
|
145
146
|
</template>
|
|
146
147
|
|
|
147
148
|
<div class="flex-1 overflow-y-auto px-6 py-5">
|
|
148
|
-
<div v-if="loading && !session" class="
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
<div v-else-if="!session" class="py-16 text-center text-sm text-dimmed">
|
|
152
|
-
{{ t('consensus.empty') }}
|
|
149
|
+
<div v-if="loading && !session" class="space-y-4">
|
|
150
|
+
<USkeleton class="h-32 w-full rounded-lg" />
|
|
151
|
+
<USkeleton v-for="n in 3" :key="n" class="h-16 w-full rounded-lg" />
|
|
153
152
|
</div>
|
|
153
|
+
<EmptyState v-else-if="!session" icon="i-lucide-users" :title="t('consensus.empty')" />
|
|
154
154
|
<template v-else>
|
|
155
155
|
<!-- failure -->
|
|
156
156
|
<div
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
interviewStepReached,
|
|
26
26
|
} from '~/utils/interviewGate'
|
|
27
27
|
import SectionLabel from '~/components/common/SectionLabel.vue'
|
|
28
|
+
import EmptyState from '~/components/common/EmptyState.vue'
|
|
28
29
|
|
|
29
30
|
const board = useBoardStore()
|
|
30
31
|
const docInterview = useDocInterviewStore()
|
|
@@ -123,13 +124,12 @@ const onProceed = () =>
|
|
|
123
124
|
|
|
124
125
|
<div class="min-h-0 flex-1 overflow-y-auto px-5 py-4">
|
|
125
126
|
<!-- No session yet -->
|
|
126
|
-
<
|
|
127
|
+
<EmptyState
|
|
127
128
|
v-if="!session"
|
|
128
|
-
class="
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
</div>
|
|
129
|
+
class="h-full"
|
|
130
|
+
icon="i-lucide-messages-square"
|
|
131
|
+
:title="t('docInterview.empty')"
|
|
132
|
+
/>
|
|
133
133
|
|
|
134
134
|
<template v-else>
|
|
135
135
|
<p class="mb-4 text-sm leading-relaxed text-toned">
|
|
@@ -476,8 +476,8 @@ onMounted(() => {
|
|
|
476
476
|
<template v-else>
|
|
477
477
|
<UInput
|
|
478
478
|
v-model="query"
|
|
479
|
-
|
|
480
|
-
:
|
|
479
|
+
icon="i-lucide-search"
|
|
480
|
+
:loading="searching"
|
|
481
481
|
size="sm"
|
|
482
482
|
class="w-full"
|
|
483
483
|
:placeholder="
|
|
@@ -3,6 +3,7 @@ import type { DocumentSourceKind } from '~/types/domain'
|
|
|
3
3
|
import DocumentSyncState from '~/components/documents/DocumentSyncState.vue'
|
|
4
4
|
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
5
5
|
import SectionLabel from '~/components/common/SectionLabel.vue'
|
|
6
|
+
import EmptyState from '~/components/common/EmptyState.vue'
|
|
6
7
|
|
|
7
8
|
// Import pages from a connected document source and pick one to expand into
|
|
8
9
|
// board structure. A source selector lets the user choose which connected source
|
|
@@ -11,7 +12,7 @@ import SectionLabel from '~/components/common/SectionLabel.vue'
|
|
|
11
12
|
const { t } = useI18n()
|
|
12
13
|
const ui = useUiStore()
|
|
13
14
|
const documents = useDocumentsStore()
|
|
14
|
-
const
|
|
15
|
+
const actionToast = useActionToast()
|
|
15
16
|
const { present } = usePipelineErrorToast()
|
|
16
17
|
|
|
17
18
|
const open = computed({
|
|
@@ -58,10 +59,7 @@ async function doImport() {
|
|
|
58
59
|
try {
|
|
59
60
|
const doc = await documents.importDocument(source.value, value)
|
|
60
61
|
ref_.value = ''
|
|
61
|
-
|
|
62
|
-
title: t('documents.import.imported', { title: doc.title }),
|
|
63
|
-
icon: 'i-lucide-file-down',
|
|
64
|
-
})
|
|
62
|
+
actionToast.success('documents.import.imported', { params: { title: doc.title } })
|
|
65
63
|
} catch (e) {
|
|
66
64
|
present(e, 'documents.import.importFailed')
|
|
67
65
|
} finally {
|
|
@@ -157,9 +155,12 @@ function preview(externalId: string) {
|
|
|
157
155
|
</div>
|
|
158
156
|
</div>
|
|
159
157
|
</div>
|
|
160
|
-
<
|
|
161
|
-
|
|
162
|
-
|
|
158
|
+
<EmptyState
|
|
159
|
+
v-else
|
|
160
|
+
compact
|
|
161
|
+
icon="i-lucide-file-down"
|
|
162
|
+
:title="t('documents.import.noneImported')"
|
|
163
|
+
/>
|
|
163
164
|
</div>
|
|
164
165
|
</template>
|
|
165
166
|
</UModal>
|
|
@@ -17,7 +17,7 @@ import SecretInput from '~/components/common/SecretInput.vue'
|
|
|
17
17
|
const { t } = useI18n()
|
|
18
18
|
const ui = useUiStore()
|
|
19
19
|
const documents = useDocumentsStore()
|
|
20
|
-
const
|
|
20
|
+
const actionToast = useActionToast()
|
|
21
21
|
const { present } = usePipelineErrorToast()
|
|
22
22
|
const { confirmAction } = useConfirmAction()
|
|
23
23
|
|
|
@@ -82,10 +82,8 @@ async function submit() {
|
|
|
82
82
|
saving.value = true
|
|
83
83
|
try {
|
|
84
84
|
await documents.connect(source.value, credentials)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
icon: 'i-lucide-check',
|
|
88
|
-
color: 'success',
|
|
85
|
+
actionToast.success('documents.connect.connected', {
|
|
86
|
+
params: { source: descriptor.value!.label },
|
|
89
87
|
})
|
|
90
88
|
ui.closeDocumentConnect()
|
|
91
89
|
} catch (e) {
|
|
@@ -99,11 +97,13 @@ async function disconnect() {
|
|
|
99
97
|
if (!source.value) return
|
|
100
98
|
const label = descriptor.value?.label ?? t('documents.connect.sourceFallback')
|
|
101
99
|
if (!(await confirmAction('disconnect', label))) return
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
100
|
+
try {
|
|
101
|
+
await documents.disconnect(source.value)
|
|
102
|
+
} catch (e) {
|
|
103
|
+
present(e, 'documents.connect.disconnectFailed')
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
actionToast.success('documents.connect.disconnected', { params: { source: label } })
|
|
107
107
|
ui.closeDocumentConnect()
|
|
108
108
|
}
|
|
109
109
|
</script>
|
|
@@ -4,6 +4,7 @@ import type { DocKind, DocumentLinkRole, SourceDocument } from '~/types/domain'
|
|
|
4
4
|
import DocumentOriginLink from '~/components/documents/DocumentOriginLink.vue'
|
|
5
5
|
import IntegrationBackTitle from '~/components/layout/IntegrationBackTitle.vue'
|
|
6
6
|
import SectionLabel from '~/components/common/SectionLabel.vue'
|
|
7
|
+
import EmptyState from '~/components/common/EmptyState.vue'
|
|
7
8
|
|
|
8
9
|
// Manage the workspace's per-DocKind TEMPLATE (singular) + EXEMPLAR (multi) document links (WS1).
|
|
9
10
|
// A kind can be pointed at one of the workspace's already-imported documents so its parsed
|
|
@@ -97,13 +98,15 @@ async function unlink(doc: SourceDocument) {
|
|
|
97
98
|
<p class="text-xs text-muted">{{ t('documents.templates.intro') }}</p>
|
|
98
99
|
|
|
99
100
|
<!-- No imported documents yet: a template/exemplar must be an imported document. -->
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
<EmptyState
|
|
102
|
+
v-if="!documents.documents.length"
|
|
103
|
+
icon="i-lucide-file-plus"
|
|
104
|
+
:title="t('documents.templates.importFirst')"
|
|
105
|
+
>
|
|
103
106
|
<UButton variant="soft" icon="i-lucide-file-down" @click="ui.openDocumentImport()">
|
|
104
107
|
{{ t('documents.templates.importButton') }}
|
|
105
108
|
</UButton>
|
|
106
|
-
</
|
|
109
|
+
</EmptyState>
|
|
107
110
|
|
|
108
111
|
<template v-else>
|
|
109
112
|
<UFormField :label="t('documents.templates.kindLabel')">
|
|
@@ -194,9 +197,12 @@ async function unlink(doc: SourceDocument) {
|
|
|
194
197
|
</UButton>
|
|
195
198
|
</div>
|
|
196
199
|
</div>
|
|
197
|
-
<
|
|
198
|
-
|
|
199
|
-
|
|
200
|
+
<EmptyState
|
|
201
|
+
v-else
|
|
202
|
+
compact
|
|
203
|
+
icon="i-lucide-files"
|
|
204
|
+
:title="t('documents.templates.exemplarsEmpty')"
|
|
205
|
+
/>
|
|
200
206
|
</section>
|
|
201
207
|
|
|
202
208
|
<!-- Picker: choose an imported document, then set as template or add as example. -->
|
|
@@ -232,8 +232,8 @@ onMounted(() => {
|
|
|
232
232
|
<div v-if="fileMode === 'search'" class="space-y-2">
|
|
233
233
|
<UInput
|
|
234
234
|
v-model="fileQuery"
|
|
235
|
-
|
|
236
|
-
:
|
|
235
|
+
icon="i-lucide-file-search"
|
|
236
|
+
:loading="loadingFiles"
|
|
237
237
|
size="sm"
|
|
238
238
|
class="w-full"
|
|
239
239
|
:placeholder="t('documents.repoPicker.searchFilesPlaceholder')"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { isDesignSource } from '@cat-factory/contracts'
|
|
3
3
|
import type { DocumentBoardPlan } from '~/types/domain'
|
|
4
|
+
import Spinner from '~/components/common/Spinner.vue'
|
|
4
5
|
|
|
5
6
|
// Preview the structure an imported document expands into, then spawn it. The
|
|
6
7
|
// plan is fetched fresh on open; a badge makes clear whether an LLM or the
|
|
@@ -16,7 +17,7 @@ const { t } = useI18n()
|
|
|
16
17
|
const ui = useUiStore()
|
|
17
18
|
const board = useBoardStore()
|
|
18
19
|
const documents = useDocumentsStore()
|
|
19
|
-
const
|
|
20
|
+
const actionToast = useActionToast()
|
|
20
21
|
const { present } = usePipelineErrorToast()
|
|
21
22
|
|
|
22
23
|
/** Design documents are planned into an existing service; prose documents at the board root. */
|
|
@@ -102,16 +103,13 @@ async function spawn() {
|
|
|
102
103
|
modules: t('documents.spawn.moduleCount', { count: result.modules }, result.modules),
|
|
103
104
|
tasks: t('documents.spawn.taskCount', { count: result.tasks }, result.tasks),
|
|
104
105
|
})
|
|
105
|
-
|
|
106
|
-
title: t('documents.spawn.spawned'),
|
|
106
|
+
actionToast.success('documents.spawn.spawned', {
|
|
107
107
|
// A reused module is stated only when there was one, and never folded into the created
|
|
108
108
|
// count: a spawn into a service that already had every planned module otherwise reports
|
|
109
109
|
// "0 modules" beside its tasks, which reads as the modules having failed.
|
|
110
110
|
description: result.reusedModules
|
|
111
111
|
? `${summary} · ${t('documents.spawn.reusedCount', { count: result.reusedModules }, result.reusedModules)}`
|
|
112
112
|
: summary,
|
|
113
|
-
icon: 'i-lucide-check',
|
|
114
|
-
color: 'success',
|
|
115
113
|
})
|
|
116
114
|
ui.closeSpawnPreview()
|
|
117
115
|
ui.closeDocumentImport()
|
|
@@ -168,7 +166,7 @@ async function spawn() {
|
|
|
168
166
|
</p>
|
|
169
167
|
|
|
170
168
|
<div v-if="loadingPlan" class="flex items-center gap-2 text-sm text-muted">
|
|
171
|
-
<
|
|
169
|
+
<Spinner class="h-4 w-4" />
|
|
172
170
|
{{ t('documents.spawn.buildingPlan') }}
|
|
173
171
|
</div>
|
|
174
172
|
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
// render identically. For a designer that widening IS the defect, not a detail.
|
|
21
21
|
import { refRowFor, classifyRefFailure, type RefState } from './ContextDocumentPicker.logic'
|
|
22
22
|
import type { DocumentSourceKind, ResolvedDocumentRef } from '~/types/domain'
|
|
23
|
+
import Spinner from '~/components/common/Spinner.vue'
|
|
23
24
|
|
|
24
25
|
const { t } = useI18n()
|
|
25
26
|
const ui = useUiStore()
|
|
@@ -178,7 +179,7 @@ function stage() {
|
|
|
178
179
|
v-if="state.status === 'checking'"
|
|
179
180
|
class="flex items-center gap-2 text-sm text-muted"
|
|
180
181
|
>
|
|
181
|
-
<
|
|
182
|
+
<Spinner class="h-4 w-4" />
|
|
182
183
|
{{ t('documents.startFromDesign.checking') }}
|
|
183
184
|
</div>
|
|
184
185
|
|
|
@@ -6,6 +6,7 @@ import ContextDocumentPicker from '~/components/documents/ContextDocumentPicker.
|
|
|
6
6
|
import DocumentOriginLink from '~/components/documents/DocumentOriginLink.vue'
|
|
7
7
|
import DocumentSyncState from '~/components/documents/DocumentSyncState.vue'
|
|
8
8
|
import InspectorSection from '~/components/panels/inspector/InspectorSection.vue'
|
|
9
|
+
import EmptyState from '~/components/common/EmptyState.vue'
|
|
9
10
|
|
|
10
11
|
// Documents (from any source) attached to a task OR an initiative as agent
|
|
11
12
|
// context, shown inside the InspectorPanel. An initiative takes the same
|
|
@@ -26,7 +27,7 @@ const props = defineProps<{ block: Block }>()
|
|
|
26
27
|
const { t } = useI18n()
|
|
27
28
|
const documents = useDocumentsStore()
|
|
28
29
|
const ui = useUiStore()
|
|
29
|
-
const
|
|
30
|
+
const actionToast = useActionToast()
|
|
30
31
|
const { linkPending, presentLinkFailures } = useContextLinking()
|
|
31
32
|
|
|
32
33
|
onMounted(() => {
|
|
@@ -88,7 +89,7 @@ async function attach(item: PendingContext) {
|
|
|
88
89
|
try {
|
|
89
90
|
const failures = await linkPending(props.block.id, [item])
|
|
90
91
|
if (failures.length) presentLinkFailures(failures, props.block.id)
|
|
91
|
-
else
|
|
92
|
+
else actionToast.success('documents.taskDocs.attached')
|
|
92
93
|
} finally {
|
|
93
94
|
linking.value = false
|
|
94
95
|
}
|
|
@@ -165,8 +166,6 @@ async function attach(item: PendingContext) {
|
|
|
165
166
|
<DocumentSyncState :doc="doc" class="mt-1" />
|
|
166
167
|
</div>
|
|
167
168
|
</div>
|
|
168
|
-
<
|
|
169
|
-
{{ emptyHint }}
|
|
170
|
-
</p>
|
|
169
|
+
<EmptyState v-else compact icon="i-lucide-file-text" :title="emptyHint" />
|
|
171
170
|
</InspectorSection>
|
|
172
171
|
</template>
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
showsProviderFailure,
|
|
11
11
|
} from '~/components/environments/EnvironmentStatusPanel.logic'
|
|
12
12
|
import SectionLabel from '~/components/common/SectionLabel.vue'
|
|
13
|
+
import Spinner from '~/components/common/Spinner.vue'
|
|
14
|
+
import EmptyState from '~/components/common/EmptyState.vue'
|
|
13
15
|
|
|
14
16
|
const props = defineProps<{
|
|
15
17
|
environment: RunEnvironment | null
|
|
@@ -112,13 +114,11 @@ const envInTransition = computed(
|
|
|
112
114
|
</SectionLabel>
|
|
113
115
|
<div v-if="environment" class="space-y-2">
|
|
114
116
|
<div class="flex items-center gap-2 text-sm">
|
|
115
|
-
<
|
|
117
|
+
<Spinner
|
|
116
118
|
:name="ENV_STATUS_META[environment.status].icon"
|
|
119
|
+
:spinning="runActive && envInTransition"
|
|
117
120
|
class="h-3.5 w-3.5"
|
|
118
|
-
:class="[
|
|
119
|
-
ENV_STATUS_META[environment.status].color,
|
|
120
|
-
{ 'animate-spin': runActive && envInTransition },
|
|
121
|
-
]"
|
|
121
|
+
:class="ENV_STATUS_META[environment.status].color"
|
|
122
122
|
/>
|
|
123
123
|
<span :class="ENV_STATUS_META[environment.status].color">{{
|
|
124
124
|
ENV_STATUS_META[environment.status].label
|
|
@@ -166,8 +166,11 @@ const envInTransition = computed(
|
|
|
166
166
|
{{ t('environments.statusNote', { note: statusNote }) }}
|
|
167
167
|
</p>
|
|
168
168
|
</div>
|
|
169
|
-
<
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
<EmptyState
|
|
170
|
+
v-else
|
|
171
|
+
compact
|
|
172
|
+
icon="i-lucide-server"
|
|
173
|
+
:title="degradedReason ?? t('environments.empty')"
|
|
174
|
+
/>
|
|
172
175
|
</section>
|
|
173
176
|
</template>
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// advances to the review step. Rendered by `JourneyOutlet`, so it receives the
|
|
6
6
|
// `ModuleEntryProps` (`input` / `exit` / `goBack`) as attributes.
|
|
7
7
|
import type { EnvSelectOutput } from '~/modular/journeys/environmentSetup.logic'
|
|
8
|
+
import EmptyState from '~/components/common/EmptyState.vue'
|
|
8
9
|
|
|
9
10
|
defineProps<{
|
|
10
11
|
input: { frameId: string | null }
|
|
@@ -21,9 +22,12 @@ const { t } = useI18n()
|
|
|
21
22
|
<!-- The one-service scope caveat, stated on the step that asks for the service: nothing
|
|
22
23
|
here changes any other service, and running it twice just re-edits this one's recipe. -->
|
|
23
24
|
<p class="text-xs leading-relaxed text-dimmed">{{ t('environmentWizard.pick.scope') }}</p>
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
<EmptyState
|
|
26
|
+
v-if="!store.serviceFrames.length"
|
|
27
|
+
compact
|
|
28
|
+
icon="i-lucide-box"
|
|
29
|
+
:title="t('environmentWizard.pick.empty')"
|
|
30
|
+
/>
|
|
27
31
|
<div v-else class="space-y-1.5">
|
|
28
32
|
<UButton
|
|
29
33
|
v-for="frame in store.serviceFrames"
|