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
142 lines
6.7 KiB
TypeScript
142 lines
6.7 KiB
TypeScript
"use client";
|
|
/*
|
|
* Documentation:
|
|
* Icon Button — https://app.subframe.com/f023064a5ca0/library?component=Icon+Button_af9405b1-8c54-4e01-9786-5aad308224f6
|
|
*/
|
|
|
|
import React from "react";
|
|
import { FeatherPlus } from "@subframe/core";
|
|
import * as SubframeCore from "@subframe/core";
|
|
import { Slot, Slottable } from "@radix-ui/react-slot";
|
|
import * as SubframeUtils from "../../utils";
|
|
|
|
export interface IconButtonRootProps
|
|
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
asChild?: boolean;
|
|
disabled?: boolean;
|
|
loading?: boolean;
|
|
variant?:
|
|
| "primary"
|
|
| "secondary"
|
|
| "tertiary"
|
|
| "ghost"
|
|
| "neutral"
|
|
| "destructive"
|
|
| "success"
|
|
| "warning"
|
|
| "information";
|
|
size?: "large" | "medium" | "small" | "extra-small" | "2-x-small";
|
|
square?: boolean;
|
|
icon?: React.ReactNode;
|
|
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
|
className?: string;
|
|
}
|
|
|
|
const IconButtonRoot = React.forwardRef<HTMLButtonElement, IconButtonRootProps>(
|
|
function IconButtonRoot(
|
|
{
|
|
disabled = false,
|
|
loading = false,
|
|
variant = "primary",
|
|
size = "medium",
|
|
square = false,
|
|
icon = <FeatherPlus />,
|
|
children,
|
|
className,
|
|
type = "button",
|
|
asChild = false,
|
|
...otherProps
|
|
}: IconButtonRootProps,
|
|
ref
|
|
) {
|
|
// @subframe/sync-disable -- local accessibility fix: external links keep
|
|
// native link semantics without wrapping a button.
|
|
const Root = asChild ? Slot : "button";
|
|
return (
|
|
<Root
|
|
className={SubframeUtils.twClassNames(
|
|
// @subframe/sync-disable -- local fix: rounded-round aus dem Basis-String genommen und konditional gemacht.
|
|
// twMerge dedupliziert das custom `rounded-round` nicht gegen `rounded-md`, daher gewann immer der Kreis.
|
|
// Jetzt liegt exakt EINE Radius-Klasse an: square -> rounded-md (Squircle), sonst rounded-round (Kreis).
|
|
// @subframe/sync-disable -- local fix: focus-within -> focus-visible.
|
|
// `focus-within` traf auch den Maus-Klick, sodass nach jedem Klick ein
|
|
// blauer Ring am Icon haengen blieb. `focus-visible` zeigt ihn weiter
|
|
// bei Tastatur-Navigation, wo er gebraucht wird.
|
|
"group/af9405b1 flex h-9 w-9 cursor-pointer items-center justify-center gap-2 border-none bg-brand-brand-surface-solid-default text-left hover:bg-brand-brand-surface-solid-hover active:bg-brand-brand-surface-solid-active focus-visible:shadow-focus focus:outline-none disabled:cursor-default disabled:bg-surface-neutral-disabled disabled:pointer-events-none hover:disabled:cursor-default hover:disabled:bg-surface-neutral-disabled active:disabled:cursor-default active:disabled:bg-surface-neutral-disabled",
|
|
{
|
|
"rounded-round": !square,
|
|
"rounded-md": square,
|
|
"h-4 w-4": size === "2-x-small",
|
|
"h-6 w-6": size === "extra-small",
|
|
"h-8 w-8": size === "small",
|
|
"h-12 w-12": size === "large",
|
|
"bg-surface-feedback-solid-information hover:bg-surface-feedback-solid-information-hover active:bg-surface-feedback-solid-information-active":
|
|
variant === "information",
|
|
"bg-surface-feedback-solid-warning hover:bg-surface-feedback-solid-warning-hover active:bg-surface-feedback-solid-warning-active":
|
|
variant === "warning",
|
|
"bg-surface-feedback-solid-positive-default hover:bg-surface-feedback-solid-positive-hover active:bg-surface-feedback-solid-positive-active":
|
|
variant === "success",
|
|
"bg-surface-feedback-solid-negative-default hover:bg-surface-feedback-solid-negative-hover active:bg-surface-feedback-solid-negative-active":
|
|
variant === "destructive",
|
|
"bg-surface-feedback-solid-neutral hover:bg-surface-feedback-solid-neutral-hover active:bg-surface-feedback-solid-neutral-active":
|
|
variant === "neutral",
|
|
"bg-transparent hover:bg-surface-neutral-tertiary active:bg-surface-neutral-tertiary":
|
|
variant === "ghost",
|
|
"border border-solid border-border-default bg-surface-neutral-primary hover:border hover:border-solid hover:border-border-strong hover:bg-surface-neutral-secondary active:bg-surface-neutral-tertiary":
|
|
variant === "tertiary",
|
|
"bg-brand-brand-secondary-surface-subtle-default hover:bg-brand-brand-secondary-surface-subtle-hover active:bg-brand-brand-secondary-surface-subtle-active":
|
|
variant === "secondary",
|
|
},
|
|
className
|
|
)}
|
|
ref={ref}
|
|
{...otherProps}
|
|
type={asChild ? undefined : type}
|
|
disabled={asChild ? undefined : disabled}
|
|
aria-disabled={asChild && disabled ? true : otherProps["aria-disabled"]}
|
|
>
|
|
{icon ? (
|
|
<SubframeCore.IconWrapper
|
|
className={SubframeUtils.twClassNames(
|
|
"inline-flex h-5 w-5 flex-none items-center justify-center leading-none text-body-large-0 font-body-large-0 text-icon-neutral-on-solid group-disabled/af9405b1:text-icon-neutral-disabled [&>svg]:h-full [&>svg]:w-full",
|
|
{
|
|
"h-3.5 w-3.5 text-body-small-semibold-0 font-body-small-semibold-0":
|
|
size === "2-x-small",
|
|
"h-4 w-4 text-body-default-semibold-0 font-body-default-semibold-0":
|
|
size === "extra-small",
|
|
"h-6 w-6 text-heading-0 font-heading-0": size === "large",
|
|
"text-icon-neutral-secondary": variant === "ghost",
|
|
"text-icon-neutral-on-subtle": variant === "tertiary",
|
|
"text-icon-neutral-primary": variant === "secondary",
|
|
hidden: loading,
|
|
}
|
|
)}
|
|
>
|
|
{icon}
|
|
</SubframeCore.IconWrapper>
|
|
) : null}
|
|
<div
|
|
className={SubframeUtils.twClassNames(
|
|
"hidden h-4 w-4 flex-none items-center justify-center gap-2",
|
|
{ "h-5 w-5": size === "large", flex: loading }
|
|
)}
|
|
>
|
|
<SubframeCore.Loader
|
|
className={SubframeUtils.twClassNames(
|
|
"text-body-extra-small-regular-150 font-body-extra-small-regular-150 text-icon-neutral-on-solid group-disabled/af9405b1:text-icon-neutral-disabled group-hover/af9405b1:group-disabled/af9405b1:text-icon-neutral-on-solid group-active/af9405b1:group-disabled/af9405b1:text-icon-neutral-disabled",
|
|
{
|
|
"text-icon-neutral-secondary": variant === "ghost",
|
|
"text-icon-neutral-on-subtle": variant === "tertiary",
|
|
"text-icon-neutral-primary": variant === "secondary",
|
|
}
|
|
)}
|
|
/>
|
|
</div>
|
|
{asChild && children ? <Slottable>{children}</Slottable> : null}
|
|
</Root>
|
|
);
|
|
}
|
|
);
|
|
|
|
export const IconButton = IconButtonRoot;
|