diff --git a/docs/design/SETTINGS-INFORMATION-ARCHITECTURE.md b/docs/design/SETTINGS-INFORMATION-ARCHITECTURE.md new file mode 100644 index 00000000..38f776db --- /dev/null +++ b/docs/design/SETTINGS-INFORMATION-ARCHITECTURE.md @@ -0,0 +1,35 @@ +# Settings information architecture + +This document is the reviewed navigation and layout contract for Settings. It keeps every existing destination available while replacing the flat 20-item list with stable task-oriented families. + +## Navigation map + +| Family | Destinations | Purpose | +| ------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------ | +| Core | General, Board, Tasks, Agents, Data | Everyday product behavior, execution defaults, and operational data | +| Collaboration | Notifications, Multi-user, Workspaces, Delegation | People, communication, workspace membership, and delegated work | +| Automation | Scheduler, Queues, Reflections, Trackers | Recurring work, queue observation, learning loops, and external tracking | +| Governance | Security, Tool Policies, Enforcement, Shared Resources, Doc Freshness | Trust boundaries, policy, shared authority, and controlled knowledge | +| System | Maintenance, Manage | Installation health, recovery, and managed taxonomies | + +The family and destination order is canonical across desktop and compact navigation. Permission checks disable unavailable destinations without removing their location. `defaultTab` resolves only to an allowed destination and otherwise returns to the first allowed item. + +In Board Only mode, General, Board, and Tasks are marked as the primary path. Every other destination remains in its normal family and carries an Advanced label. This preserves discovery and administrative access without giving the full advanced inventory equal emphasis. + +## Layout contract + +- `SettingsPage` provides one page title, short purpose statement, optional page-level status or actions, a consistent content width, and the page spacing rhythm. +- `SettingsSection` provides the visible restart for one logical workflow: restrained border, neutral surface, heading, description, status, actions, and optional reset confirmation. Advanced and danger tones are semantic exceptions rather than decorative color. +- `SettingRow` uses one responsive label/description and control grid. At narrow widths the control stacks below the label; at desktop widths controls share a stable column. +- `SettingsFieldGrid` arranges related multi-field forms in one or two columns without horizontal scrolling. +- `SettingsStatusCard` uses labeled neutral, success, warning, or error state. Color is supplemental to the icon, title, and description. +- `SettingsLocalNav` is reserved for related sections on a long page. It uses in-page anchors, remains horizontally scrollable at narrow widths, and does not promote subsections into global destinations. +- `SettingsHelpText`, `SettingsErrorText`, `SettingsUnit`, and `SettingsActionGroup` standardize secondary explanation, validation, numeric units, routine transfer actions, and destructive actions. + +General, Board, Tasks, and Data are the reference migrations. Data demonstrates continuous local navigation; simpler pages omit it. Agents, Notifications, Multi-user, and Maintenance require dedicated workflow decomposition before migration because each combines several independent operational surfaces. + +## Focus and responsive behavior + +Changing a primary destination focuses and scrolls the page content to its heading. Arrow keys move through allowed destinations in canonical order. The sidebar scrolls independently at compact heights, while transfer and danger actions remain separated at its end. Compact layouts use the same grouped destination data in a select control. + +The dialog grows to a wider desktop maximum while retaining viewport bounds. Page content is capped at a readable width. Rows and field grids stack below the small-screen breakpoint, semantic text remains visible at increased text size, and no section requires horizontal page scrolling. diff --git a/web/src/__tests__/settings-dialog-mantine.test.tsx b/web/src/__tests__/settings-dialog-mantine.test.tsx index d54d7939..a48d90cb 100644 --- a/web/src/__tests__/settings-dialog-mantine.test.tsx +++ b/web/src/__tests__/settings-dialog-mantine.test.tsx @@ -1,12 +1,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { cleanup, fireEvent, screen } from '@testing-library/react'; -import { SettingsDialog } from '@/components/settings/SettingsDialog'; +import { SETTINGS_NAVIGATION_GROUPS, SettingsDialog } from '@/components/settings/SettingsDialog'; import { renderWithProviders } from './test-utils'; const mocks = vi.hoisted(() => ({ debouncedUpdate: vi.fn(), hasPermission: vi.fn(), toast: vi.fn(), + productMode: { selectedMode: 'advanced' as string }, })); vi.mock('@/hooks/useFeatureSettings', () => ({ @@ -22,6 +23,7 @@ vi.mock('@/hooks/useFeatureSettings', () => ({ docFreshness: {}, archive: {}, sharedResources: {}, + productMode: mocks.productMode, }, }), useDebouncedFeatureUpdate: () => ({ debouncedUpdate: mocks.debouncedUpdate }), @@ -98,6 +100,7 @@ vi.mock('@/components/settings/tabs/MultiUserTab', () => ({ describe('SettingsDialog Mantine shell', () => { beforeEach(() => { mocks.hasPermission.mockReturnValue(true); + mocks.productMode.selectedMode = 'advanced'; window.HTMLElement.prototype.scrollIntoView = vi.fn(); }); @@ -117,6 +120,42 @@ describe('SettingsDialog Mantine shell', () => { expect(baseElement.querySelector('.mantine-Select-root')).toBeDefined(); }); + it('groups every destination exactly once and separates destructive actions', async () => { + const { baseElement } = renderWithProviders(); + + await screen.findByText('General settings loaded'); + const tabIds = SETTINGS_NAVIGATION_GROUPS.flatMap((group) => group.tabs.map((tab) => tab.id)); + expect(tabIds).toHaveLength(20); + expect(new Set(tabIds).size).toBe(20); + expect(baseElement.querySelectorAll('[data-settings-nav-group]')).toHaveLength(5); + expect(baseElement.querySelector('[data-settings-actions="routine"]')).not.toBeNull(); + expect(baseElement.querySelector('[data-settings-actions="danger"]')).not.toBeNull(); + }); + + it('keeps advanced destinations visible but de-emphasized in Board Only mode', async () => { + mocks.productMode.selectedMode = 'board-only'; + renderWithProviders(); + + await screen.findByText('General settings loaded'); + expect(screen.getByText('Board Only')).toBeDefined(); + expect( + screen.getByRole('tab', { name: 'Board' }).getAttribute('data-board-only-priority') + ).toBe('primary'); + expect( + screen.getByRole('tab', { name: 'Agents' }).getAttribute('data-board-only-priority') + ).toBe('advanced'); + }); + + it('skips permission-disabled destinations during keyboard navigation', async () => { + mocks.hasPermission.mockImplementation((permission: string) => permission !== 'agent:read'); + renderWithProviders(); + + const generalTab = screen.getByRole('tab', { name: 'General' }); + expect(screen.getByRole('tab', { name: 'Agents' }).hasAttribute('disabled')).toBe(true); + fireEvent.keyDown(generalTab, { key: 'ArrowDown' }); + expect(await screen.findByText('Board settings loaded')).toBeDefined(); + }); + it('switches tab content through the Mantine sidebar buttons', async () => { renderWithProviders(); diff --git a/web/src/__tests__/settings-shared-mantine.test.tsx b/web/src/__tests__/settings-shared-mantine.test.tsx index ae8ade8a..c4d74860 100644 --- a/web/src/__tests__/settings-shared-mantine.test.tsx +++ b/web/src/__tests__/settings-shared-mantine.test.tsx @@ -1,6 +1,18 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { cleanup, fireEvent, screen } from '@testing-library/react'; -import { SectionHeader, SettingRow, ToggleRow } from '@/components/settings/shared'; +import { + SectionHeader, + SettingRow, + SettingsActionGroup, + SettingsErrorText, + SettingsFieldGrid, + SettingsHelpText, + SettingsLocalNav, + SettingsPage, + SettingsSection, + SettingsStatusCard, + ToggleRow, +} from '@/components/settings/shared'; import { renderWithProviders } from './test-utils'; describe('Settings shared Mantine rows', () => { @@ -8,7 +20,7 @@ describe('Settings shared Mantine rows', () => { cleanup(); }); - it('renders setting rows with direct Mantine layout and text primitives', () => { + it('renders responsive setting rows with a stable control column', () => { const { container } = renderWithProviders( Control @@ -17,10 +29,50 @@ describe('Settings shared Mantine rows', () => { expect(screen.getByText('Example setting')).toBeDefined(); expect(screen.getByText('Helpful context')).toBeDefined(); - expect(container.querySelector('.mantine-Group-root')).toBeDefined(); + expect(container.querySelector('[data-settings-row]')?.className).toContain('sm:grid-cols-'); expect(container.querySelector('.mantine-Text-root')).toBeDefined(); }); + it('provides the page, section, field-grid, and local-navigation layout contract', () => { + const { container } = renderWithProviders( + + + + + Field one + Field two + + + + ); + + expect(screen.getByRole('heading', { name: 'Example' })).toBeDefined(); + expect(screen.getByRole('heading', { name: 'Section one' })).toBeDefined(); + expect(screen.getByRole('navigation', { name: 'Example sections' })).toBeDefined(); + expect(screen.getByRole('link', { name: 'One' }).getAttribute('href')).toBe('#section-one'); + expect(container.querySelector('[data-settings-field-grid]')?.className).toContain( + 'sm:grid-cols-2' + ); + }); + + it('defines status, help, error, and destructive-action treatments', () => { + const { container } = renderWithProviders( + <> + + Helpful context + Fix this field + + + + + ); + + expect(container.querySelector('[data-settings-status="success"]')).not.toBeNull(); + expect(container.querySelector('[data-settings-help]')).not.toBeNull(); + expect(container.querySelector('[data-settings-error][role="alert"]')).not.toBeNull(); + expect(container.querySelector('[data-settings-actions="danger"]')).not.toBeNull(); + }); + it('renders toggles through Mantine Switch and preserves checked changes', () => { const onCheckedChange = vi.fn(); const { container } = renderWithProviders( diff --git a/web/src/components/settings/SettingsDialog.tsx b/web/src/components/settings/SettingsDialog.tsx index a15d7e32..f2e542af 100644 --- a/web/src/components/settings/SettingsDialog.tsx +++ b/web/src/components/settings/SettingsDialog.tsx @@ -1,5 +1,15 @@ import { useState, useRef, useCallback, lazy, Suspense, useEffect, useMemo } from 'react'; -import { Button, Group, Modal, ScrollArea, Select, Skeleton, Stack, Text } from '@mantine/core'; +import { + Badge, + Button, + Group, + Modal, + ScrollArea, + Select, + Skeleton, + Stack, + Text, +} from '@mantine/core'; import { useFeatureSettings, useDebouncedFeatureUpdate } from '@/hooks/useFeatureSettings'; import { useIdentity } from '@/hooks/useIdentity'; import { useToast } from '@/hooks/useToast'; @@ -30,7 +40,7 @@ import { } from 'lucide-react'; import { DEFAULT_FEATURE_SETTINGS } from '@veritas-kanban/shared'; import type { ClientAuthPermission } from '@veritas-kanban/shared'; -import { SettingsErrorBoundary } from './shared'; +import { SettingsActionGroup, SettingsErrorBoundary } from './shared'; import { ErrorBoundary } from '@/components/shared/ErrorBoundary'; // Lazy-load tab components @@ -133,61 +143,110 @@ interface TabDef { label: string; icon: React.ElementType; requiredPermission?: ClientAuthPermission; + boardOnlyPrimary?: boolean; } -const TABS: TabDef[] = [ - { id: 'general', label: 'General', icon: Settings2 }, - { id: 'board', label: 'Board', icon: Layout }, - { id: 'tasks', label: 'Tasks', icon: ListTodo }, - { id: 'agents', label: 'Agents', icon: Cpu, requiredPermission: 'agent:read' }, - { id: 'data', label: 'Data', icon: Database, requiredPermission: 'backup:read' }, - { id: 'notifications', label: 'Notifications', icon: Bell }, - { id: 'security', label: 'Security', icon: Shield, requiredPermission: 'settings:read' }, - { id: 'multi-user', label: 'Multi-user', icon: UserCog, requiredPermission: 'workspace:read' }, +interface NavigationGroup { + id: 'core' | 'collaboration' | 'automation' | 'governance' | 'system'; + label: string; + tabs: TabDef[]; +} + +export const SETTINGS_NAVIGATION_GROUPS: NavigationGroup[] = [ { - id: 'workspace-capabilities', - label: 'Workspaces', - icon: Network, - requiredPermission: 'workspace:read', + id: 'core', + label: 'Core', + tabs: [ + { id: 'general', label: 'General', icon: Settings2, boardOnlyPrimary: true }, + { id: 'board', label: 'Board', icon: Layout, boardOnlyPrimary: true }, + { id: 'tasks', label: 'Tasks', icon: ListTodo, boardOnlyPrimary: true }, + { id: 'agents', label: 'Agents', icon: Cpu, requiredPermission: 'agent:read' }, + { id: 'data', label: 'Data', icon: Database, requiredPermission: 'backup:read' }, + ], }, { - id: 'scheduler', - label: 'Scheduler', - icon: CalendarClock, - requiredPermission: 'workflow:read', + id: 'collaboration', + label: 'Collaboration', + tabs: [ + { id: 'notifications', label: 'Notifications', icon: Bell }, + { + id: 'multi-user', + label: 'Multi-user', + icon: UserCog, + requiredPermission: 'workspace:read', + }, + { + id: 'workspace-capabilities', + label: 'Workspaces', + icon: Network, + requiredPermission: 'workspace:read', + }, + { id: 'delegation', label: 'Delegation', icon: Plane, requiredPermission: 'agent:read' }, + ], }, { - id: 'queue-monitors', - label: 'Queues', - icon: ListChecks, - requiredPermission: 'workflow:read', + id: 'automation', + label: 'Automation', + tabs: [ + { + id: 'scheduler', + label: 'Scheduler', + icon: CalendarClock, + requiredPermission: 'workflow:read', + }, + { + id: 'queue-monitors', + label: 'Queues', + icon: ListChecks, + requiredPermission: 'workflow:read', + }, + { + id: 'reflections', + label: 'Reflections', + icon: BrainCircuit, + requiredPermission: 'workflow:read', + }, + { + id: 'trackers', + label: 'Trackers', + icon: Waypoints, + requiredPermission: 'settings:read', + }, + ], }, { - id: 'reflections', - label: 'Reflections', - icon: BrainCircuit, - requiredPermission: 'workflow:read', + id: 'governance', + label: 'Governance', + tabs: [ + { id: 'security', label: 'Security', icon: Shield, requiredPermission: 'settings:read' }, + { + id: 'tool-policies', + label: 'Tool Policies', + icon: Lock, + requiredPermission: 'policy:read', + }, + { + id: 'enforcement', + label: 'Enforcement', + icon: CheckCircle2, + requiredPermission: 'policy:read', + }, + { id: 'shared-resources', label: 'Shared Resources', icon: Boxes }, + { id: 'doc-freshness', label: 'Doc Freshness', icon: BookOpen }, + ], }, { - id: 'trackers', - label: 'Trackers', - icon: Waypoints, - requiredPermission: 'settings:read', + id: 'system', + label: 'System', + tabs: [ + { id: 'maintenance', label: 'Maintenance', icon: Wrench, requiredPermission: 'backup:read' }, + { id: 'manage', label: 'Manage', icon: Archive, requiredPermission: 'backup:read' }, + ], }, - { id: 'maintenance', label: 'Maintenance', icon: Wrench, requiredPermission: 'backup:read' }, - { id: 'delegation', label: 'Delegation', icon: Plane, requiredPermission: 'agent:read' }, - { id: 'tool-policies', label: 'Tool Policies', icon: Lock, requiredPermission: 'policy:read' }, - { - id: 'enforcement', - label: 'Enforcement', - icon: CheckCircle2, - requiredPermission: 'policy:read', - }, - { id: 'shared-resources', label: 'Shared Resources', icon: Boxes }, - { id: 'doc-freshness', label: 'Doc Freshness', icon: BookOpen }, - { id: 'manage', label: 'Manage', icon: Archive, requiredPermission: 'backup:read' }, ]; +const TABS = SETTINGS_NAVIGATION_GROUPS.flatMap((group) => group.tabs); + // ============ Settings Dialog Props ============ interface SettingsDialogProps { @@ -201,19 +260,24 @@ interface SettingsDialogProps { export function SettingsDialog({ open, onOpenChange, defaultTab }: SettingsDialogProps) { const [activeTab, setActiveTab] = useState('general'); const { hasPermission } = useIdentity(); + const { settings: currentSettings } = useFeatureSettings(); const canWriteSettings = hasPermission('settings:write'); const canUseTab = useCallback( (tab: TabDef) => !tab.requiredPermission || hasPermission(tab.requiredPermission), [hasPermission] ); + const isBoardOnly = currentSettings.productMode?.selectedMode === 'board-only'; const mobileTabOptions = useMemo( () => - TABS.map((tab) => ({ - value: tab.id, - label: tab.label, - disabled: !canUseTab(tab), + SETTINGS_NAVIGATION_GROUPS.map((group) => ({ + group: group.label, + items: group.tabs.map((tab) => ({ + value: tab.id, + label: `${tab.label}${isBoardOnly && !tab.boardOnlyPrimary ? ' · Advanced' : ''}`, + disabled: !canUseTab(tab), + })), })), - [canUseTab] + [canUseTab, isBoardOnly] ); // Set active tab when defaultTab changes @@ -230,7 +294,6 @@ export function SettingsDialog({ open, onOpenChange, defaultTab }: SettingsDialo setActiveTab(TABS.find(canUseTab)?.id ?? 'general'); } }, [activeTab, canUseTab]); - const { settings: currentSettings } = useFeatureSettings(); const { debouncedUpdate } = useDebouncedFeatureUpdate(); const settingsFileInputRef = useRef(null); const { toast } = useToast(); @@ -251,6 +314,7 @@ export function SettingsDialog({ open, onOpenChange, defaultTab }: SettingsDialo useEffect(() => { if (contentAreaRef.current) { contentAreaRef.current.focus(); + contentAreaRef.current.scrollIntoView({ block: 'start' }); } }, [activeTab]); @@ -522,7 +586,7 @@ export function SettingsDialog({ open, onOpenChange, defaultTab }: SettingsDialo opened={open} onClose={() => onOpenChange(false)} title={Settings} - size={800} + size={1040} padding={0} centered trapFocus @@ -545,9 +609,21 @@ export function SettingsDialog({ open, onOpenChange, defaultTab }: SettingsDialo onKeyDown={handleDialogKeyDown} > {/* Sidebar Tabs — hidden on narrow screens, shown as dropdown instead */} -
+
-

Settings

+ +

Settings

+ {isBoardOnly && ( + + Board Only + + )} +
+ + {isBoardOnly + ? 'Board essentials first. Advanced settings remain available.' + : 'Workspace configuration and governance.'} +
+ ))} {/* Import/Export/Reset */} - + - - - + + + + + + +
@@ -662,7 +781,7 @@ export function SettingsDialog({ open, onOpenChange, defaultTab }: SettingsDialo
- {unit && {unit}} + {unit && {unit}}
); @@ -107,7 +108,7 @@ export const NumberRow = memo(function NumberRow({ className="w-24" styles={{ input: { textAlign: 'right' } }} /> - {unit && {unit}} + {unit && {unit}} ); diff --git a/web/src/components/settings/shared/SectionHeader.tsx b/web/src/components/settings/shared/SectionHeader.tsx index af3eea98..06db98c0 100644 --- a/web/src/components/settings/shared/SectionHeader.tsx +++ b/web/src/components/settings/shared/SectionHeader.tsx @@ -1,8 +1,25 @@ import { useState } from 'react'; +import type { ReactNode } from 'react'; import { Button, Group, Modal, Stack, Text } from '@mantine/core'; import { RotateCcw } from 'lucide-react'; -export function SectionHeader({ title, onReset }: { title: string; onReset?: () => void }) { +export function SectionHeader({ + id, + title, + description, + status, + actions, + onReset, + contained = false, +}: { + id?: string; + title: string; + description?: string; + status?: ReactNode; + actions?: ReactNode; + onReset?: () => void; + contained?: boolean; +}) { const [resetOpen, setResetOpen] = useState(false); const handleReset = () => { @@ -11,41 +28,62 @@ export function SectionHeader({ title, onReset }: { title: string; onReset?: () }; return ( - - - {title} - - {onReset && ( - <> - - setResetOpen(false)} - title="Reset to defaults?" - centered - > - - - This will reset all {title.toLowerCase()} settings to their default values. - - - - - - - - + +
+ + + {title} + + {status} + + {description && ( + + {description} + + )} +
+ {(actions || onReset) && ( + + {actions} + {onReset && ( + <> + + setResetOpen(false)} + title="Reset to defaults?" + centered + > + + + This will reset all {title.toLowerCase()} settings to their default values. + + + + + + + + + )} + )}
); diff --git a/web/src/components/settings/shared/SettingRow.tsx b/web/src/components/settings/shared/SettingRow.tsx index 6b596fdd..069e5cb9 100644 --- a/web/src/components/settings/shared/SettingRow.tsx +++ b/web/src/components/settings/shared/SettingRow.tsx @@ -1,6 +1,6 @@ import { memo } from 'react'; import type { ReactNode } from 'react'; -import { Group, Stack, Text } from '@mantine/core'; +import { Stack, Text } from '@mantine/core'; export const SettingRow = memo(function SettingRow({ label, @@ -12,7 +12,10 @@ export const SettingRow = memo(function SettingRow({ children: ReactNode; }) { return ( - +
{label} @@ -23,7 +26,9 @@ export const SettingRow = memo(function SettingRow({ )} -
{children}
- +
+ {children} +
+
); }); diff --git a/web/src/components/settings/shared/SettingsLayout.tsx b/web/src/components/settings/shared/SettingsLayout.tsx new file mode 100644 index 00000000..e4e18205 --- /dev/null +++ b/web/src/components/settings/shared/SettingsLayout.tsx @@ -0,0 +1,208 @@ +import type { ReactNode } from 'react'; +import { Alert, Group, Paper, Stack, Text } from '@mantine/core'; +import { Info, TriangleAlert } from 'lucide-react'; +import { SectionHeader } from './SectionHeader'; + +export function SettingsPage({ + title, + description, + actions, + children, +}: { + title: string; + description: string; + actions?: ReactNode; + children: ReactNode; +}) { + return ( + + +
+ + {title} + + + {description} + +
+ {actions &&
{actions}
} +
+ {children} +
+ ); +} + +export function SettingsSection({ + id, + title, + description, + status, + actions, + onReset, + tone = 'default', + divided = false, + children, +}: { + id?: string; + title: string; + description?: string; + status?: ReactNode; + actions?: ReactNode; + onReset?: () => void; + tone?: 'default' | 'advanced' | 'danger'; + divided?: boolean; + children: ReactNode; +}) { + const headingId = id ? `${id}-heading` : undefined; + + return ( + + +
{children}
+
+ ); +} + +export function SettingsFieldGrid({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + +export function SettingsStatusCard({ + title, + description, + tone = 'neutral', + actions, +}: { + title: string; + description: string; + tone?: 'neutral' | 'success' | 'warning' | 'error'; + actions?: ReactNode; +}) { + const color = + tone === 'error' + ? 'red' + : tone === 'warning' + ? 'yellow' + : tone === 'success' + ? 'green' + : 'gray'; + const Icon = tone === 'warning' || tone === 'error' ? TriangleAlert : Info; + + return ( + + ); +} + +export function SettingsLocalNav({ + label, + items, +}: { + label: string; + items: Array<{ id: string; label: string }>; +}) { + return ( + + ); +} + +export function SettingsHelpText({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} + +export function SettingsErrorText({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} + +export function SettingsActionGroup({ + label, + tone = 'routine', + children, +}: { + label: string; + tone?: 'routine' | 'danger'; + children: ReactNode; +}) { + return ( + + + {label} + + {children} + + ); +} + +export function SettingsUnit({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} diff --git a/web/src/components/settings/shared/index.ts b/web/src/components/settings/shared/index.ts index af5f19e0..80dac368 100644 --- a/web/src/components/settings/shared/index.ts +++ b/web/src/components/settings/shared/index.ts @@ -4,3 +4,14 @@ export { NumberRow } from './NumberRow'; export { SectionHeader } from './SectionHeader'; export { SaveIndicator } from './SaveIndicator'; export { SettingsErrorBoundary } from './SettingsErrorBoundary'; +export { + SettingsActionGroup, + SettingsErrorText, + SettingsFieldGrid, + SettingsHelpText, + SettingsLocalNav, + SettingsPage, + SettingsSection, + SettingsStatusCard, + SettingsUnit, +} from './SettingsLayout'; diff --git a/web/src/components/settings/tabs/BoardTab.tsx b/web/src/components/settings/tabs/BoardTab.tsx index 4a460b48..9db06ab2 100644 --- a/web/src/components/settings/tabs/BoardTab.tsx +++ b/web/src/components/settings/tabs/BoardTab.tsx @@ -5,9 +5,10 @@ import { normalizeBoardColumns, normalizeBoardDefaultStatus, type BoardColumnConfig, + type BoardSettings, type DashboardWidgetSettings, } from '@veritas-kanban/shared'; -import { SettingRow, ToggleRow, SectionHeader, SaveIndicator } from '../shared'; +import { SettingRow, ToggleRow, SaveIndicator, SettingsPage, SettingsSection } from '../shared'; import { Plus, Trash2 } from 'lucide-react'; export function BoardTab() { @@ -17,7 +18,7 @@ export function BoardTab() { const columns = normalizeBoardColumns(boardSettings.columns); const defaultStatus = normalizeBoardDefaultStatus(boardSettings.defaultStatus, columns); - const update = (key: string, value: any) => { + const update = (key: K, value: BoardSettings[K]) => { debouncedUpdate({ board: { [key]: value } }); }; @@ -78,12 +79,18 @@ export function BoardTab() { }; return ( -
-
- - -
-
+ } + > + + updateDefaultRunBudget({ hardAction: value ?? 'require-approval' }) + } /> + + {defaultRunBudget?.hardAction === 'downgrade' && ( -