feat: add animated workspace identities
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s

This commit is contained in:
Jens
2026-07-26 02:31:58 +02:00
parent 5feaf76740
commit b65db69bfc
5 changed files with 285 additions and 1 deletions
+15
View File
@@ -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.
+1
View File
@@ -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
+3 -1
View File
@@ -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
</a>
<p className="sr-only" role="status" aria-live="polite">{workspaceStatusMessage}</p>
{errorMessage ? (
{errorMessage && activeWorkspace !== 'map' ? (
<div className="workspace-notice" role="alert">
<CircleAlert aria-hidden="true" />
<div>
@@ -862,6 +863,7 @@ function WorkbenchApp({ username, loggingOut, onLogout }: WorkbenchAppProps): JS
<p className="eyebrow">{selectedProject?.region ?? 'Belgie en Belgische Noordzee'}</p>
<h2>{activeWorkspaceItem.label}</h2>
</div>
<WorkspaceSignal workspace={activeWorkspace} />
<div className="workspace-heading-actions">
<p>{activeWorkspaceItem.description}</p>
{activeWorkspace !== 'overview' ? (
@@ -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 (
<div className={`workspace-signal workspace-signal-${workspace}`} aria-hidden="true">
<span className="workspace-signal-grid" />
<span className="workspace-signal-orbit workspace-signal-orbit-a" />
<span className="workspace-signal-orbit workspace-signal-orbit-b" />
<span className="workspace-signal-beam" />
<span className="workspace-signal-icon"><Icon /></span>
</div>
)
}
+226
View File
@@ -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 {