Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs-site/src/ui/context/layout/detail-panel.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ through spacing and a sentence-case title, not a card, divider, or decorative
background. Pass `icon` for a fixed section icon slot and `tone` to distinguish
portable `accent`, `neutral`, `success`, `warning`, or `danger` roles through
text color only. Pass `description` for short supporting context, `meta` for a
count or state, and `actions` for a normal section. A normal section may omit
count or state, and `actions` for a normal section. Only the description is
muted: a field in `actions` keeps its own description and error styles. A normal section may omit
its body to represent a compact, actionable empty group. Set `collapsible` for
secondary content. Closed sections show a compact action row; open sections show
a heading with a collapse button on the right. Their content stays mounted when
Expand Down
2 changes: 1 addition & 1 deletion docs-site/src/ui/context/layout/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ sidebar-first overview.
Put the page's single primary action, such as a create button or a create
menu, in `actions`. It sits at the trailing edge of the page header.

Use `AppOverview.Main` for the collection. Its `toolbar` slot suits a search field or one compact filter.
Use `AppOverview.Main` for the collection. Its `toolbar` slot suits a search field or one compact filter. A field there keeps its own description and error styles.

Use `AppOverview.Cards` when the collection is a small set of objects. It lays
out as many columns of at least 19rem as fit and collapses to one column on
Expand Down
2 changes: 2 additions & 0 deletions docs-site/src/ui/context/layout/panel-dialog.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ separately.
`surface="contained"` is the default modal treatment. `surface="floating"` makes the header, footer, and each section separate paper surfaces for settings-style pages.

Use `PanelDialog.Section` for meaningful field groups. Keep the primary save action in `PanelDialog.Footer`.
Only the section's own subtitle is muted: fields, notices, and paragraphs in the
section keep their own description, error, and text styles.

Pass `hideable` to a section for optional settings. Closed sections show a flat
summary row with an eye icon at the trailing edge. Open sections use the normal
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/layout/AppOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const AppOverviewPanelHeader = (props: Pick<AppOverviewPanelProps, "title" | "de
<div>
<h2>{props.title}</h2>
<Show when={props.description}>
<p>{props.description}</p>
<p class="k2b-app-overview__panel-description">{props.description}</p>
</Show>
</div>
<Show when={props.toolbar}>
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/layout/DetailPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ const DetailPanelSection = (props: DetailPanelSectionProps): JSX.Element => {
<div class="k2b-detail-panel__section-copy">
<h3 id={headingId}>{props.title}</h3>
<Show when={props.description}>
<p>{props.description}</p>
<p class="k2b-detail-panel__section-description">{props.description}</p>
</Show>
</div>
<Show when={props.meta}>
Expand Down Expand Up @@ -398,7 +398,7 @@ const DetailPanelSection = (props: DetailPanelSectionProps): JSX.Element => {
<div class="k2b-detail-panel__section-copy">
<h3 id={headingId}>{props.title}</h3>
<Show when={props.description}>
<p>{props.description}</p>
<p class="k2b-detail-panel__section-description">{props.description}</p>
</Show>
</div>
<Show when={props.meta}>
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/layout/PanelDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ const PanelDialogSection = (props: PanelDialogSectionProps): JSX.Element => {
<div>
<h3 id={id}>{props.title}</h3>
<Show when={props.subtitle}>
<p>{props.subtitle}</p>
<p class="k2b-panel-dialog__section-subtitle">{props.subtitle}</p>
</Show>
</div>
<Show when={props.actions}>
Expand Down
124 changes: 124 additions & 0 deletions packages/ui/src/layout/container-field-parts.browser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { resolve } from "node:path";
import { createConfig } from "@k2b/ssr";
import { type Browser, chromium } from "playwright";
import { createComponent, type JSX } from "solid-js";
import { renderToString } from "solid-js/web";

// Which rule wins for a field's description and error is decided by the
// cascade, which happy-dom does not model, so a real engine renders the
// shipped stylesheet. Containers style their own subtitle by class: a bare
// `p` selector there would outrank the styles of fields and app paragraphs inside.
const root = mkdtempSync(resolve(tmpdir(), "k2b-ui-container-field-parts-"));
const { plugin } = createConfig({ dev: true, rootDir: root });
Bun.plugin(plugin());
process.once("exit", () => rmSync(root, { recursive: true, force: true }));

const { TextInput } = await import("../inputs/TextInput");
const { default: AppOverview } = await import("./AppOverview");
const { default: DetailPanel } = await import("./DetailPanel");
const { default: PanelDialog } = await import("./PanelDialog");

const css = readFileSync(resolve(import.meta.dir, "../../dist/styles.css"), "utf8");

let browser: Browser;
beforeAll(async () => {
browser = await chromium.launch();
}, 30_000);
afterAll(async () => {
await browser?.close();
});

const field = () => createComponent(TextInput, { label: "Name", description: "Shown to everyone.", error: "Name is required.", value: "" });

const containers: [string, () => JSX.Element, string][] = [
[
"panel dialog section body",
() =>
createComponent(PanelDialog.Section, {
title: "General",
subtitle: "Basic details.",
get children() {
return field();
},
}),
".k2b-panel-dialog__section-subtitle",
],
[
"app overview toolbar",
() =>
createComponent(AppOverview.Main, {
title: "Your bases",
description: "Everything you can open.",
get toolbar() {
return field();
},
children: "",
}),
".k2b-app-overview__panel-description",
],
[
"detail panel section actions",
() =>
createComponent(DetailPanel.Section, {
title: "Owner",
description: "Who answers questions.",
get actions() {
return field();
},
}),
".k2b-detail-panel__section-description",
],
];

const partStyles = (selector: string) => {
const element = document.querySelector(selector);
if (!element) throw new Error(`Missing ${selector}`);
const style = getComputedStyle(element);
return { color: style.color, fontSize: style.fontSize, margin: style.margin };
};

describe("field parts inside containers", () => {
for (const [name, container, subtitle] of containers) {
test(`${name} keeps the styles of a field and of an app paragraph beside it`, async () => {
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
try {
// Applications style their paragraphs with utilities in a cascade
// layer, which any unlayered container rule for `p` would outrank.
await page.setContent(
`<!doctype html><html><head><style>${css}</style>` +
`<style>@layer utilities { .app-hint { color: rgb(1, 2, 3); font-size: 14px; } }</style></head><body class="k2b-ui">` +
`<div id="reference">${renderToString(field)}</div><div id="container">${renderToString(container)}</div></body></html>`,
);
const read = (selector: string) => page.evaluate(partStyles, selector);

const muted = await page.evaluate(() => {
const probe = document.createElement("span");
probe.style.color = "var(--k2b-text-muted)";
document.body.append(probe);
return getComputedStyle(probe).color;
});
const error = await read("#container .k2b-field__error");
expect(error).toEqual(await read("#reference .k2b-field__error"));
expect(error.color).not.toBe(muted);
expect(await read("#container .k2b-field__description")).toEqual(await read("#reference .k2b-field__description"));
expect((await read(`#container ${subtitle}`)).color).toBe(muted);

const hint = await page.evaluate(() => {
const paragraph = document.createElement("p");
paragraph.className = "app-hint";
const field = document.querySelector("#container .k2b-field");
if (!field) throw new Error("Missing field");
field.after(paragraph);
const style = getComputedStyle(paragraph);
return { color: style.color, fontSize: style.fontSize };
});
expect(hint).toEqual({ color: "rgb(1, 2, 3)", fontSize: "14px" });
} finally {
await page.close();
}
});
}
});
11 changes: 5 additions & 6 deletions packages/ui/src/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -2818,7 +2818,6 @@
background: color-mix(in srgb, var(--k2b-detail-panel-accent, var(--k2b-accent-600)) 9%, transparent);
}

.k2b-ui .k2b-detail-panel__section-header p,
.k2b-ui .k2b-detail-panel__section-description {
margin: 0.125rem 0 0;
color: var(--k2b-text-muted);
Expand Down Expand Up @@ -3074,8 +3073,7 @@

.k2b-ui .k2b-panel-dialog__heading h2,
.k2b-ui .k2b-panel-dialog__heading p,
.k2b-ui .k2b-panel-dialog__section h3,
.k2b-ui .k2b-panel-dialog__section p {
.k2b-ui .k2b-panel-dialog__section h3 {
margin: 0;
}

Expand All @@ -3095,10 +3093,11 @@
overflow-wrap: anywhere;
}

.k2b-ui .k2b-panel-dialog__section p {
margin-top: 0.125rem;
/* A class, not `p`: section bodies hold fields, and a bare `p` here would
outrank their description and error styles. */
.k2b-ui .k2b-panel-dialog__section > header .k2b-panel-dialog__section-subtitle {
margin: 0.125rem 0 0;
color: var(--k2b-text-muted);
font-size: 0.6875rem;
}

.k2b-ui .k2b-panel-dialog__actions {
Expand Down
6 changes: 4 additions & 2 deletions packages/ui/src/styles/layout-parity.css
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@
}

.k2b-ui .k2b-app-overview__panel-header h2,
.k2b-ui .k2b-app-overview__panel-header p {
.k2b-ui .k2b-app-overview__panel-description {
margin: 0;
}

Expand All @@ -140,7 +140,9 @@
font-weight: 650;
}

.k2b-ui .k2b-app-overview__panel-header p {
/* A class, not `p`: the toolbar beside it holds fields with their own
description and error styles. */
.k2b-ui .k2b-app-overview__panel-description {
color: var(--k2b-text-muted);
font-size: 0.75rem;
}
Expand Down
Loading