mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-02 21:03:34 +03:00
Create primitive for CardRail
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
.sui-card-rail {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
/* A real horizontal scroller: keep the bounce, but don't chain to the browser's back gesture. */
|
||||
overscroll-behavior-x: contain;
|
||||
/* Room for the scrollbar so it doesn't sit under the last row of item content. */
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
/* Gap scale mirrors Stack / Inline. */
|
||||
.sui-card-rail--gap-0 {
|
||||
gap: var(--space-0);
|
||||
}
|
||||
.sui-card-rail--gap-0_5 {
|
||||
gap: var(--space-0_5);
|
||||
}
|
||||
.sui-card-rail--gap-1 {
|
||||
gap: var(--space-1);
|
||||
}
|
||||
.sui-card-rail--gap-1_5 {
|
||||
gap: var(--space-1_5);
|
||||
}
|
||||
.sui-card-rail--gap-2 {
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.sui-card-rail--gap-3 {
|
||||
gap: var(--space-3);
|
||||
}
|
||||
.sui-card-rail--gap-4 {
|
||||
gap: var(--space-4);
|
||||
}
|
||||
.sui-card-rail--gap-5 {
|
||||
gap: var(--space-5);
|
||||
}
|
||||
.sui-card-rail--gap-6 {
|
||||
gap: var(--space-6);
|
||||
}
|
||||
.sui-card-rail--gap-8 {
|
||||
gap: var(--space-8);
|
||||
}
|
||||
|
||||
/* Uniform item sizing: fixed width so items don't stretch/shrink, optional fixed height so a row of
|
||||
cards is equal-height. Both default to natural sizing when the caller sets no dimension. */
|
||||
.sui-card-rail > * {
|
||||
flex: 0 0 var(--sui-card-rail-item-w, auto);
|
||||
height: var(--sui-card-rail-item-h, auto);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import ShieldOutlinedIcon from "@mui/icons-material/ShieldOutlined";
|
||||
import CategoryOutlinedIcon from "@mui/icons-material/CategoryOutlined";
|
||||
import GavelOutlinedIcon from "@mui/icons-material/GavelOutlined";
|
||||
import LayersOutlinedIcon from "@mui/icons-material/LayersOutlined";
|
||||
import AltRouteOutlinedIcon from "@mui/icons-material/AltRouteOutlined";
|
||||
import ScheduleOutlinedIcon from "@mui/icons-material/ScheduleOutlined";
|
||||
import { CardRail } from "@app/ui/CardRail";
|
||||
import { OptionCard } from "@app/ui/OptionCard";
|
||||
|
||||
const items = [
|
||||
{
|
||||
icon: <ShieldOutlinedIcon />,
|
||||
title: "Security",
|
||||
desc: "Redact, sanitize, and watermark every document.",
|
||||
},
|
||||
{
|
||||
icon: <CategoryOutlinedIcon />,
|
||||
title: "Classification",
|
||||
desc: "Tag each document against your team's labels.",
|
||||
},
|
||||
{
|
||||
icon: <GavelOutlinedIcon />,
|
||||
title: "Compliance",
|
||||
desc: "Enforce frameworks and keep an audit trail.",
|
||||
},
|
||||
{
|
||||
icon: <LayersOutlinedIcon />,
|
||||
title: "Ingestion",
|
||||
desc: "OCR and flatten documents as they arrive.",
|
||||
},
|
||||
{
|
||||
icon: <AltRouteOutlinedIcon />,
|
||||
title: "Routing",
|
||||
desc: "Send finished documents where they belong.",
|
||||
},
|
||||
{
|
||||
icon: <ScheduleOutlinedIcon />,
|
||||
title: "Retention",
|
||||
desc: "Archive and expire on your schedule.",
|
||||
},
|
||||
];
|
||||
|
||||
const meta: Meta<typeof CardRail> = {
|
||||
title: "Primitives/CardRail",
|
||||
component: CardRail,
|
||||
tags: ["autodocs"],
|
||||
parameters: { layout: "padded" },
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof CardRail>;
|
||||
|
||||
/** A row of equal-size cards that scrolls sideways when they overflow the container. */
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<CardRail itemWidth="16rem" itemHeight="11rem">
|
||||
{items.map((it) => (
|
||||
<OptionCard
|
||||
key={it.title}
|
||||
icon={it.icon}
|
||||
title={it.title}
|
||||
description={it.desc}
|
||||
cta="Set up"
|
||||
onSelect={() => {}}
|
||||
/>
|
||||
))}
|
||||
</CardRail>
|
||||
),
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import type {
|
||||
CSSProperties,
|
||||
ElementType,
|
||||
HTMLAttributes,
|
||||
ReactNode,
|
||||
} from "react";
|
||||
import type { StackGap } from "@app/ui/Stack";
|
||||
import "@app/ui/CardRail.css";
|
||||
|
||||
export interface CardRailProps extends HTMLAttributes<HTMLElement> {
|
||||
/** Token-aligned gap between items (maps to `--space-*`). */
|
||||
gap?: StackGap;
|
||||
/** Fixed width for every item (any CSS length); omit to let items size themselves. */
|
||||
itemWidth?: string;
|
||||
/** Fixed height for every item; omit for natural height. Equal heights line item footers up. */
|
||||
itemHeight?: string;
|
||||
as?: ElementType;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* A horizontal row of equal-sized items that scrolls sideways rather than wrapping - the "rail" of
|
||||
* cards motif (template galleries, tier pickers, at-a-glance strips). The scrolling sibling to
|
||||
* {@link Stack} (vertical) and {@link Inline} (horizontal, wraps): it keeps items on one line,
|
||||
* contains the overscroll so it doesn't trigger the browser back-gesture, and sizes every child
|
||||
* uniformly so their footers align.
|
||||
*/
|
||||
export function CardRail({
|
||||
gap = "3",
|
||||
itemWidth,
|
||||
itemHeight,
|
||||
as,
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
...rest
|
||||
}: CardRailProps) {
|
||||
const Tag: ElementType = as ?? "div";
|
||||
const vars = {
|
||||
...(itemWidth ? { "--sui-card-rail-item-w": itemWidth } : {}),
|
||||
...(itemHeight ? { "--sui-card-rail-item-h": itemHeight } : {}),
|
||||
...style,
|
||||
} as CSSProperties;
|
||||
const classes = [
|
||||
"sui-card-rail",
|
||||
`sui-card-rail--gap-${gap}`,
|
||||
className ?? "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
return (
|
||||
<Tag className={classes} style={vars} {...rest}>
|
||||
{children}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ export * from "@app/ui/ProgressBar";
|
||||
export * from "@app/ui/MetricCard";
|
||||
export * from "@app/ui/NodeCard";
|
||||
export * from "@app/ui/OptionCard";
|
||||
export * from "@app/ui/CardRail";
|
||||
export * from "@app/ui/IconPicker";
|
||||
export * from "@app/ui/NavItem";
|
||||
export * from "@app/ui/NavSurface";
|
||||
|
||||
@@ -39,24 +39,6 @@
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* Template gallery: a single row of cards that scrolls sideways rather than wrapping. */
|
||||
.portal-pipelines__templates-scroll {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 0.75rem;
|
||||
overflow-x: auto;
|
||||
/* A real horizontal scroller: keep the bounce, but don't chain to the browser's back gesture. */
|
||||
overscroll-behavior-x: contain;
|
||||
/* Room for the scrollbar so it doesn't sit under the last row of card text. */
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
/* Fixed track height so every OptionCard in the row is the same height and their footers line up. */
|
||||
.portal-pipelines__templates-scroll > * {
|
||||
flex: 0 0 16rem;
|
||||
height: 12rem;
|
||||
}
|
||||
|
||||
.portal-pipelines__section-title {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useLocation, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import AddRoundedIcon from "@mui/icons-material/AddRounded";
|
||||
import { Banner, Button, EmptyState, Skeleton } from "@app/ui";
|
||||
import { Banner, Button, CardRail, EmptyState, Skeleton } from "@app/ui";
|
||||
import { errorMessage } from "@portal/api/http";
|
||||
import { useSectionFlags } from "@portal/hooks/useAsync";
|
||||
import { usePipelines } from "@portal/queries/pipelines";
|
||||
@@ -311,7 +311,7 @@ export function Pipelines() {
|
||||
<h2 className="portal-pipelines__section-title">
|
||||
{t("portal.pipelines.templates.title")}
|
||||
</h2>
|
||||
<div className="portal-pipelines__templates-scroll">
|
||||
<CardRail itemWidth="16rem" itemHeight="12rem">
|
||||
{galleryEntries.map((entry) => (
|
||||
<PipelineTemplateCard
|
||||
key={entry.category.id}
|
||||
@@ -321,7 +321,7 @@ export function Pipelines() {
|
||||
lockedLabel={t("portal.policies.card.requiresAiEngine")}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</CardRail>
|
||||
</section>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user