Der Prototyp hatte eine eigene Seitenleiste mit den fünf Analyse-Seiten als Menüpunkten. Die Software hat eine andere Navigation, in der Analytics noch gar nicht vorkommt. Beides zusammen ergab kein Bild davon, wo die Analyse später sitzt. Jetzt: - Die Seitenleiste spiegelt die von HiveGrid Cloud — dieselbe Komponente (SidebarWithSections aus der Cloud-Fassung portiert, inkl. der dortigen lokalen Fixes: Collapse-Breite, verdrahteter Toggle, Logo-Slot, Hover-Sweep), dieselben Gruppen, dieselbe Reihenfolge, dasselbe Logo. - 'Analytics' ist ein einzelner Menüpunkt, gleichrangig neben 'Dashboard'. - Die fünf Analyse-Seiten sind darin über Reiter erreichbar, nicht als Unterpunkte in der Navigation. Zwei Navigationsebenen für dieselbe Sache würden nur konkurrieren. Die Reiter folgen dem Hausmuster aus SettingsHubShell (Link um Tabs.Item), damit jeder Reiter eine echte Adresse hat und mittelklickbar bleibt. - Routen: /analytics, /analytics/finanzen, /auftraege, /kunden, /team. '/' leitet auf /analytics; das Dashboard liegt in der Software unter '/'. - Alle übrigen Einträge sind Kontext: Sie zeigen den Ort in der fertigen Anwendung und melden beim Klick, dass sie zur Software gehören. Kopfzeile und Filterleiste sind aus den fünf Seiten in die Analytics-Hülle gewandert — sie sind für alle gleich, und der Filterzustand gilt bereichsweit. Nebenbei: @radix-ui/react-slot war eine Phantom-Dependency (nur transitiv über @subframe/core vorhanden, vom portierten IconButton direkt importiert) und ist jetzt deklariert. verify-layout prüfte noch die alten Routen und lief damit auf 404-Seiten, die naturgemäß keine Kollisionen haben — Routen korrigiert und eine Absicherung ergänzt, die bei Statuscode >= 400 abbricht. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPeZNwSeU7qG3RRCwaxEZX
196 lines
8.5 KiB
TypeScript
196 lines
8.5 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
import { FeatherCircleDashed } from "@subframe/core";
|
|
import * as SubframeCore from "@subframe/core";
|
|
import * as SubframeUtils from "../../utils";
|
|
import { useSidebarHoverSweep } from "../../sidebar-hover-sweep";
|
|
|
|
export interface NavItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
icon?: React.ReactNode;
|
|
children?: React.ReactNode;
|
|
selected?: boolean;
|
|
variant?: "icon-and-text" | "icon-only";
|
|
className?: string;
|
|
}
|
|
|
|
const NavItem = React.forwardRef<HTMLDivElement, NavItemProps>(function NavItem(
|
|
{ icon = <FeatherCircleDashed />, children, selected = false, variant = "icon-and-text", className, ...otherProps }: NavItemProps,
|
|
ref
|
|
) {
|
|
return (
|
|
<div
|
|
className={SubframeUtils.twClassNames(
|
|
// @subframe/sync-disable -- local fix: keep the item left-anchored in both states (no items-center flip on collapse) so the icon rail does not jump horizontally.
|
|
"group/7aa51c7e flex w-full cursor-pointer flex-col items-start gap-1 pt-2",
|
|
className
|
|
)}
|
|
ref={ref}
|
|
data-sidebar-hover-item="true"
|
|
{...otherProps}
|
|
>
|
|
{/* @subframe/sync-disable -- local fix: no items-center flip on collapse; the icon column stays put. */}
|
|
<div className={SubframeUtils.twClassNames("flex w-full flex-col items-start gap-2")}>
|
|
<div
|
|
className={SubframeUtils.twClassNames(
|
|
// @subframe/sync-disable -- local fix: constant left-anchored 32px icon column in BOTH states; only the label column (1fr) collapses with the animating width so the icon never shifts horizontally.
|
|
// @subframe/sync-disable -- local fix: deliberately without `will-change`, for the same reason as the app sidebar and the chat list rows: a permanent compositing layer turns the label's antialiasing greyscale and leaves it looking faded at rest.
|
|
// @subframe/sync-disable -- local fix: `scale` is only a transitioned property while the row is lifted, so it drops back the instant the pointer leaves instead of easing down behind the cursor. Same as the app sidebar rail.
|
|
"group-hover/7aa51c7e:bg-surface-neutral-tertiary pointer-events-none grid h-8 w-full origin-center scale-100 grid-cols-[32px_minmax(0,1fr)] items-center rounded-md pr-2 transition-[background-color] duration-150 ease-[cubic-bezier(0.4,0,0.2,1)] group-hover/7aa51c7e:scale-[1.04] group-hover/7aa51c7e:transition-[background-color,scale] data-[sidebar-sweep-hover=true]:scale-[1.04] data-[sidebar-sweep-hover=true]:transition-[background-color,scale] motion-reduce:transition-none",
|
|
{
|
|
"bg-surface-neutral-tertiary group-hover/7aa51c7e:bg-surface-neutral-modal group-active/7aa51c7e:bg-surface-neutral-tertiary": selected,
|
|
}
|
|
)}
|
|
data-slot="inner-sidebar-nav-item-surface"
|
|
data-sidebar-hover-surface="true"
|
|
>
|
|
{icon ? (
|
|
<SubframeCore.IconWrapper className="text-body-default-semibold-0 font-body-default-semibold-0 text-icon-neutral-primary group-active/7aa51c7e:text-icon-neutral-on-subtle col-start-1 inline-flex h-5 w-5 flex-none items-center justify-center justify-self-center leading-none [&>svg]:h-5 [&>svg]:max-h-5 [&>svg]:w-5 [&>svg]:max-w-5">
|
|
{icon}
|
|
</SubframeCore.IconWrapper>
|
|
) : null}
|
|
{children ? (
|
|
<span
|
|
className={SubframeUtils.twClassNames(
|
|
"text-body-small-regular-0 font-body-small-regular-0 text-text-neutral-secondary group-active/7aa51c7e:text-text-neutral-on-subtle col-start-2 min-w-0 truncate overflow-hidden whitespace-nowrap opacity-100 transition-[opacity] duration-150 ease-[cubic-bezier(0.22,1,0.36,1)]",
|
|
{
|
|
hidden: variant === "icon-only",
|
|
"text-text-neutral-on-subtle": selected,
|
|
}
|
|
)}
|
|
>
|
|
{children}
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
});
|
|
|
|
export interface NavSectionProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
children?: React.ReactNode;
|
|
label?: React.ReactNode;
|
|
variant?: "default" | "subsection";
|
|
showLabel?: boolean;
|
|
showDivider?: boolean;
|
|
className?: string;
|
|
}
|
|
|
|
const NavSection = React.forwardRef<HTMLDivElement, NavSectionProps>(function NavSection(
|
|
{ children, label, variant = "default", showLabel = false, showDivider = false, className, ...otherProps }: NavSectionProps,
|
|
ref
|
|
) {
|
|
return (
|
|
<div className={SubframeUtils.twClassNames("group/c119f2b2 flex w-full flex-col items-start gap-2 pt-2", className)} ref={ref} {...otherProps}>
|
|
<div
|
|
className={SubframeUtils.twClassNames("hidden w-full items-start", {
|
|
flex: showDivider,
|
|
})}
|
|
>
|
|
<SidebarInside.NavDivider />
|
|
</div>
|
|
<div
|
|
className={SubframeUtils.twClassNames("hidden w-full items-start", {
|
|
flex: showLabel,
|
|
})}
|
|
>
|
|
<SidebarInside.NavLabel label={label} />
|
|
</div>
|
|
{children ? (
|
|
<div className={SubframeUtils.twClassNames("flex w-full shrink-0 grow basis-0 flex-col items-center", { "py-0 pr-0 pl-5": variant === "subsection" })}>
|
|
{children}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
});
|
|
|
|
export interface NavLabelProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
label?: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
const NavLabel = React.forwardRef<HTMLDivElement, NavLabelProps>(function NavLabel({ label, className, ...otherProps }: NavLabelProps, ref) {
|
|
return (
|
|
<div className={SubframeUtils.twClassNames("flex w-full flex-col items-start gap-4 px-3 py-1", className)} ref={ref} {...otherProps}>
|
|
{label ? <span className="text-body-extra-small-medium-0 font-body-extra-small-medium-0 text-text-neutral-secondary w-full">{label}</span> : null}
|
|
</div>
|
|
);
|
|
});
|
|
|
|
export interface NavDividerProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
className?: string;
|
|
}
|
|
|
|
const NavDivider = React.forwardRef<HTMLDivElement, NavDividerProps>(function NavDivider({ className, ...otherProps }: NavDividerProps, ref) {
|
|
return (
|
|
<div className={SubframeUtils.twClassNames("flex w-full flex-col items-center justify-center gap-2 px-1 pt-2", className)} ref={ref} {...otherProps}>
|
|
<div className="bg-border-default flex h-px w-full flex-none flex-col items-center justify-center gap-2" />
|
|
</div>
|
|
);
|
|
});
|
|
|
|
export interface SidebarInsideRootProps extends React.HTMLAttributes<HTMLElement> {
|
|
variant?: "open" | "closed";
|
|
footer?: React.ReactNode;
|
|
children?: React.ReactNode;
|
|
visible?: boolean;
|
|
className?: string;
|
|
}
|
|
|
|
const SidebarInsideRoot = React.forwardRef<HTMLElement, SidebarInsideRootProps>(function SidebarInsideRoot(
|
|
{ variant = "open", footer, children, visible = false, className, onPointerMove, onPointerLeave, ...otherProps }: SidebarInsideRootProps,
|
|
ref
|
|
) {
|
|
const { handlePointerMove, handlePointerLeave } = useSidebarHoverSweep({ onPointerMove, onPointerLeave });
|
|
|
|
return (
|
|
<nav
|
|
className={SubframeUtils.twClassNames(
|
|
"group/f6538807 border-border-default bg-surface-neutral-secondary relative hidden h-full w-[208px] min-w-0 flex-col items-start overflow-visible rounded-l-md border-r border-solid transition-[width] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] will-change-[width]",
|
|
{
|
|
flex: visible,
|
|
"w-12": variant === "closed",
|
|
},
|
|
className
|
|
)}
|
|
ref={ref}
|
|
onPointerMove={handlePointerMove}
|
|
onPointerLeave={handlePointerLeave}
|
|
{...otherProps}
|
|
>
|
|
{children ? (
|
|
<div
|
|
className={SubframeUtils.twClassNames(
|
|
// @subframe/sync-disable -- local fix: do not center children when closed (items-center removed) so the icon column stays left-anchored through the width animation.
|
|
"flex shrink-0 grow basis-0 flex-col items-start overflow-x-hidden overflow-y-auto pt-1 pb-2 transition-[padding,width] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)]",
|
|
{
|
|
"w-full px-2": variant === "open",
|
|
"w-12 px-2": variant === "closed",
|
|
}
|
|
)}
|
|
>
|
|
{children}
|
|
</div>
|
|
) : null}
|
|
{footer ? (
|
|
<div
|
|
className={SubframeUtils.twClassNames("border-border-default hidden w-full items-center gap-4 border-t border-solid px-6 py-6", {
|
|
"justify-center": variant === "closed",
|
|
})}
|
|
>
|
|
{footer}
|
|
</div>
|
|
) : null}
|
|
</nav>
|
|
);
|
|
});
|
|
|
|
export const SidebarInside = Object.assign(SidebarInsideRoot, {
|
|
NavItem,
|
|
NavSection,
|
|
NavLabel,
|
|
NavDivider,
|
|
});
|