Merge branch 'Michael' into experimental
Deploy / Test, build & deploy (push) Successful in 2m3s
Details
Deploy / Test, build & deploy (push) Successful in 2m3s
Details
# Conflicts: # docs/IMPLEMENTATION_PLAN.md # src/components/ResizableResultTable/ResizableResultTable.tsx # src/styles/datahub-redesign.css # src/styles/datahub.css # src/styles/obsidian-graph.css # tests/features/datahub-centered-layout.test.ts # tests/features/obsidian-graph.test.ts
This commit is contained in:
commit
c179ac1b2c
|
|
@ -9,6 +9,7 @@ src-tauri/gen/
|
|||
*.tsbuildinfo
|
||||
vite.config.js
|
||||
vite.config.d.ts
|
||||
*.png
|
||||
|
||||
.env.postgres
|
||||
.assessor-data/
|
||||
|
|
|
|||
29
AGENTS.md
29
AGENTS.md
|
|
@ -20,6 +20,10 @@
|
|||
|
||||
## UI typography
|
||||
|
||||
- Use the locally bundled Inter variable font as the single application typeface. Do not introduce view-specific display, condensed, serif, or monospace families; hierarchy comes from size and weight.
|
||||
- Use only the visual weights Regular 400, Semibold 600, and Bold 700. Bold is reserved for branding and exceptional emphasis.
|
||||
- Use the semantic typography roles: H1 56px/600, H2 32px/600, H3 18px/600, H4 16px/600, body and table content 16px/400, labels 14px/600, metadata and table headers 13px, and buttons 14px/600.
|
||||
- Preserve normal title case for H1-H4, field labels, and buttons. Uppercase is limited to compact table headers, badges/categories, and the ASSESSOR wordmark.
|
||||
- Render continuous prose and other content-bearing text at no less than `16px`, including finding content, report text, Grid cell content, and Raw Data.
|
||||
- Compact interface text such as button captions, field labels, metadata, badges, and comparable control annotations may use a smaller type size.
|
||||
- When adding or changing styles, preserve this distinction instead of reducing content text to a compact UI-label token.
|
||||
|
|
@ -31,6 +35,31 @@
|
|||
- Apply the inset exactly once on the view's root content container. Split panes and edge-to-edge structures belong inside that inset and must not add a second outer inset.
|
||||
- Keep the 20px outer inset at responsive breakpoints unless the user explicitly changes this standard.
|
||||
|
||||
## UI segments and tiles
|
||||
|
||||
- Treat segments and tiles as a binding two-level visual grammar independent of their content. A segment is the rounded grouped background; a tile is a rounded, individually bordered element inside it.
|
||||
- Use only the shared `--segment-*` and `--tile-*` tokens for segment surfaces, borders, radii, insets, gaps, tile surfaces, shadows, widths, hover states, and selected contours. Do not create view-local substitute geometry.
|
||||
- Segments use the light neutral group surface, a fine border, `--segment-radius`, one uniform inset, and one uniform gap. Segments remain flat and do not receive a floating shadow.
|
||||
- Tiles use the common white or theme-equivalent surface, fine gray border, `--tile-radius`, and the single subtle `--tile-card-shadow`. Hover may change border and surface but must not lift or translate the tile.
|
||||
- Status color belongs only in a small accent line, marker, data visualization, or selected contour. Never fill the complete tile with a status color.
|
||||
- Selected tiles use the shared selected border/outline. Action tiles and add tiles use the same geometry as content tiles.
|
||||
- Keep unused segment space intentionally empty: lay tiles out from the start at their shared compact or content width and never stretch a partial final row merely to fill the container.
|
||||
- Tile content must never cross the tile boundary or cover a neighboring tile. Nothing may be clipped, clamped, truncated, or replaced by an ellipsis. Text and actions reflow inside the tile; media scales within the available inner area. If content does not fit, enlarge or reflow the tile.
|
||||
- Avoid line breaks in compact tile labels where practical. If wrapping is necessary, wrap only at word boundaries: never split ordinary words or numeric values across lines. Use a wider or responsive tile whenever a complete word or value cannot fit.
|
||||
|
||||
## UI donut charts
|
||||
|
||||
- Every donut chart uses the same full 360-degree geometry: a continuous neutral-gray track and a colored clockwise value arc starting at 12 o'clock.
|
||||
- Donut arcs use flat ends and the shared `--donut-ring-stroke`, `--donut-track`, value, and typography tokens. Do not introduce 270-degree gauges, open gaps, rounded arc caps, or local substitute geometry.
|
||||
- Place the primary numeric value itself on the exact visual center of the ring. Append a percent sign or other short unit to its right on the same baseline in the smaller shared UI size without letting that unit shift the number away from the center. Supporting counts may remain as subdued secondary text inside the ring, but every text element must stay fully within the clear inner area and must never touch or overlap the ring at any supported value width.
|
||||
- Multi-segment distributions retain their categorical segments, but use the same track, stroke proportion, start position, centered value treatment, and shared tokens as single-value donuts.
|
||||
|
||||
## UI branding position
|
||||
|
||||
- Use the Data Hub header as the binding position reference for the ASSESSOR logo, wordmark, and version.
|
||||
- Keep the brand container geometry identical across every application view; do not add area- or view-specific brand padding, margin, alignment, height, or transforms.
|
||||
- Switching views must never move the logo, wordmark, or version.
|
||||
|
||||
## Security and verification
|
||||
|
||||
- Preserve local-only behavior until a server transition is explicitly implemented and documented.
|
||||
|
|
|
|||
|
|
@ -1350,6 +1350,12 @@ Aktualisierung nach dem Schritt-11-Orchestrierungsvertikalschnitt vom 2026-08-04
|
|||
| Datum | Entscheidung | Begründung |
|
||||
|---|---|---|
|
||||
| 2026-08-24 | Die PostgreSQL-Zieladresse der Serverlaufzeit wird bei Einzelvariablen-Konfiguration aus `ASSESSOR_POSTGRES_HOST` gelesen; ohne Wert bleibt `127.0.0.1` der lokale Standard. Eine explizite `ASSESSOR_DATABASE_URL` behaelt Vorrang. | Container teilen keinen Loopback-Namensraum. Der Compose-Dienstname `postgres` muss deshalb bis in die Backend-Verbindungszeichenfolge gelangen, ohne den bestehenden lokalen Entwicklungsbetrieb zu brechen. |
|
||||
| 2026-08-24 | Segmente und Kacheln folgen anwendungsweit einer gemeinsamen visuellen Grammatik: flacher abgerundeter Segmentcontainer mit einheitlichem Inset und Gap; darin gleichartige abgerundete Kacheln mit feiner Kontur, dezentem gemeinsamem Schatten und fester Inhaltsbreite. Statusfarbe bleibt Akzent oder Auswahlkontur. Unvollstaendige Zeilen werden nicht gestreckt. Jede Kachel begrenzt ihren Inhalt; Titel, Werte, Labels, Zusatztexte, Medien und Aktionen duerfen weder abgeschnitten, geklemmt, ellipsiert oder verborgen werden noch die Kachelgrenze ueberschreiten oder Nachbarkacheln ueberdecken. Notwendige Umbrueche erfolgen ausschliesslich an Wortgrenzen, Zahlen werden nicht getrennt. | Ein gemeinsamer Vertrag verhindert springende Geometrie und ansichtsspezifische Sonderkarten. Die harte Inhaltsgrenze schuetzt Nachbarkacheln, waehrend vollstaendige Inhalte die fachliche Lesbarkeit erhalten. Wenn Inhalt nicht vollstaendig passt, wird er intern neu angeordnet oder die Kachel verbreitert beziehungsweise responsiv neu angeordnet. |
|
||||
| 2026-08-24 | Die neueste Nutzerkorrektur ersetzt fuer Finding-Titel die unmittelbar folgende fruehere 18-px-Reader-Entscheidung: Trefferlistentitel bleiben auf der 16-px-Inhaltsstufe, werden aber mit Medium 500 statt Semibold gesetzt; der Titel des geoeffneten Findings verwendet eine eigene 20-px-Detailtitelrolle mit Semibold. Die Finding-Abschnittsueberschrift bleibt 18 px, regulaere Matrixkacheln bleiben 16 px. | Die Trefferliste soll ruhig und scanbar bleiben, waehrend das geoeffnete Finding als primaerer Detailkontext klar hervortreten muss. Die getrennten semantischen Rollen loesen den Kontrast ohne die 16-px-Untergrenze fuer inhaltstragenden Text zu unterschreiten. |
|
||||
| 2026-08-24 | Finding-Titel verwenden innerhalb von Work eine kompakte semantische Hierarchie: Reader-Titel und Finding-Abschnitt nutzen H3 mit 18 px; Titel in Trefferlisten und regulaeren Matrixkacheln nutzen H4 mit 16 px. Die globale H1-/H2-Hierarchie und die eigenstaendige Pathfinder-Micro-Card bleiben unveraendert. | Lange Scannerbezeichnungen benoetigen in Listen und Kacheln sonst zu viele Zeilen und verdraengen Status sowie Metadaten. Die vorhandenen Typografietokens erhalten die Lesbarkeitsuntergrenze von 16 px fuer inhaltstragenden Text und vermeiden lokale Sondergroessen. |
|
||||
| 2026-08-24 | Alle Donutdiagramme verwenden einen gemeinsamen visuellen Vertrag: vollstaendige neutrale 360-Grad-Ringspur, farbiger Wertbogen im Uhrzeigersinn ab 12 Uhr, flache Bogenenden, gemeinsame Strichstaerke und eine mittige Primaerkennzahl mit kleiner Einheit auf derselben Grundlinie. Mehrteilige Verteilungen behalten ihre fachlichen Farbsegmente innerhalb derselben Geometrie. | Eine gemeinsame Form verhindert wechselnde Gauge-, Ring- und Prozentdarstellungen zwischen Datahub, Dashboard und Sidebar. Die neutrale Restspur macht den verbleibenden Anteil sichtbar; die zentrale Kennzahl bleibt unabhaengig von Kartenbreite und Legende unmittelbar lesbar. |
|
||||
| 2026-08-24 | ASSESSOR verwendet anwendungsweit die lokal gebuendelte variable Schriftfamilie Inter. Die verbindliche Hierarchie ist H1 56 px/600, H2 32 px/600, H3 18 px/600, H4 und Fliesstext 16 px mit 600 beziehungsweise 400, UI-Labels und Buttons 14 px, Metadaten und Tabellenkoepfe 13 px sowie kompakte Finding-Badges 12 px. Uppercase bleibt Tabellenkoepfen und kurzen technischen Kennzeichnungen vorbehalten; regulaere Ueberschriften und Feldbezeichnungen verwenden Title Case. | Eine lokal gebuendelte Einzelschrift verhindert plattformabhaengige Fallbacks und visuelles Springen. Semantische Rollen statt lokaler Einzelwerte schaffen nachvollziehbare Abstufungen, waehrend 16 px fuer inhaltstragenden Text die bestehende Lesbarkeitsgrenze bewahrt und dichte Finding-Zustandszeilen visuell hinter den Titel treten. |
|
||||
| 2026-08-24 | HTML-Berichte oberhalb von 1 MiB werden in Browser- und Backendlaufzeit direkt mit dem begrenzten lexikalischen Extraktor verarbeitet; kleinere Dokumente duerfen weiterhin den DOM-Adapter verwenden. Die isolierte Worker-Zeitgrenze von 30 Sekunden und alle bestehenden Ressourcenlimits bleiben unveraendert. | Scanner-HTML kann schon deutlich unter der bisherigen 4-MiB-Schwelle in eine DOM-Struktur expandieren, die das Worker-Zeitbudget ueberschreitet. Der vorhandene lineare Extraktor bewahrt Tabellen-, Locator-, Sicherheits- und Source-Extractor-Vertraege, ohne einen vollstaendigen DOM-Baum aufzubauen oder die Schutzgrenze aufzuweichen. |
|
||||
| 2026-08-23 | Im Pathfinder gerenderte Knoteninformationen sind das eigenstaendige Designelement `Pathfinder Micro Card` und keine allgemeine Matrix-/Finding-TileCard. Der Vertrag besitzt die Varianten `finding` und `relation`; Category bleibt eine groessere Relation-Auspragung. Bestaetigte Geometrie, Typografie und Radien sind eigene Tokens beziehungsweise Konstanten, und die gesamte Komposition skaliert unveraendert mit Faktor 0,4 im Graphkoordinatensystem. | Graphkarten beschriften Knoten und muessen Beziehungen sichtbar lassen. Eine Uebernahme allgemeiner Kartenmasse oder nachtraegliche kamerainverse Typografie wuerde die bestaetigte Darstellung verdecken beziehungsweise ihre inneren Proportionen veraendern. Ein eigener Vertrag erlaubt gezielte Regressionen, ohne regulaere TileCards ausserhalb des Pathfinders zu beeinflussen. |
|
||||
| 2026-08-23 | Scanner-Kontext wird zweistufig gespeichert: quellengetreue Reportabschnitte mit Prosa und Tabellen sowie typisierte EnvironmentObservations mit stabilen Schluesseln und explizitem Erfassungszustand. Findings bleiben ein getrennter Vertrag. | Nur die Rohstruktur verhindert Informationsverlust bei neuen PingCastle-Versionen; nur typisierte Fakten erlauben zugleich belastbare Dashboard-, Result-DB-, LLM- und Toolabfragen. Ein Zustandsfeld trennt echte Abwesenheit von leerer Ausgabe, fehlender Berechtigung, Lizenzgrenze und Parserluecke. |
|
||||
| 2026-08-23 | Typisierte Scanner-EnvironmentObservations werden als eigener Unified-Result-Quelltyp `environment-observation` projiziert und niemals als Finding umgedeutet. Die Projektion ist ein abgeleitetes Readmodell ueber die bestehende Environment-Persistenz; sie bewahrt Original-/Normalwert und vollstaendige Provenienz, bietet eigene Environment-Facetten und nimmt diese Zeilen aus Finding-spezifischen Facetten heraus. | Result DB, Exporte, LLMs und weitere Tools benoetigen einen gemeinsamen timeslotgebundenen Zugriff, ohne aus DFL, OS, PKI, Trusts oder Coverage kuenstliche Schweregrade, IOEs, Abweichungen oder Reviews abzuleiten. Ein Readmodell vermeidet Datenkopien und eine weitere Migration, waehrend der getrennte SourceKind den Vertrag eindeutig haelt. |
|
||||
|
|
@ -1605,6 +1611,13 @@ Diese Entscheidungen werden erst getroffen, wenn die jeweilige Phase ansteht ode
|
|||
| Risiko | Gegenmaßnahme |
|
||||
|---|---|
|
||||
| Die Compose-Konfiguration benennt den PostgreSQL-Dienst korrekt, waehrend das Backend den Host ignoriert und innerhalb des App-Containers gegen `127.0.0.1` verbindet; Build und Datenbank-Health bleiben dabei gruen, aber die App wird unhealthy. | `ASSESSOR_POSTGRES_HOST` in den Repository-Umgebungsvertrag und die Verbindungszeichenfolge aufnehmen, Loopback nur als lokalen Fallback verwenden und Docker-Hostname sowie lokalen Standard durch einen reinen Unit-Test absichern. |
|
||||
| Lokale Kartenraster koennen Kacheln wieder auf Restbreite strecken; lange Titel, Werte, Labels, Zusatztexte, Medien oder Aktionen koennen abgeschnitten, ellipsiert oder verborgen werden, aus einer Kachel herausragen, Nachbarkacheln ueberdecken oder durch aggressive Umbruchregeln mitten im Wort getrennt werden. | Gemeinsame Segment-/Kacheltokens, feste Startausrichtung und eine zuletzt geladene Kachel-Vertragsschicht verwenden. Woerter nur an Wortgrenzen umbrechen und Zahlen zusammenhalten. Bei Platzmangel Inhalt intern neu anordnen oder die Kachel verbreitern beziehungsweise responsiv neu anordnen; Clipping, Clamp, Ellipse und Truncation sind keine zulaessigen Rueckfaelle. Statische Regressionen sichern Vertrag, relevante Ansichten und Kaskadenreihenfolge. |
|
||||
| Trefferlistentitel und geoeffneter Finding-Titel koennen trotz unterschiedlicher Informationshierarchie optisch gleich laut oder in der falschen Reihenfolge erscheinen. | Trefferliste auf 16 px/500 und geoeffneten Detailtitel auf 20 px/600 ueber eigene zentrale Tokens binden; Finding-Abschnitt und Matrixkacheln getrennt regressionspruefen. |
|
||||
| Finding-Badges und Outcome-Texte koennen bei derselben Groesse wie regulaere Labels eine lange Titelzeile optisch ueberladen und MITRE-Zuordnungen unnoetig umbrechen. | Fuer Finding-Zustandszeilen ausschliesslich den zentralen 12-px-Badgetoken verwenden; Farbe, Glyph, Rahmen und feste Dimensionsreihenfolge bleiben als zusaetzliche Bedeutungstraeger erhalten. |
|
||||
| Lange Finding-Titel koennen bei Verwendung der globalen H2- oder allgemeinen Heading-Rolle Reader, Trefferlisten und Matrixkacheln visuell dominieren und Statusinformationen aus dem sichtbaren Bereich druecken. | Finding-spezifische Titelrollen regressionspruefen: Reader 18 px, Listen- und Matrixkacheln 16 px; Fliesstext bleibt mindestens 16 px. Globale Seitentitel und Pathfinder-Micro-Cards nicht ueber lokale Work-Regeln mitveraendern. |
|
||||
| Unabhaengige Donutimplementierungen koennen erneut in offene 270-Grad-Gauges, abweichende Ringspuren, runde Bogenenden oder unterschiedlich platzierte Prozentwerte auseinanderlaufen; mehrteilige Verteilungen koennten den gemeinsamen Standard umgehen. | Vollkreis, Startposition, flache Enden, neutrale Spur, Strichstaerke und zentrale Wertebehandlung ueber gemeinsame Donuttokens und Regressionen fuer Datahub-Karten, Sidebar, Dashboardstatus und mehrteilige Risiko-Verteilung absichern. Neue Donuts muessen denselben Vertrag verwenden. |
|
||||
| Lokale Systemschriften, konkurrierende Font-Stacks oder frei gesetzte Schriftgroessen koennen zwischen Betriebssystemen und Ansichten unterschiedliche Laufweiten, Zeilenumbrueche und Hierarchien erzeugen. | Inter als lokale WOFF2-Abhaengigkeit ohne CDN laden, den Font-Stack und alle semantischen Schriftrollen zentral tokenisieren und Import, Hierarchie, Mindestgroesse, erlaubte Grossschreibung sowie das Fehlen abweichender Display-Schriften regressionspruefen. |
|
||||
| Mittelgrosse, strukturell komplexe Scanner-HTML-Berichte koennen vor der groessenbasierten DOM-Umschaltung die isolierte 30-Sekunden-Parsergrenze erreichen; ein pauschal hoeheres Timeout wuerde problematische Eingaben nur laenger binden. | Berichte oberhalb von 1 MiB frueh in den bestehenden begrenzten lexikalischen Extraktor routen, Workerzeit-, Speicher-, Tabellen- und Zeilengrenzen beibehalten und den 1,6-MiB-PingCastle-Pfad mit synthetischer Tabelle sowie `html-bounded-extraction`-Diagnose regressionspruefen. |
|
||||
| Eine Anzeige der technischen Paketversion koennte faelschlich als aktueller fachlicher Verarbeitungsstand verstanden werden; eine lokal im React-Code hartcodierte Zahl koennte ausserdem vom Processingvertrag abweichen. | Die zentrale Konstante `CURRENT_ASSESSOR_PROCESSING_VERSION` aus dem Processingvertrag im Markenblock rendern, weder `v` noch `Version` voranstellen und Quelle, Wert, Position sowie kompakte Metadaten-Typografie regressionspruefen. |
|
||||
| Ein Scanner-Reimport koennte den alten Lauf vor erfolgreicher Validierung loeschen, den falschen Timeslot beziehungsweise ein anderes Werkzeug ersetzen oder alte und neue Ergebnisse gleichzeitig in Work und Result DB anzeigen. | Vorschau und Abschluss kryptografisch an dasselbe Projekt-, Assessment- und ScanRun-Ziel binden, das erkannte Werkzeug gegen den aktiven Vorgaenger pruefen und Supersession samt neuem kanonischem Lauf in einer Repositorymutation committen. Den alten Lauf nicht loeschen; aktive Read-Modelle filtern `supersededByScanRunId`, waehrend der Projektexport beide Laeufe und ihre Beziehung erhaelt. |
|
||||
| Ein Bericht desselben Werkzeugs mit gleichem Dateinamen oder Datum koennte faelschlich als Reimport akzeptiert werden; bei Mehrdatei-Reports koennte eine Teilmenge oder eine zusaetzliche Datei den falschen Bestand ersetzen. Ein Client koennte ausserdem eine reine Vorschaupruefung umgehen. | Anzahl und vollstaendiges SHA-256-Multiset der Originaldateien in Preview und Complete vergleichen und dieselbe Invariante in Local- und PostgreSQL-Repository sowie beim Uebernehmen bereits importierter Ersatzlaeufe erzwingen. Fehlende Originalhashes, Offline-Fallback und jede Abweichung fail closed ablehnen; der bestehende Scan bleibt aktiv. |
|
||||
|
|
@ -1783,6 +1796,15 @@ Diese Entscheidungen werden erst getroffen, wenn die jeweilige Phase ansteht ode
|
|||
|
||||
| Datum | Änderung |
|
||||
| 2026-08-24 | Phase-14-Containerverbindung korrigiert: Die Repository-Laufzeit uebernimmt `ASSESSOR_POSTGRES_HOST` in die PostgreSQL-Verbindungszeichenfolge, sodass der App-Container den Compose-Dienst `postgres` statt seines eigenen Loopbacks anspricht. `ASSESSOR_DATABASE_URL` behaelt Vorrang und `127.0.0.1` bleibt der lokale Standard. Docker-Hostname und lokaler Fallback sind synthetisch regressionsgeprueft; Gesamtlauf 735 bestanden, 8 optionale PostgreSQL-Tests uebersprungen, Local-only- und Designpruefung sowie Client-/Server-Produktionsbuild erfolgreich. Keine Schema- oder Datenmigration; Phase 14 bleibt offen. |
|
||||
| 2026-08-24 | Phase-7-Segment-/Kachelgrammatik nach Nutzerreferenz anwendungsweit umgesetzt und als Projektvorgabe verankert: gemeinsame Segment- und Kacheltokens fuer Flaeche, Kontur, Radius, Inset, Gap, Schatten, Auswahl und Breiten; flache Gruppencontainer; gleichartige Inhalts-, Aktions- und Hinzufuegekacheln; Statusfarben nur als Akzent; unvollstaendige Zeilen bleiben unstretched. Datahub, Dashboard, Sidebar, Work-TileGrid, DB Structure, Policy Analysis und statische Pathfinder-Karten verwenden den Vertrag. Die verbindliche Nutzerregel verbietet abgeschnittene, geklemmte, ellipsierte oder verborgene Titel, Werte, Labels, Zusatztexte und Aktionen. Dashboardstatuskarten wurden auf 290 beziehungsweise 210 px abgestimmt; Anzahl und Prozentwert stehen getrennt, sodass `Compliance` und Werte wie `453` vollstaendig bleiben. Das Donut-Raster nutzt auf Desktop sechs Spalten und die Matrix verteilt ihre feste Spaltenzahl ueber die volle Segmentbreite. Bei Platzmangel wird Inhalt neu angeordnet oder die Kachel verbreitert beziehungsweise responsiv umgebrochen. Sechzehn fokussierte Layout-/Donuttests und der Gesamtlauf mit 736 bestandenen sowie 8 uebersprungenen optionalen PostgreSQL-Tests, Local-only- und Designpruefung mit 0 Rohfarben sowie Client-/Server-Produktionsbuild sind erfolgreich. Keine Fachlogik-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. |
|
||||
| 2026-08-24 | Phase-7-Finding-Titelhierarchie nach visueller Nutzerkorrektur nachgeschaerft: Trefferlistentitel bleiben 16 px, verwenden aber Medium 500 statt Semibold; der geoeffnete Finding-Titel verwendet eine neue zentrale 20-px-Detailtitelrolle mit Semibold. Finding-Abschnitt 18 px, Matrixkacheln 16 px und die 16-px-Untergrenze fuer Inhalt bleiben unveraendert. Titel brechen nur an Wortgrenzen. Drei fokussierte Typografieregressionen sowie der Gesamtlauf mit 736 bestandenen und 8 uebersprungenen Tests, Local-only-/Designpruefung und Client-/Server-Produktionsbuild erfolgreich. Keine Fachlogik-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. |
|
||||
| 2026-08-24 | Phase-7-Finding-Labels nach Nutzerkorrektur verdichtet: Outcome (`Failed`/`Passed`), Weight, Severity, Category, Score, CIS- und MITRE-Zuordnungen verwenden in Reader und regulaeren Matrixkacheln nun einen zentralen 12-px-Badgetoken statt der bisherigen 13-px-Labelstufe. Farb-, Glyph-, Rahmen-, Reihenfolge- und Statussemantik bleiben unveraendert; inhaltstragender Text wird nicht verkleinert. Fuenf fokussierte Typografieregressionen, 736 Standardtests, Local-only-Pruefung, Designpruefer und vollstaendiger Client-/Serverbuild erfolgreich; 8 optionale PostgreSQL-Tests uebersprungen. Keine Daten-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. |
|
||||
| 2026-08-24 | Phase-7-Finding-Typografie nach Nutzerkorrektur verdichtet: Der Reader-Titel und die Finding-Abschnittsueberschrift verwenden jetzt die bestehende H3-Rolle mit 18 px statt H2 mit 32 px; Titel in Trefferlisten und regulaeren Matrixkacheln verwenden die H4-Rolle mit 16 px statt der bisherigen 18-px-Heading-Rolle. Lange Scannerbezeichnungen beanspruchen damit weniger Zeilen, waehrend inhaltstragender Text die verbindliche 16-px-Untergrenze behaelt. Die Aenderung betrifft ausschliesslich bestehende Work-Selektoren; globale Seitentitel, Pathfinder-Micro-Cards, Daten-, Import- und Persistenzlogik bleiben unveraendert. Zwei neue Typografieregressionen, 735 Standardtests, Local-only-Pruefung, Designpruefer und vollstaendiger Client-/Serverbuild erfolgreich; 8 optionale PostgreSQL-Tests uebersprungen. Phase 7 bleibt abgeschlossen. |
|
||||
| 2026-08-24 | Phase-7-Donut-Innenraum nach visueller Nutzerpruefung korrigiert: Zahl, Prozentzeichen und vorhandener Unterwert bleiben unveraendert im Ring, duerfen seine Kontur aber nicht beruehren. Die Zahl ist unabhaengig von der Einheit geometrisch zentriert; dreistellige Prozentwerte erhalten eine automatische Kompaktstufe. Fuer die kleineren Sidebar-Ringe sind Wert, Einheit und Einheitenabstand ueber eigene gemeinsame Tokens reduziert. 13 fokussierte Donut-, Sidebar- und Datahub-Regressionen, Local-only-Sicherheitspruefung, Designcheck mit 0 Rohfarben sowie Client-/Server-Produktionsbuild erfolgreich. Keine Daten-, Filter-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. |
|
||||
| 2026-08-24 | Phase-7-Donutstandard nach Nutzerreferenz vereinheitlicht und verbindlich dokumentiert: Datahub-Statuskarten und Sidebar-Severity-Karten verwenden statt offener 270-Grad-Gauges nun vollstaendige neutrale Ringspuren mit farbigem Uhrzeigersinnbogen ab 12 Uhr, flachen Enden und mittigem Prozentwert samt kleiner Einheit; der absolute Wert bleibt als dezente Sekundaerinformation erhalten. Dashboard-Statusringe verwenden dieselbe Spur und Strichstaerke. Die mehrteilige Risk-Severity-Verteilung behaelt ihre fachlichen Segmente, uebernimmt aber Geometrie, Spur, Strichstaerke, Startposition und zentrierte Prozentdarstellung. 730 Tests bestanden, 8 optionale PostgreSQL-Tests uebersprungen; Local-only-Sicherheitspruefung, Designcheck mit 0 Rohfarben sowie Client-/Server-Produktionsbuild erfolgreich. Keine Fachlogik-, Filter-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. |
|
||||
| 2026-08-24 | Phase-7-Typografiesystem vereinheitlicht und als verbindliche Projektvorgabe verankert: Inter Variable wird ausschliesslich lokal gebuendelt und ersetzt abweichende System- und Display-Schriften. Zentrale semantische Rollen definieren H1 bis H4, Fliesstext, UI-Labels, Metadaten, Tabellenkoepfe und Buttons; Uppercase ist auf Tabellenkoepfe und kurze technische Kennzeichnungen begrenzt. Datahub, Bereichsheader, Mandantendetail, Tabellen, Controls, Raw Data und Kennzahlen verwenden die gemeinsame Hierarchie. 730 Tests bestanden, 8 optionale PostgreSQL-Tests uebersprungen; Local-only-Sicherheitspruefung, Designcheck mit 0 Rohfarben sowie Client-/Server-Produktionsbuild erfolgreich. Keine Fachlogik-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. |
|
||||
| 2026-08-24 | Phase-7-Designkonflikte bereinigt: Mandantenkarten und die gemeinsam verwendeten Data-/Pathfinder-Tiles tragen weder regulaere noch Hover-Schatten und verwenden keinen Hover-Lift; Interaktion bleibt ueber Haarlinie und Hintergrund sichtbar. Redundante Pathfinder-Null-Ueberschreibungen wurden entfernt, Radius-/Control-Verweise auf vorhandene Tokens vereinheitlicht, die dynamische Dashboard-Segmentbreite erhielt einen CSS-Fallback und berechnete Result-Spaltenbreiten bleiben als erlaubte Geometrie gekapselt. Die Schattenobergrenze fuer `legacy-views.css` wurde von sechs auf drei abgesenkt. 728 Tests bestanden, 8 optionale PostgreSQL-Tests uebersprungen; Local-only-Sicherheitspruefung, vollstaendiger Designcheck mit 0 Rohfarben sowie Client-/Server-Produktionsbuild erfolgreich. Keine Fachlogik-, API-, Persistenz- oder Schemaaenderung; Phase 7 bleibt abgeschlossen. |
|
||||
| 2026-08-24 | Phase-13A-HTML-Import gegen den reproduzierten 30-Sekunden-Abbruch mittelgrosser Scannerberichte nachgehaertet: Die gemeinsame Browser-/Node-Umschaltung verwendet den bereits begrenzten lexikalischen HTML-Extraktor nun ab mehr als 1 MiB statt erst oberhalb von 4 MiB. Worker-Timeout und Ressourcenlimits bleiben unveraendert. Ein synthetischer 1,6-MiB-PingCastle-Bericht bestaetigt Format, Profil, Tabelleninhalt und `html-bounded-extraction`; 30 fokussierte Parser-/Worker-/PingCastle-Tests sowie vollstaendiger Client-/Server-Produktionsbuild erfolgreich. Keine Realdaten gelesen, keine API-, Persistenz-, Schema- oder Profilversionsaenderung; Phase 13A bleibt abgeschlossen. |
|
||||
| 2026-08-24 | Phase-7-Dashboard-IOE-Kennzahl als Filtereinstieg umgesetzt: `IOEs found` ist bei vorhandenem Trefferbestand anklickbar, setzt bestehende Dashboardfilter zurueck, uebergibt exakt die als explizite `IOE Found`/`IOEs found` gezaehlten Datensatz-IDs an die vorhandene Work-Teilmenge und oeffnet Work. Bei null Treffern bleibt das Steuerelement deaktiviert, damit kein ungefilterter Bestand erscheint. Zehn fokussierte Dashboard- und Filtertests sowie vollstaendiger Client-/Server-Produktionsbuild erfolgreich; keine Import-, API-, Persistenz- oder Schemaaenderung. |
|
||||
| 2026-08-24 | Phase-7-Dashboard-Ringspur nach Nutzerreferenz sichtbar gemacht: Der bislang mit dem Panel verschmelzende ungefuellte Anteil der oberen Statusringe verwendet nun den vorhandenen neutralgrauen Donut-Rahmentoken; Farbbogen, Geometrie, Werte und Interaktionen bleiben unveraendert. Sieben fokussierte Dashboardtests und vollstaendiger Client-/Server-Produktionsbuild erfolgreich; keine Import-, API-, Persistenz- oder Schemaaenderung. |
|
||||
| 2026-08-24 | Phase-7-Dashboard-Statusleiste nach Nutzerreferenz korrigiert: Die sechs gleichartigen Donutkarten sind durch eine fuenfteilige Leiste ersetzt. Compliance Score erhaelt den grossen Ring mit Formelhinweis; Deviation, Passed und Not Run verwenden kompakte Ringe mit realer Anzahl und Prozentwert. Not Applicable und Error stehen gemeinsam in einer Zusammenfassung neben `IOEs found`. Der IOE-Wert zaehlt ausschliesslich explizite `IOE Found`-/`IOEs found`-Quellenfelder und wird nicht aus Deviations abgeleitet; Statuskarten bleiben direkte Filtereinstiege in Work. 14 fokussierte Dashboard-, Donut- und Inset-Tests sowie vollstaendiger Client-/Server-Produktionsbuild erfolgreich. Der bekannte Designcheck bleibt ausschliesslich an den unveraenderten Schattenregeln in `datahub-redesign.css` Zeilen 39/42 rot; keine Import-, API-, Persistenz- oder Schemaaenderung. |
|
||||
|
|
@ -2057,4 +2079,3 @@ Diese Entscheidungen werden erst getroffen, wenn die jeweilige Phase ansteht ode
|
|||
| 2026-07-17 | Phase 1 begonnen: Format- und Fehlerfalltests erweitert, generische Fixture und Testbaseline ergänzt, Gesamtworkflow und Belastungsfälle automatisiert, leere Importe abgewiesen und englische Titelzuordnung korrigiert; Phase bleibt bis zum vollständigen Quellenkorpus in Arbeit. |
|
||||
| 2026-07-17 | Ersten versionierten Umsetzungsplan mit Zielarchitektur, Phasen, Abnahmekriterien, Entscheidungen und Risiken angelegt. |
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
"name": "assessor",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@fontsource-variable/inter": "^5.3.0",
|
||||
"jsdom": "^29.1.1",
|
||||
"pg": "^8.22.0",
|
||||
"react": "19.2.4",
|
||||
|
|
@ -961,6 +962,15 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@fontsource-variable/inter": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.3.0.tgz",
|
||||
"integrity": "sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==",
|
||||
"license": "OFL-1.1",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
"test:watch": "vitest --config vitest.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource-variable/inter": "^5.3.0",
|
||||
"jsdom": "^29.1.1",
|
||||
"pg": "^8.22.0",
|
||||
"react": "19.2.4",
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ function AreaIcon({ area }: { area: AppArea }) {
|
|||
const AREA_TOPBAR_TITLE: Record<AppArea, string> = {
|
||||
'db-structure': 'DB Structure',
|
||||
'data-hub': 'Data Hub',
|
||||
dashboard: 'Dashboard',
|
||||
dashboard: 'Client Info',
|
||||
graph: 'Pathfinder',
|
||||
work: 'Work',
|
||||
results: 'Result DB'
|
||||
|
|
@ -52,7 +52,7 @@ const AREA_TOPBAR_TITLE: Record<AppArea, string> = {
|
|||
const NAV_AREAS = [
|
||||
['db-structure', 'DB Structure'],
|
||||
['data-hub', 'Datahub'],
|
||||
['dashboard', 'Dashboard'],
|
||||
['dashboard', 'Client Info'],
|
||||
['work', 'Work'],
|
||||
['results', 'Result DB'],
|
||||
['graph', 'Pathfinder']
|
||||
|
|
|
|||
|
|
@ -37,21 +37,23 @@ export function DonutMetricCard({
|
|||
onClick
|
||||
}: DonutMetricCardProps) {
|
||||
const boundedPercentage = Math.min(100, Math.max(0, Math.round(percentage)));
|
||||
const arcLength = boundedPercentage * 0.75;
|
||||
const dashArray = String(arcLength) + ' ' + String(100 - arcLength);
|
||||
const dashArray = String(boundedPercentage) + ' ' + String(100 - boundedPercentage);
|
||||
const progressStyle = { '--donut-progress-dash': dashArray } as CSSProperties;
|
||||
const classes = ['donut-metric-card', 'donut-metric-card--' + tone, className].filter(Boolean).join(' ');
|
||||
const ariaLabel = label + ': ' + value.toLocaleString('en-US') + ', ' + boundedPercentage + '%';
|
||||
const content = <>
|
||||
<h3 className="donut-metric-card__title">{label}</h3>
|
||||
<div className="donut-metric-card__plot">
|
||||
<svg viewBox="0 0 200 200" aria-hidden="true">
|
||||
<circle className="donut-metric-card__track" cx="100" cy="100" r="78" pathLength="100" strokeDasharray="75 25" />
|
||||
<circle key={label + '-' + value + '-' + boundedPercentage} className="donut-metric-card__progress" cx="100" cy="100" r="78" pathLength="100" strokeDasharray={dashArray} style={progressStyle}/>
|
||||
<svg viewBox="0 0 120 120" aria-hidden="true">
|
||||
<circle className="donut-metric-card__track" cx="60" cy="60" r="48" pathLength="100" />
|
||||
<circle key={label + '-' + value + '-' + boundedPercentage} className="donut-metric-card__progress" cx="60" cy="60" r="48" pathLength="100" strokeDasharray={dashArray} style={progressStyle}/>
|
||||
</svg>
|
||||
<div className="donut-metric-card__values">
|
||||
<strong>{value.toLocaleString('en-US')}{valueSuffix}</strong>
|
||||
{showPercentage && <span>{boundedPercentage}%</span>}
|
||||
<div className={`donut-metric-card__primary${boundedPercentage >= 100 ? ' is-compact' : ''}`}>
|
||||
<strong>{showPercentage ? boundedPercentage : value.toLocaleString('en-US')}{showPercentage ? '' : valueSuffix}</strong>
|
||||
{showPercentage && <span>%</span>}
|
||||
</div>
|
||||
{showPercentage && <small>{value.toLocaleString('en-US')}{valueSuffix}</small>}
|
||||
</div>
|
||||
</div>
|
||||
</>;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ function clampColumnWidth(width: number): number {
|
|||
return Math.max(MIN_COLUMN_WIDTH, Math.min(MAX_COLUMN_WIDTH, Math.round(width)));
|
||||
}
|
||||
|
||||
function columnWidthStyle(width: number): CSSProperties {
|
||||
return { width: `${width}px` };
|
||||
}
|
||||
|
||||
function measuredColumnWidths(table: HTMLTableElement, columnCount: number): number[] {
|
||||
const headers = Array.from(table.tHead?.rows[0]?.cells ?? []);
|
||||
return Array.from({ length: columnCount }, (_, index) => clampColumnWidth(headers[index]?.getBoundingClientRect().width || MIN_COLUMN_WIDTH));
|
||||
|
|
@ -101,7 +105,7 @@ export function ResizableResultTable({ headers, children, enabled = false, class
|
|||
return (
|
||||
<table ref={tableRef} className={`${className}${enabled ? ' is-column-resizable' : ''}${appliedWidths.length ? ' has-custom-column-widths' : ''}`} style={tableStyle}>
|
||||
{enabled && <caption className="result-column-instructions">Column widths can be changed by dragging a header separator or fitted by double-clicking it.</caption>}
|
||||
{appliedWidths.length > 0 && <colgroup>{appliedWidths.map((width, index) => <col key={index} width={width} />)}</colgroup>}
|
||||
{appliedWidths.length > 0 && <colgroup>{appliedWidths.map((width, index) => <col key={index} style={columnWidthStyle(width)} />)}</colgroup>}
|
||||
<thead><tr>{headers.map((header, index) => <th key={index}>{header}{enabled && index < columnCount - 1 && <span
|
||||
className="result-column-resizer"
|
||||
role="separator"
|
||||
|
|
|
|||
|
|
@ -51,11 +51,11 @@ function DashboardStatusMetric({ label, value, percentage, tone, large = false,
|
|||
<circle className="dashboard-status-ring-track" cx="60" cy="60" r="48" pathLength="100" />
|
||||
<circle className="dashboard-status-ring-progress" cx="60" cy="60" r="48" pathLength="100" strokeDasharray={`${roundedPercentage} ${100 - roundedPercentage}`} />
|
||||
</svg>
|
||||
<span><strong>{roundedPercentage}</strong><small>%</small></span>
|
||||
<span><span className={`dashboard-donut-primary${roundedPercentage >= 100 ? ' is-compact' : ''}`}><strong>{roundedPercentage}</strong><small>%</small></span></span>
|
||||
</div>
|
||||
<div className="dashboard-status-copy">
|
||||
<span>{label}</span>
|
||||
<strong>{large ? `${roundedPercentage} %` : <>{value.toLocaleString('en-US')} <small>· {roundedPercentage}%</small></>}</strong>
|
||||
<strong>{large ? `${roundedPercentage} %` : <>{value.toLocaleString('en-US')}<small>{roundedPercentage}%</small></>}</strong>
|
||||
{helper && <small>{helper}</small>}
|
||||
</div>
|
||||
</>;
|
||||
|
|
@ -328,24 +328,27 @@ function RiskProfile({ sourceFacet, categoryFacet, severityFacet, records, profi
|
|||
) : <p className="dashboard-empty">No risk categories available.</p>}
|
||||
<div className="dashboard-risk-severity">
|
||||
{severities.length > 0 ? <>
|
||||
<svg className="dashboard-risk-donut" viewBox="0 0 120 120" role="img" aria-label="Risk severity distribution">
|
||||
<circle className="dashboard-risk-donut-track" cx="60" cy="60" r="48" pathLength="100" />
|
||||
{severities.map(severity => {
|
||||
const percentage = share(severity.count, severityTotal);
|
||||
const offset = severityOffset;
|
||||
severityOffset += percentage;
|
||||
return <circle
|
||||
className={`dashboard-risk-donut-segment severity-${severity.value}`}
|
||||
cx="60"
|
||||
cy="60"
|
||||
r="48"
|
||||
key={severity.value}
|
||||
pathLength="100"
|
||||
strokeDasharray={`${percentage} ${100 - percentage}`}
|
||||
strokeDashoffset={-offset}
|
||||
/>;
|
||||
})}
|
||||
</svg>
|
||||
<div className="dashboard-risk-donut" role="img" aria-label="Risk severity distribution: 100%">
|
||||
<svg viewBox="0 0 120 120" aria-hidden="true">
|
||||
<circle className="dashboard-risk-donut-track" cx="60" cy="60" r="48" pathLength="100" />
|
||||
{severities.map(severity => {
|
||||
const percentage = share(severity.count, severityTotal);
|
||||
const offset = severityOffset;
|
||||
severityOffset += percentage;
|
||||
return <circle
|
||||
className={`dashboard-risk-donut-segment severity-${severity.value}`}
|
||||
cx="60"
|
||||
cy="60"
|
||||
r="48"
|
||||
key={severity.value}
|
||||
pathLength="100"
|
||||
strokeDasharray={`${percentage} ${100 - percentage}`}
|
||||
strokeDashoffset={-offset}
|
||||
/>;
|
||||
})}
|
||||
</svg>
|
||||
<span><span className="dashboard-donut-primary is-compact"><strong>100</strong><small>%</small></span></span>
|
||||
</div>
|
||||
<ul className="dashboard-risk-legend">
|
||||
{severities.map(severity => (
|
||||
<li key={severity.value}>
|
||||
|
|
|
|||
|
|
@ -34,10 +34,10 @@ import { toolIdForRecord } from '../../importers/status-normalization';
|
|||
* zweizeilige Titel bleibt durch den Overscan unbemerkbar.
|
||||
*/
|
||||
const RECORD_ROW_HEIGHT = 78;
|
||||
/** Mindestbreite einer Kachel, siehe `.tile-grid` in legacy-views.css. */
|
||||
const TILE_MIN_WIDTH = 280;
|
||||
/** `gap` von `.tile-grid` (--panel-inset-Token, 20px). */
|
||||
const TILE_GAP = 20;
|
||||
/** Verbindliche Inhaltskachelbreite, siehe `--tile-content-width`. */
|
||||
const TILE_MIN_WIDTH = 320;
|
||||
/** Einheitlicher Segmentabstand, siehe `--segment-gap`. */
|
||||
const TILE_GAP = 8;
|
||||
/** `padding` von `.tile-grid` (--panel-inset-Token, 20px je Seite). */
|
||||
const TILE_GRID_INSET = 20;
|
||||
/** Geschaetzte Kachelzeilenhoehe inklusive Zeilenabstand, aus demselben Grund
|
||||
|
|
@ -316,7 +316,7 @@ export function DatasetViewer() {
|
|||
// literalen Inline-Style-Objekts, damit die Darstellung sonst in
|
||||
// src/styles bleibt (siehe FloatingResultsWindow.tsx fuer denselben
|
||||
// Kompromiss bei einer anderen laufzeitberechneten Position).
|
||||
const tileGridStyle = { gridTemplateColumns: `repeat(${tileColumns}, 1fr)` };
|
||||
const tileGridStyle = { gridTemplateColumns: `repeat(${tileColumns}, minmax(var(--tile-content-width), 1fr))` };
|
||||
const tileTopSpacerStyle = { gridColumn: '1 / -1', height: tileWindow.paddingTop };
|
||||
const tileBottomSpacerStyle = { gridColumn: '1 / -1', height: tileWindow.paddingBottom };
|
||||
const recordTopSpacerStyle = { height: recordWindow.paddingTop };
|
||||
|
|
@ -332,7 +332,7 @@ export function DatasetViewer() {
|
|||
{!records.length ? (
|
||||
<EmptyWorkspace showOpenData={false} />
|
||||
) : view === 'columns' ? (
|
||||
<div className="viewer-scroll" ref={setTileScrollNode}><div className="tile-grid" style={tileGridStyle}>
|
||||
<div className="viewer-scroll tile-view-scroll" ref={setTileScrollNode}><div className="tile-grid" style={tileGridStyle}>
|
||||
{tileWindow.paddingTop > 0 && <div aria-hidden="true" style={tileTopSpacerStyle} />}
|
||||
{records.slice(tileWindow.startIndex * tileColumns, tileWindow.endIndex * tileColumns).map(record => (
|
||||
<TileCard key={record.id} record={record} title={recordTitle(record)} bookmarked={Boolean(reviews[record.id]?.include)} resultColor={resultColor(record.id)} toolId={activeSheet?.toolId ?? dataset?.profileId} search={search} onOpen={() => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import type { DecodedHtmlDocument, DecodedHtmlTable } from './runtime-adapters';
|
||||
|
||||
const MAXIMUM_DOM_HTML_CHARACTERS = 4 * 1024 * 1024;
|
||||
// Real-world scanner reports can exceed the isolated worker's time budget well
|
||||
// before four MiB when their markup expands into a large DOM. Route reports
|
||||
// above one MiB through the bounded lexical parser, which preserves the table
|
||||
// and source-extractor contract without constructing the complete DOM tree.
|
||||
const MAXIMUM_DOM_HTML_CHARACTERS = 1 * 1024 * 1024;
|
||||
const MAXIMUM_EXTRACTED_TABLES = 10_001;
|
||||
const MAXIMUM_EXTRACTED_ROWS_PER_TABLE = 250_001;
|
||||
const MAXIMUM_EXTRACTED_ROWS_TOTAL = 500_001;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { StrictMode } from 'react';
|
|||
import { createRoot } from 'react-dom/client';
|
||||
import { App } from './app/App';
|
||||
import { AppProvider } from './app/AppProvider';
|
||||
import '@fontsource-variable/inter/wght.css';
|
||||
import './styles/layers.css';
|
||||
import './styles/ui-tokens.css';
|
||||
/* styles/ui-components.css wird absichtlich noch nicht global geladen.
|
||||
|
|
@ -25,6 +26,7 @@ import './styles/import-workflow.css';
|
|||
import './styles/legacy-views.css';
|
||||
import './styles/obsidian-graph.css';
|
||||
import './styles/functional-icons.css';
|
||||
import './styles/tile-contract.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
* { box-sizing: border-box; }
|
||||
html, body, #root { width: 100%; min-width: 0; min-height: 100%; margin: 0; }
|
||||
body { min-height: 100vh; overflow: hidden; color: var(--text); background: var(--bg); font: var(--ui-body-weight) var(--ui-body-size)/1.45 var(--font); }
|
||||
body { min-height: 100vh; overflow: hidden; color: var(--text); background: var(--bg); font: var(--fw-regular) var(--type-body-size)/1.5 var(--font-ui); }
|
||||
button, input, select, textarea { color: inherit; font: inherit; }
|
||||
button, select { cursor: pointer; }
|
||||
button:disabled, select:disabled { cursor: not-allowed; opacity: .42; }
|
||||
h1, h2, h3, h4, p { margin-top: 0; }
|
||||
h1 { font-size: var(--type-h1-size); font-weight: var(--fw-semibold); line-height: 1.1; letter-spacing: -.04em; }
|
||||
h2 { font-size: var(--type-h2-size); font-weight: var(--fw-semibold); line-height: 1.15; letter-spacing: -.03em; }
|
||||
h3 { font-size: var(--type-h3-size); font-weight: var(--fw-semibold); line-height: 1.25; letter-spacing: 0; }
|
||||
h4 { font-size: var(--type-h4-size); font-weight: var(--fw-semibold); line-height: 1.3; letter-spacing: 0; }
|
||||
svg { display: block; width: 17px; height: 17px; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { padding: 9px 10px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; overflow-wrap: anywhere; }
|
||||
th { position: sticky; top: 0; z-index: 1; color: var(--muted); background: var(--panel-soft); font-size: var(--ui-label-size); font-weight: var(--ui-label-weight); letter-spacing: var(--ui-label-tracking); text-transform: uppercase; }
|
||||
td { font-size: var(--type-body-size); font-weight: var(--fw-regular); }
|
||||
th { position: sticky; top: 0; z-index: 1; color: var(--muted); background: var(--panel-soft); font-size: var(--type-table-head-size); font-weight: var(--fw-semibold); letter-spacing: var(--ls-caps); text-transform: uppercase; }
|
||||
tbody tr:hover td { background: var(--panel-hover); }
|
||||
pre { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
mark { padding: 1px 2px; color: var(--state-active-text); background: var(--state-active); }
|
||||
|
|
@ -19,13 +24,13 @@ button, .btn, .icon-btn, .tab-btn, .bookmark-btn, .results-btn {
|
|||
border: 1px solid var(--button-border);
|
||||
color: var(--button-text);
|
||||
background: var(--button-bg);
|
||||
font-size: var(--ui-control-size);
|
||||
font-weight: 500;
|
||||
font-size: var(--type-button-size);
|
||||
font-weight: var(--fw-semibold);
|
||||
line-height: 1.15;
|
||||
}
|
||||
.select, .input {
|
||||
min-height: var(--control-height); border: var(--border-hair) solid var(--line); border-radius: 0;
|
||||
color: var(--text); background-color: var(--panel-raised); font-size: var(--ui-control-size); font-weight: 500;
|
||||
color: var(--text); background-color: var(--panel-raised); font-size: var(--type-label-size); font-weight: var(--fw-regular);
|
||||
}
|
||||
/* Verbindliche Buttonform: eine Hoehe, ein Radius, ein waagerechtes Polster.
|
||||
Senkrecht traegt der Knopf kein eigenes Polster - die Hoehe kommt aus
|
||||
|
|
@ -42,7 +47,7 @@ button, .btn, .icon-btn, .tab-btn, .bookmark-btn, .results-btn {
|
|||
border: 1px solid var(--button-border);
|
||||
color: var(--button-marked-text);
|
||||
background: var(--button-marked-bg);
|
||||
font-weight: 600;
|
||||
font-weight: var(--fw-semibold);
|
||||
}
|
||||
.btn.primary:hover, .btn--primary:hover, .btn.active:hover, .tab-btn.active:hover, .bookmark-btn[aria-pressed="true"]:hover, .icon-btn[aria-pressed="true"]:hover, .results-btn.is-open:hover {
|
||||
border-color: var(--button-border);
|
||||
|
|
|
|||
|
|
@ -13,13 +13,13 @@ mark, .search-highlight {
|
|||
.empty-brand-logo { position: relative; display: block; width: 82px; height: 82px; }
|
||||
.empty-brand-logo img { position: absolute; inset: 0; width: 82px; height: 82px; object-fit: contain; }
|
||||
.empty-brand-logo-light { opacity: 0; }
|
||||
.empty-brand-mark strong { color: var(--heading); font-size: clamp(34px, 4vw, 58px); font-weight: 750; line-height: 1; letter-spacing: .08em; }
|
||||
.empty-open-data { position: relative; display: flex; align-items: center; gap: 13px; width: min(310px, 100%); min-height: 50px; padding: 10px 20px; border: 0; border-radius: 0 var(--legacy-radius-md) var(--legacy-radius-md) 0; color: var(--text); background: var(--panel-raised); box-shadow: none; font-size: var(--ui-heading-size); font-weight: 750; text-align: left; }
|
||||
.empty-brand-mark strong { color: var(--heading); font-size: clamp(34px, 4vw, 58px); font-weight: var(--fw-bold); line-height: 1; letter-spacing: .08em; }
|
||||
.empty-open-data { position: relative; display: flex; align-items: center; gap: 13px; width: min(310px, 100%); min-height: 50px; padding: 10px 20px; border: 0; border-radius: 0 var(--legacy-radius-md) var(--legacy-radius-md) 0; color: var(--text); background: var(--panel-raised); box-shadow: none; font-size: var(--ui-heading-size); font-weight: var(--fw-semibold); text-align: left; }
|
||||
.empty-open-data::before { content: ''; position: absolute; inset: 0 auto 0 0; width: 4px; background: var(--legacy-accent); }
|
||||
.empty-open-data svg { flex: 0 0 25px; width: 25px; height: 25px; }
|
||||
.empty-open-data:hover { color: var(--heading); background: var(--panel-hover); }
|
||||
.empty-open-data:active { background: var(--panel-soft); }
|
||||
.empty-supported-formats { color: var(--muted); font-size: var(--ui-body-size); font-weight: 500; line-height: 1.8; letter-spacing: .025em; }
|
||||
.empty-supported-formats { color: var(--muted); font-size: var(--ui-body-size); font-weight: var(--fw-regular); line-height: 1.8; letter-spacing: .025em; }
|
||||
html[data-theme="light"] .empty-brand-logo-dark { opacity: 0; }
|
||||
html[data-theme="light"] .empty-brand-logo-light { opacity: 1; }
|
||||
/* Die drei Light-Sonderregeln fuer .empty-open-data sind entfallen: die
|
||||
|
|
@ -39,15 +39,15 @@ html[data-theme="light"] .empty-brand-logo-light { opacity: 1; }
|
|||
.record-list .finding-card.active, .record-list .finding-card.active:hover, .record-list .finding-card.active:focus-visible { z-index: 1; outline: 0; border-color: transparent; border-left: 0; color: var(--state-selected-flat-text); background: var(--state-selected-flat); box-shadow: none; }
|
||||
.record-list .finding-card.active h3, .record-list .finding-card.active .finding-card-status { color: var(--state-selected-flat-text); }
|
||||
.record-list .finding-card-actions, .record-list .finding-badges { display: none; }
|
||||
.record-list .finding-card h3 { margin-top: 1px; font-size: var(--ui-heading-size); font-weight: 700; line-height: 1.35; }
|
||||
.record-list .finding-card h3 { margin-top: 1px; font-size: var(--type-h4-size); font-weight: var(--fw-content-medium); line-height: 1.35; }
|
||||
.finding-card-status { display: none; }
|
||||
.record-list .finding-card-status { display: block; margin: 5px 0 0; color: var(--muted); font-size: var(--ui-control-size); line-height: 1.2; }
|
||||
.finding-hex { display: grid; place-items: center; width: 29px; height: 34px; margin-top: 1px; color: var(--sev-on); background: var(--severity, var(--muted)); clip-path: polygon(50% 0,94% 24%,94% 76%,50% 100%,6% 76%,6% 24%); font-size: var(--ui-control-size); font-weight: 900; }
|
||||
.finding-hex { display: grid; place-items: center; width: 29px; height: 34px; margin-top: 1px; color: var(--sev-on); background: var(--severity, var(--muted)); clip-path: polygon(50% 0,94% 24%,94% 76%,50% 100%,6% 76%,6% 24%); font-size: var(--ui-control-size); font-weight: var(--fw-bold); }
|
||||
.finding-hex > span { transform: translate(-3px, 1px); }
|
||||
.reader-title-hex { position: relative; display: block; width: 30px; height: 34px; margin-top: 1px; background: var(--severity, var(--muted)); clip-path: polygon(50% 0,94% 24%,94% 76%,50% 100%,6% 76%,6% 24%); }
|
||||
.reader-title-hex::after { content: ''; position: absolute; left: 2px; top: 14px; width: 8px; height: 4px; border-left: 2px solid var(--sev-on); border-bottom: 2px solid var(--sev-on); transform: rotate(-45deg); pointer-events: none; }
|
||||
.finding-card h3 { margin: 0; color: var(--heading); font-size: var(--ui-heading-size); font-weight: 650; line-height: 1.42; overflow-wrap: anywhere; }
|
||||
.workspace-label { display: inline-flex; align-items: center; justify-content: center; margin-left: 6px; padding: 1px 6px; border: 1px solid var(--border-default); border-radius: 0; color: var(--text-primary); background: var(--surface-input); font-size: var(--ui-label-size); font-weight: 700; font-variant-numeric: tabular-nums; vertical-align: middle; }
|
||||
.finding-card h3 { margin: 0; color: var(--heading); font-size: var(--type-h4-size); font-weight: var(--fw-semibold); line-height: 1.42; overflow-wrap: normal; word-break: normal; hyphens: manual; }
|
||||
.workspace-label { display: inline-flex; align-items: center; justify-content: center; margin-left: 6px; padding: 1px 6px; border: 1px solid var(--border-default); border-radius: 0; color: var(--text-primary); background: var(--surface-input); font-size: var(--ui-label-size); font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; vertical-align: middle; }
|
||||
.finding-badges, .mitre-line { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
|
||||
.finding-card-actions { display: grid; align-content: start; gap: 8px; }
|
||||
.bookmark-btn { color: var(--button-text); border: 1px solid var(--button-border); border-radius: var(--btn-radius); background: var(--button-bg); }
|
||||
|
|
@ -89,18 +89,18 @@ html[data-theme="light"] .empty-brand-logo-light { opacity: 1; }
|
|||
/* Der Fundname nutzt die volle Breite der Lesespalte; die Schaltflaechen
|
||||
ruecken in eine eigene Zeile darunter, statt den Titel zu beschneiden. */
|
||||
.reader-heading { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--legacy-space-3) 16px; align-items: start; padding-bottom: 28px; border-bottom: 1px solid var(--line); }
|
||||
.reader-heading h2 { margin: 0; color: var(--heading); font-size: var(--ui-reading-heading-size); font-weight: var(--ui-strong-weight); line-height: 1.3; letter-spacing: 0; }
|
||||
.reader-heading h2 { margin: 0; color: var(--heading); font-size: var(--type-detail-title-size); font-weight: var(--ui-strong-weight); line-height: 1.3; letter-spacing: 0; overflow-wrap: normal; word-break: normal; hyphens: manual; }
|
||||
.reader-heading-actions { display: flex; grid-column: 1 / -1; align-items: center; justify-content: flex-end; gap: 10px; align-self: center; }
|
||||
/* Der PingCastle-Score-Erlaeuterungsabsatz wurde samt Markup entfernt. */
|
||||
.bookmark-label { color: var(--text); font-size: var(--ui-control-size); font-weight: var(--ui-label-weight); }
|
||||
.results-detail-btn { min-width: 136px; }
|
||||
.dashboard-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin: 28px auto 24px; }
|
||||
.dashboard-grid label, .review-grid > label { display: grid; gap: 7px; color: var(--heading); font-size: var(--ui-value-size); font-weight: 750; letter-spacing: .035em; text-transform: uppercase; }
|
||||
.dashboard-grid label, .review-grid > label { display: grid; gap: 7px; color: var(--heading); font-size: var(--ui-value-size); font-weight: var(--fw-semibold); letter-spacing: 0; text-transform: none; }
|
||||
.dashboard-grid input, .dashboard-grid select, .review-grid textarea { width: 100%; min-height: 39px; padding: 8px 11px; border: var(--border-hair) solid var(--line-strong); border-radius: var(--legacy-radius-sm); color: var(--text); background-color: var(--surface-inset); text-transform: none; }
|
||||
.reader-summary, .review-grid, .all-fields { margin-top: 14px; padding: 20px 0; border: 0; border-bottom: 1px solid var(--line); background: transparent; }
|
||||
/* Traegt seit der Zusammenfuehrung mit "Check title" die Pruefbeschreibung der
|
||||
Regel, also einen Satz. Versalien waeren dafuer unlesbar. */
|
||||
.reader-summary h3 { margin-bottom: var(--legacy-space-5); color: var(--heading); font-size: var(--ui-reading-heading-size); text-transform: none; }
|
||||
.reader-summary h3 { margin-bottom: var(--legacy-space-5); color: var(--heading); font-size: var(--type-h3-size); text-transform: none; }
|
||||
.reader-summary h4 { margin: 0 0 7px; color: var(--heading); font-size: var(--ui-reading-size); text-transform: uppercase; }
|
||||
.reader-summary > div { padding: 0 0 15px; margin: 0 0 15px; border-bottom: 1px solid var(--line); }
|
||||
.reader-summary > div:last-child { margin-bottom: 0; }
|
||||
|
|
@ -424,12 +424,12 @@ html[data-theme="light"] .empty-brand-logo-light { opacity: 1; }
|
|||
.donut-metric-rack {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-3);
|
||||
border: var(--border-hair) solid var(--donut-border);
|
||||
border-radius: var(--donut-rack-radius);
|
||||
background: var(--donut-rack-surface);
|
||||
box-shadow: var(--donut-rack-shadow);
|
||||
gap: var(--segment-gap);
|
||||
padding: var(--segment-inset);
|
||||
border: var(--border-hair) solid var(--segment-border);
|
||||
border-radius: var(--segment-radius);
|
||||
background: var(--segment-surface);
|
||||
box-shadow: none;
|
||||
}
|
||||
.donut-metric-card {
|
||||
position: relative;
|
||||
|
|
@ -439,11 +439,11 @@ html[data-theme="light"] .empty-brand-logo-light { opacity: 1; }
|
|||
min-height: var(--donut-card-min-h);
|
||||
padding: var(--space-5) var(--space-3) var(--space-2);
|
||||
overflow: hidden;
|
||||
border: var(--border-hair) solid var(--donut-border);
|
||||
border-radius: var(--donut-card-radius);
|
||||
border: var(--border-hair) solid var(--tile-border);
|
||||
border-radius: var(--tile-radius);
|
||||
color: var(--donut-all);
|
||||
background: linear-gradient(145deg, var(--donut-card-surface), var(--donut-card-surface-end));
|
||||
box-shadow: var(--donut-card-shadow);
|
||||
background: var(--tile-surface);
|
||||
box-shadow: var(--tile-card-shadow);
|
||||
}
|
||||
.donut-metric-card::before {
|
||||
position: absolute;
|
||||
|
|
@ -478,7 +478,7 @@ html[data-theme="light"] .empty-brand-logo-light { opacity: 1; }
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
transform: rotate(135deg);
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
.donut-metric-card__plot circle {
|
||||
fill: none;
|
||||
|
|
@ -487,7 +487,6 @@ html[data-theme="light"] .empty-brand-logo-light { opacity: 1; }
|
|||
.donut-metric-card__track { stroke: var(--donut-track); }
|
||||
.donut-metric-card__progress {
|
||||
stroke: currentColor;
|
||||
stroke-linecap: round;
|
||||
animation: donut-progress-build var(--dur-data-build) var(--ease-data-build) both;
|
||||
}
|
||||
@keyframes donut-progress-build {
|
||||
|
|
@ -505,20 +504,37 @@ html[data-theme="light"] .empty-brand-logo-light { opacity: 1; }
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: var(--space-4);
|
||||
font-family: var(--font-donut-display);
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1;
|
||||
}
|
||||
.donut-metric-card__values strong {
|
||||
.donut-metric-card__primary {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
.donut-metric-card__primary strong {
|
||||
color: var(--donut-value);
|
||||
font-size: var(--donut-value-size);
|
||||
font-weight: var(--fw-semibold);
|
||||
}
|
||||
.donut-metric-card__values span {
|
||||
margin-top: var(--space-2);
|
||||
color: currentColor;
|
||||
.donut-metric-card__primary span {
|
||||
position: absolute;
|
||||
left: calc(100% + var(--space-1));
|
||||
bottom: 0;
|
||||
color: var(--donut-value);
|
||||
font-size: var(--donut-percent-size);
|
||||
font-weight: var(--fw-semibold);
|
||||
}
|
||||
.donut-metric-card__primary.is-compact strong {
|
||||
font-size: calc(var(--donut-value-size) * .85);
|
||||
}
|
||||
.donut-metric-card__primary.is-compact span {
|
||||
font-size: var(--type-meta-size);
|
||||
}
|
||||
.donut-metric-card__values > small {
|
||||
margin-top: var(--space-2);
|
||||
color: var(--donut-label);
|
||||
font-size: var(--type-meta-size);
|
||||
font-weight: var(--fw-regular);
|
||||
}
|
||||
.donut-metric-card--all { color: var(--donut-all); }
|
||||
|
|
@ -539,17 +555,17 @@ html[data-theme="light"] .empty-brand-logo-light { opacity: 1; }
|
|||
.sidebar-severity-rack {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: var(--space-2);
|
||||
gap: var(--segment-gap);
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
margin: 0 var(--space-3) var(--space-2);
|
||||
padding: var(--space-2);
|
||||
padding: var(--segment-inset);
|
||||
overflow-y: auto;
|
||||
border: var(--border-hair) solid var(--donut-border);
|
||||
border-radius: var(--donut-rack-radius);
|
||||
background: var(--donut-rack-surface);
|
||||
box-shadow: var(--donut-rack-shadow);
|
||||
scrollbar-color: var(--border-strong) var(--donut-rack-surface);
|
||||
border: var(--border-hair) solid var(--segment-border);
|
||||
border-radius: var(--segment-radius);
|
||||
background: var(--segment-surface);
|
||||
box-shadow: none;
|
||||
scrollbar-color: var(--border-strong) var(--segment-surface);
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.sidebar-severity-card {
|
||||
|
|
@ -563,15 +579,12 @@ html[data-theme="light"] .empty-brand-logo-light { opacity: 1; }
|
|||
.sidebar-severity-card .donut-metric-card__plot {
|
||||
width: calc(var(--donut-ring-size) - var(--space-8));
|
||||
}
|
||||
.sidebar-severity-card .donut-metric-card__values {
|
||||
padding-top: var(--space-2);
|
||||
.sidebar-severity-card .donut-metric-card__primary strong {
|
||||
font-size: var(--donut-compact-value-size);
|
||||
}
|
||||
.sidebar-severity-card .donut-metric-card__values strong {
|
||||
font-size: var(--donut-value-size);
|
||||
}
|
||||
.sidebar-severity-card .donut-metric-card__values span {
|
||||
margin-top: var(--space-1);
|
||||
font-size: var(--donut-percent-size);
|
||||
.sidebar-severity-card .donut-metric-card__primary span {
|
||||
left: calc(100% + var(--donut-compact-unit-gap));
|
||||
font-size: var(--donut-compact-percent-size);
|
||||
}
|
||||
.sidebar-severity-card:hover,
|
||||
.sidebar-severity-card[aria-pressed="true"] {
|
||||
|
|
|
|||
|
|
@ -35,12 +35,12 @@
|
|||
.datahub-reference-knob { font-style: normal; }
|
||||
.datahub-comparison-error { display: block; margin: 0 var(--legacy-space-3) var(--legacy-space-2); color: var(--danger-text); font-size: var(--ui-control-size); font-style: normal; }
|
||||
.datahub-tenants-module { min-height: 0; padding: 0; border: 0; background: transparent; }
|
||||
.datahub-tenant-card-rack { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: var(--space-2); padding: var(--space-3); border: var(--border-hair) solid var(--donut-border); border-radius: var(--donut-rack-radius); background: var(--donut-rack-surface); box-shadow: var(--donut-rack-shadow); }
|
||||
.datahub-tenant-card { position: relative; min-height: var(--donut-card-min-h); padding: 0; transition: background .12s ease, border-color .12s ease, transform .12s ease; }
|
||||
.datahub-tenant-card-rack { display: grid; grid-template-columns: repeat(auto-fill, var(--tile-compact-width)); justify-content: start; gap: var(--segment-gap); padding: var(--segment-inset); border: var(--border-hair) solid var(--segment-border); border-radius: var(--segment-radius); background: var(--segment-surface); box-shadow: none; }
|
||||
.datahub-tenant-card { position: relative; min-height: var(--donut-card-min-h); padding: 0; transition: background .12s ease, border-color .12s ease; }
|
||||
.datahub-tenant-card::before { content: none; }
|
||||
.datahub-tenant-card:hover,
|
||||
.datahub-tenant-card:focus-within { border-color: var(--line-strong); background: var(--panel-hover); transform: translateY(-2px); }
|
||||
.datahub-tenant-card.is-selected { outline: var(--bw-emphasis) solid var(--button-marked-bg); outline-offset: calc(-1 * var(--bw-emphasis)); }
|
||||
.datahub-tenant-card:focus-within { border-color: var(--line-strong); background: var(--tile-surface-hover); box-shadow: var(--tile-card-shadow); }
|
||||
.datahub-tenant-card.is-selected { border-color: var(--tile-selected-border); outline: var(--bw-emphasis) solid var(--tile-selected-border); outline-offset: calc(-1 * var(--bw-emphasis)); }
|
||||
.datahub-tenant-processing-dot { position: absolute; z-index: 3; top: var(--datahub-readiness-dot-inset); left: var(--datahub-readiness-dot-inset); width: var(--datahub-readiness-dot-size); height: var(--datahub-readiness-dot-size); border: var(--border-hair) solid var(--surface-app); border-radius: 50%; background: var(--state-online); pointer-events: none; }
|
||||
.datahub-tenant-processing-dot[data-status="update"] { background: var(--warning); }
|
||||
.datahub-tenant-processing-dot[data-status="error"] { background: var(--state-offline); }
|
||||
|
|
@ -70,14 +70,13 @@
|
|||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.datahub-tenant-card { transition: none; }
|
||||
.datahub-tenant-card:hover, .datahub-tenant-card:focus-within { transform: none; }
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.datahub-tenant-card-rack { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.datahub-tenant-card-rack { grid-template-columns: repeat(auto-fill, var(--tile-compact-width)); }
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.datahub-tenant-card-rack { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.datahub-tenant-card-rack { grid-template-columns: minmax(0, var(--tile-compact-width)); }
|
||||
}
|
||||
|
||||
.datahub-reference-module { overflow: hidden; }
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@
|
|||
.datahub-projects, .datahub-create { margin-top: var(--legacy-space-6); }
|
||||
.datahub-section-heading { display: flex; align-items: end; justify-content: space-between; gap: var(--legacy-space-5); margin-bottom: var(--legacy-space-3); }
|
||||
.datahub-section-heading h3 { margin: 0; color: var(--heading); font-size: var(--ui-body-size); }
|
||||
.datahub-section-heading > strong { display: inline-flex; align-items: center; justify-content: center; min-width: 28px; height: 22px; padding: 1px 6px; border: 1px solid var(--border-default); border-radius: 0; color: var(--text-primary); background: var(--surface-input); font-size: var(--ui-value-size); font-weight: 700; font-variant-numeric: tabular-nums; }
|
||||
.datahub-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(310px, 1fr)); gap: var(--legacy-space-2); }
|
||||
.datahub-card { display: grid; grid-template-columns: 40px minmax(0,1fr) auto; align-items: center; gap: var(--legacy-space-3); min-height: 96px; padding: var(--legacy-space-4); border: var(--border-hair) solid var(--line); border-radius: var(--legacy-radius-sm); color: var(--text); background: var(--panel); text-align: left; transition: border-color .12s ease, background .12s ease; }
|
||||
.datahub-card:hover { border-color: var(--line-strong); background: var(--panel-hover); }
|
||||
.datahub-section-heading > strong { display: inline-flex; align-items: center; justify-content: center; min-width: 28px; height: 22px; padding: 1px 6px; border: 1px solid var(--border-default); border-radius: 0; color: var(--text-primary); background: var(--surface-input); font-size: var(--ui-value-size); font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; }
|
||||
.datahub-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(0, var(--tile-content-width))); justify-content: start; gap: var(--segment-gap); padding: var(--segment-inset); border: var(--border-hair) solid var(--segment-border); border-radius: var(--segment-radius); background: var(--segment-surface); }
|
||||
.datahub-card { display: grid; grid-template-columns: 40px minmax(0,1fr) auto; align-items: center; gap: var(--legacy-space-3); min-width: 0; min-height: 96px; padding: var(--legacy-space-4); overflow: hidden; border: var(--border-hair) solid var(--tile-border); border-radius: var(--tile-radius); color: var(--text); background: var(--tile-surface); box-shadow: var(--tile-card-shadow); text-align: left; transition: border-color .12s ease, background .12s ease; }
|
||||
.datahub-card:hover { border-color: var(--line-strong); background: var(--tile-surface-hover); }
|
||||
.datahub-card-mark { display: grid; place-items: center; width: 40px; height: 40px; border-radius: var(--legacy-radius-sm); color: var(--text-on-accent); background: var(--legacy-accent); font-size: var(--ui-control-size); font-weight: var(--ui-strong-weight); letter-spacing: .05em; }
|
||||
.datahub-card-body { display: grid; gap: var(--legacy-space-1); min-width: 0; }
|
||||
.datahub-card-body strong { color: var(--heading); font-size: var(--ui-body-size); overflow: hidden; text-overflow: ellipsis; }
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
.datahub-create label small { color: var(--muted); font-weight: 400; }
|
||||
.datahub-offline, .datahub-alert, .datahub-empty, .datahub-loading { margin-top: var(--legacy-space-6); border: var(--border-hair) solid var(--line); border-radius: var(--legacy-radius-sm); background: var(--panel); }
|
||||
.datahub-offline { display: flex; gap: var(--legacy-space-5); padding: var(--legacy-space-5); border: var(--border-hair) solid var(--line); }
|
||||
.datahub-offline-icon { display: grid; place-items: center; flex: 0 0 32px; width: 32px; height: 32px; border-radius: var(--legacy-radius-sm); color: var(--state-active-text); background: var(--state-active); font-size: var(--ui-heading-size); font-weight: 900; }
|
||||
.datahub-offline-icon { display: grid; place-items: center; flex: 0 0 32px; width: 32px; height: 32px; border-radius: var(--legacy-radius-sm); color: var(--state-active-text); background: var(--state-active); font-size: var(--ui-heading-size); font-weight: var(--fw-bold); }
|
||||
.datahub-offline h3, .datahub-empty h3 { margin: 0 0 var(--legacy-space-2); color: var(--heading); }
|
||||
.datahub-offline p, .datahub-empty p { max-width: 650px; color: var(--muted); line-height: 1.5; }
|
||||
.datahub-alert { display: flex; align-items: center; justify-content: space-between; gap: var(--legacy-space-3); padding: var(--legacy-space-3); border-color: var(--danger); color: var(--danger-text); background: var(--danger-surface); }
|
||||
|
|
@ -62,18 +62,18 @@
|
|||
.tenant-duplicate-badge { width: fit-content; padding: 1px var(--legacy-space-1); border: var(--border-hair) solid var(--warning); border-radius: var(--legacy-radius-sm); color: var(--warning) !important; background: transparent; font-size: var(--ui-small-size); font-weight: var(--ui-label-weight); letter-spacing: .03em; text-transform: uppercase; }
|
||||
.tenant-scan-delete { min-width: 64px; padding-inline: 8px; }
|
||||
.datahub-dialog-form { display: grid; gap: 16px; }
|
||||
.datahub-dialog-form label { display: grid; gap: 7px; color: var(--heading); font-size: var(--ui-label-size); font-weight: 750; }
|
||||
.datahub-dialog-form label { display: grid; gap: 7px; color: var(--heading); font-size: var(--ui-label-size); font-weight: var(--fw-semibold); }
|
||||
.datahub-dialog-form label small { color: var(--muted); font-weight: 400; }
|
||||
.datahub-dialog-form textarea { min-height: 100px; resize: vertical; }
|
||||
.datahub-dialog-form footer, .datahub-delete-warning footer { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; padding-top: 16px; border-top: 1px solid var(--line); }
|
||||
.datahub-delete-warning { display: grid; gap: 10px; }
|
||||
.datahub-delete-warning > strong { color: var(--heading); font-size: var(--ui-heading-size); }
|
||||
.datahub-delete-warning p { color: var(--muted); line-height: 1.6; }
|
||||
@media (max-width: 1050px) { .tenant-file-head { display: none; } .tenant-file-row { grid-template-columns: 1fr 1fr; gap: 8px 18px; padding: 16px; } .tenant-file-row > span::before { content: attr(data-label); display: block; margin-bottom: 3px; color: var(--muted); font-size: var(--ui-label-size); font-weight: 800; letter-spacing: .05em; text-transform: uppercase; } }
|
||||
@media (max-width: 1050px) { .tenant-file-head { display: none; } .tenant-file-row { grid-template-columns: 1fr 1fr; gap: 8px 18px; padding: 16px; } .tenant-file-row > span::before { content: attr(data-label); display: block; margin-bottom: 3px; color: var(--muted); font-size: var(--ui-label-size); font-weight: var(--fw-semibold); letter-spacing: var(--ls-caps); text-transform: uppercase; } }
|
||||
@media (max-width: 800px) { .tenant-header { grid-template-columns: 1fr; } .tenant-actions { flex-wrap: wrap; } .tenant-file-row { grid-template-columns: 1fr; } .datahub-card-actions { flex-wrap: wrap; } }
|
||||
|
||||
/* Full-page tenant editor */
|
||||
.datahub-card.active { border-color: var(--line-strong); background: var(--panel-hover); }
|
||||
.datahub-card.active { border-color: var(--tile-selected-border); outline: var(--bw-emphasis) solid var(--tile-selected-border); outline-offset: calc(-1 * var(--bw-emphasis)); background: var(--tile-surface-hover); }
|
||||
.tenant-editor-header { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: end; gap: var(--legacy-space-5); padding-bottom: var(--legacy-space-6); border-bottom: var(--border-hair) solid var(--line); }
|
||||
.tenant-editor-header .tenant-back { grid-column: 1 / -1; }
|
||||
.tenant-editor-header h2 { margin: var(--legacy-space-1) 0; color: var(--heading); font-size: clamp(17px, 2vw, 22px); line-height: 1.15; letter-spacing: -.01em; }
|
||||
|
|
@ -81,7 +81,7 @@
|
|||
.tenant-editor { margin-top: var(--legacy-space-6); padding: var(--legacy-space-5); border: var(--border-hair) solid var(--line); border-radius: var(--legacy-radius-sm); background: var(--panel); }
|
||||
.tenant-editor-form { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--legacy-space-4); }
|
||||
.tenant-editor-form label { display: grid; grid-template-columns: auto 1fr; gap: var(--legacy-space-2); color: var(--heading); font-size: var(--ui-control-size); font-weight: var(--ui-label-weight); }
|
||||
.tenant-editor-form label > span { justify-self: end; color: var(--muted); font-size: var(--ui-small-size); font-weight: 500; }
|
||||
.tenant-editor-form label > span { justify-self: end; color: var(--muted); font-size: var(--ui-small-size); font-weight: var(--fw-semibold); }
|
||||
.tenant-editor-form .input { grid-column: 1 / -1; }
|
||||
.tenant-editor-description { grid-column: 1 / -1; }
|
||||
.tenant-editor-description textarea { min-height: 118px; resize: vertical; }
|
||||
|
|
@ -101,7 +101,8 @@
|
|||
.viewer.app-area-data-hub, .viewer.app-area-dashboard, .viewer.app-area-db-structure { overflow-x: hidden; overflow-y: auto; background: var(--surface-app); scrollbar-color: var(--line-strong) var(--surface-app); scrollbar-width: thin; }
|
||||
.viewer.app-area-data-hub { scrollbar-gutter: stable; }
|
||||
.datahub-page { box-sizing: border-box; min-height: 100%; background: var(--surface-app); }
|
||||
.datahub-card, .tenant-editor, .tenant-file-table, .tenant-import-pending { border-radius: var(--legacy-radius-sm); box-shadow: var(--shadow); }
|
||||
.datahub-card { border-radius: var(--tile-radius); box-shadow: var(--tile-card-shadow); }
|
||||
.tenant-editor, .tenant-file-table, .tenant-import-pending { border-radius: var(--legacy-radius-sm); box-shadow: var(--shadow); }
|
||||
.tenant-editor-header { align-items: center; }
|
||||
.tenant-editor-header h2 { font-weight: var(--ui-strong-weight); letter-spacing: -.01em; }
|
||||
.tenant-editor { padding: var(--legacy-space-5); }
|
||||
|
|
@ -164,33 +165,35 @@
|
|||
.dashboard-page { box-sizing: border-box; display: grid; align-content: start; gap: var(--space-4); width: 100%; min-height: 100%; padding: var(--panel-inset); color: var(--text); background: var(--surface-app); }
|
||||
.dashboard-context { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2) var(--space-5); margin: 0; padding: 0 var(--space-2); color: var(--muted); font-size: var(--ui-label-size); }
|
||||
.dashboard-context strong { color: var(--heading); font-size: var(--ui-body-size); }
|
||||
.dashboard-status-rack { display: grid; grid-template-columns: minmax(270px, 1.45fr) repeat(3, minmax(180px, .9fr)) minmax(250px, 1.2fr); gap: var(--space-2); }
|
||||
.dashboard-status-metric { display: grid; grid-template-columns: 98px minmax(0, 1fr); align-items: center; gap: var(--space-4); min-width: 0; min-height: 190px; padding: var(--space-4); border: var(--border-hair) solid var(--line); border-radius: 0; color: var(--text); background: var(--panel); text-align: left; }
|
||||
.dashboard-status-rack { display: grid; grid-template-columns: 290px repeat(3, 210px) 250px; justify-content: start; gap: var(--segment-gap); padding: var(--segment-inset); border: var(--border-hair) solid var(--segment-border); border-radius: var(--segment-radius); background: var(--segment-surface); }
|
||||
.dashboard-status-metric { display: grid; grid-template-columns: 98px minmax(0, 1fr); align-items: center; gap: var(--space-3); min-width: 0; min-height: 190px; padding: var(--space-3); overflow: hidden; border: var(--border-hair) solid var(--tile-border); border-radius: var(--tile-radius); color: var(--text); background: var(--tile-surface); box-shadow: var(--tile-card-shadow); text-align: left; }
|
||||
button.dashboard-status-metric { cursor: pointer; }
|
||||
button.dashboard-status-metric:hover { border-color: currentColor; }
|
||||
button.dashboard-status-metric:hover { border-color: currentColor; background: var(--tile-surface-hover); }
|
||||
button.dashboard-status-metric:focus-visible { outline: var(--border-hair) solid var(--focus-ring); outline-offset: -1px; }
|
||||
button.dashboard-status-metric[aria-pressed="true"] { outline: var(--bw-emphasis) solid currentColor; outline-offset: calc(-1 * var(--bw-emphasis)); }
|
||||
button.dashboard-status-metric[aria-pressed="true"] { border-color: var(--tile-selected-border); outline: var(--bw-emphasis) solid var(--tile-selected-border); outline-offset: calc(-1 * var(--bw-emphasis)); }
|
||||
.dashboard-status-metric-large { grid-template-columns: 154px minmax(0, 1fr); }
|
||||
.dashboard-status-ring { position: relative; width: 98px; height: 98px; color: var(--donut-all); }
|
||||
.dashboard-status-metric-large .dashboard-status-ring { width: 154px; height: 154px; }
|
||||
.dashboard-status-ring svg { display: block; width: 100%; height: 100%; transform: rotate(-90deg); }
|
||||
.dashboard-status-ring circle { fill: none; stroke-width: 12; }
|
||||
.dashboard-status-metric-large .dashboard-status-ring circle { stroke-width: 10; }
|
||||
.dashboard-status-ring-track { stroke: var(--donut-border); }
|
||||
.dashboard-status-ring circle { fill: none; stroke-width: var(--donut-ring-stroke); }
|
||||
.dashboard-status-ring-track { stroke: var(--donut-track); }
|
||||
.dashboard-status-ring-progress { stroke: currentColor; }
|
||||
.dashboard-status-ring > span { position: absolute; inset: 0; display: flex; align-items: baseline; justify-content: center; padding-top: 36px; color: var(--heading); font-variant-numeric: tabular-nums; }
|
||||
.dashboard-status-ring > span { position: absolute; inset: 0; display: flex; align-items: flex-start; justify-content: center; padding-top: 36px; color: var(--heading); font-variant-numeric: tabular-nums; }
|
||||
.dashboard-status-metric-large .dashboard-status-ring > span { padding-top: 57px; }
|
||||
.dashboard-status-ring > span strong { font: var(--fw-semibold) var(--donut-value-size) / 1 var(--font-donut-display); }
|
||||
.dashboard-donut-primary { position: relative; display: block; line-height: 1; }
|
||||
.dashboard-status-ring > span strong { display: block; font: var(--fw-semibold) var(--donut-value-size) / 1 var(--font-donut-display); }
|
||||
.dashboard-status-metric-large .dashboard-status-ring > span strong { font-size: calc(var(--donut-value-size) * 1.45); }
|
||||
.dashboard-status-ring > span small { margin-left: var(--space-1); color: var(--heading); font-size: var(--ui-body-size); font-weight: var(--fw-semibold); }
|
||||
.dashboard-status-ring > span small { position: absolute; left: calc(100% + var(--space-1)); bottom: 0; color: var(--heading); font-size: var(--donut-percent-size); font-weight: var(--fw-semibold); }
|
||||
.dashboard-status-ring .dashboard-donut-primary.is-compact strong, .dashboard-risk-donut .dashboard-donut-primary.is-compact strong { font-size: calc(var(--donut-value-size) * .85); }
|
||||
.dashboard-status-ring .dashboard-donut-primary.is-compact small, .dashboard-risk-donut .dashboard-donut-primary.is-compact small { font-size: var(--type-meta-size); }
|
||||
.dashboard-status-ring-deviation { color: var(--donut-error); }
|
||||
.dashboard-status-ring-passed { color: var(--donut-passed); }
|
||||
.dashboard-status-ring-not-run { color: var(--donut-deviation); }
|
||||
.dashboard-status-copy { display: grid; gap: var(--space-3); min-width: 0; }
|
||||
.dashboard-status-copy > span { color: var(--heading); font-size: var(--ui-body-size); }
|
||||
.dashboard-status-copy > strong { color: var(--heading); font-size: calc(var(--ui-heading-size) * 1.35); font-variant-numeric: tabular-nums; }
|
||||
.dashboard-status-copy > strong small, .dashboard-status-copy > small { color: var(--muted); font-size: var(--ui-body-size); font-weight: var(--fw-regular); line-height: 1.35; }
|
||||
.dashboard-status-summary { display: grid; grid-template-columns: minmax(0, 1fr) minmax(100px, .8fr); align-items: stretch; min-width: 0; min-height: 190px; padding: var(--space-4); border: var(--border-hair) solid var(--line); background: var(--panel); }
|
||||
.dashboard-status-copy { display: grid; gap: var(--space-3); min-width: 0; max-width: 100%; overflow: hidden; }
|
||||
.dashboard-status-copy > span { min-width: 0; color: var(--heading); font-size: var(--ui-body-size); overflow-wrap: normal; word-break: normal; hyphens: manual; }
|
||||
.dashboard-status-copy > strong { display: grid; gap: var(--space-1); min-width: 0; color: var(--heading); font-size: calc(var(--ui-heading-size) * 1.35); font-variant-numeric: tabular-nums; white-space: normal; }
|
||||
.dashboard-status-copy > strong small, .dashboard-status-copy > small { display: block; color: var(--muted); font-size: var(--ui-body-size); font-weight: var(--fw-regular); line-height: 1.35; overflow-wrap: normal; word-break: normal; hyphens: manual; }
|
||||
.dashboard-status-summary { display: grid; grid-template-columns: minmax(0, 1fr) minmax(100px, .8fr); align-items: stretch; min-width: 0; min-height: 190px; padding: var(--space-4); overflow: hidden; border: var(--border-hair) solid var(--tile-border); border-radius: var(--tile-radius); background: var(--tile-surface); box-shadow: var(--tile-card-shadow); }
|
||||
.dashboard-status-summary-stack { display: grid; align-content: center; padding-right: var(--space-4); border-right: var(--border-hair) solid var(--line-strong); }
|
||||
.dashboard-status-summary-stack button { display: grid; gap: var(--space-2); min-height: 64px; padding: var(--space-2); border: 0; border-radius: 0; color: var(--text); background: transparent; text-align: left; cursor: pointer; }
|
||||
.dashboard-status-summary-stack button + button { border-top: var(--border-hair) solid var(--line); }
|
||||
|
|
@ -202,9 +205,9 @@ button.dashboard-status-metric[aria-pressed="true"] { outline: var(--bw-emphasis
|
|||
.dashboard-ioe-summary:focus-visible { outline: var(--border-hair) solid var(--focus-ring); outline-offset: -1px; }
|
||||
.dashboard-ioe-summary:disabled { cursor: default; }
|
||||
.dashboard-ioe-summary strong { color: var(--accent); font: var(--fw-semibold) calc(var(--donut-value-size) * 1.45) / 1 var(--font-donut-display); font-variant-numeric: tabular-nums; }
|
||||
.dashboard-distribution-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-3); }
|
||||
.dashboard-lower-grid { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(420px, 1fr); align-items: stretch; gap: var(--space-3); }
|
||||
.dashboard-environment { min-width: 0; margin: 0; padding: var(--space-4); border: var(--border-hair) solid var(--line); border-left: 3px solid var(--legacy-accent); border-radius: var(--legacy-radius-sm); background: var(--panel); }
|
||||
.dashboard-distribution-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--segment-gap); padding: var(--segment-inset); border: var(--border-hair) solid var(--segment-border); border-radius: var(--segment-radius); background: var(--segment-surface); }
|
||||
.dashboard-lower-grid { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(420px, 1fr); align-items: stretch; gap: var(--segment-gap); padding: var(--segment-inset); border: var(--border-hair) solid var(--segment-border); border-radius: var(--segment-radius); background: var(--segment-surface); }
|
||||
.dashboard-environment { min-width: 0; margin: 0; padding: var(--space-4); overflow: hidden; border: var(--border-hair) solid var(--tile-border); border-left: 3px solid var(--legacy-accent); border-radius: var(--tile-radius); background: var(--tile-surface); box-shadow: var(--tile-card-shadow); }
|
||||
.dashboard-environment .datahub-section-heading h2, .dashboard-block h2 { margin: 0; color: var(--heading); font-size: var(--ui-body-size); font-weight: var(--ui-label-weight); letter-spacing: var(--ui-label-tracking); text-transform: uppercase; }
|
||||
.dashboard-coverage-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin-bottom: var(--space-4); padding-bottom: var(--space-4); border-bottom: var(--border-hair) solid var(--line); }
|
||||
.dashboard-coverage-summary > span { display: grid; gap: var(--space-1); min-width: 0; padding: 0 var(--space-3); text-align: center; }
|
||||
|
|
@ -215,9 +218,9 @@ button.dashboard-status-metric[aria-pressed="true"] { outline: var(--bw-emphasis
|
|||
.dashboard-facts div { min-width: 0; }
|
||||
.dashboard-facts dt { margin-bottom: var(--legacy-space-1); color: var(--muted); font-size: var(--ui-label-size); }
|
||||
.dashboard-facts dd { margin: 0; color: var(--heading); font-size: var(--ui-body-size); overflow-wrap: anywhere; }
|
||||
.dashboard-block { min-width: 0; padding: var(--space-4); border: var(--border-hair) solid var(--line); border-radius: var(--legacy-radius-sm); background: var(--panel); }
|
||||
.dashboard-block { min-width: 0; padding: var(--space-4); overflow: hidden; border: var(--border-hair) solid var(--tile-border); border-radius: var(--tile-radius); background: var(--tile-surface); box-shadow: var(--tile-card-shadow); }
|
||||
.dashboard-distribution { display: grid; align-content: start; gap: var(--space-4); }
|
||||
.dashboard-segmented-bar { display: flex; width: 100%; height: 28px; overflow: hidden; background: var(--surface-active); }
|
||||
.dashboard-segmented-bar { display: flex; width: 100%; height: 28px; overflow: hidden; border-radius: var(--tile-radius); background: var(--surface-active); }
|
||||
.dashboard-segment { --share: 0%; flex: 0 0 var(--share); min-width: 2px; height: 100%; background: var(--severity, var(--viz-1)); }
|
||||
.dashboard-segment + .dashboard-segment { border-left: var(--border-hair) solid var(--surface-panel); }
|
||||
.dashboard-distribution-legend { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: var(--space-2); margin: 0; padding: 0; list-style: none; }
|
||||
|
|
@ -255,20 +258,24 @@ button.dashboard-status-metric[aria-pressed="true"] { outline: var(--bw-emphasis
|
|||
.dashboard-risk-categories span, .dashboard-risk-legend span { min-width: 0; overflow: hidden; font-size: var(--ui-body-size); text-overflow: ellipsis; white-space: nowrap; }
|
||||
.dashboard-risk-categories strong, .dashboard-risk-legend strong { color: var(--heading); font-size: var(--ui-body-size); font-variant-numeric: tabular-nums; }
|
||||
.dashboard-risk-severity { display: flex; align-items: center; justify-content: center; gap: var(--space-5); min-width: 0; padding-left: var(--space-5); }
|
||||
.dashboard-risk-donut { flex: 0 0 150px; width: 150px; height: 150px; overflow: visible; transform: rotate(-90deg); }
|
||||
.dashboard-risk-donut circle { fill: none; stroke-width: 13; }
|
||||
.dashboard-risk-donut-track { stroke: var(--surface-active); }
|
||||
.dashboard-risk-donut { position: relative; flex: 0 0 150px; width: 150px; height: 150px; overflow: visible; }
|
||||
.dashboard-risk-donut svg { display: block; width: 100%; height: 100%; overflow: visible; transform: rotate(-90deg); }
|
||||
.dashboard-risk-donut circle { fill: none; stroke-width: var(--donut-ring-stroke); }
|
||||
.dashboard-risk-donut-track { stroke: var(--donut-track); }
|
||||
.dashboard-risk-donut-segment { stroke: var(--severity, var(--info)); }
|
||||
.dashboard-risk-donut > span { position: absolute; inset: 0; display: flex; align-items: flex-start; justify-content: center; padding-top: 57px; color: var(--heading); font-variant-numeric: tabular-nums; }
|
||||
.dashboard-risk-donut > span strong { display: block; font: var(--fw-semibold) calc(var(--donut-value-size) * 1.45) / 1 var(--font-donut-display); }
|
||||
.dashboard-risk-donut > span small { position: absolute; left: calc(100% + var(--space-1)); bottom: 0; font-size: var(--donut-percent-size); font-weight: var(--fw-semibold); }
|
||||
.dashboard-risk-legend { flex: 1 1 auto; min-width: 120px; }
|
||||
.dashboard-risk-legend button { grid-template-columns: 12px minmax(0, 1fr) auto; gap: var(--space-3); }
|
||||
.dashboard-risk-legend i { width: 12px; height: 12px; background: var(--severity, var(--info)); }
|
||||
@media (max-width: 1200px) {
|
||||
.dashboard-status-rack { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
@media (max-width: 1400px) {
|
||||
.dashboard-status-rack { grid-template-columns: repeat(2, minmax(0, 290px)); }
|
||||
.dashboard-status-summary { grid-column: 1 / -1; }
|
||||
.dashboard-distribution-grid, .dashboard-lower-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.dashboard-status-rack { grid-template-columns: 1fr; }
|
||||
.dashboard-status-rack { grid-template-columns: minmax(0, 290px); }
|
||||
.dashboard-status-summary { grid-column: auto; }
|
||||
.dashboard-coverage-summary, .dashboard-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.dashboard-risk-body { grid-template-columns: 1fr; }
|
||||
|
|
@ -283,10 +290,10 @@ button.dashboard-status-metric[aria-pressed="true"] { outline: var(--bw-emphasis
|
|||
eigenen Aussenabstand. */
|
||||
.db-structure-meta { margin: 0; color: var(--muted); font-size: var(--ui-body-size); }
|
||||
.page-actions-row { display: flex; align-items: center; justify-content: space-between; gap: var(--legacy-space-4); margin-top: var(--legacy-space-4); }
|
||||
.db-structure-layers { display: grid; gap: var(--legacy-space-5); margin-top: var(--legacy-space-5); width: 100%; }
|
||||
.db-structure-layer { border: var(--border-hair) solid var(--line); border-left: 3px solid var(--legacy-accent); border-radius: var(--legacy-radius-sm); padding: var(--legacy-space-4); background: var(--panel); }
|
||||
.db-structure-layers { display: grid; gap: var(--segment-gap); margin-top: var(--legacy-space-5); width: 100%; }
|
||||
.db-structure-layer { padding: var(--segment-inset); border: var(--border-hair) solid var(--segment-border); border-left: 3px solid var(--legacy-accent); border-radius: var(--segment-radius); background: var(--segment-surface); }
|
||||
.db-structure-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: var(--legacy-space-2); }
|
||||
.db-structure-card { border: var(--border-hair) solid var(--line); border-radius: var(--legacy-radius-sm); background: var(--panel-raised); overflow: hidden; }
|
||||
.db-structure-card { overflow: hidden; border: var(--border-hair) solid var(--tile-border); border-radius: var(--tile-radius); background: var(--tile-surface); box-shadow: var(--tile-card-shadow); }
|
||||
.db-structure-card-header { display: flex; align-items: center; justify-content: space-between; gap: var(--legacy-space-2); width: 100%; padding: var(--legacy-space-3); border: 0; color: inherit; background: transparent; text-align: left; cursor: pointer; }
|
||||
.db-structure-card-header:hover { background: var(--panel-hover); }
|
||||
.db-structure-card-title { display: grid; gap: var(--legacy-space-1); min-width: 0; }
|
||||
|
|
@ -708,8 +715,9 @@ button.dashboard-status-metric[aria-pressed="true"] { outline: var(--bw-emphasis
|
|||
}
|
||||
.tenant-detail-page .tenant-identity h2 {
|
||||
padding: 0;
|
||||
font-size: calc(var(--fs-xl) * 2);
|
||||
line-height: 1.1;
|
||||
font-size: var(--type-h2-size);
|
||||
font-weight: var(--fw-semibold);
|
||||
line-height: 1.15;
|
||||
letter-spacing: -.03em;
|
||||
}
|
||||
.tenant-detail-page .tenant-identity p {
|
||||
|
|
@ -736,11 +744,11 @@ button.dashboard-status-metric[aria-pressed="true"] { outline: var(--bw-emphasis
|
|||
}
|
||||
.tenant-timeslots-heading {
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--fs-md);
|
||||
color: var(--text-primary);
|
||||
font-size: var(--type-h3-size);
|
||||
font-weight: var(--fw-semibold);
|
||||
letter-spacing: var(--ls-caps);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
}
|
||||
.tenant-timeslot-add {
|
||||
flex: 0 0 auto;
|
||||
|
|
@ -851,7 +859,7 @@ button.dashboard-status-metric[aria-pressed="true"] { outline: var(--bw-emphasis
|
|||
min-height: calc(var(--row-h) + var(--space-2));
|
||||
color: var(--text-secondary);
|
||||
background: var(--surface-panel);
|
||||
font-size: var(--fs-sm);
|
||||
font-size: var(--type-table-head-size);
|
||||
font-weight: var(--fw-semibold);
|
||||
letter-spacing: var(--ls-caps);
|
||||
text-transform: uppercase;
|
||||
|
|
@ -872,7 +880,7 @@ button.dashboard-status-metric[aria-pressed="true"] { outline: var(--bw-emphasis
|
|||
.tenant-report-copy small {
|
||||
overflow: hidden;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--fs-sm);
|
||||
font-size: var(--type-meta-size);
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
|
@ -880,7 +888,7 @@ button.dashboard-status-metric[aria-pressed="true"] { outline: var(--bw-emphasis
|
|||
.tenant-report-imported {
|
||||
overflow: hidden;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--fs-md);
|
||||
font-size: var(--type-body-size);
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
|
@ -891,7 +899,7 @@ button.dashboard-status-metric[aria-pressed="true"] { outline: var(--bw-emphasis
|
|||
gap: var(--space-2);
|
||||
min-width: 0;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--fs-md);
|
||||
font-size: var(--type-body-size);
|
||||
font-weight: var(--fw-regular);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
@ -973,11 +981,11 @@ button.dashboard-status-metric[aria-pressed="true"] { outline: var(--bw-emphasis
|
|||
justify-content: flex-end;
|
||||
}
|
||||
.tenant-information-title > strong {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--fs-md);
|
||||
color: var(--text-primary);
|
||||
font-size: var(--type-h3-size);
|
||||
font-weight: var(--fw-semibold);
|
||||
letter-spacing: var(--ls-caps);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
}
|
||||
.tenant-edit-client,
|
||||
.tenant-save-client {
|
||||
|
|
@ -1005,10 +1013,10 @@ button.dashboard-status-metric[aria-pressed="true"] { outline: var(--bw-emphasis
|
|||
padding: 0;
|
||||
color: var(--text-primary);
|
||||
background: transparent;
|
||||
font-size: var(--fs-md);
|
||||
font-size: var(--type-label-size);
|
||||
font-weight: var(--fw-semibold);
|
||||
letter-spacing: var(--ls-caps);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
}
|
||||
.tenant-detail-page .tenant-editor-form .tenant-field-label i {
|
||||
color: var(--text-secondary);
|
||||
|
|
@ -1020,7 +1028,7 @@ button.dashboard-status-metric[aria-pressed="true"] { outline: var(--bw-emphasis
|
|||
display: block;
|
||||
justify-self: start;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--fs-lg);
|
||||
font-size: var(--type-body-size);
|
||||
font-weight: var(--fw-regular);
|
||||
letter-spacing: normal;
|
||||
line-height: var(--lh-prose);
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
.import-preview-heading { display: flex; align-items: center; gap: 9px; min-width: 0; }
|
||||
.import-preview-main { min-width: 0; }
|
||||
.import-preview-heading strong { min-width: 0; overflow-wrap: anywhere; }
|
||||
.import-status { flex: 0 0 auto; font-size: var(--ui-label-size); font-weight: 700; }
|
||||
.import-status { flex: 0 0 auto; font-size: var(--ui-label-size); font-weight: var(--fw-semibold); }
|
||||
.import-status-ready { color: var(--low); }
|
||||
.import-status-replacement { color: var(--low); }
|
||||
.import-status-warning, .import-status-duplicate { color: var(--medium); }
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
.app-shell { display: grid; grid-template-rows: 124px minmax(0, 1fr); height: 100vh; min-height: 0; }
|
||||
.topbar { display: flex; align-items: flex-start; gap: 10px; min-width: 0; height: 124px; padding: 6px 12px; background: var(--header); overflow: hidden; }
|
||||
.brand { display: flex; align-items: center; gap: 12px; flex: 0 0 204px; min-width: 204px; padding: 10px 14px 10px 16px; margin-top: 2px; white-space: nowrap; }
|
||||
.app-shell:not(.app-shell-work) .brand { min-height: 112px; padding-block: 6px; margin-top: 0; }
|
||||
.brand h1 { margin: 0; color: var(--heading); font-size: var(--ui-heading-size); line-height: 1; letter-spacing: .045em; }
|
||||
.brand { display: flex; align-items: center; gap: 12px; flex: 0 0 204px; min-width: 204px; min-height: 112px; padding: 6px 14px 6px 16px; margin-top: 0; white-space: nowrap; }
|
||||
.brand h1 { margin: 0; color: var(--heading); font-size: var(--type-h4-size); font-weight: var(--fw-bold); line-height: 1; letter-spacing: .045em; }
|
||||
.brand-copy { display: grid; gap: var(--space-1); }
|
||||
.brand-version { color: var(--muted); font-size: var(--fs-micro); font-variant-numeric: tabular-nums; line-height: 1; letter-spacing: var(--ls-caps); }
|
||||
.brand-logo { position: relative; display: block; flex: 0 0 48px; width: 48px; height: 48px; }
|
||||
|
|
@ -23,9 +22,9 @@ html[data-theme="light"] .brand-logo-light { opacity: 1; }
|
|||
.toolbar, .header-export-row, .live-summary, .search-tools, .view-tabs { display: flex; align-items: center; gap: 6px; min-width: 0; }
|
||||
.toolbar { flex: 0 0 auto; justify-content: flex-end; margin: 0; padding: 0; border: 0; min-inline-size: 0; }
|
||||
.live-summary { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; height: 30px; margin-left: 0; padding: 3px 8px; gap: 10px; border: 1px solid var(--border-default); border-radius: 0; background: var(--surface-panel-raised); white-space: nowrap; }
|
||||
.live-summary span { display: inline-flex; align-items: center; gap: 8px; color: var(--text-secondary); font-weight: 500; font-size: var(--ui-label-size); }
|
||||
.live-summary span { display: inline-flex; align-items: center; gap: 8px; color: var(--text-secondary); font-weight: var(--fw-regular); font-size: var(--ui-label-size); }
|
||||
.live-summary span + span { border-left: 1px solid var(--border-subtle); padding-left: 10px; }
|
||||
.live-summary b { display: inline-flex; align-items: center; justify-content: center; min-width: 28px; height: 22px; padding: 1px 6px; border: 1px solid var(--border-default); border-radius: 0; color: var(--text-primary); background: var(--surface-input); font-size: var(--ui-label-size); font-variant-numeric: tabular-nums; font-weight: 700; line-height: 1; text-align: center; }
|
||||
.live-summary b { display: inline-flex; align-items: center; justify-content: center; min-width: 28px; height: 22px; padding: 1px 6px; border: 1px solid var(--border-default); border-radius: 0; color: var(--text-primary); background: var(--surface-input); font-size: var(--ui-label-size); font-variant-numeric: tabular-nums; font-weight: var(--fw-semibold); line-height: 1; text-align: center; }
|
||||
.delimiter-select { width: 220px; }
|
||||
.view-tabs { flex: 0 0 auto; gap: 0; border: 1px solid var(--button-border); border-radius: 0; overflow: visible; background: var(--surface-input); }
|
||||
.tab-btn { flex: 0 0 auto; min-width: 72px; padding: 2px 8px; border: 0; border-right: 1px solid var(--button-border); border-radius: 0; color: var(--button-text); background: var(--button-bg); font-size: var(--ui-label-size); font-weight: var(--fw-medium); line-height: 1.15; white-space: nowrap; transition: background-color .12s ease, color .12s ease; }
|
||||
|
|
@ -49,12 +48,12 @@ aside { position: relative; z-index: 10; min-width: 0; min-height: 0; background
|
|||
.theme-switcher { display: flex; align-items: center; justify-content: space-between; gap: 4px; padding: 4px; border: 1px solid var(--button-border); border-radius: 10px; background: var(--surface-input); }
|
||||
.theme-switcher-btn { display: grid; place-items: center; flex: 1 1 0; height: 28px; padding: 0; border: 1px solid transparent; border-radius: 7px; color: var(--button-text); background: transparent; box-shadow: none; font-size: var(--ui-body-size); line-height: 1; transition: background-color .12s ease, color .12s ease, border-color .12s ease; cursor: pointer; }
|
||||
.theme-switcher-btn:hover:not(.active) { color: var(--button-text); background: var(--button-bg-hover); }
|
||||
.theme-switcher-btn.active { border-color: var(--button-border); color: var(--button-marked-text); background: var(--button-marked-bg); box-shadow: none; font-weight: 700; }
|
||||
.sidebar-toggle-inline { display: grid; place-items: center; width: 28px; height: 28px; padding: 0; border: 1px solid transparent; border-radius: 7px; color: var(--button-text); background: transparent; font-size: 16px; font-weight: bold; cursor: pointer; }
|
||||
.theme-switcher-btn.active { border-color: var(--button-border); color: var(--button-marked-text); background: var(--button-marked-bg); box-shadow: none; font-weight: var(--fw-semibold); }
|
||||
.sidebar-toggle-inline { display: grid; place-items: center; width: 28px; height: 28px; padding: 0; border: 1px solid transparent; border-radius: 7px; color: var(--button-text); background: transparent; font-size: 16px; font-weight: var(--fw-bold); cursor: pointer; }
|
||||
.sidebar-toggle-inline:hover { background: var(--button-bg-hover); color: var(--text-primary); }
|
||||
|
||||
.sidebar-primary-actions { display: flex; flex-direction: column; gap: 8px; margin: 10px 0 0 0; padding: 0 0 12px 10px; border-top: 0; border-bottom: 0; }
|
||||
.sidebar-tab-btn { position: relative; display: flex; align-items: center; gap: 13px; width: calc(100% - 10px); min-height: 42px; padding: 8px 14px; border: 1px solid transparent; border-radius: 8px; color: var(--text-secondary); background: transparent; box-shadow: none; font-size: var(--ui-heading-size); font-weight: 500; letter-spacing: 0; text-align: left; text-shadow: none; cursor: pointer; transition: color .15s ease, background .15s ease, border-color .15s ease; }
|
||||
.sidebar-tab-btn { position: relative; display: flex; align-items: center; gap: 13px; width: calc(100% - 10px); min-height: 42px; padding: 8px 14px; border: 1px solid transparent; border-radius: 8px; color: var(--text-secondary); background: transparent; box-shadow: none; font-size: var(--ui-heading-size); font-weight: var(--fw-regular); letter-spacing: 0; text-align: left; text-shadow: none; cursor: pointer; transition: color .15s ease, background .15s ease, border-color .15s ease; }
|
||||
.sidebar-tab-btn svg { flex: 0 0 22px; width: 22px; height: 22px; color: var(--text-muted); }
|
||||
.sidebar-tab-btn span { min-width: 0; }
|
||||
.sidebar-tab-btn:hover:not(.active):not(:disabled) { color: var(--text-primary); background: var(--surface-hover); border-color: transparent; box-shadow: none; transform: none; }
|
||||
|
|
@ -74,7 +73,7 @@ aside { position: relative; z-index: 10; min-width: 0; min-height: 0; background
|
|||
background: var(--surface-app);
|
||||
box-shadow: none;
|
||||
outline: 0;
|
||||
font-weight: 700;
|
||||
font-weight: var(--fw-semibold);
|
||||
}
|
||||
.sidebar-tab-btn.active svg {
|
||||
color: var(--band-surface);
|
||||
|
|
@ -89,7 +88,7 @@ aside { position: relative; z-index: 10; min-width: 0; min-height: 0; background
|
|||
.toolbar:disabled, .workbar:disabled { opacity: .5; }
|
||||
.toolbar:disabled *, .workbar:disabled * { pointer-events: none; }
|
||||
.search-tools { grid-column: 1 / -1; display: flex; align-items: center; gap: 6px; flex: 1 1 auto; flex-wrap: wrap; margin: 0; padding: 0; }
|
||||
.standard-filter-btn { flex: 0 0 auto; min-width: 96px; margin-left: 0; font-weight: 700; }
|
||||
.standard-filter-btn { flex: 0 0 auto; min-width: 96px; margin-left: 0; font-weight: var(--fw-semibold); }
|
||||
.title-select { flex: 0 0 215px; width: 215px; }
|
||||
.sort-select { flex: 0 0 345px; width: 345px; }
|
||||
.status-select { flex: 0 0 190px; width: 190px; }
|
||||
|
|
@ -122,16 +121,17 @@ aside { position: relative; z-index: 10; min-width: 0; min-height: 0; background
|
|||
.area-kicker { display: block; margin-bottom: 10px; color: var(--muted); font-size: var(--ui-label-size); font-weight: var(--ui-label-weight); letter-spacing: var(--ui-label-tracking); text-transform: uppercase; }
|
||||
.area-landing-card h2 { margin: 0 0 12px; color: var(--heading); font-size: var(--ui-glyph-size); }
|
||||
.area-landing-card p { max-width: 510px; margin: 0 auto 24px; color: var(--muted); line-height: 1.6; }
|
||||
.area-landing-card .hidden-input + .btn { min-width: 170px; font-weight: 800; }
|
||||
.area-landing-card .hidden-input + .btn { min-width: 170px; font-weight: var(--fw-semibold); }
|
||||
.records-layout { display: grid; grid-template-columns: clamp(420px, 27vw, 520px) minmax(0, 1fr); height: 100%; min-height: 0; background: var(--surface-app); }
|
||||
.record-list { min-width: 0; min-height: 0; padding: 8px 6px; background: var(--surface-app); overflow-y: auto; scrollbar-color: var(--line-strong) var(--surface-inset); scrollbar-width: thin; }
|
||||
.tile-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); align-content: start; gap: 14px; min-height: 100%; padding: var(--panel-inset); background: var(--surface-app); }
|
||||
.tile-view-scroll { padding: var(--panel-inset); }
|
||||
.tile-grid { box-sizing: border-box; display: grid; grid-template-columns: repeat(auto-fill, minmax(0, var(--tile-content-width))); align-content: start; justify-content: start; gap: var(--segment-gap); min-height: 100%; padding: var(--segment-inset); border: var(--border-hair) solid var(--segment-border); border-radius: var(--segment-radius); background: var(--segment-surface); }
|
||||
.table-scroll { max-width: 100%; overflow: auto; background: var(--surface-app); }
|
||||
.raw-view { min-height: 100%; padding: var(--panel-inset); color: var(--text); background: var(--surface-app); font: var(--fs-lg)/1.55 var(--font-mono); }
|
||||
@media (max-width: 1500px) { .topbar-kpi { min-width: 74px; padding-inline: 10px; } .topbar-kpi.kpi-format { min-width: 190px; } .delimiter-select { width: 170px; } .tab-btn { min-width: 68px; } }
|
||||
@media (max-width: 1200px) { .status-line-tools { position: static; width: min(520px, 100%); margin-left: auto; transform: none; } }
|
||||
@media (max-width: 1050px) { .workbar { grid-template-columns: minmax(0, 1fr); } .workbar-action { grid-column: 1; justify-content: flex-end; } .records-layout { grid-template-columns: minmax(320px, 42%) minmax(0, 1fr); } }
|
||||
@media (max-width: 760px) { .app-shell > main { grid-template-columns: 0 minmax(0, 1fr); } .app-shell > main > aside { visibility: hidden; } .sidebar-toggle { left: 0; } .records-layout { grid-template-columns: 1fr; } .record-list { max-height: 280px; border-right: 0; border-bottom: 1px solid var(--line); } .tile-grid { grid-template-columns: 1fr; padding: var(--panel-inset); } }
|
||||
@media (max-width: 760px) { .app-shell > main { grid-template-columns: 0 minmax(0, 1fr); } .app-shell > main > aside { visibility: hidden; } .sidebar-toggle { left: 0; } .records-layout { grid-template-columns: 1fr; } .record-list { max-height: 280px; border-right: 0; border-bottom: 1px solid var(--line); } .tile-grid { grid-template-columns: minmax(0, 1fr); } }
|
||||
.app-sidebar { display: flex; flex-direction: column; min-height: 100%; padding: 0; }
|
||||
.sidebar-primary-actions { order: 0; }
|
||||
.sidebar-severity-rack { order: 1; }
|
||||
|
|
@ -144,7 +144,7 @@ aside { position: relative; z-index: 10; min-width: 0; min-height: 0; background
|
|||
.connection-unavailable .connection-state-square.is-offline,
|
||||
.connection-checking .connection-state-square.is-offline { opacity: 1; }
|
||||
.area-topbar { display: flex; align-items: center; gap: 14px; flex: 1; min-width: 0; }
|
||||
.area-topbar strong { margin-right: auto; color: var(--heading); font-size: calc(var(--fs-xl) * 4); font-weight: var(--fw-semibold); line-height: 1.15; letter-spacing: -.04em; }
|
||||
.area-topbar strong { margin-right: auto; color: var(--heading); font-size: var(--type-h1-size); font-weight: var(--fw-semibold); line-height: 1.1; letter-spacing: -.04em; }
|
||||
.area-topbar-heading { display: flex; align-items: center; gap: 18px; min-width: 0; margin-right: auto; overflow: hidden; }
|
||||
.area-topbar-heading > strong { flex: 0 0 auto; margin-right: 0; }
|
||||
.pathfinder-filter-trail { display: flex; align-items: center; gap: 12px; min-width: 0; color: var(--heading); font-size: calc(var(--fs-xl) * 1.5); font-weight: var(--fw-semibold); line-height: 1.15; white-space: nowrap; }
|
||||
|
|
|
|||
|
|
@ -206,16 +206,16 @@ button, input, select, textarea { font-family: inherit; }
|
|||
.reader-nav { margin-bottom: var(--legacy-space-6); color: var(--muted); font-size: var(--ui-control-size); }
|
||||
.reader-nav button { color: var(--text); }
|
||||
.reader-heading { padding-bottom: var(--legacy-space-5); border-color: var(--line); }
|
||||
/* Der Fundname steht auf derselben Stufe wie die Finding-Ueberschrift. */
|
||||
/* Der Fundname steht als kompakter Detailtitel eine Stufe ueber dem Inhalt. */
|
||||
.reader-heading h2 {
|
||||
color: var(--heading);
|
||||
font-size: var(--ui-reading-heading-size);
|
||||
font-size: var(--type-detail-title-size);
|
||||
font-weight: var(--ui-strong-weight);
|
||||
line-height: 1.3;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.reader-title-hex { width: 26px; height: 30px; }
|
||||
.finding-badges, .mitre-line { gap: var(--legacy-space-2); margin-top: var(--legacy-space-3); }
|
||||
.finding-badges, .mitre-line { gap: var(--legacy-space-2); margin-top: var(--legacy-space-3); font-size: var(--ui-badge-size); }
|
||||
.status-badge, .severity-badge, .mitre-badge, .reader-severity {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
@ -226,7 +226,7 @@ button, input, select, textarea { font-family: inherit; }
|
|||
border-radius: var(--legacy-radius-sm);
|
||||
color: var(--text);
|
||||
background: transparent;
|
||||
font-size: var(--ui-label-size);
|
||||
font-size: var(--ui-badge-size);
|
||||
font-weight: var(--ui-label-weight);
|
||||
line-height: 1.18;
|
||||
letter-spacing: var(--ui-label-tracking);
|
||||
|
|
@ -244,7 +244,7 @@ button, input, select, textarea { font-family: inherit; }
|
|||
.reader-summary, .review-grid, .all-fields { border-color: var(--line); }
|
||||
/* Die Ueberschrift des Finding-Abschnitts traegt die Pruefbeschreibung der
|
||||
Regel und ist deshalb ein Satz, keine Bereichsmarke - entsprechend gesetzt. */
|
||||
.reader-summary h3 { color: var(--heading); font-size: var(--ui-reading-heading-size); font-weight: var(--ui-strong-weight); line-height: 1.3; letter-spacing: 0; }
|
||||
.reader-summary h3 { color: var(--heading); font-size: var(--type-h3-size); font-weight: var(--ui-strong-weight); line-height: 1.3; letter-spacing: 0; }
|
||||
/* Der Finding-Abschnitt ist die Lesefläche des Berichts. Beschriftungen und
|
||||
Fliesstext stehen deshalb auf der Lesestufe, nicht auf der dichten
|
||||
Werkzeugtypografie der Leisten, Kacheln und Raster. */
|
||||
|
|
@ -264,12 +264,16 @@ button, input, select, textarea { font-family: inherit; }
|
|||
flach mit Haarlinien. */
|
||||
.tile-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(0, var(--tile-content-width)));
|
||||
align-items: stretch;
|
||||
gap: var(--legacy-space-5);
|
||||
align-content: start;
|
||||
justify-content: start;
|
||||
gap: var(--segment-gap);
|
||||
min-height: 100%;
|
||||
padding: var(--panel-inset);
|
||||
background: var(--bg);
|
||||
padding: var(--segment-inset);
|
||||
border: var(--border-hair) solid var(--segment-border);
|
||||
border-radius: var(--segment-radius);
|
||||
background: var(--segment-surface);
|
||||
}
|
||||
.data-tile {
|
||||
position: relative;
|
||||
|
|
@ -280,23 +284,22 @@ button, input, select, textarea { font-family: inherit; }
|
|||
min-width: 0;
|
||||
min-height: 112px;
|
||||
padding: var(--legacy-space-4);
|
||||
border: var(--border-hair) solid var(--line);
|
||||
border-radius: var(--radius-lg);
|
||||
border: var(--border-hair) solid var(--tile-border);
|
||||
border-radius: var(--tile-radius);
|
||||
color: var(--text);
|
||||
background: var(--panel-raised);
|
||||
box-shadow: var(--shadow-raised);
|
||||
background: var(--tile-surface);
|
||||
box-shadow: var(--tile-card-shadow);
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
transition: background .12s ease, border-color .12s ease, box-shadow .12s ease, transform .12s ease;
|
||||
transition: background .12s ease, border-color .12s ease;
|
||||
}
|
||||
.data-tile:hover, .data-tile:focus-visible {
|
||||
border-color: var(--line-strong);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--panel-hover);
|
||||
border-radius: var(--tile-radius);
|
||||
background: var(--tile-surface-hover);
|
||||
box-shadow: var(--tile-card-shadow);
|
||||
outline: 0;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-raised-hover);
|
||||
}
|
||||
.data-tile-hex {
|
||||
position: relative;
|
||||
|
|
@ -324,9 +327,9 @@ button, input, select, textarea { font-family: inherit; }
|
|||
.data-tile.severity-low .data-tile-hex { background: var(--low); }
|
||||
.data-tile.severity-info .data-tile-hex { background: var(--info); }
|
||||
.data-tile-content { min-width: 0; }
|
||||
/* Bleibt bewusst gleich gross wie `.record-title`: dieselbe Datenmenge darf in
|
||||
zwei Ansichten nicht unterschiedlich gewichtet wirken. */
|
||||
.data-tile-content h3 { margin: 0; color: var(--heading); font-size: var(--ui-heading-size); font-weight: var(--ui-strong-weight); line-height: 1.35; overflow-wrap: anywhere; }
|
||||
/* Matrix- und Listentitel teilen die kompakte Inhaltsstufe, damit lange
|
||||
Scannerbezeichnungen nicht die Kachelstruktur verdraengen. */
|
||||
.data-tile-content h3 { margin: 0; color: var(--heading); font-size: var(--type-h4-size); font-weight: var(--ui-strong-weight); line-height: 1.35; overflow-wrap: normal; word-break: normal; hyphens: manual; }
|
||||
/* Die Groesse gilt dem Outcome-Text ("IOE Found"), der sonst die 14 px des
|
||||
Rumpfes erbte und damit groesser stand als dieselbe Aussage in der
|
||||
Trefferliste. Die Plaketten daneben tragen ihre eigene Groesse. */
|
||||
|
|
@ -395,9 +398,9 @@ button, input, select, textarea { font-family: inherit; }
|
|||
padding: var(--panel-inset);
|
||||
color: var(--text);
|
||||
background: var(--panel);
|
||||
font-family: "Cascadia Code", Consolas, monospace;
|
||||
font-family: var(--font-ui);
|
||||
font-size: var(--fs-lg);
|
||||
font-weight: 400;
|
||||
font-weight: var(--fw-regular);
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
|
|
@ -416,7 +419,7 @@ button, input, select, textarea { font-family: inherit; }
|
|||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.data-tile, .record-card { transition: none; }
|
||||
.data-tile:hover, .data-tile:focus-visible, .record-card:hover { transform: none; }
|
||||
.record-card:hover { transform: none; }
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
/* Steht im gemeinsamen Kopfcontainer der Shell. */
|
||||
.finding-path-controls { display: flex; align-items: end; gap: 10px; min-width: 0; height: 100%; padding: 8px 0 0; background: transparent; overflow-x: auto; }
|
||||
.finding-path-title { display: grid; align-self: stretch; align-content: center; min-width: 240px; margin-right: auto; }
|
||||
.finding-path-title span { color: var(--heading); font-size: var(--ui-heading-size); font-weight: 800; }
|
||||
.finding-path-title strong { color: var(--muted); font-size: var(--ui-control-size); font-weight: 650; }
|
||||
.finding-path-controls label { display: grid; gap: 6px; min-width: 180px; color: var(--muted); font-size: var(--ui-value-size); font-weight: 750; letter-spacing: .045em; text-transform: uppercase; }
|
||||
.finding-path-title span { color: var(--heading); font-size: var(--ui-heading-size); font-weight: var(--fw-semibold); }
|
||||
.finding-path-title strong { color: var(--muted); font-size: var(--ui-control-size); font-weight: var(--fw-semibold); }
|
||||
.finding-path-controls label { display: grid; gap: 6px; min-width: 180px; color: var(--muted); font-size: var(--ui-value-size); font-weight: var(--fw-semibold); letter-spacing: 0; text-transform: none; }
|
||||
/* Dieselben Masse wie die Filter im Command Band von Work: dort sind Controls
|
||||
`--control-height` hoch und tragen `--ui-control-size`. Mit 40 px und
|
||||
`--ui-body-size` waren die Graph-Filter als einzige Ansicht groesser. */
|
||||
|
|
@ -14,44 +14,44 @@
|
|||
.finding-path-search { min-width: 260px !important; }
|
||||
.finding-path-layout { display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: auto auto; width: 100%; max-width: 100%; min-width: 0; min-height: 0; overflow: hidden; }
|
||||
.finding-path-main { min-width: 0; min-height: 0; padding: 16px; overflow: auto; background-image: radial-gradient(circle at 1px 1px, color-mix(in srgb, var(--line) 55%, transparent) 1px, transparent 0); background-size: 22px 22px; }
|
||||
.active-finding-path { min-width: max-content; margin-bottom: 18px; padding: 14px; border: var(--border-hair) solid var(--line); border-radius: var(--legacy-radius-md); background: color-mix(in srgb, var(--panel) 94%, transparent); }
|
||||
.active-finding-path { min-width: max-content; margin-bottom: 18px; padding: var(--segment-inset); border: var(--border-hair) solid var(--segment-border); border-radius: var(--segment-radius); background: var(--segment-surface); }
|
||||
.active-finding-path > header { display: flex; align-items: center; justify-content: space-between; gap: 20px; margin-bottom: 13px; }
|
||||
.active-finding-path > header div { display: grid; }
|
||||
.active-finding-path > header span:first-child { color: var(--heading); font-size: var(--ui-heading-size); font-weight: 850; }
|
||||
.active-finding-path > header span:first-child { color: var(--heading); font-size: var(--ui-heading-size); font-weight: var(--fw-semibold); }
|
||||
.active-finding-path > header strong { color: var(--muted); font-size: var(--ui-control-size); font-weight: 600; }
|
||||
.path-method { color: var(--muted); font-size: var(--ui-value-size); text-transform: uppercase; }
|
||||
.active-path-track { display: flex; align-items: stretch; gap: 8px; }
|
||||
.active-path-step-wrap { display: flex; align-items: center; gap: 8px; }
|
||||
.active-path-arrow { color: var(--muted); font-size: var(--ui-glyph-size); font-weight: 300; }
|
||||
.active-path-step { position: relative; display: grid; gap: 5px; width: 280px; min-height: 126px; padding: 12px 13px 11px 16px; border: 1px solid var(--line); border-radius: 4px; color: var(--text); background: var(--panel); text-align: left; }
|
||||
.active-path-arrow { color: var(--muted); font-size: var(--ui-glyph-size); font-weight: var(--fw-regular); }
|
||||
.active-path-step { position: relative; display: grid; gap: 5px; width: 280px; min-width: 0; min-height: 126px; padding: 12px 13px 11px 16px; overflow: hidden; border: var(--border-hair) solid var(--tile-border); border-radius: var(--tile-radius); color: var(--text); background: var(--tile-surface); box-shadow: var(--tile-card-shadow); text-align: left; }
|
||||
.active-path-step::before { content: ''; position: absolute; inset: 0 auto 0 0; width: var(--marker-width); background: var(--severity); }
|
||||
.active-path-step:hover { border-color: var(--severity); background: var(--panel-hover); }
|
||||
.active-path-step small { color: var(--severity); font-size: var(--ui-value-size); font-weight: 850; letter-spacing: .04em; text-transform: uppercase; }
|
||||
.active-path-step small { color: var(--severity); font-size: var(--ui-value-size); font-weight: var(--fw-semibold); letter-spacing: var(--ls-caps); text-transform: uppercase; }
|
||||
.active-path-step strong { color: var(--heading); font-size: var(--ui-body-size); line-height: 1.35; }
|
||||
.active-path-step span { align-self: end; color: var(--muted); font-size: var(--ui-value-size); }
|
||||
.finding-tactic-board { display: flex; align-items: flex-start; gap: 12px; min-width: max-content; }
|
||||
.finding-tactic-column { width: 310px; border: 1px solid var(--line); border-radius: 4px; background: color-mix(in srgb, var(--panel) 92%, transparent); }
|
||||
.finding-tactic-column { width: 310px; border: var(--border-hair) solid var(--segment-border); border-radius: var(--segment-radius); background: var(--segment-surface); }
|
||||
.finding-tactic-column > header { display: flex; align-items: center; justify-content: space-between; min-height: 42px; padding: 10px 12px; border-bottom: 1px solid var(--line); background: var(--panel-soft); }
|
||||
.finding-tactic-column > header strong { color: var(--heading); font-size: var(--ui-body-size); }
|
||||
.finding-tactic-column > header span { display: grid; place-items: center; min-width: 26px; height: 26px; border-radius: var(--radius-pill); color: var(--muted); background: var(--surface-inset); font-size: var(--ui-value-size); font-weight: 800; }
|
||||
.finding-tactic-column > header span { display: grid; place-items: center; min-width: 26px; height: 26px; border-radius: var(--radius-pill); color: var(--muted); background: var(--surface-inset); font-size: var(--ui-value-size); font-weight: var(--fw-semibold); }
|
||||
.finding-tactic-column > div { display: grid; gap: 7px; max-height: 560px; padding: 8px; overflow-y: auto; }
|
||||
.finding-path-card { position: relative; display: grid; gap: 7px; width: 100%; min-height: 126px; padding: 11px 11px 10px 14px; border: 1px solid var(--line); border-radius: 3px; color: var(--text); background: var(--panel); text-align: left; }
|
||||
.finding-path-card:hover { border-color: var(--line-strong); background: var(--panel-hover); }
|
||||
.finding-path-card { position: relative; display: grid; gap: 7px; width: 100%; min-width: 0; min-height: 126px; padding: 11px 11px 10px 14px; overflow: hidden; border: var(--border-hair) solid var(--tile-border); border-radius: var(--tile-radius); color: var(--text); background: var(--tile-surface); box-shadow: var(--tile-card-shadow); text-align: left; }
|
||||
.finding-path-card:hover { border-color: var(--line-strong); background: var(--tile-surface-hover); }
|
||||
.finding-path-card.is-path-step { border-left: var(--marker-width) solid var(--severity); padding-left: 7px; }
|
||||
.finding-path-card.is-selected { outline: 2px solid var(--legacy-accent); outline-offset: -2px; }
|
||||
.finding-path-card-head { display: flex; align-items: center; gap: 7px; color: var(--severity); font-size: var(--ui-label-size); font-weight: 850; letter-spacing: .04em; text-transform: uppercase; }
|
||||
.finding-path-card.is-selected { border-color: var(--tile-selected-border); outline: var(--bw-emphasis) solid var(--tile-selected-border); outline-offset: calc(-1 * var(--bw-emphasis)); }
|
||||
.finding-path-card-head { display: flex; align-items: center; gap: 7px; color: var(--severity); font-size: var(--ui-label-size); font-weight: var(--fw-semibold); letter-spacing: var(--ls-caps); text-transform: uppercase; }
|
||||
.finding-path-card-head i { width: 8px; height: 8px; border-radius: 50%; background: var(--severity); }
|
||||
.finding-path-card-head b { margin-left: auto; color: var(--success); font-size: var(--ui-label-size); }
|
||||
.finding-path-card > strong { color: var(--heading); font-size: var(--ui-body-size); line-height: 1.4; }
|
||||
.finding-path-metrics, .finding-techniques { color: var(--muted); font-size: var(--ui-value-size); }
|
||||
.finding-techniques { color: var(--text); }
|
||||
.finding-path-detail { display: grid; grid-template-columns: minmax(260px, 1.35fr) minmax(320px, 1fr); column-gap: 40px; align-items: start; width: 100%; min-width: 0; min-height: 320px; box-sizing: border-box; padding: 28px 32px 40px; border-top: 1px solid var(--line); background: var(--panel); overflow: visible; }
|
||||
.finding-detail-severity { justify-self: start; display: inline-block; padding: 5px 9px; color: var(--severity-ink); background: var(--severity); font-size: var(--ui-value-size); font-weight: 850; text-transform: uppercase; }
|
||||
.finding-detail-severity { justify-self: start; display: inline-block; padding: 5px 9px; color: var(--severity-ink); background: var(--severity); font-size: var(--ui-value-size); font-weight: var(--fw-bold); text-transform: uppercase; }
|
||||
.finding-path-detail h2 { grid-column: 1; margin: 14px 0 22px; color: var(--heading); font-size: var(--ui-glyph-size); line-height: 1.35; }
|
||||
.finding-path-detail dl { grid-column: 1; display: grid; grid-template-columns: 150px minmax(0, 1fr); gap: 12px 18px; margin: 0; }
|
||||
.finding-path-detail dt { color: var(--muted); font-size: var(--ui-value-size); font-weight: 800; text-transform: uppercase; }
|
||||
.finding-path-detail dt { color: var(--muted); font-size: var(--ui-value-size); font-weight: var(--fw-semibold); text-transform: none; }
|
||||
.finding-path-detail dd { margin: 0; color: var(--text); font-size: var(--ui-body-size); overflow-wrap: anywhere; }
|
||||
.finding-open-detail { grid-column: 2; grid-row: 1 / span 2; align-self: end; width: 100%; min-height: 42px; margin: 0; font-size: var(--ui-body-size); font-weight: 750; }
|
||||
.finding-open-detail { grid-column: 2; grid-row: 1 / span 2; align-self: end; width: 100%; min-height: 42px; margin: 0; font-size: var(--ui-body-size); font-weight: var(--fw-semibold); }
|
||||
.finding-path-detail p { grid-column: 2; margin: 18px 0 0; color: var(--muted); font-size: var(--ui-control-size); line-height: 1.65; }
|
||||
.finding-path-empty { display: grid; place-content: center; gap: 8px; min-height: 100%; color: var(--muted); text-align: center; }
|
||||
.finding-path-empty strong { color: var(--heading); font-size: var(--ui-heading-size); }
|
||||
|
|
@ -79,10 +79,10 @@
|
|||
.finding-detail-main { min-width: 0; }
|
||||
.finding-detail-main h2 { margin: 14px 0 22px; color: var(--heading); font-size: var(--ui-glyph-size); line-height: 1.35; }
|
||||
.finding-detail-main dl { display: grid; grid-template-columns: 150px minmax(0, 1fr); gap: 12px 18px; margin: 0; }
|
||||
.finding-detail-main dt { color: var(--muted); font-size: var(--ui-value-size); font-weight: 800; text-transform: uppercase; }
|
||||
.finding-detail-main dt { color: var(--muted); font-size: var(--ui-value-size); font-weight: var(--fw-semibold); text-transform: none; }
|
||||
.finding-detail-main dd { margin: 0; color: var(--text); font-size: var(--ui-body-size); overflow-wrap: anywhere; }
|
||||
.finding-detail-actions { display: grid; align-content: start; gap: 18px; min-width: 0; padding-top: 34px; }
|
||||
.finding-detail-actions .finding-open-detail { grid-column: auto; grid-row: auto; align-self: auto; width: 100%; min-height: 44px; margin: 0; font-size: var(--ui-body-size); font-weight: 750; }
|
||||
.finding-detail-actions .finding-open-detail { grid-column: auto; grid-row: auto; align-self: auto; width: 100%; min-height: 44px; margin: 0; font-size: var(--ui-body-size); font-weight: var(--fw-semibold); }
|
||||
.finding-detail-actions p { grid-column: auto; grid-row: auto; margin: 0; color: var(--muted); font-size: var(--ui-control-size); line-height: 1.65; }
|
||||
@media (max-width: 1050px) {
|
||||
.finding-path-detail { grid-template-columns: 1fr; }
|
||||
|
|
@ -116,7 +116,7 @@
|
|||
.finding-tactic-heading { display: grid; min-width: 0; }
|
||||
|
||||
|
||||
.finding-tactic-heading strong { color: var(--heading); font-size: var(--ui-heading-size) !important; font-weight: 800; line-height: 1.25; }
|
||||
.finding-tactic-heading strong { color: var(--heading); font-size: var(--ui-heading-size) !important; font-weight: var(--fw-semibold); line-height: 1.25; }
|
||||
.finding-tactic-tools { display: flex; align-items: center; gap: 9px; margin-left: auto; padding-left: 10px; }
|
||||
.finding-tactic-tools > span { min-width: 26px; height: 26px; margin: 0; }
|
||||
.finding-tactic-workspace { display: grid; place-items: center; width: 26px; height: 26px; cursor: pointer; }
|
||||
|
|
@ -172,7 +172,7 @@
|
|||
margin: 0;
|
||||
color: var(--heading);
|
||||
font-size: var(--ui-heading-size);
|
||||
font-weight: 850;
|
||||
font-weight: var(--fw-semibold);
|
||||
line-height: 1.2;
|
||||
}
|
||||
.finding-path-title span {
|
||||
|
|
@ -196,12 +196,12 @@
|
|||
.meter-label {
|
||||
color: var(--muted);
|
||||
font-size: var(--ui-value-size);
|
||||
font-weight: 700;
|
||||
font-weight: var(--fw-semibold);
|
||||
}
|
||||
.meter-val {
|
||||
color: var(--heading);
|
||||
font-size: var(--ui-body-size);
|
||||
font-weight: 850;
|
||||
font-weight: var(--fw-bold);
|
||||
}
|
||||
.meter-gauge {
|
||||
width: 90px;
|
||||
|
|
@ -225,7 +225,7 @@
|
|||
border-color: var(--legacy-accent);
|
||||
background: var(--legacy-accent-active);
|
||||
color: var(--text-on-accent);
|
||||
font-weight: 800;
|
||||
font-weight: var(--fw-semibold);
|
||||
}
|
||||
|
||||
/* ---------- SVG Canvas & Board ---------- */
|
||||
|
|
@ -263,7 +263,7 @@
|
|||
border: 1px solid var(--line);
|
||||
border-radius: var(--legacy-radius-sm);
|
||||
font-size: var(--ui-label-size);
|
||||
font-weight: 750;
|
||||
font-weight: var(--fw-semibold);
|
||||
}
|
||||
.origin-pill.native {
|
||||
color: var(--text-on-accent);
|
||||
|
|
@ -317,7 +317,7 @@
|
|||
margin: 0;
|
||||
color: var(--heading);
|
||||
font-size: var(--ui-body-size);
|
||||
font-weight: 800;
|
||||
font-weight: var(--fw-semibold);
|
||||
}
|
||||
.box-body {
|
||||
display: flex;
|
||||
|
|
@ -337,7 +337,7 @@
|
|||
.mitre-ref-link {
|
||||
color: var(--legacy-accent);
|
||||
font-size: var(--ui-body-size);
|
||||
font-weight: 700;
|
||||
font-weight: var(--fw-semibold);
|
||||
text-decoration: none;
|
||||
}
|
||||
.mitre-ref-link:hover {
|
||||
|
|
|
|||
|
|
@ -113,10 +113,7 @@
|
|||
gap: var(--space-2);
|
||||
padding: var(--space-3);
|
||||
overflow: hidden;
|
||||
box-shadow: inset 0 0 0 0 transparent;
|
||||
}
|
||||
.obsidian-node-matrix-card .data-tile:hover,
|
||||
.obsidian-node-matrix-card .data-tile:focus-visible { box-shadow: inset 0 0 0 0 transparent; transform: none; }
|
||||
.obsidian-node-matrix-card .data-tile-hex { width: 18px; height: 20px; }
|
||||
.obsidian-node-matrix-card .data-tile-content h3 {
|
||||
display: -webkit-box;
|
||||
|
|
@ -161,14 +158,8 @@
|
|||
gap: var(--legacy-space-3);
|
||||
padding: var(--legacy-space-3);
|
||||
overflow: hidden;
|
||||
box-shadow: inset 0 0 0 0 transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
.obsidian-node-card.data-tile:hover,
|
||||
.obsidian-node-card.data-tile:focus-visible {
|
||||
box-shadow: inset 0 0 0 0 transparent;
|
||||
transform: none;
|
||||
}
|
||||
.obsidian-node-card-technique .data-tile-hex,
|
||||
.obsidian-node-card-category .data-tile-hex,
|
||||
.obsidian-node-card-defense .data-tile-hex,
|
||||
|
|
@ -206,7 +197,6 @@
|
|||
border-color: var(--graph-selected);
|
||||
outline: 3px solid var(--graph-selected);
|
||||
outline-offset: -3px;
|
||||
box-shadow: inset 0 0 0 0 transparent;
|
||||
}
|
||||
|
||||
.obsidian-graph-toolbar,
|
||||
|
|
@ -216,7 +206,7 @@
|
|||
position: absolute;
|
||||
z-index: 5;
|
||||
border: var(--bw-hairline) solid var(--border-default);
|
||||
border-radius: 0;
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text-primary);
|
||||
background: color-mix(in srgb, var(--surface-panel-raised) 94%, transparent);
|
||||
backdrop-filter: blur(var(--space-2));
|
||||
|
|
@ -424,7 +414,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
min-height: var(--control-height);
|
||||
min-height: var(--control-h);
|
||||
color: var(--text-primary);
|
||||
font-size: var(--ui-label-size);
|
||||
cursor: pointer;
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
.policy-profile-editor legend { padding-inline: var(--legacy-space-1); color: var(--muted); font-size: var(--ui-label-size); font-weight: var(--ui-label-weight); letter-spacing: var(--ui-label-tracking); text-transform: uppercase; }
|
||||
.policy-profile-editor .policy-profile-product-toggle { display: flex; grid-column: 1 / -1; grid-template-columns: auto 1fr; align-items: center; }
|
||||
.policy-profile-editor footer { display: flex; grid-column: 1 / -1; justify-content: flex-end; }
|
||||
.policy-action-stages { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--legacy-space-3); margin-top: var(--legacy-space-4); }
|
||||
.policy-action-stages > section { min-width: 0; padding: var(--legacy-space-3); border: var(--border-hair) solid var(--line); background: var(--panel); }
|
||||
.policy-action-stages { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--segment-gap); margin-top: var(--legacy-space-4); padding: var(--segment-inset); border: var(--border-hair) solid var(--segment-border); border-radius: var(--segment-radius); background: var(--segment-surface); }
|
||||
.policy-action-stages > section { min-width: 0; padding: var(--legacy-space-3); overflow: hidden; border: var(--border-hair) solid var(--tile-border); border-radius: var(--tile-radius); background: var(--tile-surface); box-shadow: var(--tile-card-shadow); }
|
||||
.policy-action-stages h4 { margin: 0 0 var(--legacy-space-2); color: var(--heading); font-size: var(--ui-body-size); }
|
||||
.policy-action-stages section > p { margin: 0; color: var(--muted); font-size: var(--ui-control-size); }
|
||||
.policy-action-stages article { display: flex; align-items: center; justify-content: space-between; gap: var(--legacy-space-3); padding: var(--legacy-space-2) 0; border-top: var(--border-hair) solid var(--line); }
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
.policy-analysis-panel { margin-top: var(--legacy-space-6); padding-top: var(--legacy-space-5); border-top: var(--border-hair) solid var(--line); }
|
||||
.policy-analysis-boundary { margin: 0 0 var(--legacy-space-4); padding: var(--legacy-space-3); border-left: 3px solid var(--warning); color: var(--heading); background: var(--panel-soft); font-size: var(--ui-control-size); line-height: 1.5; }
|
||||
.policy-analysis-empty { padding-block: 28px; }
|
||||
.policy-analysis-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--legacy-space-2); margin-bottom: var(--legacy-space-4); }
|
||||
.policy-analysis-summary article { display: grid; gap: var(--legacy-space-1); padding: var(--legacy-space-3); border: var(--border-hair) solid var(--line); background: var(--panel-raised); }
|
||||
.policy-analysis-summary { display: grid; grid-template-columns: repeat(auto-fill, var(--tile-compact-width)); justify-content: start; gap: var(--segment-gap); margin-bottom: var(--legacy-space-4); padding: var(--segment-inset); border: var(--border-hair) solid var(--segment-border); border-radius: var(--segment-radius); background: var(--segment-surface); }
|
||||
.policy-analysis-summary article { display: grid; gap: var(--legacy-space-1); min-width: 0; padding: var(--legacy-space-3); overflow: hidden; border: var(--border-hair) solid var(--tile-border); border-radius: var(--tile-radius); background: var(--tile-surface); box-shadow: var(--tile-card-shadow); }
|
||||
.policy-analysis-summary span { color: var(--muted); font-size: var(--ui-control-size); }
|
||||
.policy-analysis-summary strong { color: var(--heading); font-family: var(--font-mono); font-size: var(--ui-heading-size); }
|
||||
.policy-analysis-columns { display: grid; grid-template-columns: minmax(0, .9fr) minmax(0, 1.1fr); gap: var(--legacy-space-4); align-items: start; }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
/* Letzte Kaskadenschicht fuer die verbindliche Segment-/Kachelgrammatik.
|
||||
Ansichtsspezifische Breakpoints duerfen eine unvollstaendige Kachelzeile
|
||||
nicht wieder auf die gesamte Segmentbreite strecken. */
|
||||
.donut-metric-rack {
|
||||
grid-template-columns: repeat(auto-fill, var(--tile-compact-width));
|
||||
justify-content: start;
|
||||
}
|
||||
|
|
@ -143,14 +143,15 @@
|
|||
Alte rem-Skala auf die sechs Stufen der Designsprache abgebildet. Die
|
||||
Groessen bleiben dadurch unveraendert, nur ihre Quelle wechselt. */
|
||||
--font: var(--font-ui);
|
||||
--ui-label-size: var(--fs-sm);
|
||||
--ui-small-size: var(--fs-sm);
|
||||
--ui-value-size: var(--fs-sm);
|
||||
--ui-control-size: var(--fs-base);
|
||||
--ui-body-size: var(--fs-md);
|
||||
--ui-heading-size: var(--fs-lg);
|
||||
--ui-reading-size: var(--fs-lg);
|
||||
--ui-reading-heading-size: var(--fs-xl);
|
||||
--ui-label-size: var(--type-table-head-size);
|
||||
--ui-badge-size: var(--type-badge-size);
|
||||
--ui-small-size: var(--type-meta-size);
|
||||
--ui-value-size: var(--type-label-size);
|
||||
--ui-control-size: var(--type-button-size);
|
||||
--ui-body-size: var(--type-body-size);
|
||||
--ui-heading-size: var(--type-h3-size);
|
||||
--ui-reading-size: var(--type-body-size);
|
||||
--ui-reading-heading-size: var(--type-h2-size);
|
||||
/* Ausnahme: Glyphengroesse der Symbolschaltflaechen. Die Typografieskala
|
||||
endet bei 18px, weil sie Text beschreibt; ein Symbol ist kein Text.
|
||||
Faellt mit der Umstellung auf .icon-btn. */
|
||||
|
|
|
|||
|
|
@ -421,26 +421,32 @@
|
|||
|
||||
.tile-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(var(--tile-min-w), 1fr));
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-3);
|
||||
grid-template-columns: repeat(auto-fill, minmax(0, var(--tile-content-width)));
|
||||
justify-content: start;
|
||||
gap: var(--segment-gap);
|
||||
padding: var(--segment-inset);
|
||||
border: var(--bw-hairline) solid var(--segment-border);
|
||||
border-radius: var(--segment-radius);
|
||||
background: var(--segment-surface);
|
||||
}
|
||||
|
||||
.tile {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
min-width: 0;
|
||||
height: var(--tile-h);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background: var(--surface-panel);
|
||||
border: var(--bw-hairline) solid var(--border-subtle);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--tile-surface);
|
||||
border: var(--bw-hairline) solid var(--tile-border);
|
||||
border-radius: var(--tile-radius);
|
||||
box-shadow: var(--tile-card-shadow);
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
.tile:hover { background: var(--surface-hover); border-color: var(--border-strong); }
|
||||
.tile:hover { background: var(--tile-surface-hover); border-color: var(--border-strong); }
|
||||
.tile[aria-selected="true"] {
|
||||
background: var(--surface-selected);
|
||||
border-color: var(--selection-border);
|
||||
background: var(--tile-surface-hover);
|
||||
border-color: var(--tile-selected-border);
|
||||
border-width: var(--bw-emphasis);
|
||||
padding: calc(var(--space-2) - 1px) calc(var(--space-3) - 1px);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,20 +24,53 @@
|
|||
|
||||
:root {
|
||||
/* ---------- Typografie ---------- */
|
||||
--font-ui: "Inter", "Segoe UI", system-ui, -apple-system, sans-serif;
|
||||
--font-mono: "JetBrains Mono", ui-monospace, "Cascadia Mono", Consolas, monospace;
|
||||
--font-donut-display: "Bahnschrift Condensed", "Arial Narrow", "Aptos Narrow", "Segoe UI", sans-serif;
|
||||
/* Inter wird lokal mit dem Client-Build ausgeliefert. Auch Datenwerte und
|
||||
Rohansichten verwenden dieselbe Familie; Hierarchie entsteht nicht durch
|
||||
wechselnde Schriften, sondern ausschliesslich durch Groesse und Gewicht. */
|
||||
--font-ui: "Inter Variable", sans-serif;
|
||||
--font-mono: var(--font-ui);
|
||||
--font-donut-display: var(--font-ui);
|
||||
|
||||
/* Semantische Typografierollen. Diese Rollen sind die verbindliche Quelle
|
||||
fuer neue Ansichten; die darunterliegende fs-Skala bleibt nur als
|
||||
Kompatibilitaetsschicht fuer bereits migrierte Komponenten bestehen. */
|
||||
--type-h1-size: 56px;
|
||||
--type-h2-size: 32px;
|
||||
--type-h3-size: 18px;
|
||||
--type-detail-title-size: 20px;
|
||||
--type-h4-size: 16px;
|
||||
--type-body-size: 16px;
|
||||
--type-label-size: 14px;
|
||||
--type-meta-size: 13px;
|
||||
--type-badge-size: 12px;
|
||||
--type-table-head-size: 13px;
|
||||
--type-button-size: 14px;
|
||||
|
||||
/* ---------- Verbindlicher Donut-Standard ---------- */
|
||||
/* Der Donut-Rack ist ein Panel wie jedes andere: Rack = Hintergrund,
|
||||
Karte = Element, Ringspur = eingesenkt. Geometrie und Datenfarben des
|
||||
Donut-Standards bleiben unveraendert, nur die Grundtoene folgen jetzt
|
||||
dem Panelstandard. */
|
||||
--donut-rack-surface: #242426;
|
||||
--donut-card-surface: #323238;
|
||||
--donut-card-surface-end: #323238;
|
||||
--donut-track: #242426;
|
||||
--donut-border: #383840;
|
||||
/* ---------- Verbindliche Segment-/Kachelgrammatik ---------- */
|
||||
--segment-surface: #242426;
|
||||
--segment-border: #383840;
|
||||
--segment-radius: 10px;
|
||||
--segment-inset: var(--space-3);
|
||||
--segment-gap: var(--space-2);
|
||||
--tile-surface: #323238;
|
||||
--tile-surface-hover: #3A3A42;
|
||||
--tile-border: #555B65;
|
||||
--tile-selected-border: #A68AF9;
|
||||
--tile-radius: 8px;
|
||||
--tile-compact-width: 186px;
|
||||
--tile-content-width: 320px;
|
||||
--tile-card-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 2px 8px rgba(0, 0, 0, 0.24);
|
||||
|
||||
--donut-rack-surface: var(--segment-surface);
|
||||
--donut-card-surface: var(--tile-surface);
|
||||
--donut-card-surface-end: var(--tile-surface);
|
||||
--donut-track: #555B65;
|
||||
--donut-border: var(--segment-border);
|
||||
--donut-value: #E2DFE6;
|
||||
--donut-label: #B7BAC0;
|
||||
--donut-all: #348CF2;
|
||||
|
|
@ -46,26 +79,29 @@
|
|||
--donut-not-applicable: #7B36F3;
|
||||
--donut-not-run: #8A8E95;
|
||||
--donut-error: #F23C47;
|
||||
--donut-rack-shadow: 0 10px 28px rgba(0, 0, 0, 0.30), inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||
--donut-card-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 2px 8px rgba(0, 0, 0, 0.24);
|
||||
--donut-rack-radius: 10px;
|
||||
--donut-card-radius: 8px;
|
||||
--donut-rack-shadow: none;
|
||||
--donut-card-shadow: var(--tile-card-shadow);
|
||||
--donut-rack-radius: var(--segment-radius);
|
||||
--donut-card-radius: var(--tile-radius);
|
||||
--donut-card-min-h: 196px;
|
||||
--donut-ring-size: 128px;
|
||||
--donut-ring-stroke: 11px;
|
||||
--donut-ring-stroke: 12px;
|
||||
--donut-progress-dash: 0 100;
|
||||
--donut-title-size: 16px;
|
||||
--donut-value-size: 28px;
|
||||
--donut-percent-size: 17px;
|
||||
--donut-value-size: 26px;
|
||||
--donut-percent-size: 14px;
|
||||
--donut-compact-value-size: 22px;
|
||||
--donut-compact-percent-size: 12px;
|
||||
--donut-compact-unit-gap: 2px;
|
||||
--datahub-readiness-dot-size: 14px;
|
||||
--datahub-readiness-dot-inset: var(--space-1);
|
||||
|
||||
--fs-micro: 11px; /* Badges, Spaltenkoepfe, Zaehler */
|
||||
--fs-sm: 12px; /* Tabellenzellen, Sekundaertext, Facetten */
|
||||
--fs-base: 13px; /* Controls, Standardtext, Listenzeilen */
|
||||
--fs-md: 14px; /* Objekttitel (Tile, ListRow, Inspector) */
|
||||
--fs-lg: 16px; /* Panel- und Detailtitel */
|
||||
--fs-xl: 18px; /* Seitentitel in der Topbar. Groesster Wert im System. */
|
||||
--fs-micro: var(--type-meta-size);
|
||||
--fs-sm: var(--type-meta-size);
|
||||
--fs-base: var(--type-label-size);
|
||||
--fs-md: var(--type-label-size);
|
||||
--fs-lg: var(--type-body-size);
|
||||
--fs-xl: var(--type-h3-size);
|
||||
|
||||
/* Pathfinder Micro Card: eigenstaendige, im Graphen begrenzt
|
||||
bildschirmstabil skalierte Knotenkarte. Keine allgemeine
|
||||
|
|
@ -80,8 +116,11 @@
|
|||
--pathfinder-micro-action-highlight-mix: #FFFFFF;
|
||||
|
||||
--fw-regular: 400;
|
||||
--fw-medium: 500;
|
||||
--fw-content-medium: 500;
|
||||
--fw-semibold: 600;
|
||||
--fw-bold: 700;
|
||||
/* Kompatibilitaetsname: visuell gibt es keine vierte Gewichtsstufe. */
|
||||
--fw-medium: var(--fw-semibold);
|
||||
|
||||
--lh-ui: 1.35;
|
||||
--lh-prose: 1.55;
|
||||
|
|
@ -376,11 +415,18 @@
|
|||
color-scheme: light;
|
||||
|
||||
/* Donut-Standard: identische Geometrie, helle Referenzpalette. */
|
||||
--donut-rack-surface: #E7E9ED;
|
||||
--donut-card-surface: #FFFFFF;
|
||||
--donut-card-surface-end: #F3F4F6;
|
||||
--donut-track: #D5D8DE;
|
||||
--donut-border: #B9BEC7;
|
||||
--segment-surface: #E7E9ED;
|
||||
--segment-border: #B9BEC7;
|
||||
--tile-surface: #FFFFFF;
|
||||
--tile-surface-hover: #F3F4F6;
|
||||
--tile-border: #B9BEC7;
|
||||
--tile-selected-border: #D99A14;
|
||||
--tile-card-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.86), 0 2px 7px rgba(23, 25, 29, 0.12);
|
||||
--donut-rack-surface: var(--segment-surface);
|
||||
--donut-card-surface: var(--tile-surface);
|
||||
--donut-card-surface-end: var(--tile-surface);
|
||||
--donut-track: #B9BEC7;
|
||||
--donut-border: var(--segment-border);
|
||||
--donut-value: #20242A;
|
||||
--donut-label: #555B65;
|
||||
--donut-all: #1C70C9;
|
||||
|
|
@ -389,8 +435,8 @@
|
|||
--donut-not-applicable: #6D2CE8;
|
||||
--donut-not-run: #626873;
|
||||
--donut-error: #D42836;
|
||||
--donut-rack-shadow: 0 8px 22px rgba(23, 25, 29, 0.14), inset 0 1px 0 rgba(255, 255, 255, 0.70);
|
||||
--donut-card-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.86), 0 2px 7px rgba(23, 25, 29, 0.12);
|
||||
--donut-rack-shadow: none;
|
||||
--donut-card-shadow: var(--tile-card-shadow);
|
||||
--surface-app: #F1F1F4;
|
||||
--surface-shell: #E4E4EA;
|
||||
--surface-panel: #FFFFFF;
|
||||
|
|
|
|||
|
|
@ -14,5 +14,7 @@ describe('ASSESSOR version in the brand header', () => {
|
|||
expect(app).not.toContain('>v{CURRENT_ASSESSOR_PROCESSING_VERSION}');
|
||||
expect(layout).toMatch(/\.brand-copy\s*\{[^}]*display:\s*grid;[^}]*gap:\s*var\(--space-1\);/s);
|
||||
expect(layout).toMatch(/\.brand-version\s*\{[^}]*font-size:\s*var\(--fs-micro\);[^}]*font-variant-numeric:\s*tabular-nums;/s);
|
||||
expect(layout).toMatch(/\.brand\s*\{[^}]*min-height:\s*112px;[^}]*padding:\s*6px 14px 6px 16px;[^}]*margin-top:\s*0;/s);
|
||||
expect(layout).not.toMatch(/\.app-shell[^,{]* \.brand\s*\{/s);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const main = readFileSync(new URL('../../src/main.tsx', import.meta.url), 'utf8');
|
||||
const tokens = readFileSync(new URL('../../src/styles/ui-tokens.css', import.meta.url), 'utf8');
|
||||
const base = readFileSync(new URL('../../src/styles/base.css', import.meta.url), 'utf8');
|
||||
const datahub = readFileSync(new URL('../../src/styles/datahub.css', import.meta.url), 'utf8');
|
||||
|
||||
describe('binding ASSESSOR typography system', () => {
|
||||
it('bundles one font family and exposes the approved semantic hierarchy', () => {
|
||||
expect(main).toContain("import '@fontsource-variable/inter/wght.css';");
|
||||
expect(tokens).toContain('--font-ui: "Inter Variable", sans-serif;');
|
||||
expect(tokens).toContain('--font-mono: var(--font-ui);');
|
||||
expect(tokens).toContain('--font-donut-display: var(--font-ui);');
|
||||
expect(tokens).toContain('--type-h1-size: 56px;');
|
||||
expect(tokens).toContain('--type-h2-size: 32px;');
|
||||
expect(tokens).toContain('--type-h3-size: 18px;');
|
||||
expect(tokens).toContain('--type-h4-size: 16px;');
|
||||
expect(tokens).toContain('--type-body-size: 16px;');
|
||||
expect(tokens).toContain('--type-label-size: 14px;');
|
||||
expect(tokens).toContain('--type-meta-size: 13px;');
|
||||
expect(tokens).toContain('--type-badge-size: 12px;');
|
||||
expect(tokens).toContain('--type-table-head-size: 13px;');
|
||||
expect(tokens).toContain('--type-button-size: 14px;');
|
||||
expect(tokens).toContain('--fw-regular: 400;');
|
||||
expect(tokens).toContain('--fw-semibold: 600;');
|
||||
expect(tokens).toContain('--fw-bold: 700;');
|
||||
});
|
||||
|
||||
it('uses title case for semantic headings and labels while reserving uppercase for table heads', () => {
|
||||
expect(base).toMatch(/h1\s*\{[^}]*font-size:\s*var\(--type-h1-size\);[^}]*font-weight:\s*var\(--fw-semibold\);/s);
|
||||
expect(base).toMatch(/td\s*\{[^}]*font-size:\s*var\(--type-body-size\);/s);
|
||||
expect(base).toMatch(/th\s*\{[^}]*font-size:\s*var\(--type-table-head-size\);[^}]*text-transform:\s*uppercase;/s);
|
||||
expect(datahub).toMatch(/\.tenant-timeslots-heading\s*\{[^}]*font-size:\s*var\(--type-h3-size\);[^}]*text-transform:\s*none;/s);
|
||||
expect(datahub).toMatch(/\.tenant-information-title > strong\s*\{[^}]*font-size:\s*var\(--type-h3-size\);[^}]*text-transform:\s*none;/s);
|
||||
expect(datahub).toMatch(/\.tenant-detail-page \.tenant-editor-form \.tenant-field-label\s*\{[^}]*font-size:\s*var\(--type-label-size\);[^}]*text-transform:\s*none;/s);
|
||||
});
|
||||
});
|
||||
|
|
@ -4,25 +4,39 @@ import { describe, expect, it } from 'vitest';
|
|||
import { DonutMetricCard } from '../../src/components/DonutMetricCard/DonutMetricCard';
|
||||
|
||||
const styles = readFileSync(new URL('../../src/styles/components.css', import.meta.url), 'utf8');
|
||||
const tileContract = readFileSync(new URL('../../src/styles/tile-contract.css', import.meta.url), 'utf8');
|
||||
const tokens = readFileSync(new URL('../../src/styles/ui-tokens.css', import.meta.url), 'utf8');
|
||||
|
||||
describe('DonutMetricCard binding design contract', () => {
|
||||
it('renders the reference card structure, 270-degree progress and visible values', () => {
|
||||
it('renders the shared full-circle reference with centered percentage and supporting count', () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<DonutMetricCard label="Deviation" value={453} percentage={51} tone="deviation" />
|
||||
);
|
||||
|
||||
expect(html).toContain('class="donut-metric-card donut-metric-card--deviation"');
|
||||
expect(html).toContain('class="donut-metric-card__title">Deviation</h3>');
|
||||
expect(html).toContain('stroke-dasharray="75 25"');
|
||||
expect(html).toContain('stroke-dasharray="38.25 61.75"');
|
||||
expect(html).toContain('<strong>453</strong>');
|
||||
expect(html).toContain('<span>51%</span>');
|
||||
expect(html).toContain('--donut-progress-dash:38.25 61.75');
|
||||
expect(html).toContain('class="donut-metric-card__track" cx="60" cy="60" r="48" pathLength="100"');
|
||||
expect(html).toContain('stroke-dasharray="51 49"');
|
||||
expect(html).toContain('<div class="donut-metric-card__primary"><strong>51</strong><span>%</span></div>');
|
||||
expect(html).toContain('<small>453</small>');
|
||||
expect(html).toContain('--donut-progress-dash:51 49');
|
||||
expect(styles).toContain('transform: rotate(-90deg);');
|
||||
expect(styles).toContain('left: calc(100% + var(--space-1));');
|
||||
expect(styles).toMatch(/\.donut-metric-card__primary\s*\{[^}]*position:\s*relative;/s);
|
||||
expect(styles).not.toContain('stroke-linecap: round;');
|
||||
expect(styles).toContain('@keyframes donut-progress-build');
|
||||
expect(styles).toContain('@keyframes donut-accent-build');
|
||||
expect(styles).toContain('animation: donut-progress-build var(--dur-data-build) var(--ease-data-build) both;');
|
||||
expect(styles).toMatch(/\.donut-metric-rack\s*\{[^}]*grid-template-columns:\s*repeat\(6, minmax\(0, 1fr\)\);/s);
|
||||
expect(tileContract).toMatch(/\.donut-metric-rack\s*\{[^}]*grid-template-columns:\s*repeat\(auto-fill, var\(--tile-compact-width\)\);[^}]*justify-content:\s*start;/s);
|
||||
expect(styles).toMatch(/@media \(max-width: 1100px\)\s*\{[^}]*repeat\(3, minmax\(0, 1fr\)\)/s);
|
||||
expect(styles).toMatch(/@media \(max-width: 760px\)\s*\{[^}]*repeat\(2, minmax\(0, 1fr\)\)/s);
|
||||
expect(tokens).toContain('--dur-data-build: 700ms;');
|
||||
expect(tokens).toContain('--donut-value-size: 26px;');
|
||||
expect(tokens).toContain('--donut-percent-size: 14px;');
|
||||
expect(tokens).toContain('--donut-compact-value-size: 22px;');
|
||||
expect(tokens).toContain('--donut-compact-percent-size: 12px;');
|
||||
expect(styles).toMatch(/\.sidebar-severity-card \.donut-metric-card__primary span\s*\{[^}]*left:\s*calc\(100% \+ var\(--donut-compact-unit-gap\)\);/s);
|
||||
});
|
||||
|
||||
it('renders an empty progress arc at zero and clamps percentages', () => {
|
||||
|
|
@ -34,9 +48,10 @@ describe('DonutMetricCard binding design contract', () => {
|
|||
);
|
||||
|
||||
expect(zero).toContain('stroke-dasharray="0 100"');
|
||||
expect(zero).toContain('<span>0%</span>');
|
||||
expect(clamped).toContain('stroke-dasharray="75 25"');
|
||||
expect(clamped).toContain('<span>100%</span>');
|
||||
expect(zero).toContain('<strong>0</strong><span>%</span>');
|
||||
expect(clamped).toContain('stroke-dasharray="100 0"');
|
||||
expect(clamped).toContain('donut-metric-card__primary is-compact');
|
||||
expect(clamped).toContain('<strong>100</strong><span>%</span>');
|
||||
expect(zero).not.toContain('<animate');
|
||||
});
|
||||
|
||||
|
|
@ -46,7 +61,7 @@ describe('DonutMetricCard binding design contract', () => {
|
|||
);
|
||||
|
||||
expect(html).toContain('<strong>36%</strong>');
|
||||
expect(html).not.toContain('<span>36%</span>');
|
||||
expect(html).not.toContain('<span>%</span>');
|
||||
});
|
||||
|
||||
it('becomes an accessible active filter control when clickable', () => {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ describe('ResizableResultTable', () => {
|
|||
Object.defineProperty(firstColumnCells[1], 'scrollWidth', { configurable: true, value: 238 });
|
||||
|
||||
act(() => container.querySelector('.result-column-resizer')?.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true })));
|
||||
expect(container.querySelector('col')?.getAttribute('width')).toBe('240');
|
||||
expect((container.querySelector('col') as HTMLTableColElement | null)?.style.width).toBe('240px');
|
||||
expect(container.querySelector('table')?.classList.contains('has-custom-column-widths')).toBe(true);
|
||||
|
||||
let handle = container.querySelector('.result-column-resizer') as HTMLElement;
|
||||
|
|
@ -49,7 +49,7 @@ describe('ResizableResultTable', () => {
|
|||
act(() => handle.dispatchEvent(pointerEvent('pointerdown', 7, 100)));
|
||||
handle = container.querySelector('.result-column-resizer') as HTMLElement;
|
||||
act(() => handle.dispatchEvent(pointerEvent('pointermove', 7, 148)));
|
||||
expect(container.querySelector('col')?.getAttribute('width')).toBe('168');
|
||||
expect((container.querySelector('col') as HTMLTableColElement | null)?.style.width).toBe('168px');
|
||||
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
|
@ -66,7 +66,7 @@ describe('ResizableResultTable', () => {
|
|||
headers.forEach(cell => { cell.getBoundingClientRect = () => ({ width: 100 } as DOMRect); });
|
||||
|
||||
act(() => container.querySelector('.result-column-resizer')?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true })));
|
||||
expect(container.querySelector('col')?.getAttribute('width')).toBe('116');
|
||||
expect((container.querySelector('col') as HTMLTableColElement | null)?.style.width).toBe('116px');
|
||||
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
|
|
|||
|
|
@ -85,17 +85,16 @@ const prohibitions = [
|
|||
label: 'einen Schatten ausserhalb der Overlay-Rollen',
|
||||
/* `inset` bleibt ausgenommen: eingesenkte Wertfelder sind Flaechen-
|
||||
* modellierung, kein schwebendes Element. */
|
||||
/* Der bindende Donut-Vertrag vom 2026-08-05 traegt zwei eigene,
|
||||
* tokenisierte Tiefenschatten. Sie sind Teil der Nutzerreferenz und
|
||||
* ausschliesslich an DonutMetricCard/Rack gebunden. */
|
||||
matches: line => line.includes('box-shadow') && !/shadow-overlay|shadow-popover|inset|var\(--donut-(?:rack|card)-shadow\)/.test(line),
|
||||
remedy: 'Schatten tragen nur echte Overlays (--shadow-overlay, --shadow-popover). Panels grenzen ueber Haarlinien ab.',
|
||||
/* Der bindende Segment-/Kachelvertrag erlaubt genau den gemeinsamen,
|
||||
* dezenten Kachelschatten. Segmente selbst bleiben flach. */
|
||||
matches: line => line.includes('box-shadow') && !/box-shadow:\s*none|shadow-overlay|shadow-popover|inset|var\(--(?:donut-(?:rack|card)-shadow|tile-card-shadow)\)/.test(line),
|
||||
remedy: 'Nur echte Overlays und Kacheln verwenden ihre gemeinsamen Schattentokens; Segmente und Panels grenzen ueber Haarlinien ab.',
|
||||
budgets: new Map([
|
||||
['components.css', 6],
|
||||
['components.css', 2],
|
||||
['datahub.css', 2],
|
||||
['import-workflow.css', 1],
|
||||
['layout.css', 7],
|
||||
['legacy-views.css', 6]
|
||||
['layout.css', 1],
|
||||
['legacy-views.css', 0]
|
||||
])
|
||||
},
|
||||
{
|
||||
|
|
@ -310,13 +309,18 @@ assert.match(searchFilterControls, /placeholder="Search"/, 'Das Freitextfeld mus
|
|||
assert.doesNotMatch(searchFilterControls, /Filter text/, 'Die alte Bezeichnung Filter text darf nicht sichtbar bleiben.');
|
||||
assert.match(
|
||||
shellLayout,
|
||||
/\.area-topbar strong\s*\{[^}]*font-size:\s*calc\(var\(--fs-xl\) \* 4\);[^}]*font-weight:\s*var\(--fw-semibold\);[^}]*line-height:\s*1\.15;[^}]*letter-spacing:\s*-\.04em;/,
|
||||
/\.area-topbar strong\s*\{[^}]*font-size:\s*var\(--type-h1-size\);[^}]*font-weight:\s*var\(--fw-semibold\);[^}]*line-height:\s*1\.1;[^}]*letter-spacing:\s*-\.04em;/,
|
||||
'layout.css muss die Bereichsbezeichnungen aller regulaeren Kopfpanels wie den Mandantentitel darstellen.'
|
||||
);
|
||||
assert.match(
|
||||
shellLayout,
|
||||
/\.app-shell:not\(\.app-shell-work\) \.brand\s*\{[^}]*min-height:\s*112px;[^}]*padding-block:\s*6px;[^}]*margin-top:\s*0;/,
|
||||
'layout.css muss die Marke mittig zur horizontalen Achse der grossen Bereichsbezeichnung ausrichten.'
|
||||
/\.brand\s*\{[^}]*min-height:\s*112px;[^}]*padding:\s*6px 14px 6px 16px;[^}]*margin-top:\s*0;/,
|
||||
'layout.css muss die Marke in allen Ansichten fest auf der Datahub-Position halten.'
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
shellLayout,
|
||||
/\.app-shell[^,{]* \.brand\s*\{/,
|
||||
'Ansichtsspezifische Brand-Geometrie ist nicht erlaubt, damit Logo und Schrift beim Ansichtswechsel nicht springen.'
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
shellLayout,
|
||||
|
|
|
|||
|
|
@ -18,9 +18,13 @@ describe('dashboard reference layout', () => {
|
|||
expect(component).toContain('<span>Error</span>');
|
||||
expect(component).toContain('className="dashboard-ioe-summary" type="button"');
|
||||
expect(component).toContain('setPathFindingIds(explicitIoeRecordIds)');
|
||||
expect(styles).toMatch(/\.dashboard-status-rack\s*\{[^}]*repeat\(3,/s);
|
||||
expect(styles).toContain('.dashboard-status-ring-track { stroke: var(--donut-border); }');
|
||||
expect(styles).toContain('button.dashboard-status-metric:hover { border-color: currentColor; }');
|
||||
expect(styles).toMatch(/\.dashboard-status-rack\s*\{[^}]*grid-template-columns:\s*290px repeat\(3, 210px\) 250px;/s);
|
||||
expect(styles).toMatch(/\.dashboard-status-copy > strong\s*\{[^}]*display:\s*grid;[^}]*white-space:\s*normal;/s);
|
||||
expect(styles).toMatch(/\.dashboard-status-copy > strong small, \.dashboard-status-copy > small\s*\{[^}]*display:\s*block;/s);
|
||||
expect(styles).toContain('grid-template-columns: repeat(2, minmax(0, 290px));');
|
||||
expect(component).toContain("{value.toLocaleString('en-US')}<small>{roundedPercentage}%</small>");
|
||||
expect(styles).toContain('.dashboard-status-ring-track { stroke: var(--donut-track); }');
|
||||
expect(styles).toContain('button.dashboard-status-metric:hover { border-color: currentColor; background: var(--tile-surface-hover); }');
|
||||
});
|
||||
|
||||
it('uses paired segmented distributions and a compact lower dashboard row', () => {
|
||||
|
|
@ -39,6 +43,10 @@ describe('dashboard reference layout', () => {
|
|||
expect(component).toContain('className="dashboard-risk-tool"');
|
||||
expect(component).toContain('<option value={fullRiskProfileId}>Full Risk Profile</option>');
|
||||
expect(component).toContain('className="dashboard-risk-donut"');
|
||||
expect(component).toContain('<span className="dashboard-donut-primary is-compact"><strong>100</strong><small>%</small></span>');
|
||||
expect(styles).toContain('.dashboard-risk-donut-track { stroke: var(--donut-track); }');
|
||||
expect(styles).toMatch(/\.dashboard-donut-primary\s*\{[^}]*position:\s*relative;/s);
|
||||
expect(styles).toMatch(/\.dashboard-status-ring > span small\s*\{[^}]*left:\s*calc\(100% \+ var\(--space-1\)\);/s);
|
||||
expect(component).toContain('categories.slice(0, 4)');
|
||||
expect(component).toContain('matchesDynamicFacets(record, [sourceFacet]');
|
||||
expect(component).not.toContain('dashboard-review-block');
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const baselines = readFileSync(new URL('../../src/features/project-import/Micros
|
|||
describe('Datahub centered reference rack layout', () => {
|
||||
it('uses the binding six-card donut design for the selected timeslot', () => {
|
||||
expect(datahubStyles).toContain('.datahub-page > .datahub-rack-module { width: min(1180px, 100%); margin-inline: auto; }');
|
||||
expect(datahubStyles).toContain('.datahub-tenant-card-rack { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr));');
|
||||
expect(datahubStyles).toContain('.datahub-tenant-card-rack { display: grid; grid-template-columns: repeat(auto-fill, var(--tile-compact-width)); justify-content: start;');
|
||||
expect(datahubStyles).toContain('.datahub-tenant-card.is-selected');
|
||||
expect(datahubStyles).toContain('.datahub-tenant-processing-dot { position: absolute; z-index: 3; top: var(--datahub-readiness-dot-inset); left: var(--datahub-readiness-dot-inset);');
|
||||
expect(datahubStyles).toContain('background: var(--state-online); pointer-events: none;');
|
||||
|
|
@ -24,11 +24,13 @@ describe('Datahub centered reference rack layout', () => {
|
|||
expect(datahubStyles).toContain('.datahub-tenant-processing-dot[data-status="error"] { background: var(--state-offline); }');
|
||||
expect(datahubStyles).toContain('.datahub-tenant-show[data-status="update"]');
|
||||
expect(datahubStyles).toContain('border-color: var(--warning); color: var(--warning);');
|
||||
expect(datahubStyles).toContain('outline: var(--bw-emphasis) solid var(--button-marked-bg);');
|
||||
expect(datahubStyles).toContain('outline: var(--bw-emphasis) solid var(--tile-selected-border);');
|
||||
expect(datahubStyles).toContain('.datahub-tenant-card::before { content: none; }');
|
||||
expect(datahubStyles).toContain('transition: background .12s ease, border-color .12s ease, transform .12s ease;');
|
||||
expect(datahubStyles).toMatch(/\.datahub-tenant-card:hover,\s*\.datahub-tenant-card:focus-within\s*\{[^}]*border-color:\s*var\(--line-strong\);[^}]*background:\s*var\(--panel-hover\);[^}]*transform:\s*translateY\(-2px\);/s);
|
||||
expect(datahubStyles).not.toContain('box-shadow: var(--shadow-raised-hover)');
|
||||
expect(datahubStyles).toContain('transition: background .12s ease, border-color .12s ease;');
|
||||
expect(datahubStyles).toMatch(/\.datahub-tenant-card:hover,\s*\.datahub-tenant-card:focus-within\s*\{[^}]*border-color:\s*var\(--line-strong\);[^}]*background:\s*var\(--tile-surface-hover\);[^}]*box-shadow:\s*var\(--tile-card-shadow\);/s);
|
||||
expect(datahubStyles).not.toMatch(/\.datahub-tenant-card\s*\{[^}]*box-shadow/s);
|
||||
expect(datahubStyles).toContain('box-shadow: var(--tile-card-shadow);');
|
||||
expect(datahubStyles).not.toContain('translateY(-2px)');
|
||||
expect(datahubStyles).toContain('.datahub-tenant-select { position: absolute; inset: 0;');
|
||||
expect(datahubStyles).toContain('grid-template-rows: minmax(0, 1fr) auto auto;');
|
||||
expect(datahubStyles).toContain('font: var(--fw-semibold) calc(var(--donut-title-size) * 1.35)');
|
||||
|
|
@ -45,12 +47,13 @@ describe('Datahub centered reference rack layout', () => {
|
|||
|
||||
expect(componentStyles).toContain('Verbindlicher Donut-Standard: Referenz vom 2026-08-05.');
|
||||
expect(componentStyles).toContain('grid-template-columns: repeat(6, minmax(0, 1fr));');
|
||||
expect(componentStyles).toContain('padding: var(--space-3);');
|
||||
expect(componentStyles).toContain('padding: var(--segment-inset);');
|
||||
expect(componentStyles).toContain('.donut-metric-card::before');
|
||||
expect(componentStyles).toContain('border-radius: var(--donut-card-radius);');
|
||||
expect(componentStyles).toContain('background: linear-gradient(145deg, var(--donut-card-surface), var(--donut-card-surface-end));');
|
||||
expect(componentStyles).toContain('border-radius: var(--tile-radius);');
|
||||
expect(componentStyles).toContain('background: var(--tile-surface);');
|
||||
expect(componentStyles).toContain('.donut-metric-card__progress');
|
||||
expect(componentStyles).toContain('stroke-linecap: round;');
|
||||
expect(componentStyles).not.toContain('stroke-linecap: round;');
|
||||
expect(componentStyles).toContain('transform: rotate(-90deg);');
|
||||
expect(componentStyles).toContain('.donut-metric-card--all { color: var(--donut-all); }');
|
||||
expect(componentStyles).toContain('.donut-metric-card--deviation { color: var(--donut-deviation); }');
|
||||
expect(componentStyles).toContain('.donut-metric-card--passed { color: var(--donut-passed); }');
|
||||
|
|
@ -58,16 +61,19 @@ describe('Datahub centered reference rack layout', () => {
|
|||
expect(componentStyles).toContain('.donut-metric-card--not-run { color: var(--donut-not-run); }');
|
||||
expect(componentStyles).toContain('.donut-metric-card--error { color: var(--donut-error); }');
|
||||
|
||||
expect(tokens).toContain('--font-donut-display: "Bahnschrift Condensed"');
|
||||
expect(tokens).toContain('--font-ui: "Inter Variable", sans-serif;');
|
||||
expect(tokens).toContain('--font-donut-display: var(--font-ui);');
|
||||
expect(tokens).toContain('--donut-rack-surface:');
|
||||
expect(tokens).toContain('--donut-ring-size: 128px;');
|
||||
expect(tokens).toContain('--donut-ring-stroke: 12px;');
|
||||
expect(tokens).toContain('--datahub-readiness-dot-size: 14px;');
|
||||
expect(tokens).toContain('--datahub-readiness-dot-inset: var(--space-1);');
|
||||
expect(tokens).toContain('Donut-Standard: identische Geometrie, helle Referenzpalette.');
|
||||
expect(tokens).toContain('--donut-rack-surface: #E7E9ED;');
|
||||
expect(tokens).toContain('--segment-surface: #E7E9ED;');
|
||||
expect(tokens).toContain('--donut-rack-surface: var(--segment-surface);');
|
||||
expect(tokens).toContain('--donut-deviation: #9A6500;');
|
||||
expect(donut).toContain('strokeDasharray="75 25"');
|
||||
expect(donut).toContain('const arcLength = boundedPercentage * 0.75;');
|
||||
expect(donut).toContain('<circle className="donut-metric-card__track" cx="60" cy="60" r="48" pathLength="100" />');
|
||||
expect(donut).toContain("const dashArray = String(boundedPercentage) + ' ' + String(100 - boundedPercentage);");
|
||||
expect(donut).toContain("const progressStyle = { '--donut-progress-dash': dashArray } as CSSProperties;");
|
||||
expect(donut).toContain('key={label + \'-\' + value + \'-\' + boundedPercentage}');
|
||||
expect(donut).not.toContain('<animate');
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ describe('Mandant detail reference', () => {
|
|||
expect(dataHub).toContain('className="tenant-field-value tenant-field-prose"');
|
||||
expect(dataHub).toContain('<span className="tenant-field-label">Name');
|
||||
expect(dataHub).not.toContain('tenant-identity-name-input');
|
||||
expect(styles).toMatch(/\.tenant-detail-page \.tenant-editor-form \.tenant-field-value\s*\{[^}]*justify-self:\s*start;[^}]*font-size:\s*var\(--fs-lg\);[^}]*text-align:\s*left;/s);
|
||||
expect(styles).toMatch(/\.tenant-detail-page \.tenant-editor-form \.tenant-field-value\s*\{[^}]*justify-self:\s*start;[^}]*font-size:\s*var\(--type-body-size\);[^}]*text-align:\s*left;/s);
|
||||
expect(dataHub).toContain('const [excludedScanRunIds, setExcludedScanRunIds]');
|
||||
expect(dataHub).toMatch(/const openEdit = \(project: AssessmentProject\) => \{[\s\S]*?setSelectedTimeslotIds\(latestTimeslot \? \[latestTimeslot\.id\] : \[\]\);[\s\S]*?setExpandedTimeslotIds\(affectedTimeslotIds\);/);
|
||||
expect(dataHub).toContain('const toggleScanReport =');
|
||||
|
|
@ -81,9 +81,9 @@ describe('Mandant detail reference', () => {
|
|||
expect(styles).toMatch(/\.tenant-detail-page \.tenant-editor-form\s*\{[^}]*grid-template-columns:\s*minmax\(0, 1fr\);/s);
|
||||
expect(styles).toMatch(/accent-color:\s*var\(--state-active\);/s);
|
||||
expect(styles).toMatch(/\.tenant-detail-page \.tenant-editor-form label\s*\{[^}]*display:\s*block;[^}]*border-bottom:/s);
|
||||
expect(styles).toMatch(/\.tenant-field-label\s*\{[^}]*background:\s*transparent;[^}]*text-transform:\s*uppercase;/s);
|
||||
expect(styles).toMatch(/\.tenant-detail-page \.tenant-editor-form \.tenant-field-label\s*\{[^}]*background:\s*transparent;[^}]*font-size:\s*var\(--type-label-size\);[^}]*text-transform:\s*none;/s);
|
||||
expect(styles).toMatch(/\.tenant-report-copy b\s*\{[^}]*font-size:\s*var\(--fs-lg\);/s);
|
||||
expect(styles).toMatch(/\.tenant-report-copy small\s*\{[^}]*font-size:\s*var\(--fs-sm\);/s);
|
||||
expect(styles).toMatch(/\.tenant-report-copy small\s*\{[^}]*font-size:\s*var\(--type-meta-size\);/s);
|
||||
expect(styles).toMatch(/\.tenant-report-row\s*\{[^}]*grid-template-columns:[^;}]*calc\(var\(--space-8\) \* 4\.25\)/s);
|
||||
expect(dataHub).toContain('rows={10} placeholder="Notes about the client or project"');
|
||||
expect(styles).toMatch(/\.tenant-detail-page \.tenant-editor-form textarea\.input\s*\{[^}]*width:\s*100%;/s);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const components = readFileSync(new URL('../../src/styles/components.css', import.meta.url), 'utf8');
|
||||
const legacyViews = readFileSync(new URL('../../src/styles/legacy-views.css', import.meta.url), 'utf8');
|
||||
const tokens = readFileSync(new URL('../../src/styles/tokens.css', import.meta.url), 'utf8');
|
||||
|
||||
describe('Finding title typography', () => {
|
||||
it('uses the emphasized 20px detail-title role in the reader', () => {
|
||||
expect(components).toMatch(/\.reader-heading h2\s*\{[^}]*font-size:\s*var\(--type-detail-title-size\);/s);
|
||||
expect(components).toMatch(/\.reader-summary h3\s*\{[^}]*font-size:\s*var\(--type-h3-size\);/s);
|
||||
expect(legacyViews).toMatch(/\.reader-heading h2\s*\{[^}]*font-size:\s*var\(--type-detail-title-size\);/s);
|
||||
expect(legacyViews).toMatch(/\.reader-summary h3\s*\{[^}]*font-size:\s*var\(--type-h3-size\);/s);
|
||||
});
|
||||
|
||||
it('uses the readable 16px content-title role in list and matrix cards', () => {
|
||||
expect(components).toMatch(/\.record-list \.finding-card h3\s*\{[^}]*font-size:\s*var\(--type-h4-size\);/s);
|
||||
expect(components).toMatch(/\.record-list \.finding-card h3\s*\{[^}]*font-weight:\s*var\(--fw-content-medium\);/s);
|
||||
expect(components).toMatch(/\.finding-card h3\s*\{[^}]*font-size:\s*var\(--type-h4-size\);/s);
|
||||
expect(legacyViews).toMatch(/\.data-tile-content h3\s*\{[^}]*font-size:\s*var\(--type-h4-size\);/s);
|
||||
});
|
||||
|
||||
it('uses the compact badge role for finding labels and outcomes', () => {
|
||||
expect(tokens).toContain('--ui-badge-size: var(--type-badge-size);');
|
||||
expect(legacyViews).toMatch(/\.finding-badges, \.mitre-line\s*\{[^}]*font-size:\s*var\(--ui-badge-size\);/s);
|
||||
expect(legacyViews).toMatch(/\.status-badge, \.severity-badge, \.mitre-badge, \.reader-severity\s*\{[^}]*font-size:\s*var\(--ui-badge-size\);/s);
|
||||
});
|
||||
});
|
||||
|
|
@ -224,8 +224,8 @@ describe('Obsidian-style interactive graph', () => {
|
|||
expect(graphStyles).toMatch(/\.obsidian-graph-card\.is-selected \.obsidian-node-card,[\s\S]*?\.obsidian-graph-card\.is-selected \.obsidian-node-matrix-card \.data-tile\s*\{/s);
|
||||
expect(graphStyles).toMatch(/\.obsidian-node-matrix-card\s*\{[^}]*width:\s*100%;[^}]*height:\s*100%;/s);
|
||||
expect(graphStyles).not.toContain('--graph-card-content-scale');
|
||||
expect(graphStyles).toMatch(/\.obsidian-node-card\.data-tile:hover,[\s\S]*?\{[^}]*box-shadow:\s*inset 0 0 0 0 transparent;[^}]*transform:\s*none;/);
|
||||
expect(graphStyles).toMatch(/\.obsidian-node-matrix-card \.data-tile:hover,[\s\S]*?\{[^}]*box-shadow:\s*inset 0 0 0 0 transparent;[^}]*transform:\s*none;/);
|
||||
expect(graphStyles).not.toContain('box-shadow');
|
||||
expect(graphStyles).not.toContain('translateY(-2px)');
|
||||
expect(graphStyles).toMatch(/\.obsidian-node-card \.data-tile-content h3\s*\{[^}]*font-size:\s*var\(--pathfinder-micro-title-size\);/s);
|
||||
expect(graphStyles).toMatch(/\.obsidian-node-inline-label\s*\{[^}]*font-size:\s*var\(--fs-lg\);/s);
|
||||
expect(graphStyles).toMatch(/\.obsidian-node-card \.reader-severity\s*\{[^}]*font-size:\s*var\(--pathfinder-micro-relation-badge-size\);/s);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ describe('rounded panel content inset standard', () => {
|
|||
});
|
||||
|
||||
it('uses the shared inset in every Work presentation including responsive tiles', () => {
|
||||
expect(layout).toMatch(/\.tile-grid\s*\{[^}]*padding:\s*var\(--panel-inset\);/s);
|
||||
expect(layout).toMatch(/\.tile-view-scroll\s*\{[^}]*padding:\s*var\(--panel-inset\);/s);
|
||||
expect(layout).toMatch(/\.raw-view\s*\{[^}]*padding:\s*var\(--panel-inset\);/s);
|
||||
expect(legacy).toMatch(/\.records-layout\s*\{[^}]*padding:\s*var\(--panel-inset\);/s);
|
||||
expect(legacy).toMatch(/\.table-scroll\s*\{[^}]*padding:\s*var\(--panel-inset\);/s);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const read = (path: string) => readFileSync(new URL(path, import.meta.url), 'utf8');
|
||||
const tokens = read('../../src/styles/ui-tokens.css');
|
||||
const components = read('../../src/styles/components.css');
|
||||
const tileContract = read('../../src/styles/tile-contract.css');
|
||||
const datahub = read('../../src/styles/datahub.css');
|
||||
const datahubRedesign = read('../../src/styles/datahub-redesign.css');
|
||||
const legacy = read('../../src/styles/legacy-views.css');
|
||||
const layout = read('../../src/styles/layout.css');
|
||||
const uiComponents = read('../../src/styles/ui-components.css');
|
||||
const policy = read('../../src/styles/policy-analysis.css');
|
||||
const pathfinder = read('../../src/styles/mitre-graph.css');
|
||||
const viewer = read('../../src/features/dataset-viewer/DatasetViewer.tsx');
|
||||
const overview = read('../../src/features/project-import/DatahubHistoryOverview.tsx');
|
||||
const guidance = read('../../AGENTS.md');
|
||||
|
||||
describe('binding segment and tile grammar', () => {
|
||||
it('defines one semantic geometry and surface contract for both themes', () => {
|
||||
for (const token of [
|
||||
'--segment-surface:', '--segment-border:', '--segment-radius: 10px;',
|
||||
'--segment-inset: var(--space-3);', '--segment-gap: var(--space-2);',
|
||||
'--tile-surface:', '--tile-surface-hover:', '--tile-border:',
|
||||
'--tile-selected-border:', '--tile-radius: 8px;',
|
||||
'--tile-compact-width: 186px;', '--tile-content-width: 320px;',
|
||||
'--tile-card-shadow:'
|
||||
]) expect(tokens).toContain(token);
|
||||
|
||||
expect(tokens).toContain('--donut-rack-surface: var(--segment-surface);');
|
||||
expect(tokens).toContain('--donut-card-surface: var(--tile-surface);');
|
||||
expect(tokens).toContain('--donut-card-shadow: var(--tile-card-shadow);');
|
||||
});
|
||||
|
||||
it('binds the shared grammar to Datahub, dashboard, sidebar, Work, policy and Pathfinder tiles', () => {
|
||||
expect(components).toMatch(/\.donut-metric-rack\s*\{[^}]*grid-template-columns:\s*repeat\(6, minmax\(0, 1fr\)\);[^}]*padding:\s*var\(--segment-inset\);[^}]*border-radius:\s*var\(--segment-radius\);/s);
|
||||
expect(tileContract).toMatch(/\.donut-metric-rack\s*\{[^}]*grid-template-columns:\s*repeat\(auto-fill, var\(--tile-compact-width\)\);[^}]*justify-content:\s*start;/s);
|
||||
expect(components).toMatch(/\.donut-metric-card\s*\{[^}]*border-radius:\s*var\(--tile-radius\);[^}]*background:\s*var\(--tile-surface\);[^}]*box-shadow:\s*var\(--tile-card-shadow\);/s);
|
||||
expect(components).toMatch(/\.sidebar-severity-rack\s*\{[^}]*gap:\s*var\(--segment-gap\);[^}]*background:\s*var\(--segment-surface\);/s);
|
||||
|
||||
expect(datahubRedesign).toMatch(/\.datahub-tenant-card-rack\s*\{[^}]*repeat\(auto-fill, var\(--tile-compact-width\)\);[^}]*justify-content:\s*start;/s);
|
||||
expect(datahubRedesign).toMatch(/\.datahub-tenant-card\.is-selected\s*\{[^}]*border-color:\s*var\(--tile-selected-border\);/s);
|
||||
expect(overview).toContain('donut-metric-card donut-metric-card--all datahub-tenant-card datahub-tenant-add');
|
||||
|
||||
expect(datahub).toMatch(/\.dashboard-status-rack\s*\{[^}]*padding:\s*var\(--segment-inset\);[^}]*background:\s*var\(--segment-surface\);/s);
|
||||
expect(datahub).toMatch(/\.dashboard-status-metric\s*\{[^}]*border-radius:\s*var\(--tile-radius\);[^}]*box-shadow:\s*var\(--tile-card-shadow\);/s);
|
||||
expect(datahub).toMatch(/\.dashboard-distribution-grid\s*\{[^}]*border-radius:\s*var\(--segment-radius\);/s);
|
||||
expect(datahub).toMatch(/\.db-structure-card\s*\{[^}]*background:\s*var\(--tile-surface\);[^}]*box-shadow:\s*var\(--tile-card-shadow\);/s);
|
||||
|
||||
expect(layout).toMatch(/\.tile-view-scroll\s*\{[^}]*padding:\s*var\(--panel-inset\);/s);
|
||||
expect(legacy).toMatch(/\.tile-grid\s*\{[^}]*justify-content:\s*start;[^}]*border-radius:\s*var\(--segment-radius\);/s);
|
||||
expect(legacy).toMatch(/\.data-tile\s*\{[^}]*border-radius:\s*var\(--tile-radius\);[^}]*box-shadow:\s*var\(--tile-card-shadow\);/s);
|
||||
expect(viewer).toContain('gridTemplateColumns: `repeat(${tileColumns}, minmax(var(--tile-content-width), 1fr))`');
|
||||
|
||||
expect(policy).toContain('grid-template-columns: repeat(auto-fill, var(--tile-compact-width));');
|
||||
expect(pathfinder).toMatch(/\.finding-path-card\s*\{[^}]*border-radius:\s*var\(--tile-radius\);[^}]*box-shadow:\s*var\(--tile-card-shadow\);/s);
|
||||
});
|
||||
|
||||
it('keeps the rules binding and prevents status-filled or stretched substitute tiles', () => {
|
||||
expect(guidance).toContain('Treat segments and tiles as a binding two-level visual grammar independent of their content.');
|
||||
expect(guidance).toContain('never stretch a partial final row merely to fill the container');
|
||||
expect(guidance).toContain('Never fill the complete tile with a status color.');
|
||||
expect(guidance).toContain('Tile content must never cross the tile boundary or cover a neighboring tile.');
|
||||
expect(guidance).toContain('never split ordinary words or numeric values across lines');
|
||||
expect(datahub).toMatch(/\.dashboard-status-metric\s*\{[^}]*min-width:\s*0;[^}]*overflow:\s*hidden;/s);
|
||||
expect(datahub).toMatch(/\.dashboard-status-copy\s*\{[^}]*min-width:\s*0;[^}]*overflow:\s*hidden;/s);
|
||||
expect(datahub).toMatch(/\.dashboard-status-copy\s*>\s*span\s*\{[^}]*overflow-wrap:\s*normal;[^}]*word-break:\s*normal;/s);
|
||||
expect(datahub).toMatch(/\.dashboard-status-copy\s*>\s*strong\s*\{[^}]*display:\s*grid;[^}]*white-space:\s*normal;/s);
|
||||
expect(legacy).toMatch(/\.data-tile\s*\{[^}]*min-width:\s*0;[^}]*overflow:\s*hidden;/s);
|
||||
expect(uiComponents).toMatch(/\.tile\s*\{[^}]*min-width:\s*0;[^}]*overflow:\s*hidden;/s);
|
||||
expect(datahubRedesign).not.toMatch(/\.datahub-tenant-card(?::hover|:focus-within|\s*\{)[^}]*translateY/s);
|
||||
expect(legacy).not.toMatch(/\.data-tile(?::hover|:focus-visible|\s*\{)[^}]*translateY/s);
|
||||
expect(datahub).not.toMatch(/\.dashboard-status-metric(?::hover|\s*\{)[^}]*translateY/s);
|
||||
});
|
||||
});
|
||||
|
|
@ -44,7 +44,7 @@ assert.doesNotMatch(indexHtml, /<(?:script|link)\b[^>]+(?:src|href)=["']https?:/
|
|||
const packageJson = JSON.parse(read('package.json'));
|
||||
assert.deepEqual(
|
||||
Object.keys(packageJson.dependencies).sort(),
|
||||
['jsdom', 'pg', 'react', 'react-dom', 'saxes', 'xlsx'],
|
||||
['@fontsource-variable/inter', 'jsdom', 'pg', 'react', 'react-dom', 'saxes', 'xlsx'],
|
||||
'production dependencies changed; review same-origin and local-processing guarantees before accepting new packages'
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -104,4 +104,27 @@ describe('Node import runtime', () => {
|
|||
severity: 'info'
|
||||
}));
|
||||
}, 30_000);
|
||||
|
||||
it('routes a 1.6 MB PingCastle-style HTML report through bounded extraction before the worker time limit', async () => {
|
||||
const html = '<!doctype html><html><head><title>PingCastle Healthcheck</title></head><body>'
|
||||
+ '<!--' + 'x'.repeat(1_600_000) + '-->'
|
||||
+ '<table id="rules"><caption>Rules</caption>'
|
||||
+ '<tr><th>Rule ID</th><th>Title</th><th>Severity</th></tr>'
|
||||
+ '<tr><td>PC-BOUNDED-1</td><td>Bounded synthetic report</td><td>High</td></tr>'
|
||||
+ '</table></body></html>';
|
||||
const bytes = new TextEncoder().encode(html);
|
||||
const dataset = await importSource({
|
||||
metadata: { name: 'ad_hc_bounded.synthetic.html', sizeBytes: bytes.length, lastModified: 0 }, bytes
|
||||
}, { profiles: importProfiles }, nodeImportRuntimeAdapters);
|
||||
|
||||
expect(dataset.format).toBe('HTML');
|
||||
expect(dataset.profileId).toBe('pingcastle');
|
||||
expect(dataset.sheets.find(sheet => sheet.name === 'Rules')?.rows[1]).toEqual([
|
||||
'PC-BOUNDED-1', 'Bounded synthetic report', 'High'
|
||||
]);
|
||||
expect(dataset.intermediate?.diagnostics).toContainEqual(expect.objectContaining({
|
||||
code: 'html-bounded-extraction',
|
||||
severity: 'info'
|
||||
}));
|
||||
}, 30_000);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue