diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 1c5921070..285b3d1f0 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -168,6 +168,12 @@ work extends shared components instead of creating new local variants. ## Extension Points 1. Add shared primitives in `frontend-modern/src/components/shared/` + Framed product table surfaces must consume the shared + `TableCard` primitive instead of composing page-local `Card` border, + background, and overflow classes. Feature owners may own the table data, + filters, columns, and row behavior, but the outer product-table frame and + its light/dark border treatment belong to frontend primitives so + Infrastructure, Workloads, Storage, and Recovery do not drift visually. Shared monitored-system warning primitives under that path must stay compact app-shell pointers into the owned Pulse Pro billing surface. The shared banner may announce posture and route to the relevant billing tab, but diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md index 797d7b2ea..5aa2d81a7 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -224,6 +224,11 @@ regression protection. the shared `.grouped-table-row` CSS contract in `frontend-modern/src/index.css`, rather than local `bg-surface-alt` or hover-fill variants that drift between pages. + Framed product-table cards on the same hot-path surfaces must consume the + frontend-primitives-owned `TableCard` wrapper so Workloads, + Infrastructure, Storage, and Recovery keep one border/background/overflow + frame instead of introducing page-local table shells with different + light/dark contrast. Infrastructure table responsive behavior belongs to that same hot-path owner. `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts` must derive column visibility from the measured table surface width, with diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 348353190..ab3ea6183 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -68,6 +68,11 @@ querying, and the operator-facing storage health presentation layer. 1. Add or change recovery-point persistence, rollups, or series derivation through `internal/recovery/` 2. Add or change recovery page UX through `frontend-modern/src/components/Recovery/` and keep canonical route/query/filter state ownership in `frontend-modern/src/features/recovery/useRecoverySurfaceState.ts` + Recovery table surfaces must consume the frontend-primitives-owned + `TableCard` frame for protected-item, recovery-event, and adjacent + table-fallback chrome. Storage/recovery may own table content, filters, + columns, and rows, but it must not fork border/background/overflow table + shells or reintroduce lighter open-sided Recovery table frames. Recovery inventory protection posture and recovery-event outcome filtering must stay separate in that owner: protected inventory uses the route-backed `state` query for health, stale, failed, warning, running, unknown, and diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 8643c795e..868639ca2 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -337,7 +337,11 @@ AI-only summary payloads, or page-local heuristics. plus `frontend-modern/src/components/Infrastructure/useInfrastructureSummaryState.ts` must consume that scope through the shared page/group/entity interaction contract rather than inventing infrastructure-local summary filters or - route-backed cluster hover state. Deliberate cluster focus must also stay + route-backed cluster hover state. Host and service infrastructure table + card frames must consume the frontend-primitives-owned `TableCard` wrapper; + unified-resource ownership remains on resource identity, grouping, and row + semantics rather than forking a table border/background shell. Deliberate + cluster focus must also stay on the canonical infrastructure route through the shared `summaryGroup` query state, so pinned scope is shareable, reversible, and owned by the same route-backed summary contract as row focus. Infrastructure must stay diff --git a/frontend-modern/src/__tests__/App.architecture.test.ts b/frontend-modern/src/__tests__/App.architecture.test.ts index 3f3143130..3ef4046df 100644 --- a/frontend-modern/src/__tests__/App.architecture.test.ts +++ b/frontend-modern/src/__tests__/App.architecture.test.ts @@ -122,6 +122,9 @@ describe('App architecture', () => { ); expect(appStylesSource).toContain('tr.grouped-table-row > td'); expect(appStylesSource).toContain('--color-grouped-table-row-bg'); + expect(appStylesSource).toContain('--color-grouped-table-row-bg: rgba(226, 232, 240, 0.72);'); + expect(appStylesSource).toContain('--color-grouped-table-row-bg: rgba(51, 65, 85, 0.58);'); + expect(appStylesSource).not.toContain('--color-grouped-table-row-bg: theme('); expect(appStylesSource).not.toContain('@keyframes pulse-brand-wordmark'); expect(appStylesSource).not.toContain('text-shadow'); expect(appLayoutSource).toContain("props.versionInfo()?.channel === 'rc'"); diff --git a/frontend-modern/src/components/Dashboard/DashboardWorkloadTable.tsx b/frontend-modern/src/components/Dashboard/DashboardWorkloadTable.tsx index 98c6190b9..a4f676ceb 100644 --- a/frontend-modern/src/components/Dashboard/DashboardWorkloadTable.tsx +++ b/frontend-modern/src/components/Dashboard/DashboardWorkloadTable.tsx @@ -1,9 +1,9 @@ import { For } from 'solid-js'; import { ComponentErrorBoundary } from '@/components/ErrorBoundary'; -import { Card } from '@/components/shared/Card'; import { SummaryTableCardHeader } from '@/components/shared/SummaryTableCardHeader'; import { Table } from '@/components/shared/Table'; +import { TableCard } from '@/components/shared/TableCard'; import { getGuestColumnWidthStyle } from './guestRowModel'; import type { DashboardState } from './useDashboardState'; @@ -60,10 +60,8 @@ export function DashboardWorkloadTable(props: DashboardWorkloadTableProps) { return ( - - + ); } diff --git a/frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx b/frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx index 083ac16c9..c2a02d5ba 100644 --- a/frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx +++ b/frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx @@ -794,6 +794,7 @@ describe('Dashboard performance contract', () => { expect(dashboardSource).toContain('clearPinnedSummaryScope={state.clearPinnedSummaryScope}'); expect(dashboardWorkloadTableSource).toContain('data-summary-clear-surface'); expect(dashboardWorkloadTableSource).toContain('data-testid="workloads-table-surface"'); + expect(dashboardWorkloadTableSource).toContain('TableCard'); expect(dashboardWorkloadTableSource).toContain('SummaryTableCardHeader'); expect(dashboardWorkloadTableSource).toContain('showClearSelection'); expect(dashboardWorkloadTableSource).toContain('clearPinnedSummaryScope'); diff --git a/frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx b/frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx index 883ad5428..dfabb92bb 100644 --- a/frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx +++ b/frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx @@ -3,9 +3,9 @@ import type { Component } from 'solid-js'; import type { Disk } from '@/types/api'; import { formatBytes, formatSpeed, formatUptime, normalizeDiskArray } from '@/utils/format'; import { formatTemperature } from '@/utils/temperature'; -import { Card } from '@/components/shared/Card'; import { getInteractiveGroupedTableRowClass } from '@/components/shared/groupedTableRowPresentation'; import { SummaryTableCardHeader } from '@/components/shared/SummaryTableCardHeader'; +import { TableCard } from '@/components/shared/TableCard'; import { Table, TableBody, @@ -63,7 +63,7 @@ export const UnifiedResourceHostTableCard: Component - + - + ); }; diff --git a/frontend-modern/src/components/Infrastructure/UnifiedResourceServiceInfrastructureCard.tsx b/frontend-modern/src/components/Infrastructure/UnifiedResourceServiceInfrastructureCard.tsx index 70e861bf5..fda003897 100644 --- a/frontend-modern/src/components/Infrastructure/UnifiedResourceServiceInfrastructureCard.tsx +++ b/frontend-modern/src/components/Infrastructure/UnifiedResourceServiceInfrastructureCard.tsx @@ -1,7 +1,7 @@ import { Show } from 'solid-js'; import type { Component } from 'solid-js'; -import { Card } from '@/components/shared/Card'; import { SummaryTableCardHeader } from '@/components/shared/SummaryTableCardHeader'; +import { TableCard } from '@/components/shared/TableCard'; import { type UnifiedResourceTableProps, type UnifiedResourceTableState, @@ -21,7 +21,7 @@ export const UnifiedResourceServiceInfrastructureCard: Component< return ( 0 || table.sortedPMGResources().length > 0}> - + - + ); }; diff --git a/frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx index 02fc881cc..11adc58ce 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx +++ b/frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx @@ -517,10 +517,12 @@ describe('UnifiedResourceTable performance contract', () => { expect(unifiedResourceHostTableCardSource).not.toContain('kind="scope"'); expect(unifiedResourceHostTableCardSource).toContain('getInteractiveGroupedTableRowClass'); expect(unifiedResourceHostTableCardSource).not.toContain('cursor-pointer bg-surface-alt'); + expect(unifiedResourceHostTableCardSource).toContain('TableCard'); expect(unifiedResourceHostTableCardSource).toContain('SummaryTableCardHeader'); expect(unifiedResourceHostTableCardSource).toContain( 'onClear={tableProps.clearPinnedSummaryScope}', ); + expect(unifiedResourceServiceInfrastructureCardSource).toContain('TableCard'); expect(unifiedResourceServiceInfrastructureCardSource).toContain('SummaryTableCardHeader'); expect(unifiedResourceServiceInfrastructureCardSource).toContain('showClearAction'); expect(unifiedResourceServiceInfrastructureCardSource).toContain( diff --git a/frontend-modern/src/components/Recovery/Recovery.tsx b/frontend-modern/src/components/Recovery/Recovery.tsx index 15bf3098d..149ff090c 100644 --- a/frontend-modern/src/components/Recovery/Recovery.tsx +++ b/frontend-modern/src/components/Recovery/Recovery.tsx @@ -5,10 +5,10 @@ import { RecoveryActivitySection } from '@/components/Recovery/RecoveryActivityS import { RecoveryHistorySection } from '@/components/Recovery/RecoveryHistorySection'; import { RecoveryProtectedInventorySection } from '@/components/Recovery/RecoveryProtectedInventorySection'; import { RecoverySummary } from '@/components/Recovery/RecoverySummary'; -import { Card } from '@/components/shared/Card'; import { EmptyState } from '@/components/shared/EmptyState'; import { PageHeader } from '@/components/shared/PageHeader'; import { Subtabs } from '@/components/shared/Subtabs'; +import { TableCard } from '@/components/shared/TableCard'; import { useRecoverySurfaceState } from '@/features/recovery/useRecoverySurfaceState'; import type { ProtectedStateFilter } from '@/features/recovery/useRecoverySurfaceState'; import { useBreakpoint } from '@/hooks/useBreakpoint'; @@ -669,11 +669,7 @@ const Recovery: Component = () => { {eventsActivity()} - +
{ )} />
-
+
diff --git a/frontend-modern/src/components/Recovery/RecoveryHistorySection.tsx b/frontend-modern/src/components/Recovery/RecoveryHistorySection.tsx index 051320a53..d156eed0a 100644 --- a/frontend-modern/src/components/Recovery/RecoveryHistorySection.tsx +++ b/frontend-modern/src/components/Recovery/RecoveryHistorySection.tsx @@ -12,6 +12,7 @@ import { } from '@/components/shared/FilterToolbar'; import { PageControls } from '@/components/shared/PageControls'; import { SearchInput } from '@/components/shared/SearchInput'; +import { TableCard } from '@/components/shared/TableCard'; import type { ColumnDef } from '@/hooks/useColumnVisibility'; import { STORAGE_KEYS } from '@/utils/localStorage'; import type { RecoveryOutcome, RecoveryPoint } from '@/types/recovery'; @@ -469,7 +470,7 @@ export const RecoveryHistorySection: Component = (p - + = (p toggleSelectedPoint={toggleSelectedPoint} totalPages={props.totalPages} /> - + ); }; diff --git a/frontend-modern/src/components/Recovery/RecoveryProtectedInventorySection.tsx b/frontend-modern/src/components/Recovery/RecoveryProtectedInventorySection.tsx index c8743227c..eb9be2657 100644 --- a/frontend-modern/src/components/Recovery/RecoveryProtectedInventorySection.tsx +++ b/frontend-modern/src/components/Recovery/RecoveryProtectedInventorySection.tsx @@ -8,7 +8,15 @@ import { PageControls } from '@/components/shared/PageControls'; import { SearchInput } from '@/components/shared/SearchInput'; import { StatusDot } from '@/components/shared/StatusDot'; import { getSourcePlatformBadge } from '@/components/shared/sourcePlatformBadges'; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/shared/Table'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/shared/Table'; +import { TableCard } from '@/components/shared/TableCard'; import { STORAGE_KEYS } from '@/utils/localStorage'; import { formatAbsoluteTime, formatRelativeTime } from '@/utils/format'; import type { ProtectionRollup } from '@/types/recovery'; @@ -122,7 +130,8 @@ const getProtectionInsight = ( if (inventoryStatus === 'failed') return 'Latest protection event failed; open event details'; if (inventoryStatus === 'warning') return 'Latest event completed with warnings; review details'; - if (inventoryStatus === 'running') return 'Protection event in progress; check events for completion'; + if (inventoryStatus === 'running') + return 'Protection event in progress; check events for completion'; return ''; }; @@ -174,8 +183,12 @@ export const RecoveryProtectedInventorySection: Component< return multiplier * leftLabel.localeCompare(rightLabel); } case 'type': { - const leftType = getRecoveryItemTypePresentation(getRecoveryRollupItemTypeKey(left))?.label.toLowerCase(); - const rightType = getRecoveryItemTypePresentation(getRecoveryRollupItemTypeKey(right))?.label.toLowerCase(); + const leftType = getRecoveryItemTypePresentation( + getRecoveryRollupItemTypeKey(left), + )?.label.toLowerCase(); + const rightType = getRecoveryItemTypePresentation( + getRecoveryRollupItemTypeKey(right), + )?.label.toLowerCase(); return multiplier * (leftType || '').localeCompare(rightType || ''); } case 'platform': { @@ -204,9 +217,7 @@ export const RecoveryProtectedInventorySection: Component< const leftTimestamp = getRecoveryRollupTimestampMs(left); const rightTimestamp = getRecoveryRollupTimestampMs(right); const naturalTieBreak = - leftPriority === 4 - ? leftTimestamp - rightTimestamp - : rightTimestamp - leftTimestamp; + leftPriority === 4 ? leftTimestamp - rightTimestamp : rightTimestamp - leftTimestamp; if (naturalTieBreak !== 0) return multiplier * naturalTieBreak; const leftOutcome = normalizeRecoveryOutcome(left.lastOutcome); @@ -337,9 +348,7 @@ export const RecoveryProtectedInventorySection: Component< {(state) => ( )} @@ -349,7 +358,7 @@ export const RecoveryProtectedInventorySection: Component< - +
0}>
- - - - {( - [ - ['item', getRecoveryArtifactColumnLabel('item', 'Item')], - ['type', 'Item Type'], - ['platform', getRecoveryArtifactColumnLabel('platform', 'Platform')], - ['lastBackup', 'Latest Point'], - ['outcome', 'Protection State'], - ] as const - ).map(([column, label]) => ( - - ))} - - - - - {(rollup) => { - const resourceIndex = props.resourcesById(); - const label = getRecoveryRollupItemLabel(rollup, resourceIndex); - const secondaryLabel = getRecoveryRollupItemSecondaryLabel(rollup); - const attemptMs = rollup.lastAttemptAt ? Date.parse(rollup.lastAttemptAt) : 0; - const successMs = rollup.lastSuccessAt ? Date.parse(rollup.lastSuccessAt) : 0; - const inventoryStatus = getRecoveryRollupInventoryStatus(rollup); - const inventoryStatusLabel = getRecoveryRollupInventoryStatusLabel(inventoryStatus); - const platforms = getRecoveryRollupPlatforms(rollup) - .map((platform) => String(platform || '').trim()) - .filter(Boolean) - .sort((left, right) => - getSourcePlatformLabel(left).localeCompare(getSourcePlatformLabel(right)), - ); - const itemTypePresentation = - getRecoveryItemTypePresentation(getRecoveryRollupItemTypeKey(rollup)) || null; - const nowMs = Date.now(); - const neverSucceeded = inventoryStatus === 'never-succeeded'; - const protectionInsight = getProtectionInsight(rollup, inventoryStatus, nowMs); - - return ( - props.onSelectRollup(rollup.rollupId)} +
+ + + {( + [ + ['item', getRecoveryArtifactColumnLabel('item', 'Item')], + ['type', 'Item Type'], + ['platform', getRecoveryArtifactColumnLabel('platform', 'Platform')], + ['lastBackup', 'Latest Point'], + ['outcome', 'Protection State'], + ] as const + ).map(([column, label]) => ( + + ))} + + + + + {(rollup) => { + const resourceIndex = props.resourcesById(); + const label = getRecoveryRollupItemLabel(rollup, resourceIndex); + const secondaryLabel = getRecoveryRollupItemSecondaryLabel(rollup); + const attemptMs = rollup.lastAttemptAt ? Date.parse(rollup.lastAttemptAt) : 0; + const successMs = rollup.lastSuccessAt ? Date.parse(rollup.lastSuccessAt) : 0; + const inventoryStatus = getRecoveryRollupInventoryStatus(rollup); + const inventoryStatusLabel = + getRecoveryRollupInventoryStatusLabel(inventoryStatus); + const platforms = getRecoveryRollupPlatforms(rollup) + .map((platform) => String(platform || '').trim()) + .filter(Boolean) + .sort((left, right) => + getSourcePlatformLabel(left).localeCompare(getSourcePlatformLabel(right)), + ); + const itemTypePresentation = + getRecoveryItemTypePresentation(getRecoveryRollupItemTypeKey(rollup)) || null; + const nowMs = Date.now(); + const neverSucceeded = inventoryStatus === 'never-succeeded'; + const protectionInsight = getProtectionInsight(rollup, inventoryStatus, nowMs); + + return ( + props.onSelectRollup(rollup.rollupId)} > -
-
- -
- {label} - - - {secondaryLabel} + +
+
+ +
+ {label} + + + {secondaryLabel} + + +
+
+ +
+ {protectionInsight} +
+
+
+ + + {itemTypePresentation?.label} + 0}> + + {(platform) => { + const badge = getSourcePlatformBadge(platform); + return ( + + {badge?.label || getSourcePlatformLabel(platform)} + + ); + }} + +
- -
- {protectionInsight} -
-
-
- - - {itemTypePresentation?.label} - - - 0}> - - {(platform) => { - const badge = getSourcePlatformBadge(platform); - return ( - - {badge?.label || getSourcePlatformLabel(platform)} - - ); - }} - - -
-
- + - - - - - 0 - ? formatAbsoluteTime(successMs) - : attemptMs > 0 - ? formatAbsoluteTime(attemptMs) - : undefined - } - > -
- - {successMs > 0 ? ( - formatRelativeTime(successMs) - ) : neverSucceeded ? ( - never - ) : ( - '—' - )} - - 0}> - - Attempt {formatRelativeTime(attemptMs)} + } + > + + {itemTypePresentation?.label} -
-
+ - - +
+ + {(platform) => { + const badge = getSourcePlatformBadge(platform); + return ( + + {badge?.label || getSourcePlatformLabel(platform)} + + ); + }} + +
+
+ + 0 + ? formatAbsoluteTime(successMs) + : attemptMs > 0 + ? formatAbsoluteTime(attemptMs) + : undefined + } > - {inventoryStatusLabel} - - - - ); - }} - - -
-
-
-
- 0} - fallback={Showing 0 of 0 protected items} - > - Showing {sortedRollups().length} protected items - +
+ + {successMs > 0 ? ( + formatRelativeTime(successMs) + ) : neverSucceeded ? ( + + never + + ) : ( + '—' + )} + + 0}> + + Attempt {formatRelativeTime(attemptMs)} + + +
+ + + + + {inventoryStatusLabel} + + + + ); + }} + + + +
+
+
+ 0} + fallback={Showing 0 of 0 protected items} + > + Showing {sortedRollups().length} protected items + +
-
- +
); }; diff --git a/frontend-modern/src/components/Storage/StorageContentCard.tsx b/frontend-modern/src/components/Storage/StorageContentCard.tsx index 7d063b56b..c0112ae03 100644 --- a/frontend-modern/src/components/Storage/StorageContentCard.tsx +++ b/frontend-modern/src/components/Storage/StorageContentCard.tsx @@ -1,6 +1,6 @@ import { Component, Show } from 'solid-js'; -import { Card } from '@/components/shared/Card'; import { SummaryTableCardHeader } from '@/components/shared/SummaryTableCardHeader'; +import { TableCard } from '@/components/shared/TableCard'; import { DiskList } from '@/components/Storage/DiskList'; import StoragePoolsTable from '@/components/Storage/StoragePoolsTable'; import { STORAGE_CONTENT_CARD_BODY_CLASS } from '@/features/storageBackups/storagePagePresentation'; @@ -59,11 +59,8 @@ export const StorageContentCard: Component = (props) => Boolean(props.focusedSummaryGroupId() || props.expandedPoolId() || props.selectedDiskId()); return ( - @@ -115,7 +112,7 @@ export const StorageContentCard: Component = (props) => onHoverChange={props.setHoveredStorageResourceId} />
-
+ ); }; diff --git a/frontend-modern/src/components/shared/Card.tsx b/frontend-modern/src/components/shared/Card.tsx index 825e4cafa..390252148 100644 --- a/frontend-modern/src/components/shared/Card.tsx +++ b/frontend-modern/src/components/shared/Card.tsx @@ -3,7 +3,7 @@ import { JSX, splitProps, mergeProps } from 'solid-js'; type Tone = 'default' | 'muted' | 'info' | 'success' | 'warning' | 'danger' | 'card' | 'glass'; type Padding = 'none' | 'sm' | 'md' | 'lg'; -type CardProps = { +export type CardProps = { tone?: Tone; padding?: Padding; hoverable?: boolean; diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts index eb42bbf92..4e208185a 100644 --- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts +++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts @@ -64,6 +64,7 @@ import summaryRowActionButtonSource from '@/components/shared/SummaryRowActionBu import summaryInteractionA11ySource from '@/components/shared/summaryInteractionA11y.ts?raw'; import summaryTableCardHeaderSource from '@/components/shared/SummaryTableCardHeader.tsx?raw'; import summaryTableFocusSource from '@/components/shared/summaryTableFocus.ts?raw'; +import tableCardSource from '@/components/shared/TableCard.tsx?raw'; import groupedTableRowPresentationSource from '@/components/shared/groupedTableRowPresentation.ts?raw'; import infrastructureSummaryTableSource from '@/components/shared/InfrastructureSummaryTable.tsx?raw'; import infrastructureSummaryTableRowSource from '@/components/shared/InfrastructureSummaryTableRow.tsx?raw'; @@ -118,15 +119,20 @@ import dashboardSelectionStateSource from '@/components/Dashboard/useDashboardSe import infrastructureSummarySource from '@/components/Infrastructure/InfrastructureSummary.tsx?raw'; import infrastructureSummaryStateSource from '@/components/Infrastructure/useInfrastructureSummaryState.ts?raw'; import unifiedResourceHostTableCardSource from '@/components/Infrastructure/UnifiedResourceHostTableCard.tsx?raw'; +import unifiedResourceServiceInfrastructureCardSource from '@/components/Infrastructure/UnifiedResourceServiceInfrastructureCard.tsx?raw'; import unifiedResourcePBSTableSectionSource from '@/components/Infrastructure/UnifiedResourcePBSTableSection.tsx?raw'; import unifiedResourcePMGTableSectionSource from '@/components/Infrastructure/UnifiedResourcePMGTableSection.tsx?raw'; import nodeGroupHeaderSource from '@/components/shared/NodeGroupHeader.tsx?raw'; import storageGroupRowSource from '@/components/Storage/StorageGroupRow.tsx?raw'; import storageGroupPresentationSource from '@/features/storageBackups/groupPresentation.ts?raw'; import storagePoolRowSource from '@/components/Storage/StoragePoolRow.tsx?raw'; +import storageContentCardSource from '@/components/Storage/StorageContentCard.tsx?raw'; import diskListSource from '@/components/Storage/DiskList.tsx?raw'; import storageSummarySource from '@/components/Storage/StorageSummary.tsx?raw'; import workloadsSummarySource from '@/components/Workloads/WorkloadsSummary.tsx?raw'; +import recoveryComponentSource from '@/components/Recovery/Recovery.tsx?raw'; +import recoveryHistorySectionSource from '@/components/Recovery/RecoveryHistorySection.tsx?raw'; +import recoveryProtectedInventorySectionSource from '@/components/Recovery/RecoveryProtectedInventorySection.tsx?raw'; import recoveryTablePresentationSource from '@/utils/recoveryTablePresentation.ts?raw'; import resourceDetailDrawerOverviewSource from '@/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx?raw'; import aiSettingsDialogsSource from '@/components/Settings/AISettingsDialogs.tsx?raw'; @@ -278,6 +284,28 @@ describe('shared primitive guardrails', () => { expect(storagePoolRowSource).not.toContain('Clear selection'); }); + it('keeps framed product table surfaces on the shared TableCard owner', () => { + expect(tableCardSource).toContain("export const TABLE_CARD_FRAME_CLASS = 'overflow-hidden'"); + expect(tableCardSource).toContain("Omit"); + expect(tableCardSource).toContain('border={true}'); + expect(tableCardSource).toContain('padding="none"'); + expect(tableCardSource).toContain('tone="card"'); + expect(tableCardSource).toContain(' { expect(subtabsSource).not.toContain("variant?: 'default' | 'control'"); expect(subtabsSource).not.toContain('subtabsControlShellClass'); @@ -387,6 +415,13 @@ describe('shared primitive guardrails', () => { expect(frontendIndexCssSource).toContain('--color-summary-group-member-pinned-accent'); expect(frontendIndexCssSource).toContain('tr.grouped-table-row > td'); expect(frontendIndexCssSource).toContain('--color-grouped-table-row-bg'); + expect(frontendIndexCssSource).toContain( + '--color-grouped-table-row-bg: rgba(226, 232, 240, 0.72);', + ); + expect(frontendIndexCssSource).toContain( + '--color-grouped-table-row-bg: rgba(51, 65, 85, 0.58);', + ); + expect(frontendIndexCssSource).not.toContain('--color-grouped-table-row-bg: theme('); expect(groupedTableRowPresentationSource).toContain('GROUPED_TABLE_ROW_CLASS'); expect(groupedTableRowPresentationSource).not.toContain('GROUPED_TABLE_ROW_DIVIDER_CLASS'); diff --git a/frontend-modern/src/components/shared/TableCard.tsx b/frontend-modern/src/components/shared/TableCard.tsx new file mode 100644 index 000000000..bc60c4362 --- /dev/null +++ b/frontend-modern/src/components/shared/TableCard.tsx @@ -0,0 +1,23 @@ +import { splitProps } from 'solid-js'; + +import { Card, type CardProps } from '@/components/shared/Card'; + +export type TableCardProps = Omit; + +export const TABLE_CARD_FRAME_CLASS = 'overflow-hidden'; + +export function TableCard(props: TableCardProps) { + const [local, rest] = splitProps(props, ['class']); + + return ( + + ); +} + +export default TableCard; diff --git a/frontend-modern/src/index.css b/frontend-modern/src/index.css index 48a4a247e..507d2121d 100644 --- a/frontend-modern/src/index.css +++ b/frontend-modern/src/index.css @@ -85,9 +85,10 @@ --color-summary-group-member-pinned-bg: rgba(14, 165, 233, 0.04); --color-summary-group-member-pinned-bg-hover: rgba(14, 165, 233, 0.055); --color-summary-group-member-pinned-accent: rgba(14, 165, 233, 0.22); - --color-grouped-table-row-bg: theme('colors.slate.200'); - --color-grouped-table-row-bg-hover: theme('colors.slate.300'); - --color-grouped-table-row-border: theme('colors.slate.300'); + /* Shared product-table subgroup band. It should read as body segmentation, not table chrome. */ + --color-grouped-table-row-bg: rgba(226, 232, 240, 0.72); + --color-grouped-table-row-bg-hover: rgba(203, 213, 225, 0.82); + --color-grouped-table-row-border: rgba(203, 213, 225, 0.9); } .dark { @@ -110,9 +111,9 @@ --color-summary-group-member-pinned-bg: rgba(56, 189, 248, 0.07); --color-summary-group-member-pinned-bg-hover: rgba(56, 189, 248, 0.09); --color-summary-group-member-pinned-accent: rgba(56, 189, 248, 0.28); - --color-grouped-table-row-bg: theme('colors.slate.900'); - --color-grouped-table-row-bg-hover: theme('colors.slate.800'); - --color-grouped-table-row-border: theme('colors.slate.700'); + --color-grouped-table-row-bg: rgba(51, 65, 85, 0.58); + --color-grouped-table-row-bg-hover: rgba(51, 65, 85, 0.82); + --color-grouped-table-row-border: rgba(71, 85, 105, 0.85); } body { diff --git a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts index b96cdbbcb..10341ded1 100644 --- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts +++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts @@ -1107,8 +1107,17 @@ describe('frontend resource type boundaries', () => { "import { useRecoverySurfaceState } from '@/features/recovery/useRecoverySurfaceState';", ); expect(recoveryComponentSource).toContain('useRecoverySurfaceState'); + expect(recoveryComponentSource).toContain('TableCard'); expect(recoveryHistorySectionSource).toContain('useRecoveryHistorySectionState'); expect(recoveryHistorySectionSource).toContain('RecoveryHistoryTable'); + expect(recoveryHistorySectionSource).toContain('TableCard'); + expect(recoveryHistorySectionSource).not.toContain( + 'overflow-hidden border-border-subtle bg-surface', + ); + expect(recoveryProtectedInventorySectionSource).toContain('TableCard'); + expect(recoveryProtectedInventorySectionSource).not.toContain( + 'overflow-hidden border-border-subtle bg-surface', + ); expect(recoveryHistorySectionStateSource).toContain( 'export function useRecoveryHistorySectionState', ); @@ -2050,6 +2059,7 @@ describe('frontend resource type boundaries', () => { expect(storageContentCardSource).toContain('useStorageContentCardModel'); expect(storageContentCardSource).toContain('DiskList'); expect(storageContentCardSource).toContain('StoragePoolsTable'); + expect(storageContentCardSource).toContain('TableCard'); expect(storageContentCardSource).toContain('SummaryTableCardHeader'); expect(storageContentCardSource).toContain('STORAGE_CONTENT_CARD_BODY_CLASS'); expect(storageContentCardSource).not.toContain(