@cat-factory/app 0.308.0 → 0.310.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 +32 -2
- package/app/assets/css/main.css +22 -0
- package/app/assets/css/prose.css +4 -4
- package/app/assets/css/type.css +24 -0
- package/app/components/assistant/AssistantModal.vue +8 -6
- package/app/components/auth/BackendMisconfiguredScreen.vue +4 -3
- package/app/components/auth/LoginScreen.vue +40 -26
- package/app/components/auth/ResetPasswordScreen.vue +9 -4
- package/app/components/auth/UserMenu.vue +5 -4
- package/app/components/binaryCandidates/BinaryCandidatesWindow.vue +48 -37
- package/app/components/binaryOutput/BinaryOutputReport.vue +10 -14
- package/app/components/binaryOutput/StoredAssetView.vue +13 -11
- package/app/components/board/AddTaskModal.vue +30 -27
- package/app/components/board/AgentFailureCard.vue +21 -18
- package/app/components/board/AgentFailureHistory.vue +18 -9
- package/app/components/board/CreateInitiativeModal.vue +7 -6
- package/app/components/board/FailureDetail.vue +22 -12
- package/app/components/board/FailureHistoryList.vue +5 -5
- package/app/components/board/RecurringPipelineModal.vue +12 -11
- package/app/components/board/ReviewFrictionDialog.vue +8 -6
- package/app/components/board/StepExecutionHistory.vue +8 -8
- package/app/components/board/nodes/AgentChip.vue +1 -1
- package/app/components/board/nodes/BlockNode.vue +25 -24
- package/app/components/board/nodes/DecisionBadge.vue +4 -3
- package/app/components/board/nodes/EpicNode.vue +4 -4
- package/app/components/board/nodes/FrameSwimlanes.vue +8 -10
- package/app/components/board/nodes/InitiativeCard.vue +4 -4
- package/app/components/board/nodes/LaneGroup.vue +3 -5
- package/app/components/board/nodes/TaskCard.vue +20 -15
- package/app/components/board/nodes/TaskLane.vue +3 -3
- package/app/components/board/nodes/TaskPipelineMini.vue +21 -18
- package/app/components/bootstrap/AdoptionReviewModal.vue +36 -26
- package/app/components/bootstrap/BootstrapModal.vue +14 -13
- package/app/components/bootstrap/BootstrapRunSteps.vue +1 -1
- package/app/components/brainstorm/BrainstormWindow.vue +13 -10
- package/app/components/bugFishing/BugFishingWindow.vue +81 -58
- package/app/components/clarity/ClarityReviewWindow.vue +13 -10
- package/app/components/common/AsyncViewError.vue +1 -1
- package/app/components/common/ClarificationItem.vue +6 -6
- package/app/components/common/CopyButton.vue +12 -11
- package/app/components/common/DescriptorFields.vue +1 -1
- package/app/components/common/EmptyState.vue +1 -1
- package/app/components/common/IconButton.vue +34 -9
- package/app/components/common/InterviewGateNotice.vue +4 -4
- package/app/components/common/MarkdownProse.vue +8 -2
- package/app/components/common/SectionLabel.vue +38 -0
- package/app/components/consensus/ConsensusSessionWindow.vue +10 -9
- package/app/components/context/ContextAttachmentFields.vue +32 -20
- package/app/components/docs/DocInterviewWindow.vue +10 -9
- package/app/components/documents/ContextDocumentPicker.vue +22 -21
- package/app/components/documents/DocumentImportModal.vue +7 -5
- package/app/components/documents/DocumentOriginLink.vue +12 -9
- package/app/components/documents/DocumentSourceConnectModal.vue +2 -2
- package/app/components/documents/DocumentSyncState.vue +4 -4
- package/app/components/documents/DocumentTemplatesModal.vue +5 -4
- package/app/components/documents/RepoContextDocPicker.vue +7 -9
- package/app/components/documents/StartFromDesignModal.vue +4 -4
- package/app/components/documents/TaskContextDocs.vue +1 -1
- package/app/components/environments/EnvSetupStepper.vue +3 -3
- package/app/components/environments/EnvironmentStatusPanel.vue +15 -13
- package/app/components/environments/steps/EnvPreflightStep.vue +8 -8
- package/app/components/environments/steps/EnvReviewStep.vue +23 -22
- package/app/components/environments/steps/EnvSaveStep.vue +5 -5
- package/app/components/focus/BlockFocusView.vue +11 -10
- package/app/components/followUp/FollowUpWindow.vue +18 -15
- package/app/components/forkDecision/ForkDecisionWindow.vue +85 -97
- package/app/components/foundational/FoundationalContractSummary.vue +2 -2
- package/app/components/foundational/FoundationalServiceCatalogList.vue +6 -5
- package/app/components/foundational/FoundationalServiceRegistry.vue +10 -9
- package/app/components/foundational/FoundationalServiceSources.vue +7 -6
- package/app/components/foundational/FoundationalSuppressions.vue +2 -2
- package/app/components/fragments/FragmentLibraryManager.vue +8 -7
- package/app/components/fragments/FragmentSelector.vue +22 -20
- package/app/components/gates/GateFailingCheckList.vue +7 -6
- package/app/components/gates/GateResultView.vue +55 -49
- package/app/components/github/AddServiceFromRepoModal.vue +16 -11
- package/app/components/github/BranchProtectionPreflight.vue +5 -5
- package/app/components/github/GitHubConnect.vue +5 -3
- package/app/components/github/GitHubOnboarding.vue +7 -2
- package/app/components/github/GitHubPanel.vue +23 -19
- package/app/components/github/RepoTreeBrowser.vue +16 -13
- package/app/components/humanTest/HumanTestWindow.vue +29 -24
- package/app/components/initiative/InitiativePlanNotice.vue +3 -3
- package/app/components/initiative/InitiativePlanReview.vue +46 -38
- package/app/components/initiative/InitiativePlanningWindow.vue +5 -5
- package/app/components/initiative/InitiativeTrackerWindow.vue +213 -165
- package/app/components/judge/JudgeResultView.vue +28 -27
- package/app/components/kaizen/KaizenPanel.vue +70 -56
- package/app/components/kaizen/KaizenStepStatus.vue +7 -6
- package/app/components/layout/AccountAuditLog.vue +1 -1
- package/app/components/layout/AccountDeploymentSettings.vue +10 -10
- package/app/components/layout/AccountFailureKindRules.vue +13 -13
- package/app/components/layout/AccountFoundationalSettings.vue +1 -1
- package/app/components/layout/AccountFragmentSettings.vue +1 -1
- package/app/components/layout/AccountModelPolicySettings.vue +7 -7
- package/app/components/layout/AccountPlatformAlertSettings.vue +8 -8
- package/app/components/layout/AccountRiskPolicySettings.vue +2 -2
- package/app/components/layout/AccountRunCredentialSettings.vue +3 -3
- package/app/components/layout/AccountSkillSettings.vue +1 -1
- package/app/components/layout/AccountTeamSettings.vue +13 -9
- package/app/components/layout/AiProvidersBanner.vue +1 -1
- package/app/components/layout/AppearanceSwitcher.vue +5 -4
- package/app/components/layout/BoardSwitcher.vue +13 -10
- package/app/components/layout/BoardToolbar.vue +10 -5
- package/app/components/layout/CommandBar.vue +8 -5
- package/app/components/layout/DefaultTestEnvBanner.vue +6 -5
- package/app/components/layout/IntegrationsHub.vue +12 -9
- package/app/components/layout/LanguageSwitcher.vue +5 -4
- package/app/components/layout/ModelProvidersHub.vue +10 -7
- package/app/components/layout/NotificationsInbox.vue +47 -36
- package/app/components/layout/PersonalSetupModal.vue +9 -6
- package/app/components/layout/RolePrompt.vue +5 -4
- package/app/components/layout/SideBar.vue +7 -8
- package/app/components/layout/SpendWarningBanner.vue +3 -3
- package/app/components/layout/TranslationWarningBanner.vue +5 -4
- package/app/components/layout/UiModeSwitcher.vue +15 -12
- package/app/components/layout/UiRoleSwitcher.vue +10 -8
- package/app/components/layout/WorkspaceMembersSettings.vue +2 -2
- package/app/components/media/ArtifactLightbox.vue +74 -44
- package/app/components/media/ImageCompare.vue +80 -85
- package/app/components/merge/MergeEffortChips.vue +4 -6
- package/app/components/notifications/NotificationSettingsPanel.vue +6 -5
- package/app/components/observability/OutcomeFilterChips.vue +6 -5
- package/app/components/observability/RunFailureSummary.vue +22 -23
- package/app/components/observability/StepMetricsBar.vue +9 -8
- package/app/components/observability/StepModelActivity.vue +8 -5
- package/app/components/observability/ToolCallList.vue +19 -20
- package/app/components/outcome/OutcomeSummaryWindow.vue +48 -46
- package/app/components/palettes/AgentPalette.vue +21 -15
- package/app/components/palettes/AgentTierSelect.vue +1 -1
- package/app/components/palettes/PipelinePurposeSelect.vue +1 -1
- package/app/components/panels/AgentStepDetail.vue +57 -49
- package/app/components/panels/AttemptEntryHeader.vue +2 -2
- package/app/components/panels/DecisionModal.vue +1 -1
- package/app/components/panels/GenericStructuredResultView.vue +6 -5
- package/app/components/panels/InspectorPanel.vue +15 -13
- package/app/components/panels/MergerResultView.vue +10 -9
- package/app/components/panels/ObservabilityPanel.vue +211 -186
- package/app/components/panels/OperatorDashboardPanel.vue +124 -115
- package/app/components/panels/ReportsPanel.vue +87 -83
- package/app/components/panels/ReportsSpendBreakdown.vue +2 -2
- package/app/components/panels/ResultWindowShell.vue +152 -137
- package/app/components/panels/RunDetailLoadState.vue +1 -1
- package/app/components/panels/StepContainerStatus.vue +17 -16
- package/app/components/panels/StepDelegatedStatus.vue +24 -21
- package/app/components/panels/StepEffortReport.vue +12 -17
- package/app/components/panels/StepFragmentAdherence.vue +9 -10
- package/app/components/panels/StepMetadataCard.vue +41 -43
- package/app/components/panels/StepReproductionReport.vue +18 -20
- package/app/components/panels/StepRestartControl.vue +3 -2
- package/app/components/panels/StepRunMeta.vue +27 -26
- package/app/components/panels/StepTestReport.vue +16 -15
- package/app/components/panels/StepToolServers.vue +7 -8
- package/app/components/panels/StepValidationReport.vue +5 -5
- package/app/components/panels/inspector/ContainerSummary.vue +4 -4
- package/app/components/panels/inspector/DocReferenceRepos.vue +5 -4
- package/app/components/panels/inspector/EnvProbeReport.vue +16 -13
- package/app/components/panels/inspector/EpicChildren.vue +12 -10
- package/app/components/panels/inspector/FrontendBindingsResolved.vue +5 -4
- package/app/components/panels/inspector/FrontendConfig.vue +29 -36
- package/app/components/panels/inspector/InitiativeInspector.vue +3 -3
- package/app/components/panels/inspector/InspectorSection.vue +25 -17
- package/app/components/panels/inspector/RecurringScheduleSettings.vue +9 -8
- package/app/components/panels/inspector/ServiceConnections.vue +5 -4
- package/app/components/panels/inspector/ServiceReleaseHealthConfig.vue +2 -2
- package/app/components/panels/inspector/ServiceSelfTests.vue +9 -13
- package/app/components/panels/inspector/ServiceTestConfig.vue +46 -49
- package/app/components/panels/inspector/ServiceTestSecrets.vue +3 -3
- package/app/components/panels/inspector/ServiceTestingContext.vue +2 -2
- package/app/components/panels/inspector/ServiceValidationConfig.vue +2 -2
- package/app/components/panels/inspector/TaskAgentConfig.vue +2 -2
- package/app/components/panels/inspector/TaskAprioriBranches.vue +6 -5
- package/app/components/panels/inspector/TaskDependencies.vue +1 -1
- package/app/components/panels/inspector/TaskEstimateBadge.vue +3 -7
- package/app/components/panels/inspector/TaskExecution.vue +18 -16
- package/app/components/panels/inspector/TaskRunSettings.vue +45 -44
- package/app/components/panels/inspector/TaskStructure.vue +5 -4
- package/app/components/pipeline/AgentPromptEditor.vue +15 -14
- package/app/components/pipeline/BinaryOutputStepPicker.vue +42 -52
- package/app/components/pipeline/EstimateThresholdFields.vue +14 -11
- package/app/components/pipeline/GateConfigFields.vue +19 -23
- package/app/components/pipeline/IterationCapPrompt.vue +1 -1
- package/app/components/pipeline/PipelineBuilder.vue +113 -99
- package/app/components/pipeline/PipelineHealthModal.vue +7 -7
- package/app/components/pipeline/PipelinePicker.vue +11 -9
- package/app/components/pipeline/PipelinePreview.vue +6 -7
- package/app/components/pipeline/PipelineProgress.vue +49 -45
- package/app/components/prReview/PrReviewWindow.vue +87 -65
- package/app/components/providers/AiPresetMismatchDialog.vue +4 -3
- package/app/components/providers/AiProviderOnboardingModal.vue +2 -2
- package/app/components/providers/ApiKeysSection.vue +11 -9
- package/app/components/providers/PersonalSubscriptionSection.vue +4 -3
- package/app/components/providers/VendorCredentialsModal.vue +5 -4
- package/app/components/provisioning/ProvisioningLogsDrawer.vue +10 -9
- package/app/components/ralph/RalphLoopResultView.vue +29 -29
- package/app/components/requirements/RequirementsReviewWindow.vue +22 -17
- package/app/components/riskPolicy/RiskPolicyPicker.vue +13 -11
- package/app/components/riskPolicy/RiskPolicyPreview.vue +16 -15
- package/app/components/sandbox/SandboxPanel.vue +93 -73
- package/app/components/settings/ApiTokensPanel.vue +1 -1
- package/app/components/settings/BudgetSettings.vue +18 -17
- package/app/components/settings/CapabilityCredentialsPanel.vue +5 -5
- package/app/components/settings/CloudflareHandlerSection.vue +44 -41
- package/app/components/settings/ComposeEnvironmentSetupSection.vue +14 -12
- package/app/components/settings/ConnectionTestVerdict.vue +1 -1
- package/app/components/settings/ConsensusGroupsSection.vue +58 -47
- package/app/components/settings/CustomManifestTypeEditor.vue +8 -7
- package/app/components/settings/DefaultProvisionTypeSection.vue +5 -5
- package/app/components/settings/InfraHandlersConfigurator.vue +26 -24
- package/app/components/settings/InfrastructureBackendPicker.vue +18 -18
- package/app/components/settings/IssueTrackerPanel.vue +30 -21
- package/app/components/settings/KubernetesEngineForm.vue +9 -8
- package/app/components/settings/KubernetesEnvironmentForm.vue +4 -3
- package/app/components/settings/LocalContainerPoolSettings.vue +2 -2
- package/app/components/settings/LocalModelEndpointsPanel.vue +13 -11
- package/app/components/settings/MergeClassRulesEditor.vue +6 -5
- package/app/components/settings/MergeRolePolicyEditor.vue +13 -12
- package/app/components/settings/ModelConfigurationPanel.vue +17 -19
- package/app/components/settings/ModelPresetHealthModal.vue +3 -3
- package/app/components/settings/ObservabilityConnectionPanel.vue +1 -1
- package/app/components/settings/OpenRouterCatalogPanel.vue +10 -8
- package/app/components/settings/PackageRegistriesPanel.vue +4 -4
- package/app/components/settings/ProviderConnectionTab.vue +5 -4
- package/app/components/settings/ProviderManifestEditor.vue +9 -8
- package/app/components/settings/ProviderPreferenceEditor.vue +13 -13
- package/app/components/settings/RiskPolicyCreateForm.vue +40 -26
- package/app/components/settings/RiskPolicyEditorRow.vue +21 -19
- package/app/components/settings/RiskPolicyHealthModal.vue +3 -3
- package/app/components/settings/RiskPolicyPanel.vue +11 -13
- package/app/components/settings/ServiceFragmentDefaultsPanel.vue +15 -12
- package/app/components/settings/SharedStacksPanel.vue +5 -5
- package/app/components/settings/TaskSourceCard.vue +2 -2
- package/app/components/settings/TaskTypeSuppressionsPanel.vue +4 -4
- package/app/components/settings/ToolServerChecklist.vue +22 -22
- package/app/components/settings/UsageSettings.vue +13 -13
- package/app/components/settings/UserSecretsSection.vue +4 -3
- package/app/components/settings/WorkspaceMetadataSettings.vue +6 -5
- package/app/components/settings/WorkspaceSettingsPanel.vue +72 -58
- package/app/components/skills/ReviewSkillQueue.vue +12 -10
- package/app/components/skills/SkillLibraryManager.vue +9 -8
- package/app/components/slack/SlackPanel.vue +17 -12
- package/app/components/spec/ServiceSpecWindow.vue +19 -19
- package/app/components/tasks/BugHuntModal.vue +1 -1
- package/app/components/tasks/ContextIssuePicker.vue +16 -15
- package/app/components/tasks/TaskContextIssues.vue +5 -4
- package/app/components/tasks/TaskImportModal.vue +3 -2
- package/app/components/tasks/TaskSourceConnectModal.vue +5 -5
- package/app/components/testing/TestReportWindow.vue +67 -60
- package/app/components/theme/ThemeImportModal.vue +2 -2
- package/app/components/tutorial/TutorialNudge.vue +3 -2
- package/app/components/tutorial/TutorialOverlay.vue +3 -2
- package/app/components/vcs/GitLabConnect.vue +1 -1
- package/app/components/visualConfirm/VisualConfirmationWindow.vue +79 -54
- package/app/utils/theme/builtins.ts +7 -1
- package/i18n/locales/de.json +7 -3
- package/i18n/locales/en.json +7 -3
- package/i18n/locales/es.json +7 -3
- package/i18n/locales/fr.json +7 -3
- package/i18n/locales/he.json +7 -3
- package/i18n/locales/it.json +7 -3
- package/i18n/locales/ja.json +7 -3
- package/i18n/locales/pl.json +7 -3
- package/i18n/locales/tr.json +7 -3
- package/i18n/locales/uk.json +7 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -300,6 +300,36 @@ Every colour in the SPA is a token that follows the THEME and the colour MODE. T
|
|
|
300
300
|
- **Never** a raw Tailwind hue (`bg-slate-900`, `text-amber-300`, `text-pink-300`) nor a fixed numbered alias (`text-primary-400`, `bg-warning-950/40`): the first is welded to one palette and one mode, the second follows the palette but not the mode. `scripts/check-frontend-palette.mjs` (CI) bans both. `red` and `rose` both mean `error`.
|
|
301
301
|
- **Never** a fixed `white` / `black` utility (`bg-white`, `text-black`, `border-white/5`, `bg-white/[0.02]`): invisible or wrong in light mode. Use `border-inverted` (a selection ring), `border-default` / `bg-muted` (faint chrome) or `text-inverted` (text over a filled surface). A line that genuinely needs a fixed value says why with a `fixed-colour-ok:` comment, on that line or the one above it.
|
|
302
302
|
|
|
303
|
+
### Type through named steps, never a pixel literal
|
|
304
|
+
|
|
305
|
+
Every font size in the SPA is a NAMED step, so it scales with the theme and lines up with the sizes Nuxt UI gives its own components. Two sources of steps:
|
|
306
|
+
|
|
307
|
+
- **Tailwind's own** from `text-xs` up (`text-xs`, `text-sm`, `text-base`, ...), each paired with a line height.
|
|
308
|
+
- **The app's two steps below them** (`app/assets/css/type.css`), for the dense surfaces Tailwind's scale bottoms out above: `text-2xs` (0.6875rem, 11px at the default root) for the dense body size that panel rows, node metadata and inline detail are written at, and `text-3xs` (0.625rem, 10px) for the caption tier below it: pill and badge text on a board node, a quoted source excerpt, an axis or legend caption. The section eyebrow is `text-2xs`, not `text-3xs`, and is not written by hand at all (see the last bullet). Neither declares a paired LINE HEIGHT: dense text inherits its rhythm from the surrounding block, and a block that wants a specific rhythm says `leading-*`, which wins over a step's paired value anyway.
|
|
309
|
+
- **Never** a literal size (`text-[11px]`, `text-[0.6875rem]`). A theme document's `fontSize` is applied as a root `font-size` (`utils/theme/css.ts`), so every rem step follows it and a pixel literal does not: raising the font size would grow Nuxt UI's components while the app's own text sat still. A literal is also unnameable, so a `UBadge size="sm"` beside one is sized by a system the label cannot refer to. `scripts/check-frontend-type-scale.mjs` (CI) bans the utility forms (`text-[length:11px]`, `[font-size:11px]`, `[font:…]`), a px declaration in CSS (`font-size: 11px`, the `font:` shorthand) and a px JS style property (`fontSize: '11px'`, `style.fontSize = …`); its header lists exactly what it matches. A line that genuinely needs one says why with a `type-literal-ok:` comment, on that line or the one above.
|
|
310
|
+
- **A section eyebrow is [`common/SectionLabel.vue`](./app/components/common/SectionLabel.vue)**, not a hand-written class list. The label that titles a group inside a panel, window, inspector section or dropdown is one recipe, `text-2xs font-semibold uppercase tracking-wide text-muted`, and the component owns all five: they are not overridable, because a caller wanting a different size or colour wants a different thing. The caller picks the ELEMENT with `as` (`div` by default; `h3` / `h4` for a real heading, `label` with an `:id` for a field; a collapsing section is a `UButton` around `as="span"`) and owns SPACING through the ordinary `class` attribute. An ACCENT-coloured uppercase label (`text-primary`, `text-app-info-400`) is not this component: it is signalling state or category rather than titling a section, so it keeps its own classes. Nor is a pill, a chip, a metadata row that merely shares the uppercase styling (`BlockNode.vue`'s composition line), or a caption inside a control (a switcher trigger's label line, a slider's end labels): the test is whether the text TITLES the block after it. The colour is `text-muted` and not the fainter `text-dimmed` for contrast: at 11px `text-dimmed` is 2.63:1 on a light surface, under WCAG AA, where `text-muted` is 4.76:1. (The SPA's wider use of `text-dimmed` outside this component is a separate problem.)
|
|
311
|
+
|
|
312
|
+
### Radius through the theme scale
|
|
313
|
+
|
|
314
|
+
Every `rounded-*` utility in the SPA derives from `--ui-radius`, so a theme document's `radius` reaches all of them. Nuxt UI rebuilds seven of Tailwind's steps on it (`rounded-xs` is half it, `rounded-sm` is it, then 1.5x, 2x, 3x, 4x and 6x), and [`main.css`](./app/assets/css/main.css) binds the two spellings that sat outside that: `--radius`, which the bare `rounded` alias and its side and corner forms resolve through, and `--radius-4xl`. Tailwind declares both `@theme default`, so the app's own `@theme inline` block wins, and `inline` is what makes the utility compile to `var(--ui-radius)` rather than to a value frozen at build time. Neither number moves on the default theme.
|
|
315
|
+
|
|
316
|
+
That is the whole mechanism, and it is why there is no rule here about WHICH step to pick. Two things still name a number instead of the scale:
|
|
317
|
+
|
|
318
|
+
- **Never an arbitrary value**: `rounded-[10px]`, `rounded-t-[0.6rem]`, or Tailwind v4's CSS-variable shorthand `rounded-(--x)`. Welded to one number the way a pixel font size is, and unnameable besides, so a `UBadge` beside one is shaped by a system the box cannot refer to. The SPA has none.
|
|
319
|
+
- **In CSS, reach the scale through `var(--ui-radius)`**, on its own or inside a `calc()`: `calc(var(--ui-radius) * 2)` is the `rounded-lg` step, and `calc(var(--ui-radius) - 1px)` is an inner box kept concentric with its parent, which follows the theme just as well. **Never a `--radius-*` theme variable**, even though one works today: Tailwind declares that scale in a `@theme default inline` block and emits a variable only where the COMPILED stylesheet graph references it, so `var(--radius-lg)` is correct until the last other reference is deleted and then silently computes to 0. `--ui-radius` is an ordinary custom property on `:root`, so it is there in a scoped SFC `<style>` block too, where the `--radius-*` scale never is. **Never a raw literal** in px, pt, rem or em, the per-corner longhands (`border-top-left-radius`, the logical `border-start-start-radius`) and the JS `borderRadius` / `'border-radius'` spelling a `:style` binding uses included. The pill spellings (`9999px`, `50%`) and `0` are exempt, and a shorthand is read value by value, so `50% 50% 8px 8px` is still caught on the `8px`.
|
|
320
|
+
- `scripts/check-frontend-radius.mjs` (CI) bans both. It needs no notion of where a class list lives, because `rounded-[` and `rounded-(` are not English words. A line that genuinely needs a fixed radius says why with a `radius-literal-ok:` comment, on that line or the one above.
|
|
321
|
+
|
|
322
|
+
The default `radius` lives on the `Cat Factory` document in `app/utils/theme/builtins.ts`. It equals `THEME_DEFAULTS.radius` and so writes no CSS, which is the point: changing the number there is the whole edit.
|
|
323
|
+
|
|
324
|
+
### A control is its Nuxt UI component
|
|
325
|
+
|
|
326
|
+
Every CONTROL in the SPA is the Nuxt UI component for it, never the raw HTML element. A raw `<button>` beside a `UButton` is the same control rendered twice, and only one of the two follows the theme, the focus ring, the disabled state and the size scale; the SPA carried 152 of them, 24 raw `<input>`, five hand-built tables and six hand-rolled `<details>`, each with its own border, background and hover recipe.
|
|
327
|
+
|
|
328
|
+
- **The mapping**: `<button>` → `UButton`; `<input>` → `UInput` / `UInputNumber` / `UCheckbox` / `URadioGroup` / `USlider` / `UFileUpload`, by what the control IS rather than by its `type=`; `<select>` → `USelect` (`USelectMenu` when it needs searching, `UInputMenu` when the list is open-ended); `<textarea>` → `UTextarea`; `<table>` → `UTable` with column definitions; `<details>` → `UCollapsible`; `<form>` → `UForm`; `<a href>` → `ULink`; `<datalist>` → `UInputMenu` with `mode="autocomplete"`, which is what a datalist was (the DEFAULT combobox mode writes its model only on SELECT, so a typed free-text value is dropped). Which input for which job is the skill's [component-selection matrix](https://github.com/nuxt/ui/blob/v4/skills/nuxt-ui/references/component-selection.md), not restated here.
|
|
329
|
+
- **An icon-only button is [`common/IconButton.vue`](./app/components/common/IconButton.vue)**, which wraps `UButton` in a `UTooltip` and applies `label` as the tooltip text AND `aria-label`. A `title` attribute is not a tooltip: it never fires on touch, is unstyled, waits about a second, and looks nothing like the app's other hints. `IconButton` and `CopyButton` own their tooltip, so passing either a `title` renders a second hint over the first. A `title` on a LABELLED `UButton` is still fine: there it is a hover hint beside the control's own name rather than the name itself. `class` on `IconButton` lands on the wrapper the tooltip needs, which is the box the parent lays out, so it carries layout (`ms-auto`, `shrink-0`, `nodrag`); the button's own look goes through its props and `:ui="{ base: '…' }"`. The guard refuses an icon-only `UButton` whose only name is a `title`.
|
|
330
|
+
- **Prose and layout markup is not in scope.** `<p>`, `<h1>`-`<h6>`, `<ul>`, `<li>`, `<dl>`, `<pre>`, `<code>`, `<img>`, `<nav>`, `<div>` and `<span>` are not controls and have no Nuxt UI component; heading typography is the type scale's rule above.
|
|
331
|
+
- `scripts/check-frontend-primitives.mjs` (CI) holds the count at zero. There is deliberately **no allow-list**: the migration expected two exceptions (a Vue Flow gesture handle, a full-bleed media tile) and both converted cleanly, so the exception table would have had more surface than the thing it governed. A site that genuinely cannot convert says why with a `raw-control-ok:` comment, on that line or the one above; nothing in the tree needs one, and a second one appearing is the signal that the rule needs revisiting rather than the site.
|
|
332
|
+
|
|
303
333
|
### Themes and colour mode
|
|
304
334
|
|
|
305
335
|
A **theme** is a Nuxt UI theme editor document (https://ui.nuxt.com/theme): the sparse `ThemeDoc` the editor edits and shares as a `?doc=` link, decoded by `utils/theme/link.ts` and typed in `utils/theme/doc.ts` (a port of the editor engine's tables). The app speaks THAT format rather than one of its own, so a theme built in the editor imports as-is and a built-in theme is a document a user could have exported. `utils/theme/builtins.ts` ships `Cat Factory` (the default; its `colors` MUST equal `app.config.ts`'s `ui.colors`, which paints the first frame) and the editor's own `Mono` preset, referenced from the preset table rather than copied. The `theme` store persists the pick and the user's imported documents in this browser; `plugins/theme.client.ts` applies the active document at runtime: aliases onto `appConfig.ui.colors` (Nuxt UI's own colours plugin re-emits the ramps), default variants and component overrides onto `appConfig.ui.<component>` merged INTO the layer's own overrides, and tokens / radius / fonts onto one `<style id="app-theme">` whose selectors anchor on the `data-theme` attribute on `<html>` (`utils/theme/css.ts`, which is also the write boundary for untrusted values). The faces the built-in themes name (Geist, Geist Mono) are self-hosted through `@nuxt/fonts` (`fonts.families` in `nuxt.config.ts`, pinned by `builtins.spec.ts`); a font an IMPORTED theme names is applied as a family name only, so it renders when the device or the deployment provides it. A editor link whose document equals one of the editor's presets collapses to `{ preset: 'mono' }`; `utils/theme/presets.ts` is a copy of the editor's preset table so such a link rebuilds (a decode table, not menu entries).
|
|
@@ -354,8 +384,8 @@ protects, so the two never contradict silently:
|
|
|
354
384
|
freely; here a failed backend call is reported with `usePipelineErrorToast().present(error, key)`.
|
|
355
385
|
[Rule](#every-failure-toast-goes-through-one-funnel).
|
|
356
386
|
- **Icon-only buttons carry an accessible name.** Use `common/IconButton.vue`
|
|
357
|
-
([source](./app/components/common/IconButton.vue)), which applies `label` as both
|
|
358
|
-
`aria-label`, rather than a bare `<UButton icon>`.
|
|
387
|
+
([source](./app/components/common/IconButton.vue)), which applies `label` as both the tooltip
|
|
388
|
+
and `aria-label`, rather than a bare `<UButton icon>`.
|
|
359
389
|
- **Colour goes through theme tokens, never a fixed palette shade.** The skill bans raw Tailwind
|
|
360
390
|
hues; this repo also bans fixed numbered aliases, enforced by
|
|
361
391
|
[`scripts/check-frontend-palette.mjs`](https://github.com/kibertoad/cat-factory/blob/main/scripts/check-frontend-palette.mjs).
|
package/app/assets/css/main.css
CHANGED
|
@@ -6,6 +6,28 @@
|
|
|
6
6
|
/* The app's own mode-adaptive colour tokens (`app-*`), layered on Nuxt UI's. */
|
|
7
7
|
@import './tokens.css';
|
|
8
8
|
|
|
9
|
+
/* The app's own type steps below `text-xs` (`text-2xs`, `text-3xs`). */
|
|
10
|
+
@import './type.css';
|
|
11
|
+
|
|
12
|
+
/* Bind the last two radius steps Nuxt UI leaves alone onto `--ui-radius`, so EVERY `rounded-*`
|
|
13
|
+
* utility follows a theme document's `radius` (issue #2251).
|
|
14
|
+
*
|
|
15
|
+
* Nuxt UI rebuilds seven of Tailwind's steps on `--ui-radius` (`--radius-sm` is it, `--radius-lg`
|
|
16
|
+
* is twice it, and so on), which is why a hand-written box on a named step already lines up with
|
|
17
|
+
* the Nuxt UI component beside it. Two spellings sat outside that: the bare `rounded` alias, whose
|
|
18
|
+
* `--radius` Tailwind declares `@theme default inline reference` and so inlines as a literal
|
|
19
|
+
* 0.25rem, and `rounded-4xl`, whose `--radius-4xl` keeps Tailwind's own 2rem. Both are `default`,
|
|
20
|
+
* so declaring them here wins, and `inline` is what lets `rounded` compile to `var(--ui-radius)`
|
|
21
|
+
* rather than to a value frozen at build time.
|
|
22
|
+
*
|
|
23
|
+
* Neither number moves on the default theme: `--ui-radius` is 0.25rem, and 8x it is the 2rem
|
|
24
|
+
* `--radius-4xl` already held. Declaring them here also EMITS both as ordinary custom properties,
|
|
25
|
+
* where Tailwind's `reference` kept `--radius` out of the stylesheet entirely. */
|
|
26
|
+
@theme inline {
|
|
27
|
+
--radius: var(--ui-radius);
|
|
28
|
+
--radius-4xl: calc(var(--ui-radius) * 8);
|
|
29
|
+
}
|
|
30
|
+
|
|
9
31
|
/* The two toaster rules below are scoped by the `app-toaster` marker class that
|
|
10
32
|
* `app.config.ts` puts on the toaster's viewport slot, NOT by `[data-slot='viewport']`.
|
|
11
33
|
* That attribute is Nuxt UI's name for the scroll region of ELEVEN components
|
package/app/assets/css/prose.css
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
.reader-prose.review-mode [data-src-start] {
|
|
18
18
|
position: relative;
|
|
19
19
|
cursor: pointer;
|
|
20
|
-
border-radius:
|
|
20
|
+
border-radius: calc(var(--ui-radius) * 1.5);
|
|
21
21
|
transition: background 0.12s ease;
|
|
22
22
|
}
|
|
23
23
|
.reader-prose.review-mode [data-src-start]:hover {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
width: 1.1rem;
|
|
35
35
|
align-items: center;
|
|
36
36
|
justify-content: center;
|
|
37
|
-
border-radius:
|
|
37
|
+
border-radius: var(--ui-radius);
|
|
38
38
|
background: var(--ui-primary);
|
|
39
39
|
color: var(--ui-text-inverted);
|
|
40
40
|
font-size: 0.8rem;
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
font-style: italic;
|
|
82
82
|
}
|
|
83
83
|
.reader-prose code {
|
|
84
|
-
border-radius:
|
|
84
|
+
border-radius: var(--ui-radius);
|
|
85
85
|
background: var(--ui-bg-elevated);
|
|
86
86
|
padding: 0.1rem 0.3rem;
|
|
87
87
|
font-family: var(--font-mono);
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
.reader-prose pre {
|
|
92
92
|
margin: 0.6rem 0;
|
|
93
93
|
overflow: auto;
|
|
94
|
-
border-radius:
|
|
94
|
+
border-radius: calc(var(--ui-radius) * 2);
|
|
95
95
|
background: var(--app-bg-canvas);
|
|
96
96
|
padding: 0.75rem 0.9rem;
|
|
97
97
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/* The app's own type steps below Tailwind's `text-xs` (issue #2248).
|
|
2
|
+
*
|
|
3
|
+
* Both are rem, so both scale with a theme document's `fontSize`, which `utils/theme/css.ts` applies
|
|
4
|
+
* as the root `font-size`:
|
|
5
|
+
*
|
|
6
|
+
* --text-2xs 0.6875rem (11px at the 16px default) the dense body size: panel rows, node
|
|
7
|
+
* metadata, inline detail, and the section
|
|
8
|
+
* eyebrow (`common/SectionLabel.vue`).
|
|
9
|
+
* --text-3xs 0.625rem (10px) the caption tier below the eyebrow: pill and
|
|
10
|
+
* badge text on a board node, a quoted source
|
|
11
|
+
* excerpt, axis and legend captions.
|
|
12
|
+
*
|
|
13
|
+
* Neither step declares a line height. Tailwind pairs `--text-<n>` with an optional
|
|
14
|
+
* `--text-<n>--line-height`, and when one exists the utility sets both properties. Dense text here
|
|
15
|
+
* inherits its rhythm from the surrounding block; a block that wants a specific rhythm says so with
|
|
16
|
+
* `leading-*`.
|
|
17
|
+
*
|
|
18
|
+
* `scripts/check-frontend-type-scale.mjs` bans font-size literals, so these two and Tailwind's own
|
|
19
|
+
* steps are the whole scale. */
|
|
20
|
+
|
|
21
|
+
@theme {
|
|
22
|
+
--text-2xs: 0.6875rem;
|
|
23
|
+
--text-3xs: 0.625rem;
|
|
24
|
+
}
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
// the retry belongs.
|
|
23
23
|
import type { AssistantActionId, AssistantOutcome } from '~/types/domain'
|
|
24
24
|
import { answerFor, assistantSurface, revealTarget, submitGate } from './AssistantModal.logic'
|
|
25
|
+
import SectionLabel from '~/components/common/SectionLabel.vue'
|
|
25
26
|
|
|
26
27
|
const { t } = useI18n()
|
|
27
28
|
const ui = useUiStore()
|
|
@@ -254,20 +255,21 @@ function reveal(blockId: string): void {
|
|
|
254
255
|
|
|
255
256
|
<!-- What it can do, always visible: the catalog is the affordance. -->
|
|
256
257
|
<div v-if="examples.length" class="space-y-2">
|
|
257
|
-
<
|
|
258
|
+
<SectionLabel as="p">
|
|
258
259
|
{{ t('assistant.examplesTitle') }}
|
|
259
|
-
</
|
|
260
|
+
</SectionLabel>
|
|
260
261
|
<div class="flex flex-col gap-1">
|
|
261
|
-
<
|
|
262
|
+
<UButton
|
|
263
|
+
color="neutral"
|
|
264
|
+
variant="ghost"
|
|
262
265
|
v-for="entry in examples"
|
|
263
266
|
:key="entry.actionId"
|
|
264
|
-
|
|
265
|
-
class="rounded-md px-2 py-1 text-left text-sm text-toned hover:bg-elevated"
|
|
267
|
+
class="block rounded-md px-2 py-1 text-left text-sm text-toned hover:bg-elevated"
|
|
266
268
|
@click="useExample(entry.example)"
|
|
267
269
|
>
|
|
268
270
|
<span class="text-muted">{{ entry.label }}</span>
|
|
269
271
|
<span class="block text-xs text-dimmed">“{{ entry.example }}”</span>
|
|
270
|
-
</
|
|
272
|
+
</UButton>
|
|
271
273
|
</div>
|
|
272
274
|
</div>
|
|
273
275
|
|
|
@@ -41,16 +41,17 @@ function reload() {
|
|
|
41
41
|
<span class="font-medium text-toned">{{ t('app.misconfigured.howToFix') }}</span>
|
|
42
42
|
{{ problem.remedy }}
|
|
43
43
|
</p>
|
|
44
|
-
<
|
|
44
|
+
<ULink
|
|
45
|
+
raw
|
|
45
46
|
v-if="problem.docsUrl"
|
|
46
|
-
:
|
|
47
|
+
:to="problem.docsUrl"
|
|
47
48
|
target="_blank"
|
|
48
49
|
rel="noopener noreferrer"
|
|
49
50
|
class="mt-2 inline-flex items-center gap-1 text-sm text-app-warning-300 hover:text-app-warning-200"
|
|
50
51
|
>
|
|
51
52
|
<UIcon name="i-lucide-book-open" class="h-4 w-4" />
|
|
52
53
|
{{ t('app.misconfigured.viewDocs') }}
|
|
53
|
-
</
|
|
54
|
+
</ULink>
|
|
54
55
|
</li>
|
|
55
56
|
</ul>
|
|
56
57
|
|
|
@@ -286,16 +286,17 @@ const noSignInMethod = computed(
|
|
|
286
286
|
<!-- Only when nothing can be installed here: the paste form below carries its own
|
|
287
287
|
per-provider link, so showing these too would offer the same thing twice. -->
|
|
288
288
|
<div v-if="installableProviders.length === 0" class="flex flex-wrap gap-3 px-1">
|
|
289
|
-
<
|
|
289
|
+
<ULink
|
|
290
|
+
raw
|
|
290
291
|
v-for="p in ALL_PROVIDERS"
|
|
291
292
|
:key="p"
|
|
292
|
-
:
|
|
293
|
+
:to="tokenCreateUrl(p)"
|
|
293
294
|
target="_blank"
|
|
294
295
|
rel="noopener noreferrer"
|
|
295
296
|
class="text-xs text-primary hover:underline"
|
|
296
297
|
>
|
|
297
298
|
{{ t('auth.localMode.createToken', { provider: PROVIDER_LABELS[p] }) }}
|
|
298
|
-
</
|
|
299
|
+
</ULink>
|
|
299
300
|
</div>
|
|
300
301
|
</template>
|
|
301
302
|
|
|
@@ -379,10 +380,10 @@ const noSignInMethod = computed(
|
|
|
379
380
|
</div>
|
|
380
381
|
|
|
381
382
|
<!-- Email / password -->
|
|
382
|
-
<
|
|
383
|
+
<UForm
|
|
383
384
|
v-if="auth.providers.password && mode !== 'forgot'"
|
|
384
385
|
class="space-y-3"
|
|
385
|
-
@submit
|
|
386
|
+
@submit="submitPassword"
|
|
386
387
|
>
|
|
387
388
|
<UInput
|
|
388
389
|
v-if="mode === 'signup'"
|
|
@@ -426,36 +427,43 @@ const noSignInMethod = computed(
|
|
|
426
427
|
<template v-if="mode === 'login'">
|
|
427
428
|
<i18n-t keypath="auth.login.needAccount" tag="span" scope="global">
|
|
428
429
|
<template #signUp>
|
|
429
|
-
<
|
|
430
|
-
|
|
431
|
-
|
|
430
|
+
<UButton
|
|
431
|
+
color="primary"
|
|
432
|
+
variant="link"
|
|
433
|
+
class="p-0 text-xs text-primary hover:underline"
|
|
432
434
|
@click="setMode('signup')"
|
|
433
435
|
>
|
|
434
436
|
{{ t('auth.login.signUp') }}
|
|
435
|
-
</
|
|
437
|
+
</UButton>
|
|
436
438
|
</template>
|
|
437
439
|
</i18n-t>
|
|
438
440
|
</template>
|
|
439
441
|
<template v-else>
|
|
440
442
|
<i18n-t keypath="auth.login.haveAccount" tag="span" scope="global">
|
|
441
443
|
<template #signIn>
|
|
442
|
-
<
|
|
443
|
-
|
|
444
|
-
|
|
444
|
+
<UButton
|
|
445
|
+
color="primary"
|
|
446
|
+
variant="link"
|
|
447
|
+
class="p-0 text-xs text-primary hover:underline"
|
|
445
448
|
@click="setMode('login')"
|
|
446
449
|
>
|
|
447
450
|
{{ t('auth.login.signIn') }}
|
|
448
|
-
</
|
|
451
|
+
</UButton>
|
|
449
452
|
</template>
|
|
450
453
|
</i18n-t>
|
|
451
454
|
</template>
|
|
452
455
|
</p>
|
|
453
456
|
<p v-if="mode === 'login'" class="text-center text-xs text-muted">
|
|
454
|
-
<
|
|
457
|
+
<UButton
|
|
458
|
+
color="primary"
|
|
459
|
+
variant="link"
|
|
460
|
+
class="p-0 text-xs text-primary hover:underline"
|
|
461
|
+
@click="setMode('forgot')"
|
|
462
|
+
>
|
|
455
463
|
{{ t('auth.login.forgotPassword') }}
|
|
456
|
-
</
|
|
464
|
+
</UButton>
|
|
457
465
|
</p>
|
|
458
|
-
</
|
|
466
|
+
</UForm>
|
|
459
467
|
|
|
460
468
|
<!-- Paste-a-token sign-in: your own PAT on a hosted node; on local mode the token this
|
|
461
469
|
deployment will operate with (see `remotePatProviders`). -->
|
|
@@ -472,7 +480,7 @@ const noSignInMethod = computed(
|
|
|
472
480
|
<span class="h-px flex-1 bg-elevated" /> {{ t('auth.login.or') }}
|
|
473
481
|
<span class="h-px flex-1 bg-elevated" />
|
|
474
482
|
</div>
|
|
475
|
-
<
|
|
483
|
+
<UForm class="space-y-3" @submit="submitRemotePat">
|
|
476
484
|
<div v-if="remotePatProviders.length > 1" class="flex gap-2">
|
|
477
485
|
<UButton
|
|
478
486
|
v-for="p in remotePatProviders"
|
|
@@ -517,8 +525,9 @@ const noSignInMethod = computed(
|
|
|
517
525
|
{{ t('auth.localMode.tokenBecomesCredential') }}
|
|
518
526
|
</p>
|
|
519
527
|
<p class="px-1 text-center">
|
|
520
|
-
<
|
|
521
|
-
|
|
528
|
+
<ULink
|
|
529
|
+
raw
|
|
530
|
+
:to="tokenCreateUrl(remotePatProvider)"
|
|
522
531
|
target="_blank"
|
|
523
532
|
rel="noopener noreferrer"
|
|
524
533
|
class="text-xs text-primary hover:underline"
|
|
@@ -526,9 +535,9 @@ const noSignInMethod = computed(
|
|
|
526
535
|
{{
|
|
527
536
|
t('auth.localMode.createToken', { provider: PROVIDER_LABELS[remotePatProvider] })
|
|
528
537
|
}}
|
|
529
|
-
</
|
|
538
|
+
</ULink>
|
|
530
539
|
</p>
|
|
531
|
-
</
|
|
540
|
+
</UForm>
|
|
532
541
|
</template>
|
|
533
542
|
|
|
534
543
|
<!-- No sign-in method configured on a remote deployment: explain, don't show a blank card -->
|
|
@@ -542,10 +551,10 @@ const noSignInMethod = computed(
|
|
|
542
551
|
/>
|
|
543
552
|
|
|
544
553
|
<!-- Forgot password: request a reset link by email -->
|
|
545
|
-
<
|
|
554
|
+
<UForm
|
|
546
555
|
v-if="auth.providers.password && mode === 'forgot'"
|
|
547
556
|
class="space-y-3"
|
|
548
|
-
@submit
|
|
557
|
+
@submit="submitForgot"
|
|
549
558
|
>
|
|
550
559
|
<template v-if="forgotSent">
|
|
551
560
|
<p class="text-sm text-toned">
|
|
@@ -568,11 +577,16 @@ const noSignInMethod = computed(
|
|
|
568
577
|
</UButton>
|
|
569
578
|
</template>
|
|
570
579
|
<p class="text-center text-xs text-muted">
|
|
571
|
-
<
|
|
580
|
+
<UButton
|
|
581
|
+
color="primary"
|
|
582
|
+
variant="link"
|
|
583
|
+
class="p-0 text-xs text-primary hover:underline"
|
|
584
|
+
@click="setMode('login')"
|
|
585
|
+
>
|
|
572
586
|
{{ t('auth.login.backToSignIn') }}
|
|
573
|
-
</
|
|
587
|
+
</UButton>
|
|
574
588
|
</p>
|
|
575
|
-
</
|
|
589
|
+
</UForm>
|
|
576
590
|
</div>
|
|
577
591
|
</div>
|
|
578
592
|
</template>
|
|
@@ -76,7 +76,7 @@ function goToLogin() {
|
|
|
76
76
|
</UButton>
|
|
77
77
|
</template>
|
|
78
78
|
|
|
79
|
-
<
|
|
79
|
+
<UForm v-else class="space-y-3" @submit="submit">
|
|
80
80
|
<SecretInput
|
|
81
81
|
v-model="password"
|
|
82
82
|
required
|
|
@@ -98,11 +98,16 @@ function goToLogin() {
|
|
|
98
98
|
{{ t('auth.resetPassword.submit') }}
|
|
99
99
|
</UButton>
|
|
100
100
|
<p class="text-center text-xs text-muted">
|
|
101
|
-
<
|
|
101
|
+
<UButton
|
|
102
|
+
color="primary"
|
|
103
|
+
variant="link"
|
|
104
|
+
class="p-0 text-xs text-primary hover:underline"
|
|
105
|
+
@click="goToLogin"
|
|
106
|
+
>
|
|
102
107
|
{{ t('auth.resetPassword.backToSignIn') }}
|
|
103
|
-
</
|
|
108
|
+
</UButton>
|
|
104
109
|
</p>
|
|
105
|
-
</
|
|
110
|
+
</UForm>
|
|
106
111
|
</div>
|
|
107
112
|
</div>
|
|
108
113
|
</template>
|
|
@@ -33,8 +33,9 @@ const items = computed<DropdownMenuItem[][]>(() => [
|
|
|
33
33
|
|
|
34
34
|
<template>
|
|
35
35
|
<UDropdownMenu v-if="auth.user" :items="items" :content="{ side: 'top', align: 'start' }">
|
|
36
|
-
<
|
|
37
|
-
|
|
36
|
+
<UButton
|
|
37
|
+
color="neutral"
|
|
38
|
+
variant="ghost"
|
|
38
39
|
data-testid="user-menu"
|
|
39
40
|
:title="collapsed ? auth.user.name || auth.user.login : undefined"
|
|
40
41
|
class="flex w-full items-center gap-2 rounded-lg border border-default bg-default/60 p-2 text-start transition hover:bg-elevated/60"
|
|
@@ -50,9 +51,9 @@ const items = computed<DropdownMenuItem[][]>(() => [
|
|
|
50
51
|
<div class="truncate text-xs font-medium text-highlighted">
|
|
51
52
|
{{ auth.user.name || auth.user.login }}
|
|
52
53
|
</div>
|
|
53
|
-
<div class="truncate text-
|
|
54
|
+
<div class="truncate text-3xs text-dimmed">@{{ auth.user.login }}</div>
|
|
54
55
|
</div>
|
|
55
56
|
<UIcon v-if="!collapsed" name="i-lucide-chevron-up" class="h-4 w-4 shrink-0 text-dimmed" />
|
|
56
|
-
</
|
|
57
|
+
</UButton>
|
|
57
58
|
</UDropdownMenu>
|
|
58
59
|
</template>
|
|
@@ -202,7 +202,7 @@ const { requestClose } = useUnsavedGuard({
|
|
|
202
202
|
declared its candidates and one whose block was unreadable need different fixes. -->
|
|
203
203
|
<p
|
|
204
204
|
v-if="noChoiceKey"
|
|
205
|
-
class="mb-3 rounded border border-app-warning-500/30 bg-app-warning-500/5 px-3 py-2 text-xs text-app-warning-200"
|
|
205
|
+
class="mb-3 rounded-sm border border-app-warning-500/30 bg-app-warning-500/5 px-3 py-2 text-xs text-app-warning-200"
|
|
206
206
|
data-testid="binary-candidates-no-choice"
|
|
207
207
|
>
|
|
208
208
|
{{ t(noChoiceKey) }}
|
|
@@ -211,7 +211,7 @@ const { requestClose } = useUnsavedGuard({
|
|
|
211
211
|
<!-- An automatic keep is NOT a review, and must never render as one. -->
|
|
212
212
|
<p
|
|
213
213
|
v-if="view.automatic"
|
|
214
|
-
class="mb-3 rounded border border-app-600/40 bg-elevated/40 px-3 py-2 text-xs text-toned"
|
|
214
|
+
class="mb-3 rounded-sm border border-app-600/40 bg-elevated/40 px-3 py-2 text-xs text-toned"
|
|
215
215
|
data-testid="binary-candidates-automatic"
|
|
216
216
|
>
|
|
217
217
|
{{ t('binaryCandidates.automatic') }}
|
|
@@ -235,6 +235,16 @@ const { requestClose } = useUnsavedGuard({
|
|
|
235
235
|
>
|
|
236
236
|
</p>
|
|
237
237
|
|
|
238
|
+
<!-- Single-select renders a checkbox per card, not one radio group, so this line is what
|
|
239
|
+
tells everyone (a screen reader through each box's description) that ticking one clears
|
|
240
|
+
the other. -->
|
|
241
|
+
<p
|
|
242
|
+
v-if="view.awaiting && !view.multiSelect"
|
|
243
|
+
id="binary-candidates-pick-one"
|
|
244
|
+
class="mb-3 text-xs text-muted"
|
|
245
|
+
>
|
|
246
|
+
{{ t('binaryCandidates.pickOne') }}
|
|
247
|
+
</p>
|
|
238
248
|
<div v-for="group in view.groups" :key="group.subject ?? '·'" class="mb-6">
|
|
239
249
|
<h3 class="mb-2 text-xs font-medium text-muted">
|
|
240
250
|
{{ group.subject ?? t('binaryCandidates.unlabelledSubject') }}
|
|
@@ -243,7 +253,7 @@ const { requestClose } = useUnsavedGuard({
|
|
|
243
253
|
<div
|
|
244
254
|
v-for="row in group.rows"
|
|
245
255
|
:key="row.id"
|
|
246
|
-
class="rounded border p-2 transition"
|
|
256
|
+
class="rounded-sm border p-2 transition"
|
|
247
257
|
:class="[
|
|
248
258
|
selected.includes(row.id) || row.kept
|
|
249
259
|
? 'border-app-info-400/60 bg-app-info-500/5'
|
|
@@ -255,33 +265,34 @@ const { requestClose } = useUnsavedGuard({
|
|
|
255
265
|
>
|
|
256
266
|
<!-- The REAL control, not the card's click handler: ticking a candidate is the only
|
|
257
267
|
way to keep an asset, so without a focusable input the gate could not be completed
|
|
258
|
-
by keyboard at all (UX-80).
|
|
259
|
-
|
|
260
|
-
group.
|
|
268
|
+
by keyboard at all (UX-80). In single-select mode `toggle` replaces the selection
|
|
269
|
+
across every subject rather than per group.
|
|
261
270
|
|
|
262
|
-
`@click.stop` belongs on the
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
271
|
+
`@click.stop` belongs on the WRAPPER, which is what the card's own toggle has to
|
|
272
|
+
be shielded from, and NOT on the control: activating a label forwards a synthetic
|
|
273
|
+
click to its control (which `.stop` there does not prevent, only its
|
|
274
|
+
propagation), so a click on the label TEXT bubbles to the card and toggles, then
|
|
275
|
+
the forwarded click toggles back. On a checkbox that nets to no change, which
|
|
276
|
+
means no re-render, which leaves the box ticked over a candidate that is no
|
|
277
|
+
longer selected. UCheckbox is not the place for it either: Nuxt UI forwards
|
|
278
|
+
`$attrs` onto the inner checkbox button rather than onto its root, so a listener
|
|
279
|
+
put there is the same "on the control alone" mistake spelled differently. -->
|
|
280
|
+
<!-- One control either way. Single-select is a checkbox rather than a radio group,
|
|
281
|
+
because the rows render per candidate across several subjects and a radio group
|
|
282
|
+
needs the whole set in one place. So the box announces as a checkbox, and the
|
|
283
|
+
"pick one" line above is its description: that is the cue that ticking it clears
|
|
284
|
+
the previous pick. -->
|
|
285
|
+
<div v-if="view.awaiting" class="mb-1.5" @click.stop>
|
|
286
|
+
<UCheckbox
|
|
287
|
+
size="xs"
|
|
288
|
+
:model-value="selected.includes(row.id)"
|
|
289
|
+
:label="candidateLabel(row)"
|
|
279
290
|
:aria-label="candidateLabel(row)"
|
|
291
|
+
:aria-describedby="view.multiSelect ? undefined : 'binary-candidates-pick-one'"
|
|
280
292
|
data-testid="binary-candidate-select"
|
|
281
|
-
@
|
|
293
|
+
@update:model-value="toggle(row.id)"
|
|
282
294
|
/>
|
|
283
|
-
|
|
284
|
-
</label>
|
|
295
|
+
</div>
|
|
285
296
|
<!-- Staged through the platform's OWN asset storage: we hold the bytes, so the card
|
|
286
297
|
renders them (and offers to open or save one) rather than waiting for a public
|
|
287
298
|
link the shipped storage never issues. Checked first because a candidate can
|
|
@@ -297,14 +308,14 @@ const { requestClose } = useUnsavedGuard({
|
|
|
297
308
|
v-else-if="row.previewUrl"
|
|
298
309
|
:src="row.previewUrl"
|
|
299
310
|
:alt="row.label ?? row.id"
|
|
300
|
-
class="mb-2 max-h-56 w-full rounded object-contain"
|
|
311
|
+
class="mb-2 max-h-56 w-full rounded-sm object-contain"
|
|
301
312
|
data-testid="binary-candidate-preview"
|
|
302
313
|
/>
|
|
303
314
|
<!-- No preview is ORDINARY (a private asset store issues no link), so it is stated
|
|
304
315
|
rather than left as an empty frame the reader reads as a failed generation. -->
|
|
305
316
|
<p
|
|
306
317
|
v-else
|
|
307
|
-
class="mb-2 flex h-24 items-center justify-center rounded bg-elevated/60 px-2 text-center text-
|
|
318
|
+
class="mb-2 flex h-24 items-center justify-center rounded-sm bg-elevated/60 px-2 text-center text-3xs text-muted"
|
|
308
319
|
data-testid="binary-candidate-no-preview"
|
|
309
320
|
>
|
|
310
321
|
{{ t('binaryCandidates.noPreview') }}
|
|
@@ -316,10 +327,10 @@ const { requestClose } = useUnsavedGuard({
|
|
|
316
327
|
: t('binaryCandidates.unattributed')
|
|
317
328
|
}}
|
|
318
329
|
</p>
|
|
319
|
-
<p v-if="row.note" class="mt-1 text-
|
|
320
|
-
<p class="mt-1 break-all text-
|
|
321
|
-
<p v-if="row.contentType" class="text-
|
|
322
|
-
<p v-if="row.kept" class="mt-1 text-
|
|
330
|
+
<p v-if="row.note" class="mt-1 text-2xs text-muted">{{ row.note }}</p>
|
|
331
|
+
<p class="mt-1 break-all text-3xs text-dimmed">{{ row.location }}</p>
|
|
332
|
+
<p v-if="row.contentType" class="text-3xs text-dimmed">{{ row.contentType }}</p>
|
|
333
|
+
<p v-if="row.kept" class="mt-1 text-2xs text-app-success-300">
|
|
323
334
|
{{
|
|
324
335
|
row.storeAs
|
|
325
336
|
? t('binaryCandidates.keptAs', { id: row.storeAs })
|
|
@@ -352,19 +363,19 @@ const { requestClose } = useUnsavedGuard({
|
|
|
352
363
|
/>
|
|
353
364
|
<p
|
|
354
365
|
v-if="missingAliases.length"
|
|
355
|
-
class="mt-1 text-
|
|
366
|
+
class="mt-1 text-2xs text-app-warning-300"
|
|
356
367
|
data-testid="binary-candidates-missing-alias"
|
|
357
368
|
>
|
|
358
369
|
{{ t('binaryCandidates.missingAlias') }}
|
|
359
370
|
</p>
|
|
360
371
|
<p
|
|
361
372
|
v-else-if="duplicateAliases"
|
|
362
|
-
class="mt-1 text-
|
|
373
|
+
class="mt-1 text-2xs text-app-warning-300"
|
|
363
374
|
data-testid="binary-candidates-duplicate-alias"
|
|
364
375
|
>
|
|
365
376
|
{{ t('binaryCandidates.duplicateAlias') }}
|
|
366
377
|
</p>
|
|
367
|
-
<p v-if="candidates.error" class="mt-1 text-
|
|
378
|
+
<p v-if="candidates.error" class="mt-1 text-2xs text-app-error-300">
|
|
368
379
|
{{ candidates.error }}
|
|
369
380
|
</p>
|
|
370
381
|
<div class="mt-2 flex justify-end">
|
|
@@ -395,7 +406,7 @@ const { requestClose } = useUnsavedGuard({
|
|
|
395
406
|
>
|
|
396
407
|
<UIcon name="i-lucide-unlink" class="h-8 w-8 opacity-40" />
|
|
397
408
|
<p class="text-sm">{{ t('binaryCandidates.noRun.title') }}</p>
|
|
398
|
-
<p class="max-w-md text-
|
|
409
|
+
<p class="max-w-md text-2xs text-dimmed">{{ t('binaryCandidates.noRun.hint') }}</p>
|
|
399
410
|
</div>
|
|
400
411
|
<div
|
|
401
412
|
v-else-if="absence === 'loading'"
|
|
@@ -411,7 +422,7 @@ const { requestClose } = useUnsavedGuard({
|
|
|
411
422
|
>
|
|
412
423
|
<UIcon name="i-lucide-triangle-alert" class="h-8 w-8 text-app-warning-400/70" />
|
|
413
424
|
<p class="text-sm">{{ t('binaryCandidates.loadFailed') }}</p>
|
|
414
|
-
<p class="max-w-md break-words text-
|
|
425
|
+
<p class="max-w-md break-words text-2xs text-dimmed">{{ candidates.error }}</p>
|
|
415
426
|
<UButton
|
|
416
427
|
size="xs"
|
|
417
428
|
color="neutral"
|