feat(dashboard): Platzhalterseite statt Hinweis-Toast
Der Menüpunkt 'Dashboard' löste bisher nur einen Toast aus. Ein Navigationseintrag, der nirgendwohin führt, lässt den Leser im Unklaren, ob etwas fehlt oder etwas kaputt ist. Jetzt führt er auf /dashboard: ein Leerzustand nach dem Hausmuster (ui/components/EmptyState), der benennt, warum hier nichts steht, was später hinkommt (drei Rollen-Layouts, Widgets teils aus Analytics, teils nicht — offene HR-Anträge, Aufgaben, Termine) und wo der Zuschnitt dokumentiert ist. Mit einem Weg zurück nach Analytics. Dabei: - EmptyState aus der Cloud-Fassung nachgezogen; unsere Kopie hatte den action-Slot noch nicht. - PageHeader bekommt 'standardAktionen': Auf einer leeren Seite gibt es nichts zu aktualisieren oder zu exportieren, also stehen dort auch keine Schaltflächen dafür. - Die Variante 'software' in NavZiel entfällt — jeder Eintrag führt jetzt auf eine echte Seite, der Toast-Zweig in der AppShell war damit toter Code. - verify-layout deckt /dashboard mit ab (jetzt 7 Seiten x 7 Breiten). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPeZNwSeU7qG3RRCwaxEZX
This commit is contained in:
1 parent
466c079644
commit
7183fad27a
9 files changed
+182
-140
No files matched your search
@@ -0,0 +1,5 @@
|
||||
import { DashboardPlatzhalterSeite } from "@/features/dashboard/pages/DashboardPlatzhalterSeite";
|
||||
|
||||
export default function Page() {
|
||||
return <DashboardPlatzhalterSeite />;
|
||||
}
|
||||
+6
-3
@@ -9,7 +9,7 @@ was dazukommt, nicht was schon da ist.
|
||||
|
||||
```
|
||||
HIVEGRID
|
||||
├── Dashboard eigenes Feature, nicht Teil dieses Prototyps
|
||||
├── Dashboard Platzhalterseite — eigenes Feature, hier nicht gestaltet
|
||||
└── Analytics ◄── der Prototyp lebt hier /analytics
|
||||
├─────────────────────
|
||||
└── Komponenten Bausteingalerie des Prototyps /komponenten
|
||||
@@ -60,8 +60,11 @@ Leads mit seinen acht. Drei Gründe geben den Ausschlag:
|
||||
Ein weiterer Vorteil: Die Leiste wächst. Käme eine sechste Analyse-Seite dazu,
|
||||
ist das eine Zeile mehr — ein Reiterband würde eng.
|
||||
|
||||
„Dashboard" führt hier nirgendwohin und meldet das beim Klick — es ist ein
|
||||
eigenes Feature, siehe [SCOPE-ANALYTICS-DASHBOARD.md](SCOPE-ANALYTICS-DASHBOARD.md).
|
||||
„Dashboard" führt auf eine **Platzhalterseite** unter `/dashboard`: ein
|
||||
Leerzustand, der erklärt, warum hier nichts steht und was später hinkommt. Ein
|
||||
Menüpunkt, der nur einen Hinweis auslöst, lässt offen, ob etwas fehlt oder
|
||||
etwas kaputt ist. Siehe
|
||||
[SCOPE-ANALYTICS-DASHBOARD.md](SCOPE-ANALYTICS-DASHBOARD.md).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import { FeatherBell, FeatherHelpCircle, FeatherSearch } from "@subframe/core";
|
||||
import { SidebarWithSections } from "@/ui/components/SidebarWithSections";
|
||||
import { NAV_GRUPPEN, NAV_SEKUNDAER, ANALYSE_SEITEN, type NavEintrag } from "./navigation";
|
||||
import { FeatherIcon } from "./FeatherIcon";
|
||||
import { useToast } from "../../context/ToastContext";
|
||||
import { DATENSATZ_INFO } from "../../data/seed";
|
||||
import { formatDatum, formatZahl } from "../../utils/formatters";
|
||||
import { HEUTE } from "../../utils/dateUtils";
|
||||
@@ -20,24 +19,27 @@ import { HEUTE } from "../../utils/dateUtils";
|
||||
* Software öffnet, soll dieselbe Navigation sehen; die Übergabe wird dadurch
|
||||
* eine Frage des Einsetzens und nicht des Übersetzens.
|
||||
*
|
||||
* **Nur „Analytics" führt hier irgendwohin.** Die übrigen Einträge zeigen den
|
||||
* Ort in der fertigen Anwendung und melden beim Klick, dass sie zur Software
|
||||
* gehören — eine tote Schaltfläche ohne Rückmeldung wäre schlechter als gar
|
||||
* keine.
|
||||
* Jeder Eintrag führt auf eine echte Seite. „Dashboard" landet auf einer
|
||||
* Platzhalterseite, weil es ein eigenes Feature ist und hier nicht gestaltet
|
||||
* wird — aber ein Menüpunkt, der nur einen Hinweis auslöst, lässt offen, ob
|
||||
* etwas fehlt oder etwas kaputt ist.
|
||||
*/
|
||||
export function AppShell({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const { zeigeToast } = useToast();
|
||||
const [eingeklappt, setEingeklappt] = React.useState(false);
|
||||
|
||||
const istAktiv = (eintrag: NavEintrag) =>
|
||||
eintrag.ziel.art === "route" &&
|
||||
(eintrag.ziel.href === "/analytics"
|
||||
eintrag.ziel.href === "/analytics"
|
||||
? pathname.startsWith("/analytics")
|
||||
: pathname.startsWith(eintrag.ziel.href));
|
||||
: pathname === eintrag.ziel.href;
|
||||
|
||||
const renderEintrag = (eintrag: NavEintrag) => {
|
||||
const item = (
|
||||
const renderEintrag = (eintrag: NavEintrag) => (
|
||||
<Link
|
||||
key={eintrag.id}
|
||||
href={eintrag.ziel.href}
|
||||
title={eingeklappt ? eintrag.label : undefined}
|
||||
className="w-full no-underline"
|
||||
>
|
||||
<SidebarWithSections.NavItem
|
||||
icon={<FeatherIcon name={eintrag.icon} />}
|
||||
selected={istAktiv(eintrag)}
|
||||
@@ -45,40 +47,17 @@ export function AppShell({ children }: { children: React.ReactNode }) {
|
||||
>
|
||||
{eintrag.label}
|
||||
</SidebarWithSections.NavItem>
|
||||
);
|
||||
|
||||
if (eintrag.ziel.art === "route") {
|
||||
return (
|
||||
<Link
|
||||
key={eintrag.id}
|
||||
href={eintrag.ziel.href}
|
||||
title={eingeklappt ? eintrag.label : undefined}
|
||||
className="w-full no-underline"
|
||||
>
|
||||
{item}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={eintrag.id}
|
||||
type="button"
|
||||
title={eingeklappt ? eintrag.label : undefined}
|
||||
onClick={() =>
|
||||
zeigeToast(eintrag.label, "Gehört zur Software — in diesem Prototyp nicht enthalten")
|
||||
}
|
||||
className="w-full cursor-default border-0 bg-transparent p-0 text-left"
|
||||
>
|
||||
{item}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
</Link>
|
||||
);
|
||||
|
||||
const aktiveAnalyse = ANALYSE_SEITEN.find((s) =>
|
||||
s.href === "/analytics" ? pathname === "/analytics" : pathname.startsWith(s.href)
|
||||
);
|
||||
const bereich = pathname.startsWith("/komponenten") ? "Komponenten" : "Analytics";
|
||||
const bereich = pathname.startsWith("/komponenten")
|
||||
? "Komponenten"
|
||||
: pathname.startsWith("/dashboard")
|
||||
? "Dashboard"
|
||||
: "Analytics";
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen w-full bg-neutral-50">
|
||||
|
||||
@@ -13,10 +13,17 @@ export function PageHeader({
|
||||
titel,
|
||||
beschreibung,
|
||||
aktionen,
|
||||
/**
|
||||
* Aktualisieren, Export und Teilen. Auf Seiten ohne Inhalt gibt es nichts zu
|
||||
* aktualisieren oder zu exportieren — dort schaltet man sie ab, statt
|
||||
* Schaltflächen anzubieten, die ins Leere greifen.
|
||||
*/
|
||||
standardAktionen = true,
|
||||
}: {
|
||||
titel: string;
|
||||
beschreibung: string;
|
||||
aktionen?: React.ReactNode;
|
||||
standardAktionen?: boolean;
|
||||
}) {
|
||||
const { zeigeToast } = useToast();
|
||||
|
||||
@@ -31,6 +38,8 @@ export function PageHeader({
|
||||
|
||||
<div className="dv-no-print flex flex-none items-center gap-2">
|
||||
{aktionen}
|
||||
{standardAktionen ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => zeigeToast("Ansicht aktualisiert", "Demo-Daten, Stichtag 30.07.2026")}
|
||||
className="flex h-9 items-center gap-2 rounded-md border border-solid border-border-default bg-white px-3 text-[13px] font-medium text-text-neutral-secondary hover:bg-neutral-50"
|
||||
@@ -52,6 +61,8 @@ export function PageHeader({
|
||||
>
|
||||
<FeatherShare2 className="text-[15px]" />
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,12 +12,12 @@ import type { SeitenId } from "../../data/filtering";
|
||||
* (`ui/components/SidebarWithSections`, aus der Cloud-Fassung portiert), damit
|
||||
* Aussehen und Verhalten übereinstimmen.
|
||||
*
|
||||
* „Dashboard" führt hier nirgendwohin: Es ist ein eigenes Feature und nicht
|
||||
* Teil dieses Prototyps (siehe `docs/SCOPE-ANALYTICS-DASHBOARD.md`). Der
|
||||
* Eintrag meldet das beim Klick, statt still zu bleiben.
|
||||
* „Dashboard" führt auf eine Platzhalterseite: Es ist ein eigenes Feature und
|
||||
* in diesem Prototyp nicht gestaltet (siehe
|
||||
* `docs/SCOPE-ANALYTICS-DASHBOARD.md`).
|
||||
*/
|
||||
|
||||
export type NavZiel = { art: "route"; href: string } | { art: "software" };
|
||||
export type NavZiel = { art: "route"; href: string };
|
||||
|
||||
export interface NavEintrag {
|
||||
id: string;
|
||||
@@ -43,12 +43,13 @@ export const NAV_GRUPPEN: NavGruppe[] = [
|
||||
titel: "",
|
||||
eintraege: [
|
||||
{
|
||||
// Gehört zu uns, ist aber ein eigenes Feature und nicht Teil dieses
|
||||
// Prototyps — siehe docs/SCOPE-ANALYTICS-DASHBOARD.md.
|
||||
// Eigenes Feature, in diesem Prototyp nicht gestaltet. Der Punkt führt
|
||||
// trotzdem auf eine Seite: Ein Menüeintrag, der nur einen Hinweis
|
||||
// auslöst, lässt offen, ob etwas fehlt oder etwas kaputt ist.
|
||||
id: "dashboard",
|
||||
label: "Dashboard",
|
||||
icon: "LayoutDashboard",
|
||||
ziel: { art: "software" },
|
||||
ziel: { art: "route", href: "/dashboard" },
|
||||
},
|
||||
{
|
||||
// Der neue Menüpunkt. Die fünf Analyse-Seiten sind ausschließlich hier
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { FeatherArrowRight } from "@subframe/core";
|
||||
import { Button } from "@/ui/components/Button";
|
||||
import { EmptyState } from "@/ui/components/EmptyState";
|
||||
import { PageHeader } from "../components/layout/PageHeader";
|
||||
import { PageContainer } from "../components/layout/PageContainer";
|
||||
import { ANALYTICS_START } from "../components/layout/navigation";
|
||||
|
||||
/**
|
||||
* Platzhalter für das Dashboard.
|
||||
*
|
||||
* Das Dashboard ist ein eigenes Feature und in diesem Prototyp nicht
|
||||
* gestaltet — siehe `docs/SCOPE-ANALYTICS-DASHBOARD.md`. Der Menüpunkt führt
|
||||
* trotzdem auf eine echte Seite statt nur einen Hinweis auszulösen: Ein
|
||||
* Navigationspunkt, der nirgendwohin führt, lässt den Leser im Unklaren, ob
|
||||
* etwas fehlt oder etwas kaputt ist. Diese Seite sagt es ihm.
|
||||
*
|
||||
* Der Inhalt beschreibt, was hier später steht — nicht als Gestaltung, sondern
|
||||
* als Verabredung. So bleibt die Seite ehrlich leer und trotzdem nützlich.
|
||||
*/
|
||||
export function DashboardPlatzhalterSeite() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader
|
||||
titel="Dashboard"
|
||||
beschreibung="Der Begrüßungsbildschirm nach der Anmeldung — eigenes Feature, in diesem Prototyp noch nicht gestaltet."
|
||||
standardAktionen={false}
|
||||
/>
|
||||
|
||||
<PageContainer>
|
||||
<EmptyState
|
||||
outlined
|
||||
title="Noch nicht gestaltet"
|
||||
body={
|
||||
<span className="flex flex-col gap-3">
|
||||
<span>
|
||||
Analytics und Dashboard sind zwei Features. Dieser Prototyp
|
||||
gestaltet <strong>Analytics</strong> — den Analysebereich mit
|
||||
Zeitraum- und Dimensionsfiltern. Das Dashboard beantwortet eine
|
||||
andere Frage: {"„Wie ist meine Lage?“ statt „Warum ist das so?“."}
|
||||
</span>
|
||||
<span>
|
||||
Geplant sind drei feste Rollen-Layouts (Geschäftsführung, Büro,
|
||||
Monteur) aus Widgets. Ein Teil davon kommt aus Analytics —
|
||||
Kennzahlen-Kacheln, Trend-Diagramme, Listen. Der andere Teil
|
||||
nicht: offene HR-Anträge, eigene Aufgaben, Termine des Tages.
|
||||
</span>
|
||||
<span className="text-text-neutral-tertiary">
|
||||
Zuschnitt und offene Punkte stehen in{" "}
|
||||
<code className="rounded-sm bg-neutral-100 px-1 py-0.5 text-[12px]">
|
||||
docs/SCOPE-ANALYTICS-DASHBOARD.md
|
||||
</code>
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
action={
|
||||
<Link href={ANALYTICS_START} className="no-underline">
|
||||
<Button variant="primary" iconRight={<FeatherArrowRight />}>
|
||||
Zu Analytics
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</PageContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import { chromium } from "playwright-core";
|
||||
|
||||
const BASIS = process.env.DASHBOARD_URL ?? "http://localhost:4300";
|
||||
const SEITEN = [
|
||||
"/dashboard",
|
||||
"/analytics",
|
||||
"/analytics/finanzen",
|
||||
"/analytics/auftraege",
|
||||
|
||||
@@ -7,103 +7,68 @@
|
||||
|
||||
import React from "react";
|
||||
import * as SubframeUtils from "../../utils";
|
||||
import { Button } from "../Button";
|
||||
|
||||
export interface EmptyStateRootProps
|
||||
extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
|
||||
export interface EmptyStateRootProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
|
||||
variant?: "default" | "no-results" | "error";
|
||||
outlined?: boolean;
|
||||
fillContainer?: boolean;
|
||||
title?: React.ReactNode;
|
||||
body?: React.ReactNode;
|
||||
// Optional call-to-action rendered below the text. When omitted (the default),
|
||||
// no button is shown. Replaces the previous hard-coded "Create project" button.
|
||||
action?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const EmptyStateRoot = React.forwardRef<HTMLDivElement, EmptyStateRootProps>(
|
||||
function EmptyStateRoot(
|
||||
{
|
||||
variant = "default",
|
||||
outlined = false,
|
||||
fillContainer = false,
|
||||
title,
|
||||
body,
|
||||
className,
|
||||
...otherProps
|
||||
}: EmptyStateRootProps,
|
||||
ref
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
className={SubframeUtils.twClassNames(
|
||||
"group/9fcf03a9 flex max-w-[384px] items-center gap-4 px-4 py-8",
|
||||
{
|
||||
"max-w-none": fillContainer,
|
||||
"rounded-md border border-solid border-border-default bg-surface-neutral-primary":
|
||||
outlined,
|
||||
},
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-6">
|
||||
<div className="flex flex-col items-center gap-7">
|
||||
<img
|
||||
className={SubframeUtils.twClassNames(
|
||||
"h-12 w-12 flex-none object-cover",
|
||||
{ hidden: variant === "error" || variant === "no-results" }
|
||||
)}
|
||||
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBjbGFzcz0iZmxleC1ub25lIiBkYXRhLW5vZGUtaWQ9IjM3YmE4ZTQ2Ij48cGF0aCBkPSJNMzcuNSAyMVYzNy42NjY3QzM3LjUgMzguMDIwMyAzNy4zNTk1IDM4LjM1OTQgMzcuMTA5NSAzOC42MDk1QzM2Ljg1OTQgMzguODU5NSAzNi41MjAzIDM5IDM2LjE2NjcgMzlINy41QzcuMTAyMTggMzkgNi43MjA2NCAzOC44NDIgNi40MzkzNCAzOC41NjA3QzYuMTU4MDQgMzguMjc5NCA2IDM3Ljg5NzggNiAzNy41VjE2LjVDNiAxNi4xMDIyIDYuMTU4MDQgMTUuNzIwNiA2LjQzOTM0IDE1LjQzOTNDNi43MjA2NCAxNS4xNTggNy4xMDIxOCAxNSA3LjUgMTVIMTZDMTYuMzI0NiAxNSAxNi42NDA0IDE1LjEwNTMgMTYuOSAxNS4zTDIyLjEgMTkuMkMyMi4zNTk2IDE5LjM5NDcgMjIuNjc1NCAxOS41IDIzIDE5LjVIMzZDMzYuMzk3OCAxOS41IDM2Ljc3OTQgMTkuNjU4IDM3LjA2MDcgMTkuOTM5M0MzNy4zNDIgMjAuMjIwNiAzNy41IDIwLjYwMjIgMzcuNSAyMVoiIHN0cm9rZT0iIzg1ODU4OSIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZGF0YS1ub2RlLWlkPSI4MzBkNDBlZi05N2E3LTRjMjYtODQwZi0yNDQzMTEwNjYwMjgiPjwvcGF0aD48cGF0aCBkPSJNMTIgMTVWMTAuNUMxMiAxMC4xMDIyIDEyLjE1OCA5LjcyMDY0IDEyLjQzOTMgOS40MzkzNEMxMi43MjA2IDkuMTU4MDQgMTMuMTAyMiA5IDEzLjUgOUgyMkMyMi4zMjQ2IDkgMjIuNjQwNCA5LjEwNTI3IDIyLjkgOS4zTDI4LjEgMTMuMkMyOC4zNTk2IDEzLjM5NDcgMjguNjc1NCAxMy41IDI5IDEzLjVINDJDNDIuMzk3OCAxMy41IDQyLjc3OTQgMTMuNjU4IDQzLjA2MDcgMTMuOTM5M0M0My4zNDIgMTQuMjIwNiA0My41IDE0LjYwMjIgNDMuNSAxNVYzMS42NjY3QzQzLjUgMzEuODQxOCA0My40NjU1IDMyLjAxNTEgNDMuMzk4NSAzMi4xNzY5QzQzLjMzMTUgMzIuMzM4NyA0My4yMzMzIDMyLjQ4NTcgNDMuMTA5NSAzMi42MDk1QzQyLjk4NTcgMzIuNzMzMyA0Mi44Mzg3IDMyLjgzMTUgNDIuNjc2OSAzMi44OTg1QzQyLjUxNTEgMzIuOTY1NSA0Mi4zNDE4IDMzIDQyLjE2NjcgMzNIMzcuNSIgc3Ryb2tlPSIjODU4NTg5IiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBkYXRhLW5vZGUtaWQ9IjJiZDUxMmE0LWI3YzQtNDg5MS05N2NmLTliNjkyZTc1MTNkMSI+PC9wYXRoPjwvc3ZnPg=="
|
||||
/>
|
||||
<img
|
||||
className={SubframeUtils.twClassNames(
|
||||
"hidden h-12 w-12 flex-none object-cover",
|
||||
{ block: variant === "no-results" }
|
||||
)}
|
||||
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMjEuNzUgMzcuNUMzMC40NDg1IDM3LjUgMzcuNSAzMC40NDg1IDM3LjUgMjEuNzVDMzcuNSAxMy4wNTE1IDMwLjQ0ODUgNiAyMS43NSA2QzEzLjA1MTUgNiA2IDEzLjA1MTUgNiAyMS43NUM2IDMwLjQ0ODUgMTMuMDUxNSAzNy41IDIxLjc1IDM3LjVaIiBzdHJva2U9IiM4NTg1ODkiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTMyLjg4NjMgMzIuODg2N0w0MS45OTg5IDQxLjk5OTMiIHN0cm9rZT0iIzg1ODU4OSIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48L3N2Zz4="
|
||||
/>
|
||||
<img
|
||||
className={SubframeUtils.twClassNames(
|
||||
"hidden h-12 w-12 flex-none object-cover",
|
||||
{ block: variant === "error" }
|
||||
)}
|
||||
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMjQgMTkuNVYyNyIgc3Ryb2tlPSIjQzUyMDIwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0yMS40MDM2IDcuNDk3MTNMNC45MDgwNCAzNS45OTU3QzQuNjQ0MDkgMzYuNDUxNyA0LjUwNDg5IDM2Ljk2OTIgNC41MDQ0NiAzNy40OTYxQzQuNTA0MDIgMzguMDIzIDQuNjQyMzYgMzguNTQwNyA0LjkwNTU1IDM4Ljk5NzFDNS4xNjg3NSAzOS40NTM2IDUuNTQ3NTEgMzkuODMyNyA2LjAwMzc0IDQwLjA5NjJDNi40NTk5NyA0MC4zNTk4IDYuOTc3NTcgNDAuNDk4NiA3LjUwNDQ3IDQwLjQ5ODZINDAuNDk1NkM0MS4wMjI0IDQwLjQ5ODYgNDEuNTQgNDAuMzU5OCA0MS45OTYzIDQwLjA5NjJDNDIuNDUyNSAzOS44MzI3IDQyLjgzMTMgMzkuNDUzNiA0My4wOTQ1IDM4Ljk5NzFDNDMuMzU3NyAzOC41NDA3IDQzLjQ5NiAzOC4wMjMgNDMuNDk1NiAzNy40OTYxQzQzLjQ5NTEgMzYuOTY5MiA0My4zNTU5IDM2LjQ1MTcgNDMuMDkyIDM1Ljk5NTdMMjYuNTk2NCA3LjQ5NzEzQzI2LjMzMjkgNy4wNDE4NSAyNS45NTQzIDYuNjYzODcgMjUuNDk4NiA2LjQwMTFDMjUuMDQyOSA2LjEzODMyIDI0LjUyNjEgNiAyNCA2QzIzLjQ3NCA2IDIyLjk1NzIgNi4xMzgzMiAyMi41MDE0IDYuNDAxMUMyMi4wNDU3IDYuNjYzODcgMjEuNjY3MSA3LjA0MTg1IDIxLjQwMzYgNy40OTcxM1Y3LjQ5NzEzWiIgc3Ryb2tlPSIjQzUyMDIwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0yNCAzNS42MjVDMjUuMDM1NSAzNS42MjUgMjUuODc1IDM0Ljc4NTUgMjUuODc1IDMzLjc1QzI1Ljg3NSAzMi43MTQ1IDI1LjAzNTUgMzEuODc1IDI0IDMxLjg3NUMyMi45NjQ1IDMxLjg3NSAyMi4xMjUgMzIuNzE0NSAyMi4xMjUgMzMuNzVDMjIuMTI1IDM0Ljc4NTUgMjIuOTY0NSAzNS42MjUgMjQgMzUuNjI1WiIgZmlsbD0iI0M1MjAyMCIvPjwvc3ZnPg=="
|
||||
/>
|
||||
<div className="flex w-full flex-col items-center gap-4 px-4">
|
||||
{title ? (
|
||||
<span className="w-full text-body-large-0 font-body-large-0 text-text-neutral-primary text-center">
|
||||
{title}
|
||||
</span>
|
||||
) : null}
|
||||
{body ? (
|
||||
<span className="break-words text-body-default-regular-150 font-body-default-regular-150 text-text-neutral-secondary text-center">
|
||||
{body}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<Button
|
||||
className={SubframeUtils.twClassNames("h-9 w-36 flex-none", {
|
||||
"w-28": variant === "error" || variant === "no-results",
|
||||
})}
|
||||
variant={
|
||||
variant === "error"
|
||||
? "tertiary"
|
||||
: variant === "no-results"
|
||||
? "tertiary"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{variant === "error"
|
||||
? "Try again"
|
||||
: variant === "no-results"
|
||||
? "Clear filters"
|
||||
: "Create project"}
|
||||
</Button>
|
||||
const EmptyStateRoot = React.forwardRef<HTMLDivElement, EmptyStateRootProps>(function EmptyStateRoot(
|
||||
{ variant = "default", outlined = false, fillContainer = false, title, body, action, className, ...otherProps }: EmptyStateRootProps,
|
||||
ref
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
className={SubframeUtils.twClassNames(
|
||||
// Local mod (HiveGrid): center the empty state within its container by
|
||||
// default. The root was max-w-[384px] with the flex default justify-start,
|
||||
// so the box hugged the left edge of the content area. Now the root spans
|
||||
// the width and centers; the 384px reading width moves onto the inner
|
||||
// column so body text still wraps. Re-check this on the next ~/ui sync.
|
||||
"group/9fcf03a9 flex w-full items-center justify-center gap-4 px-4 py-8",
|
||||
{
|
||||
"border-border-default bg-surface-neutral-primary rounded-md border border-solid": outlined,
|
||||
},
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={SubframeUtils.twClassNames("flex w-full max-w-[384px] flex-col items-center gap-6", { "max-w-none": fillContainer })}>
|
||||
<div className="flex flex-col items-center gap-7">
|
||||
<img
|
||||
className={SubframeUtils.twClassNames("h-12 w-12 flex-none object-cover", { hidden: variant === "error" || variant === "no-results" })}
|
||||
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBjbGFzcz0iZmxleC1ub25lIiBkYXRhLW5vZGUtaWQ9IjM3YmE4ZTQ2Ij48cGF0aCBkPSJNMzcuNSAyMVYzNy42NjY3QzM3LjUgMzguMDIwMyAzNy4zNTk1IDM4LjM1OTQgMzcuMTA5NSAzOC42MDk1QzM2Ljg1OTQgMzguODU5NSAzNi41MjAzIDM5IDM2LjE2NjcgMzlINy41QzcuMTAyMTggMzkgNi43MjA2NCAzOC44NDIgNi40MzkzNCAzOC41NjA3QzYuMTU4MDQgMzguMjc5NCA2IDM3Ljg5NzggNiAzNy41VjE2LjVDNiAxNi4xMDIyIDYuMTU4MDQgMTUuNzIwNiA2LjQzOTM0IDE1LjQzOTNDNi43MjA2NCAxNS4xNTggNy4xMDIxOCAxNSA3LjUgMTVIMTZDMTYuMzI0NiAxNSAxNi42NDA0IDE1LjEwNTMgMTYuOSAxNS4zTDIyLjEgMTkuMkMyMi4zNTk2IDE5LjM5NDcgMjIuNjc1NCAxOS41IDIzIDE5LjVIMzZDMzYuMzk3OCAxOS41IDM2Ljc3OTQgMTkuNjU4IDM3LjA2MDcgMTkuOTM5M0MzNy4zNDIgMjAuMjIwNiAzNy41IDIwLjYwMjIgMzcuNSAyMVoiIHN0cm9rZT0iIzg1ODU4OSIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZGF0YS1ub2RlLWlkPSI4MzBkNDBlZi05N2E3LTRjMjYtODQwZi0yNDQzMTEwNjYwMjgiPjwvcGF0aD48cGF0aCBkPSJNMTIgMTVWMTAuNUMxMiAxMC4xMDIyIDEyLjE1OCA5LjcyMDY0IDEyLjQzOTMgOS40MzkzNEMxMi43MjA2IDkuMTU4MDQgMTMuMTAyMiA5IDEzLjUgOUgyMkMyMi4zMjQ2IDkgMjIuNjQwNCA5LjEwNTI3IDIyLjkgOS4zTDI4LjEgMTMuMkMyOC4zNTk2IDEzLjM5NDcgMjguNjc1NCAxMy41IDI5IDEzLjVINDJDNDIuMzk3OCAxMy41IDQyLjc3OTQgMTMuNjU4IDQzLjA2MDcgMTMuOTM5M0M0My4zNDIgMTQuMjIwNiA0My41IDE0LjYwMjIgNDMuNSAxNVYzMS42NjY3QzQzLjUgMzEuODQxOCA0My40NjU1IDMyLjAxNTEgNDMuMzk4NSAzMi4xNzY5QzQzLjMzMTUgMzIuMzM4NyA0My4yMzMzIDMyLjQ4NTcgNDMuMTA5NSAzMi42MDk1QzQyLjk4NTcgMzIuNzMzMyA0Mi44Mzg3IDMyLjgzMTUgNDIuNjc2OSAzMi44OTg1QzQyLjUxNTEgMzIuOTY1NSA0Mi4zNDE4IDMzIDQyLjE2NjcgMzNIMzcuNSIgc3Ryb2tlPSIjODU4NTg5IiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBkYXRhLW5vZGUtaWQ9IjJiZDUxMmE0LWI3YzQtNDg5MS05N2NmLTliNjkyZTc1MTNkMSI+PC9wYXRoPjwvc3ZnPg=="
|
||||
alt=""
|
||||
/>
|
||||
<img
|
||||
className={SubframeUtils.twClassNames("hidden h-12 w-12 flex-none object-cover", { block: variant === "no-results" })}
|
||||
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMjEuNzUgMzcuNUMzMC40NDg1IDM3LjUgMzcuNSAzMC40NDg1IDM3LjUgMjEuNzVDMzcuNSAxMy4wNTE1IDMwLjQ0ODUgNiAyMS43NSA2QzEzLjA1MTUgNiA2IDEzLjA1MTUgNiAyMS43NUM2IDMwLjQ0ODUgMTMuMDUxNSAzNy41IDIxLjc1IDM3LjVaIiBzdHJva2U9IiM4NTg1ODkiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+PHBhdGggZD0iTTMyLjg4NjMgMzIuODg2N0w0MS45OTg5IDQxLjk5OTMiIHN0cm9rZT0iIzg1ODU4OSIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48L3N2Zz4="
|
||||
alt=""
|
||||
/>
|
||||
<img
|
||||
className={SubframeUtils.twClassNames("hidden h-12 w-12 flex-none object-cover", { block: variant === "error" })}
|
||||
src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMjQgMTkuNVYyNyIgc3Ryb2tlPSIjQzUyMDIwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0yMS40MDM2IDcuNDk3MTNMNC45MDgwNCAzNS45OTU3QzQuNjQ0MDkgMzYuNDUxNyA0LjUwNDg5IDM2Ljk2OTIgNC41MDQ0NiAzNy40OTYxQzQuNTA0MDIgMzguMDIzIDQuNjQyMzYgMzguNTQwNyA0LjkwNTU1IDM4Ljk5NzFDNS4xNjg3NSAzOS40NTM2IDUuNTQ3NTEgMzkuODMyNyA2LjAwMzc0IDQwLjA5NjJDNi40NTk5NyA0MC4zNTk4IDYuOTc3NTcgNDAuNDk4NiA3LjUwNDQ3IDQwLjQ5ODZINDAuNDk1NkM0MS4wMjI0IDQwLjQ5ODYgNDEuNTQgNDAuMzU5OCA0MS45OTYzIDQwLjA5NjJDNDIuNDUyNSAzOS44MzI3IDQyLjgzMTMgMzkuNDUzNiA0My4wOTQ1IDM4Ljk5NzFDNDMuMzU3NyAzOC41NDA3IDQzLjQ5NiAzOC4wMjMgNDMuNDk1NiAzNy40OTYxQzQzLjQ5NTEgMzYuOTY5MiA0My4zNTU5IDM2LjQ1MTcgNDMuMDkyIDM1Ljk5NTdMMjYuNTk2NCA3LjQ5NzEzQzI2LjMzMjkgNy4wNDE4NSAyNS45NTQzIDYuNjYzODcgMjUuNDk4NiA2LjQwMTFDMjUuMDQyOSA2LjEzODMyIDI0LjUyNjEgNiAyNCA2QzIzLjQ3NCA2IDIyLjk1NzIgNi4xMzgzMiAyMi41MDE0IDYuNDAxMUMyMi4wNDU3IDYuNjYzODcgMjEuNjY3MSA3LjA0MTg1IDIxLjQwMzYgNy40OTcxM1Y3LjQ5NzEzWiIgc3Ryb2tlPSIjQzUyMDIwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjxwYXRoIGQ9Ik0yNCAzNS42MjVDMjUuMDM1NSAzNS42MjUgMjUuODc1IDM0Ljc4NTUgMjUuODc1IDMzLjc1QzI1Ljg3NSAzMi43MTQ1IDI1LjAzNTUgMzEuODc1IDI0IDMxLjg3NUMyMi45NjQ1IDMxLjg3NSAyMi4xMjUgMzIuNzE0NSAyMi4xMjUgMzMuNzVDMjIuMTI1IDM0Ljc4NTUgMjIuOTY0NSAzNS42MjUgMjQgMzUuNjI1WiIgZmlsbD0iI0M1MjAyMCIvPjwvc3ZnPg=="
|
||||
alt=""
|
||||
/>
|
||||
<div className="flex w-full flex-col items-center gap-4 px-4">
|
||||
{title ? <span className="text-body-large-0 font-body-large-0 text-text-neutral-primary w-full text-center">{title}</span> : null}
|
||||
{body ? (
|
||||
<span className="text-body-default-regular-150 font-body-default-regular-150 text-text-neutral-secondary text-center break-words">{body}</span>
|
||||
) : null}
|
||||
</div>
|
||||
{action ? <div className="flex flex-none items-center gap-2">{action}</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const EmptyState = EmptyStateRoot;
|
||||
@@ -1,10 +1,18 @@
|
||||
"use client";
|
||||
import { EmptyState as EmptyStateComponent } from "./EmptyState";
|
||||
import React from "react";
|
||||
import { EmptyState as EmptyStateComponent, type EmptyStateRootProps } from "./EmptyState";
|
||||
|
||||
/**
|
||||
* Add wrapper components and business logic here.
|
||||
* If you modify this file, disable Subframe sync for it to prevent overwrites.
|
||||
* Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components
|
||||
*
|
||||
* History: this wrapper used to add `[&_button]:hidden` to suppress the synced
|
||||
* component's hardcoded, non-wired english CTA ("Create project"). The synced
|
||||
* component has since gained a proper `action` slot and no hardcoded button,
|
||||
* so the class only hid the real action buttons screens pass in (create/retry
|
||||
* CTAs). Do not re-add a blanket button-hiding class here.
|
||||
*/
|
||||
|
||||
export const EmptyState = EmptyStateComponent;
|
||||
export const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateRootProps>(function EmptyState(props, ref) {
|
||||
return <EmptyStateComponent ref={ref} {...props} />;
|
||||
});
|
||||
Reference in new issue
Block a user