fix(ui): выровнять chrome Frame и убрать Card-оболочки
Docker images / prepare-release (push) Successful in 4s
Docker images / backend-image (push) Failing after 2m36s
Docker images / frontend-image (push) Successful in 2m22s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 55s
Docker images / publish-release (push) Skipped

Подключить App Switcher, NavUser, OpsPanel и AlertDialog вместо Card-shell.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-08-17 15:36:56 +07:00
co-authored by Cursor
parent d2de8d3188
commit 5f29cfaf0f
47 changed files with 1972 additions and 860 deletions
+90
View File
@@ -0,0 +1,90 @@
import { cn } from "@/lib/utils"
type IconStackProps = React.ComponentProps<"div">
function IconStack({ className, children, style, ...props }: IconStackProps) {
return (
<div
data-slot="icon-stack"
className={cn(
"text-foreground **:data-[slot=icon-stack-layer]:fill-background relative h-20 w-18",
className
)}
style={
{
"--icon-stack-content-x": "71%",
"--icon-stack-content-y": "58%",
...style,
} as React.CSSProperties
}
{...props}
>
<svg
aria-hidden="true"
viewBox="0 0 72 81"
fill="none"
className="h-full w-full overflow-visible"
>
<ellipse
cx="36"
cy="76"
rx="30"
ry="7"
fill="currentColor"
fillOpacity="0.055"
className="blur-[4px]"
/>
<IconStackLayer opacity="0.4" />
<IconStackLayer opacity="0.6" x={13.65} y={6.04} />
<IconStackLayer opacity="0.8" x={27.32} y={12.08} active />
</svg>
{children ? (
<div
data-slot="icon-stack-content"
className="text-muted-foreground pointer-events-none absolute top-[var(--icon-stack-content-y)] left-[var(--icon-stack-content-x)] flex -translate-x-1/2 -translate-y-1/2 scale-x-90 -skew-y-26 items-center justify-center"
>
{children}
</div>
) : null}
</div>
)
}
function IconStackLayer({
active = false,
opacity,
x = 0,
y = 0,
}: {
active?: boolean
opacity: string
x?: number
y?: number
}) {
return (
<g opacity={opacity} transform={`translate(${x} ${y})`}>
<path
data-slot="icon-stack-layer"
d="M42.2538 2.046C41.4408 1.6325 40.3965 1.6677 39.2612 2.2424L7.9616 18.1934C5.3895 19.5039 3.301 23.1064 3.301 26.2322V64.3226C3.301 66.0677 3.9458 67.2943 4.962 67.8199L1.8363 66.229C0.8201 65.7104 0.1753 64.4771 0.1753 62.732V24.6412C0.1753 21.5085 2.2638 17.913 4.8359 16.6024L36.1355 0.6515C37.2778 0.0698 38.322 0.0416 39.128 0.4551L42.2538 2.046Z"
stroke="currentColor"
strokeOpacity={active ? "0.3" : "0.2"}
strokeWidth="0.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
data-slot="icon-stack-layer"
d="M42.2545 2.0456C43.2707 2.5643 43.9155 3.7979 43.9155 5.543V43.6337C43.9155 46.7665 41.827 50.3616 39.2549 51.6722L7.9554 67.6235C6.813 68.2052 5.7687 68.2331 4.9628 67.8196C3.9465 67.301 3.3018 66.0673 3.3018 64.3222V26.2318C3.3018 23.0991 5.3903 19.5036 7.9624 18.193L39.2619 2.2421C40.4043 1.6604 41.4486 1.6321 42.2545 2.0456Z"
stroke="currentColor"
strokeOpacity={active ? "0.3" : "0.2"}
strokeWidth="0.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
)
}
export { IconStack, type IconStackProps }
+125
View File
@@ -0,0 +1,125 @@
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
/**
* CSS variable architecture:
*
* The root owns four variables so every part of the tile stays in proportion
* and stays overridable from a single `className`:
*
* --icon-tile-size tile width/height
* --icon-tile-icon-size glyph size applied to child svgs
* --icon-tile-radius corner radius (also drives the nested inner card)
* --icon-tile-inset gap between the outer ring and the inner card
*
* The `frame` and `soft` variants paint their inner card with an `::after`
* pseudo element instead of a wrapper node. `isolate` makes the root a stacking
* context, so the negative z-index pseudo paints above the root background but
* below the in-flow icon - no extra DOM, and `render` composition keeps working.
*
* Tone: `soft` and `solid` derive every fill and border from `currentColor`, so
* a single text color class (e.g. `text-success`) retints the whole tile. They
* default to `text-primary`; override it to recolor without touching internals.
*/
const iconTileVariants = cva(
[
"relative inline-flex shrink-0 items-center justify-center align-middle",
"size-(--icon-tile-size) rounded-(--icon-tile-radius)",
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-(--icon-tile-icon-size)",
],
{
variants: {
variant: {
/** Plain bordered surface. The quiet default for list rows and toolbars. */
outline: "border border-border bg-background dark:bg-input/32",
/** Raised muted fill with a background-colored ring. Reads as a physical chip. */
elevated:
"border-2 border-background bg-muted text-accent-foreground shadow-[0_1px_3px_0_rgb(0_0_0/0.14)] dark:border",
/**
* Tinted double container: an opacity-filled outer ring with no border
* around a bordered inner card, all derived from `currentColor`. The
* quiet, colorful sibling of `frame`. Retint with a text color class.
*/
soft: [
"isolate p-(--icon-tile-inset) text-primary bg-current/10",
"after:absolute after:-z-10 after:inset-(--icon-tile-inset)",
"after:rounded-[calc(var(--icon-tile-radius)-var(--icon-tile-inset))]",
"after:border after:border-current/20 after:bg-current/5",
],
/** Filled tone with a contrasting glyph. Retint with `bg-*` + a text color. */
solid: "bg-primary text-primary-foreground",
/** Double container - a muted ring around an inset card, matching Frame. */
frame: [
"isolate border border-border bg-muted/50 p-(--icon-tile-inset)",
"after:absolute after:-z-10 after:inset-(--icon-tile-inset)",
"after:rounded-[calc(var(--icon-tile-radius)-var(--icon-tile-inset))]",
"after:border after:border-border after:bg-card after:shadow-xs",
],
},
size: {
xs: "[--icon-tile-size:--spacing(6)] [--icon-tile-icon-size:--spacing(3.5)] [--icon-tile-inset:--spacing(0.5)]",
sm: "[--icon-tile-size:--spacing(8)] [--icon-tile-icon-size:--spacing(4)] [--icon-tile-inset:--spacing(0.5)]",
default:
"[--icon-tile-size:--spacing(10)] [--icon-tile-icon-size:--spacing(4.5)] [--icon-tile-inset:--spacing(0.75)]",
lg: "[--icon-tile-size:--spacing(12)] [--icon-tile-icon-size:--spacing(5.5)] [--icon-tile-inset:--spacing(0.75)]",
xl: "[--icon-tile-size:--spacing(14)] [--icon-tile-icon-size:--spacing(7)] [--icon-tile-inset:--spacing(1)]",
},
/**
* `default`: active style radius. `full`: circular.
* The plain value is the fallback outside a style scope; the `style-*`
* tokens win by specificity inside one, and survive the registry
* transform as the single resolved value per generated style.
*
* Each style token is clamped to a fraction of the tile. A flat radius
* is a circle once it reaches half the box, so the soft styles used to
* render `xs` (24px) and `sm` (32px) as plain circles and swallow the
* `full` variant's meaning. Clamping keeps one corner ratio at every
* size instead, so a tile still reads as the same shape when it scales.
* Luma and Rhea take the gentler quarter ratio; Maia stays at a third.
*/
radius: {
default:
"[--icon-tile-radius:min(var(--radius-md),calc(var(--icon-tile-size)/3))] [--icon-tile-radius:min(var(--radius-md),calc(var(--icon-tile-size)/3))]",
full: "[--icon-tile-radius:calc(infinity*1px)]",
},
},
defaultVariants: {
variant: "outline",
size: "default",
radius: "default",
},
}
)
interface IconTileProps extends useRender.ComponentProps<"span"> {
variant?: VariantProps<typeof iconTileVariants>["variant"]
size?: VariantProps<typeof iconTileVariants>["size"]
radius?: VariantProps<typeof iconTileVariants>["radius"]
}
function IconTile({
className,
variant = "outline",
size = "default",
radius = "default",
render,
...props
}: IconTileProps) {
const defaultProps = {
"data-slot": "icon-tile",
"data-variant": variant,
"data-size": size,
className: cn(iconTileVariants({ variant, size, radius, className })),
}
return useRender({
defaultTagName: "span",
render,
props: mergeProps<"span">(defaultProps, props),
})
}
export { IconTile, iconTileVariants, type IconTileProps }