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 <cursoragent@cursor.com>
125 lines
5.5 KiB
TypeScript
125 lines
5.5 KiB
TypeScript
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 } |