diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 6aa05b57..e33c02ab 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -11274,3 +11274,18 @@ Validation: - frontend production build: passed; - frontend dependency audit: zero vulnerabilities at the high threshold; - dedicated tests verify readiness derivation and workspace navigation. +## 2026-07-26 - Workspace identity and calm recovery states + +Implemented: + +- added a compact code-native signal illustration to every non-map workspace heading, with distinct restrained accents for data, assistant, quality, image analysis, downloads, status and administration; +- added topographic/radar depth to shared empty-result surfaces without changing their meaning or action hierarchy; +- suppressed the global floating error notice in the map workspace because the map already renders the same error with contextual recovery guidance; +- retained decorative semantics, keyboard behavior and reduced-motion fallbacks. + +Validation: + +- frontend unit tests: 38 passed across 11 files; +- frontend TypeScript check and production build: passed; +- frontend dependency audit: zero vulnerabilities at the high threshold; +- React review: named isolated component, derived configuration, no new state/effects, decorative content hidden from assistive technology. diff --git a/docs/TODO.md b/docs/TODO.md index 13a01efb..0c2a5034 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -3,6 +3,7 @@ ## Actieve post-RC datadekkingsfase - [x] Voeg een interactieve, data-gedreven projectatlas toe aan de statuswerkruimte met toegankelijke navigatie, echte readiness-toestanden en reduced-motion ondersteuning. +- [x] Geef alle primaire werkruimtes een eigen geanimeerde signaalillustratie, verbeter lege toestanden en verwijder de dubbele kaartfoutmelding bij sessieverval. - [x] Implementeer de aangeleverde Stitch-landingspagina als echte React- toegangspoort en bescherm de workbench met één veilig geconfigureerd diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c2ce8e1b..12d18106 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -32,6 +32,7 @@ import { type WorkspaceNavigationGroup, type WorkspaceNavigationItem, } from './components/shell/WorkbenchNavigation' +import { WorkspaceSignal } from './components/shell/WorkspaceSignal' import { useChangeDetectionWorkflow } from './hooks/useChangeDetectionWorkflow' import { useDemoWorkflow } from './hooks/useDemoWorkflow' import { useCoverageResolver } from './hooks/useCoverageResolver' @@ -790,7 +791,7 @@ function WorkbenchApp({ username, loggingOut, onLogout }: WorkbenchAppProps): JS Ga naar de werkruimte
{workspaceStatusMessage}
- {errorMessage ? ( + {errorMessage && activeWorkspace !== 'map' ? ({selectedProject?.region ?? 'Belgie en Belgische Noordzee'}
{activeWorkspaceItem.description}
{activeWorkspace !== 'overview' ? ( diff --git a/frontend/src/components/shell/WorkspaceSignal.tsx b/frontend/src/components/shell/WorkspaceSignal.tsx new file mode 100644 index 00000000..ec0f4211 --- /dev/null +++ b/frontend/src/components/shell/WorkspaceSignal.tsx @@ -0,0 +1,40 @@ +import { + Bot, + Database, + Download, + Gauge, + MapPinned, + ScanLine, + Settings2, + ShieldCheck, +} from 'lucide-react' +import type { WorkspaceKey } from '../overview/OverviewWorkspace' + +interface WorkspaceSignalProps { + workspace: WorkspaceKey +} + +const workspaceIcons = { + overview: Gauge, + data: Database, + map: MapPinned, + assistant: Bot, + analysis: ShieldCheck, + ai: ScanLine, + exports: Download, + system: Settings2, +} as const + +export function WorkspaceSignal({ workspace }: WorkspaceSignalProps): JSX.Element { + const Icon = workspaceIcons[workspace] + + return ( + + ) +} diff --git a/frontend/src/styles/atlas-premium-v2.css b/frontend/src/styles/atlas-premium-v2.css index dbab2af3..f7e35847 100644 --- a/frontend/src/styles/atlas-premium-v2.css +++ b/frontend/src/styles/atlas-premium-v2.css @@ -965,6 +965,232 @@ body { } } +/* Workspace identity illustrations */ + +.workspace-heading { + position: relative; + display: grid; + grid-template-columns: minmax(12rem, auto) 8.5rem minmax(18rem, 1fr); + gap: 1.25rem; + align-items: center; + overflow: hidden; + min-height: 7.25rem; + padding: 0.9rem 1.15rem; + border: 1px solid rgba(194, 210, 205, 0.72); + border-radius: var(--atlas-radius-lg); + background: + radial-gradient(circle at 45% 0%, rgba(44, 163, 141, 0.09), transparent 12rem), + rgba(255, 255, 255, 0.76); + box-shadow: var(--atlas-shadow-xs); +} + +.workspace-heading::after { + position: absolute; + inset: auto 0 0; + height: 1px; + background: linear-gradient(90deg, transparent, rgba(13, 102, 91, 0.35), transparent); + content: ""; +} + +.workspace-heading-copy, +.workspace-heading-actions { + position: relative; + z-index: 2; +} + +.workspace-heading-actions { + justify-self: end; +} + +.workspace-signal { + --signal-accent: #2b9a86; + position: relative; + justify-self: center; + width: 7.25rem; + height: 5.25rem; + overflow: hidden; + border: 1px solid rgba(13, 102, 91, 0.14); + border-radius: 1.1rem; + background: + radial-gradient(circle at 50% 45%, color-mix(in srgb, var(--signal-accent) 18%, transparent), transparent 2.6rem), + linear-gradient(145deg, rgba(255, 255, 255, 0.92), rgba(235, 245, 242, 0.82)); + box-shadow: inset 0 1px 0 #fff, 0 12px 25px rgba(8, 42, 38, 0.08); +} + +.workspace-signal-data { --signal-accent: #298873; } +.workspace-signal-assistant { --signal-accent: #7467bd; } +.workspace-signal-analysis { --signal-accent: #c18031; } +.workspace-signal-ai { --signal-accent: #397dae; } +.workspace-signal-exports { --signal-accent: #ad6d43; } +.workspace-signal-system { --signal-accent: #62766f; } + +.workspace-signal-grid { + position: absolute; + inset: 0; + opacity: 0.5; + background-image: + linear-gradient(rgba(18, 79, 70, 0.08) 1px, transparent 1px), + linear-gradient(90deg, rgba(18, 79, 70, 0.08) 1px, transparent 1px); + background-size: 15px 15px; + mask-image: linear-gradient(to bottom, black, transparent 92%); +} + +.workspace-signal-icon { + position: absolute; + z-index: 3; + top: 50%; + left: 50%; + display: grid; + width: 2.5rem; + height: 2.5rem; + place-items: center; + border: 1px solid color-mix(in srgb, var(--signal-accent) 28%, white); + border-radius: 0.8rem; + color: #fff; + background: linear-gradient(145deg, color-mix(in srgb, var(--signal-accent) 85%, white), var(--signal-accent)); + box-shadow: 0 8px 20px color-mix(in srgb, var(--signal-accent) 25%, transparent); + transform: translate(-50%, -50%); +} + +.workspace-signal-icon svg { + width: 1.18rem; + height: 1.18rem; +} + +.workspace-signal-orbit { + position: absolute; + top: 50%; + left: 50%; + border: 1px solid color-mix(in srgb, var(--signal-accent) 24%, transparent); + border-radius: 50%; + transform: translate(-50%, -50%) rotate(-18deg); +} + +.workspace-signal-orbit::after { + position: absolute; + top: -0.2rem; + left: 50%; + width: 0.38rem; + height: 0.38rem; + border-radius: 50%; + background: var(--signal-accent); + box-shadow: 0 0 0 0.22rem color-mix(in srgb, var(--signal-accent) 12%, transparent); + content: ""; +} + +.workspace-signal-orbit-a { + width: 5.4rem; + height: 2.35rem; + animation: workspace-orbit 8s linear infinite; +} + +.workspace-signal-orbit-b { + width: 3.8rem; + height: 4.4rem; + animation: workspace-orbit-reverse 11s linear infinite; +} + +.workspace-signal-beam { + position: absolute; + z-index: 1; + top: -1rem; + left: 50%; + width: 1px; + height: 7rem; + background: linear-gradient(transparent, color-mix(in srgb, var(--signal-accent) 50%, transparent), transparent); + transform: rotate(28deg); + animation: workspace-beam 5s ease-in-out infinite; +} + +.result-state-empty, +.map-empty-state, +.empty-state { + position: relative; + isolation: isolate; + overflow: hidden; +} + +.result-state-empty::after, +.map-empty-state::after, +.empty-state::after { + position: absolute; + z-index: -1; + right: -1.25rem; + bottom: -1.35rem; + width: 7.5rem; + height: 7.5rem; + border: 1px solid rgba(13, 102, 91, 0.11); + border-radius: 50%; + background: + radial-gradient(circle, rgba(13, 102, 91, 0.16) 0 3px, transparent 4px), + repeating-radial-gradient(circle, transparent 0 12px, rgba(13, 102, 91, 0.075) 13px 14px); + content: ""; + transform: rotate(-14deg); + transition: transform 400ms ease; +} + +.result-state-empty:hover::after, +.map-empty-state:hover::after, +.empty-state:hover::after { + transform: rotate(-4deg) scale(1.06); +} + +@keyframes workspace-orbit { + to { transform: translate(-50%, -50%) rotate(342deg); } +} + +@keyframes workspace-orbit-reverse { + to { transform: translate(-50%, -50%) rotate(-378deg); } +} + +@keyframes workspace-beam { + 0%, 100% { opacity: 0.15; transform: translateX(-2.4rem) rotate(28deg); } + 50% { opacity: 0.65; transform: translateX(2.4rem) rotate(28deg); } +} + +@media (max-width: 1040px) { + .workspace-heading { + grid-template-columns: minmax(12rem, 1fr) 7.25rem; + } + + .workspace-heading-actions { + grid-column: 1 / -1; + justify-self: stretch; + } +} + +@media (max-width: 680px) { + .workspace-heading { + grid-template-columns: minmax(0, 1fr) 4.75rem; + gap: 0.75rem; + min-height: 6rem; + padding: 0.8rem; + } + + .workspace-signal { + width: 4.7rem; + height: 4.1rem; + border-radius: 0.85rem; + } + + .workspace-signal-icon { + width: 2.1rem; + height: 2.1rem; + } + + .workspace-heading-actions { + display: flex; + align-items: center; + } +} + +@media (prefers-reduced-motion: reduce) { + .workspace-signal-orbit, + .workspace-signal-beam { + animation: none !important; + } +} + /* Interactive project atlas */ .project-atlas {