style(frontend): tighten CTE plan vertical rhythm
This commit is contained in:
@@ -68,19 +68,15 @@ test("renders strategy, question, and a chain strip with CTE names in order", ()
|
|||||||
expect(screen.getAllByText("impianti_recenti").length).toBeGreaterThan(0);
|
expect(screen.getAllByText("impianti_recenti").length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("uses compact vertical and responsive lateral CTE card padding", () => {
|
test("uses compact CTE section rhythm with moderate lateral padding", () => {
|
||||||
const { container } = render(<CtePlanViewer plan={plan} />);
|
const { container } = render(<CtePlanViewer plan={plan} />);
|
||||||
|
|
||||||
for (const header of container.querySelectorAll('[data-slot="card-header"]')) {
|
for (const header of container.querySelectorAll('[data-slot="card-header"]')) {
|
||||||
expect(header).toHaveClass("px-3", "py-2", "sm:px-4", "[&.border-b]:pb-2");
|
expect(header).toHaveClass("gap-2", "px-3", "py-2", "sm:px-4", "[&.border-b]:pb-2");
|
||||||
expect(header).not.toHaveClass("p-4", "sm:p-5", "sm:[&.border-b]:pb-5");
|
expect(header).not.toHaveClass("gap-3", "p-4", "sm:p-5");
|
||||||
}
|
}
|
||||||
for (const content of container.querySelectorAll('[data-slot="card-content"]')) {
|
for (const content of container.querySelectorAll('[data-slot="card-content"]')) {
|
||||||
expect(content).toHaveClass("px-3", "py-2", "sm:px-4");
|
expect(content).toHaveClass("gap-3", "px-3", "py-2", "sm:px-4");
|
||||||
expect(content).not.toHaveClass("p-4", "sm:p-5");
|
expect(content).not.toHaveClass("gap-5", "p-4", "sm:p-5");
|
||||||
}
|
|
||||||
for (const card of container.querySelectorAll('[data-slot="card"]')) {
|
|
||||||
expect(card).toHaveClass("shadow-none");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -142,7 +138,7 @@ test("groups filters in one bordered divided list with accessible padded rows",
|
|||||||
expect(list).toHaveClass("border", "divide-y");
|
expect(list).toHaveClass("border", "divide-y");
|
||||||
for (const filter of filters) {
|
for (const filter of filters) {
|
||||||
expect(filter.parentElement).toBe(list);
|
expect(filter.parentElement).toBe(list);
|
||||||
expect(filter).toHaveClass("px-3", "py-2", "sm:px-4");
|
expect(filter).toHaveClass("px-3", "py-1", "sm:px-4");
|
||||||
expect(filter).not.toHaveClass("p-3", "sm:p-4");
|
expect(filter).not.toHaveClass("p-3", "sm:p-4");
|
||||||
}
|
}
|
||||||
lists.push(list!);
|
lists.push(list!);
|
||||||
@@ -161,22 +157,23 @@ test("renders rationale as a plain top-divided section", () => {
|
|||||||
expect(rationale).not.toHaveClass("border-l-2", "bg-primary/5", "rounded-lg");
|
expect(rationale).not.toHaveClass("border-l-2", "bg-primary/5", "rounded-lg");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("uses the same compact padding for table rows and rationale dividers", () => {
|
test("compacts table, filter, detail, and rationale rows", () => {
|
||||||
render(<CtePlanViewer plan={plan} />);
|
render(<CtePlanViewer plan={plan} />);
|
||||||
const card = getCteCard("pazienti_idonei");
|
const card = getCteCard("pazienti_idonei");
|
||||||
const tableName = within(card).getByText("pazienti");
|
const tableRow = within(card).getByText("pazienti").parentElement;
|
||||||
expect(tableName.parentElement).toHaveClass("px-3", "py-2", "sm:px-4");
|
expect(tableRow).toHaveClass("px-3", "py-1", "sm:px-4");
|
||||||
expect(tableName.parentElement).not.toHaveClass("p-3", "sm:p-4");
|
|
||||||
|
|
||||||
const filterRationale = within(card).getByText("Solo pazienti in follow-up").parentElement;
|
const filter = within(card).getByRole("group", { name: "Filter 1" });
|
||||||
expect(filterRationale).toHaveClass("pt-2");
|
expect(filter).toHaveClass("space-y-2", "px-3", "py-1", "sm:px-4");
|
||||||
expect(filterRationale).not.toHaveClass("pt-3");
|
expect(filter.firstElementChild).toHaveClass("gap-2");
|
||||||
|
const filterDetails = within(card).getByText("Solo pazienti in follow-up").parentElement;
|
||||||
|
expect(filterDetails).toHaveClass("gap-y-1", "pt-1");
|
||||||
|
|
||||||
const cteRationale = within(card)
|
const rationale = within(card)
|
||||||
.getByText("Base della catena: riduce il volume prima dei join")
|
.getByText("Base della catena: riduce il volume prima dei join")
|
||||||
.closest("section");
|
.closest("section");
|
||||||
expect(cteRationale).toHaveClass("border-t", "pt-2");
|
expect(rationale).toHaveClass("pt-1");
|
||||||
expect(cteRationale).not.toHaveClass("pt-4");
|
expect(within(rationale!).getByText("Rationale")).toHaveClass("mb-1");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("shows 'no dependencies' for a CTE with an empty depends_on", () => {
|
test("shows 'no dependencies' for a CTE with an empty depends_on", () => {
|
||||||
|
|||||||
@@ -16,34 +16,34 @@ function FilterRow({ filter, index }: { filter: CtePlanFilter; index: number })
|
|||||||
<div
|
<div
|
||||||
role="group"
|
role="group"
|
||||||
aria-label={`Filter ${index}`}
|
aria-label={`Filter ${index}`}
|
||||||
className="min-w-0 space-y-3 px-3 py-2 sm:px-4"
|
className="min-w-0 space-y-2 px-3 py-1 sm:px-4"
|
||||||
>
|
>
|
||||||
<div className="grid min-w-0 grid-cols-1 gap-3 md:grid-cols-[minmax(0,1fr)_minmax(5rem,auto)_minmax(0,1fr)]">
|
<div className="grid min-w-0 grid-cols-1 gap-2 md:grid-cols-[minmax(0,1fr)_minmax(5rem,auto)_minmax(0,1fr)]">
|
||||||
<div className="min-w-0 space-y-1">
|
<div className="min-w-0 space-y-1">
|
||||||
<p className={FIELD_LABEL}>Column</p>
|
<p className={FIELD_LABEL}>Column</p>
|
||||||
<code className="block min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] font-mono text-xs leading-relaxed text-foreground">
|
<code className="block min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] font-mono text-xs leading-4 text-foreground">
|
||||||
{filter.column}
|
{filter.column}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 space-y-1 md:text-center">
|
<div className="min-w-0 space-y-1 md:text-center">
|
||||||
<p className={FIELD_LABEL}>Operator</p>
|
<p className={FIELD_LABEL}>Operator</p>
|
||||||
<code className="block min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] font-mono text-xs font-semibold leading-relaxed text-primary">
|
<code className="block min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] font-mono text-xs font-semibold leading-4 text-primary">
|
||||||
{filter.op}
|
{filter.op}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 space-y-1">
|
<div className="min-w-0 space-y-1">
|
||||||
<p className={FIELD_LABEL}>Value</p>
|
<p className={FIELD_LABEL}>Value</p>
|
||||||
<code className="block min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] font-mono text-xs leading-relaxed text-foreground">
|
<code className="block min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] font-mono text-xs leading-4 text-foreground">
|
||||||
{filter.value}
|
{filter.value}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{(filter.description || filter.rationale) && (
|
{(filter.description || filter.rationale) && (
|
||||||
<div className="grid min-w-0 gap-x-4 gap-y-2 border-t border-border/50 pt-2 md:grid-cols-[5.5rem_minmax(0,1fr)]">
|
<div className="grid min-w-0 gap-x-4 gap-y-1 border-t border-border/50 pt-1 md:grid-cols-[5.5rem_minmax(0,1fr)]">
|
||||||
{filter.description && (
|
{filter.description && (
|
||||||
<>
|
<>
|
||||||
<span className={FIELD_LABEL}>Description</span>
|
<span className={FIELD_LABEL}>Description</span>
|
||||||
<p className="min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] text-xs leading-relaxed text-muted-foreground">
|
<p className="min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] text-xs leading-4 text-muted-foreground">
|
||||||
{filter.description}
|
{filter.description}
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
@@ -51,7 +51,7 @@ function FilterRow({ filter, index }: { filter: CtePlanFilter; index: number })
|
|||||||
{filter.rationale && (
|
{filter.rationale && (
|
||||||
<>
|
<>
|
||||||
<span className={FIELD_LABEL}>Rationale</span>
|
<span className={FIELD_LABEL}>Rationale</span>
|
||||||
<p className="min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] text-xs leading-relaxed text-muted-foreground">
|
<p className="min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] text-xs leading-4 text-muted-foreground">
|
||||||
{filter.rationale}
|
{filter.rationale}
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
@@ -67,25 +67,25 @@ function CteCard({ cte, total }: { cte: CtePlanCte; total: number }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="gap-0 rounded-lg border py-0 shadow-none">
|
<Card className="gap-0 rounded-lg border py-0 shadow-none">
|
||||||
<CardHeader className="gap-3 rounded-t-lg border-b border-border/60 bg-muted/15 px-3 py-2 sm:px-4 [&.border-b]:pb-2">
|
<CardHeader className="gap-2 rounded-t-lg border-b border-border/60 bg-muted/15 px-3 py-2 sm:px-4 [&.border-b]:pb-2">
|
||||||
<div className="grid min-w-0 grid-cols-[auto_minmax(0,1fr)] items-center gap-2.5">
|
<div className="grid min-w-0 grid-cols-[auto_minmax(0,1fr)] items-center gap-2.5">
|
||||||
<Badge variant="outline" className="font-mono">
|
<Badge variant="outline" className="font-mono">
|
||||||
CTE {cte.index}/{total}
|
CTE {cte.index}/{total}
|
||||||
</Badge>
|
</Badge>
|
||||||
<h3 className="min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] font-mono text-sm font-medium leading-relaxed">
|
<h3 className="min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] font-mono text-sm font-medium leading-5">
|
||||||
{cte.name}
|
{cte.name}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
{cte.purpose && (
|
{cte.purpose && (
|
||||||
<p className="min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] text-sm leading-relaxed text-muted-foreground">
|
<p className="min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] text-sm leading-5 text-muted-foreground">
|
||||||
{cte.purpose}
|
{cte.purpose}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex min-w-0 flex-col gap-5 px-3 py-2 sm:px-4">
|
<CardContent className="flex min-w-0 flex-col gap-3 px-3 py-2 sm:px-4">
|
||||||
<div className="grid min-w-0 gap-4 md:grid-cols-2">
|
<div className="grid min-w-0 gap-3 md:grid-cols-2">
|
||||||
<section className="min-w-0">
|
<section className="min-w-0">
|
||||||
<h4 className="thot-label mb-2">Depends on</h4>
|
<h4 className="thot-label mb-1">Depends on</h4>
|
||||||
{dependsOn.length ? (
|
{dependsOn.length ? (
|
||||||
<div className="flex min-w-0 flex-wrap gap-1.5">
|
<div className="flex min-w-0 flex-wrap gap-1.5">
|
||||||
{dependsOn.map((dependency) => (
|
{dependsOn.map((dependency) => (
|
||||||
@@ -99,7 +99,7 @@ function CteCard({ cte, total }: { cte: CtePlanCte; total: number }) {
|
|||||||
|
|
||||||
{cte.keys && cte.keys.length > 0 && (
|
{cte.keys && cte.keys.length > 0 && (
|
||||||
<section className="min-w-0">
|
<section className="min-w-0">
|
||||||
<h4 className="thot-label mb-2">Keys</h4>
|
<h4 className="thot-label mb-1">Keys</h4>
|
||||||
<div className="flex min-w-0 flex-wrap gap-1.5">
|
<div className="flex min-w-0 flex-wrap gap-1.5">
|
||||||
{cte.keys.map((key) => (
|
{cte.keys.map((key) => (
|
||||||
<Chip key={key}>{key}</Chip>
|
<Chip key={key}>{key}</Chip>
|
||||||
@@ -111,18 +111,18 @@ function CteCard({ cte, total }: { cte: CtePlanCte; total: number }) {
|
|||||||
|
|
||||||
{cte.tables && cte.tables.length > 0 && (
|
{cte.tables && cte.tables.length > 0 && (
|
||||||
<section className="min-w-0">
|
<section className="min-w-0">
|
||||||
<h4 className="thot-label mb-2">Tables</h4>
|
<h4 className="thot-label mb-1">Tables</h4>
|
||||||
<div className="min-w-0 divide-y divide-border/50 rounded-lg border border-border/60">
|
<div className="min-w-0 divide-y divide-border/50 rounded-lg border border-border/60">
|
||||||
{cte.tables.map((table) => (
|
{cte.tables.map((table) => (
|
||||||
<div
|
<div
|
||||||
key={table.name}
|
key={table.name}
|
||||||
className="grid min-w-0 gap-1 px-3 py-2 sm:px-4 md:grid-cols-[minmax(10rem,0.8fr)_minmax(0,1.2fr)] md:gap-4"
|
className="grid min-w-0 gap-1 px-3 py-1 sm:px-4 md:grid-cols-[minmax(10rem,0.8fr)_minmax(0,1.2fr)] md:gap-4"
|
||||||
>
|
>
|
||||||
<code className="min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] font-mono text-xs leading-relaxed text-foreground">
|
<code className="min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] font-mono text-xs leading-4 text-foreground">
|
||||||
{table.name}
|
{table.name}
|
||||||
</code>
|
</code>
|
||||||
{table.description && (
|
{table.description && (
|
||||||
<p className="min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] text-xs leading-relaxed text-muted-foreground">
|
<p className="min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere] text-xs leading-4 text-muted-foreground">
|
||||||
{table.description}
|
{table.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -134,7 +134,7 @@ function CteCard({ cte, total }: { cte: CtePlanCte; total: number }) {
|
|||||||
|
|
||||||
{cte.filters && cte.filters.length > 0 && (
|
{cte.filters && cte.filters.length > 0 && (
|
||||||
<section className="min-w-0">
|
<section className="min-w-0">
|
||||||
<h4 className="thot-label mb-2">Filters</h4>
|
<h4 className="thot-label mb-1">Filters</h4>
|
||||||
<div className="min-w-0 divide-y divide-border/50 rounded-lg border border-border/60">
|
<div className="min-w-0 divide-y divide-border/50 rounded-lg border border-border/60">
|
||||||
{cte.filters.map((f, i) => (
|
{cte.filters.map((f, i) => (
|
||||||
<FilterRow key={`${f.column}-${i}`} filter={f} index={i + 1} />
|
<FilterRow key={`${f.column}-${i}`} filter={f} index={i + 1} />
|
||||||
@@ -145,7 +145,7 @@ function CteCard({ cte, total }: { cte: CtePlanCte; total: number }) {
|
|||||||
|
|
||||||
{cte.output_columns && cte.output_columns.length > 0 && (
|
{cte.output_columns && cte.output_columns.length > 0 && (
|
||||||
<section className="min-w-0">
|
<section className="min-w-0">
|
||||||
<h4 className="thot-label mb-2">Output columns</h4>
|
<h4 className="thot-label mb-1">Output columns</h4>
|
||||||
<div className="flex min-w-0 flex-wrap gap-1.5">
|
<div className="flex min-w-0 flex-wrap gap-1.5">
|
||||||
{cte.output_columns.map((column) => (
|
{cte.output_columns.map((column) => (
|
||||||
<Chip key={column}>{column}</Chip>
|
<Chip key={column}>{column}</Chip>
|
||||||
@@ -155,9 +155,9 @@ function CteCard({ cte, total }: { cte: CtePlanCte; total: number }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{cte.rationale && (
|
{cte.rationale && (
|
||||||
<section className="min-w-0 border-t border-border/60 pt-2">
|
<section className="min-w-0 border-t border-border/60 pt-1">
|
||||||
<h4 className="thot-label mb-1.5">Rationale</h4>
|
<h4 className="thot-label mb-1">Rationale</h4>
|
||||||
<p className="whitespace-pre-wrap [overflow-wrap:anywhere] text-sm leading-relaxed text-foreground/90">
|
<p className="whitespace-pre-wrap [overflow-wrap:anywhere] text-sm leading-5 text-foreground/90">
|
||||||
{cte.rationale}
|
{cte.rationale}
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user