feat: add animated workspace identities
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user