shelving 1.264.1 → 1.265.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/package.json +1 -1
  2. package/ui/README.md +2 -2
  3. package/ui/block/Blockquote.module.css +0 -3
  4. package/ui/block/Card.md +3 -7
  5. package/ui/block/Card.module.css +0 -3
  6. package/ui/block/Heading.md +0 -1
  7. package/ui/block/Heading.module.css +0 -3
  8. package/ui/block/Label.module.css +0 -3
  9. package/ui/block/List.md +1 -2
  10. package/ui/block/List.module.css +0 -3
  11. package/ui/block/Panel.md +1 -2
  12. package/ui/block/Panel.module.css +0 -3
  13. package/ui/block/Paragraph.md +1 -2
  14. package/ui/block/Paragraph.module.css +0 -3
  15. package/ui/block/Preformatted.module.css +0 -3
  16. package/ui/block/Subheading.md +0 -1
  17. package/ui/block/Subheading.module.css +0 -3
  18. package/ui/block/Title.md +0 -1
  19. package/ui/block/Title.module.css +0 -3
  20. package/ui/button/Button.md +1 -2
  21. package/ui/button/Button.module.css +0 -3
  22. package/ui/docs/DocumentationPage.test.tsx +2 -4
  23. package/ui/docs/DocumentationParams.js +1 -1
  24. package/ui/docs/DocumentationParams.tsx +1 -1
  25. package/ui/docs/DocumentationProperties.js +1 -1
  26. package/ui/docs/DocumentationProperties.tsx +1 -1
  27. package/ui/docs/DocumentationReferences.js +1 -1
  28. package/ui/docs/DocumentationReferences.tsx +1 -1
  29. package/ui/docs/DocumentationReturns.js +1 -1
  30. package/ui/docs/DocumentationReturns.tsx +1 -1
  31. package/ui/docs/DocumentationThrows.js +1 -1
  32. package/ui/docs/DocumentationThrows.tsx +1 -1
  33. package/ui/form/Field.test.tsx +1 -1
  34. package/ui/form/Progress.d.ts +9 -6
  35. package/ui/form/Progress.js +12 -6
  36. package/ui/form/Progress.md +62 -0
  37. package/ui/form/Progress.module.css +18 -21
  38. package/ui/form/Progress.tsx +19 -22
  39. package/ui/inline/Deleted.md +4 -5
  40. package/ui/inline/Deleted.module.css +1 -3
  41. package/ui/inline/Inserted.md +4 -5
  42. package/ui/inline/Inserted.module.css +1 -3
  43. package/ui/inline/Link.md +2 -3
  44. package/ui/inline/Link.module.css +1 -3
  45. package/ui/inline/Mark.js +1 -2
  46. package/ui/inline/Mark.md +5 -5
  47. package/ui/inline/Mark.module.css +2 -3
  48. package/ui/inline/Mark.tsx +0 -2
  49. package/ui/input/SchemaInput.test.tsx +3 -2
  50. package/ui/layout/CenteredLayout.d.ts +7 -8
  51. package/ui/layout/CenteredLayout.js +16 -4
  52. package/ui/layout/CenteredLayout.md +14 -6
  53. package/ui/layout/CenteredLayout.module.css +16 -22
  54. package/ui/layout/CenteredLayout.tsx +30 -13
  55. package/ui/layout/SidebarLayout.js +12 -5
  56. package/ui/layout/SidebarLayout.md +4 -4
  57. package/ui/layout/SidebarLayout.module.css +6 -9
  58. package/ui/layout/SidebarLayout.tsx +20 -17
  59. package/ui/misc/MetaContext.test.tsx +2 -3
  60. package/ui/misc/Tag.md +1 -2
  61. package/ui/misc/Tag.module.css +0 -3
  62. package/ui/notice/Message.module.css +0 -3
  63. package/ui/notice/Notice.md +1 -2
  64. package/ui/notice/Notice.module.css +0 -3
  65. package/ui/router/Router.test.tsx +1 -4
  66. package/ui/style/TINT_CLASS.md +1 -1
  67. package/ui/style/Width.d.ts +2 -1
  68. package/ui/style/Width.module.css +47 -17
  69. package/ui/style/Width.tsx +22 -1
  70. package/ui/style/getWidthClass.md +5 -9
  71. package/ui/table/Table.md +1 -1
  72. package/ui/tree/TreeButton.test.tsx +2 -5
  73. package/ui/tree/TreeContext.test.tsx +2 -3
  74. package/ui/tree/TreeMarkup.test.tsx +2 -5
  75. package/ui/tree/TreeRouter.test.tsx +2 -5
@@ -1,6 +1,7 @@
1
- import type { CSSProperties, ReactElement } from "react";
1
+ import type { ReactElement } from "react";
2
2
  import { formatPercent } from "../../util/format.js";
3
- import { getPercent } from "../../util/number.js";
3
+ import { type ColorVariants, getColorClass } from "../style/Color.js";
4
+ import { getStatusClass, type StatusVariants } from "../style/Status.js";
4
5
  import { getClass, getModuleClass } from "../util/css.js";
5
6
  import styles from "./Progress.module.css";
6
7
 
@@ -9,43 +10,39 @@ import styles from "./Progress.module.css";
9
10
  *
10
11
  * @see https://shelving.cc/ui/ProgressProps
11
12
  */
12
- export interface ProgressProps {
13
+ export interface ProgressProps extends ColorVariants, StatusVariants {
13
14
  value: number;
14
15
  min?: number;
15
16
  max?: number;
16
- success?: boolean;
17
- warning?: boolean;
18
- danger?: boolean;
19
17
  }
20
18
 
21
19
  /**
22
20
  * Show progress as a single continuous horizontal bar, filled to `value` within the `min`–`max` range (matches `getPercent()` and `formatPercent()`).
23
- * - The fill is allowed to overspill; CSS clamps it to the `0%`–`100%` range via `min-width`/`max-width`.
21
+ * - Renders a native `<progress>` element, so `role="progressbar"` and the value/max semantics come from the browser.
22
+ * - `<progress>` has no `min` attribute (its implicit minimum is `0`), so the range is normalised to `value - min` / `max - min` before it's handed to the element.
23
+ * - The browser clamps `value` to the `0`–`max` range, so an out-of-range `value` shows an empty or full bar rather than overspilling.
24
+ * - Paints from the tint ladder, so `color=` / `status=` recolour the fill (and track) by moving the tint anchor.
24
25
  *
25
26
  * @returns A progress bar element.
26
27
  * @kind component
27
28
  * @example <Progress value={3} max={4} />
29
+ * @example <Progress value={90} status="success" />
28
30
  * @see https://shelving.cc/ui/Progress
29
31
  */
30
- export function Progress({ value, min = 0, max = 100, success, warning, danger }: ProgressProps): ReactElement | null {
31
- const percent = getPercent(value - min, max - min);
32
- const fillStyle = { width: `${Number.isFinite(percent) ? percent : 0}%` } as CSSProperties;
32
+ export function Progress({ value, min = 0, max = 100, ...props }: ProgressProps): ReactElement {
33
+ // `<progress>` has no `min`, so shift the range to a `0`-based one. A non-positive span would make `<progress>` indeterminate, so fall back to an empty determinate bar.
34
+ const span = max - min;
33
35
 
34
36
  return (
35
- <figure
37
+ <progress
36
38
  className={getClass(
37
- getModuleClass(styles, "track"),
38
- success && getModuleClass(styles, "success"),
39
- warning && getModuleClass(styles, "warning"),
40
- danger && getModuleClass(styles, "danger"),
39
+ getModuleClass(styles, "progress"), //
40
+ getColorClass(props),
41
+ getStatusClass(props),
41
42
  )}
42
- role="progressbar"
43
- aria-valuemin={min}
44
- aria-valuemax={max}
45
- aria-valuenow={value}
43
+ value={span > 0 ? value - min : 0}
44
+ max={span > 0 ? span : 1}
46
45
  aria-valuetext={formatPercent(value - min, max - min)}
47
- >
48
- <span className={getModuleClass(styles, "fill")} style={fillStyle} />
49
- </figure>
46
+ />
50
47
  );
51
48
  }
@@ -5,7 +5,7 @@ Deleted text — renders a `<del>` element to mark content that has been removed
5
5
  **Things to know:**
6
6
 
7
7
  - Use `Deleted` for content genuinely removed from a document (edits, diffs, retired prices), and `<Inserted>` for content added — they pair up.
8
- - It drives its colour through the [tint ladder](/ui/TINT_CLASS), so the `color` variant (`color="purple"`, `color="orange"`, …) recolours the strike-through. The base tint defaults to `--color-red`.
8
+ - It colours itself directly from `--color-red` rather than the tint ladder, so a deletion stays a consistent red even inside a tinted region; change it with `--deleted-color`.
9
9
  - Inside `<Prose>` a raw `<del>` picks up the same styling, so Markdown-rendered deletions match component ones.
10
10
 
11
11
  ## Usage
@@ -22,13 +22,12 @@ import { Paragraph, Deleted, Inserted } from "shelving/ui";
22
22
 
23
23
  ## Styling
24
24
 
25
- `Deleted` paints from the [tint ladder](/ui/TINT_CLASS): the `50` tint sets the text colour, defaulting to `--color-red`. Override `--deleted-tint` to recolour from the base, or set each property directly.
25
+ `Deleted` colours its text directly from `--color-red` (not the tint ladder), so deletions stay a consistent red regardless of the surrounding tint. Override `--deleted-color` to recolour.
26
26
 
27
27
  | Variable | Styles | Default |
28
28
  |---|---|---|
29
- | `--deleted-tint` | Base tint (`--tint-50`) the colour derives from | `var(--color-red)` |
30
- | `--deleted-color` | Text colour | `var(--tint-50)` |
29
+ | `--deleted-color` | Text colour | `var(--color-red)` |
31
30
  | `--deleted-decoration` | Text decoration | `line-through var(--stroke-normal)` |
32
31
  | `--deleted-weight` | Font weight | `var(--weight-strong)` |
33
32
 
34
- **Global tokens it reads:** `--color-red`, `--stroke-normal`, `--weight-strong`, and the tint-ladder step `--tint-50`.
33
+ **Global tokens it reads:** `--color-red`, `--stroke-normal`, and `--weight-strong`.
@@ -6,10 +6,8 @@
6
6
  @layer components {
7
7
  .deleted,
8
8
  .prose del {
9
- --tint-50: var(--deleted-tint, var(--color-red));
10
-
11
9
  /* Style */
12
- color: var(--deleted-color, var(--tint-50));
10
+ color: var(--deleted-color, var(--color-red));
13
11
  text-decoration: var(--deleted-decoration, line-through var(--stroke-normal));
14
12
 
15
13
  /* Text */
@@ -5,7 +5,7 @@ Inserted text — renders an `<ins>` element to mark content that has been added
5
5
  **Things to know:**
6
6
 
7
7
  - Use `Inserted` for content genuinely added to a document (edits, diffs, new prices), and `<Deleted>` for content removed — they pair up.
8
- - It drives its colour through the [tint ladder](/ui/TINT_CLASS), so the `color` variant (`color="purple"`, `color="blue"`, …) recolours the underline. The base tint defaults to `--color-green`.
8
+ - It colours itself directly from `--color-green` rather than the tint ladder, so an insertion stays a consistent green even inside a tinted region; change it with `--inserted-color`.
9
9
  - Inside `<Prose>` a raw `<ins>` picks up the same styling, so Markdown-rendered insertions match component ones.
10
10
 
11
11
  ## Usage
@@ -22,13 +22,12 @@ import { Paragraph, Deleted, Inserted } from "shelving/ui";
22
22
 
23
23
  ## Styling
24
24
 
25
- `Inserted` paints from the [tint ladder](/ui/TINT_CLASS): the `50` tint sets the text colour, defaulting to `--color-green`. Override `--inserted-tint` to recolour from the base, or set each property directly.
25
+ `Inserted` colours its text directly from `--color-green` (not the tint ladder), so insertions stay a consistent green regardless of the surrounding tint. Override `--inserted-color` to recolour.
26
26
 
27
27
  | Variable | Styles | Default |
28
28
  |---|---|---|
29
- | `--inserted-tint` | Base tint (`--tint-50`) the colour derives from | `var(--color-green)` |
30
- | `--inserted-color` | Text colour | `var(--tint-50)` |
29
+ | `--inserted-color` | Text colour | `var(--color-green)` |
31
30
  | `--inserted-decoration` | Text decoration | `underline var(--stroke-normal)` |
32
31
  | `--inserted-weight` | Font weight | `var(--weight-strong)` |
33
32
 
34
- **Global tokens it reads:** `--color-green`, `--stroke-normal`, `--weight-strong`, and the tint-ladder step `--tint-50`.
33
+ **Global tokens it reads:** `--color-green`, `--stroke-normal`, and `--weight-strong`.
@@ -6,10 +6,8 @@
6
6
  @layer components {
7
7
  .inserted,
8
8
  .prose ins {
9
- --tint-50: var(--inserted-tint, var(--color-green));
10
-
11
9
  /* Style */
12
- color: var(--inserted-color, var(--tint-50));
10
+ color: var(--inserted-color, var(--color-green));
13
11
  text-decoration: var(--inserted-decoration, underline var(--stroke-normal));
14
12
 
15
13
  /* Text */
package/ui/inline/Link.md CHANGED
@@ -31,12 +31,11 @@ import { Link } from "shelving/ui";
31
31
 
32
32
  ## Styling
33
33
 
34
- `Link` drives its colour through the tint ladder, so the `color` variant (`color="purple"`, `color="red"`, …) recolours the link. By default the `50` tint resolves to the global `--color-link` token.
34
+ `Link` colours itself from the global `--color-link` token by default. Inside a tinted scope (`color=` / `status=` on an ancestor) `--color-link` resolves to `inherit`, so links adopt the surrounding tinted text colour. Override `--link-color` to set it directly.
35
35
 
36
36
  | Variable | Styles | Default |
37
37
  |---|---|---|
38
- | `--link-tint` | Base tint (`--tint-50`) the link colour derives from | `var(--color-link)` |
39
- | `--link-color` | Text colour | `var(--tint-50)` |
38
+ | `--link-color` | Text colour | `var(--color-link)` |
40
39
  | `--link-weight` | Font weight | `var(--weight-strong)` |
41
40
 
42
41
  **Global tokens it reads:** `--color-link`, `--weight-strong`, and `--stroke-normal` (the underline thickness).
@@ -6,10 +6,8 @@
6
6
  @layer components {
7
7
  .link,
8
8
  .prose a {
9
- --tint-50: var(--link-tint, var(--color-link));
10
-
11
9
  /* Style */
12
- color: var(--link-color, var(--tint-50));
10
+ color: var(--link-color, var(--color-link));
13
11
  text-decoration: none;
14
12
  cursor: inherit;
15
13
 
package/ui/inline/Mark.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { TINT_CLASS } from "../style/Tint.js";
3
2
  import { getTypographyClass } from "../style/Typography.js";
4
3
  import { getClass, getModuleClass } from "../util/css.js";
5
4
  import MARK_CSS from "./Mark.module.css";
@@ -12,5 +11,5 @@ const MARK_CLASS = getModuleClass(MARK_CSS, "mark");
12
11
  */
13
12
  export function Mark({ children, ...props }) {
14
13
  return (_jsx("mark", { className: getClass(MARK_CLASS, //
15
- TINT_CLASS, getTypographyClass(props)), children: children }));
14
+ getTypographyClass(props)), children: children }));
16
15
  }
package/ui/inline/Mark.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Mark
2
2
 
3
- Highlighted text — renders a `<mark>` element to call attention to a run of text, such as a matched search term. Painted as a small inline pill with a yellow background by default.
3
+ Highlighted text — renders a `<mark>` element to call attention to a run of text, such as a matched search term. Painted as a small inline pill with a translucent yellow background by default.
4
4
 
5
5
  **Things to know:**
6
6
 
@@ -20,15 +20,15 @@ import { Mark } from "shelving/ui";
20
20
 
21
21
  ## Styling
22
22
 
23
- `Mark` paints from the [tint ladder](/ui/TINT_CLASS): the `50` tint sets the background and the `00` tint the text. By default the base tint resolves to `--color-yellow`; override `--mark-tint` to recolour the whole pill, or `--mark-background` / `--mark-color` to set the two faces independently.
23
+ `Mark` highlights with a translucent wash of `--color-yellow` `color-mix(…, transparent)`, so the text underneath stays legible with the text painted from `--tint-00`. Override `--mark-tint` to change the highlight hue, `--mark-background` to replace the fill outright, or `--mark-color` for the text.
24
24
 
25
25
  | Variable | Styles | Default |
26
26
  |---|---|---|
27
- | `--mark-tint` | Base tint (`--tint-50`) the pill derives from | `var(--color-yellow)` |
28
- | `--mark-background` | Background fill | `var(--tint-50)` |
27
+ | `--mark-tint` | Highlight hue, mixed 50% with `transparent` for the background | `var(--color-yellow)` |
28
+ | `--mark-background` | Background fill | `color-mix(in oklch, var(--mark-tint, var(--color-yellow)), transparent)` |
29
29
  | `--mark-color` | Text colour | `var(--tint-00)` |
30
30
  | `--mark-padding` | Inline padding | `var(--space-xxsmall)` |
31
31
  | `--mark-radius` | Corner radius | `var(--radius-xxsmall)` |
32
32
  | `--mark-weight` | Font weight | `var(--weight-strong)` |
33
33
 
34
- **Global tokens it reads:** `--color-yellow`, `--space-xxsmall`, `--radius-xxsmall`, `--weight-strong`, and the tint-ladder steps `--tint-50` / `--tint-00`.
34
+ **Global tokens it reads:** `--color-yellow`, `--space-xxsmall`, `--radius-xxsmall`, `--weight-strong`, and the tint-ladder step `--tint-00`.
@@ -1,4 +1,5 @@
1
1
  @import "../style/layers.css";
2
+ @import "../style/Color.module.css";
2
3
  @import "../style/Radius.module.css";
3
4
  @import "../style/Typography.module.css";
4
5
  @import "../style/Tint.module.css";
@@ -6,8 +7,6 @@
6
7
  @layer components {
7
8
  .mark,
8
9
  .prose mark {
9
- --tint-50: var(--mark-tint, var(--color-yellow));
10
-
11
10
  /* Box */
12
11
  display: inline-block;
13
12
  padding-inline: var(--mark-padding, var(--space-xxsmall));
@@ -15,7 +14,7 @@
15
14
  vertical-align: baseline;
16
15
 
17
16
  /* Style */
18
- background: var(--mark-background, var(--tint-80));
17
+ background-color: var(--mark-background, color-mix(in oklch, var(--mark-tint, var(--color-yellow)), transparent));
19
18
  color: var(--mark-color, var(--tint-00));
20
19
 
21
20
  /* Text */
@@ -1,5 +1,4 @@
1
1
  import type { ReactElement } from "react";
2
- import { TINT_CLASS } from "../style/Tint.js";
3
2
  import { getTypographyClass, type TypographyVariants } from "../style/Typography.js";
4
3
  import { getClass, getModuleClass } from "../util/css.js";
5
4
  import type { OptionalChildProps } from "../util/props.js";
@@ -25,7 +24,6 @@ export function Mark({ children, ...props }: MarkProps): ReactElement {
25
24
  <mark
26
25
  className={getClass(
27
26
  MARK_CLASS, //
28
- TINT_CLASS,
29
27
  getTypographyClass(props),
30
28
  )}
31
29
  >
@@ -1,7 +1,8 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { renderToStaticMarkup } from "react-dom/server";
3
- import { PASSTHROUGH, StringSchema } from "../../index.js";
4
- import { StringSchemaInput } from "../index.js";
3
+ import { StringSchema } from "shelving/schema";
4
+ import { StringSchemaInput } from "shelving/ui";
5
+ import { PASSTHROUGH } from "shelving/util/function";
5
6
 
6
7
  describe("StringSchemaInput", () => {
7
8
  test("formats the initial value to its clean sanitized value", () => {
@@ -1,16 +1,15 @@
1
1
  import type { ReactElement } from "react";
2
+ import { type IndentVariants } from "../style/Indent.js";
3
+ import { type PaddingVariants } from "../style/Padding.js";
4
+ import { type WidthVariants } from "../style/Width.js";
2
5
  import type { OptionalChildProps } from "../util/props.js";
3
6
  /**
4
- * Props for `<CenteredLayout>` — optional `children` and a `fullWidth` flag to drop the max-width.
7
+ * Props for `<CenteredLayout>` — optional `children` plus `width` (of the centred column) and `padding` / `indent`
8
+ * (block / inline padding of the scroll area) variants.
5
9
  *
6
10
  * @see https://shelving.cc/ui/CenteredLayoutProps
7
11
  */
8
- export interface CenteredLayoutProps extends OptionalChildProps {
9
- /**
10
- * Drop the narrow max-width and let content fill the width.
11
- * @default false
12
- */
13
- fullWidth?: boolean;
12
+ export interface CenteredLayoutProps extends WidthVariants, PaddingVariants, IndentVariants, OptionalChildProps {
14
13
  }
15
14
  /**
16
15
  * Layout that centres its content with no header/footer and a narrow max-width.
@@ -19,4 +18,4 @@ export interface CenteredLayoutProps extends OptionalChildProps {
19
18
  * @kind component
20
19
  * @see https://shelving.cc/ui/CenteredLayout
21
20
  */
22
- export declare function CenteredLayout({ children, fullWidth }: CenteredLayoutProps): ReactElement;
21
+ export declare function CenteredLayout({ children, ...props }: CenteredLayoutProps): ReactElement;
@@ -1,7 +1,12 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { RouteCache } from "../router/RouteCache.js";
3
- import { getModuleClass } from "../util/css.js";
4
- import CENTERED_LAYOUT_CSS from "./CenteredLayout.module.css";
3
+ import { getIndentClass } from "../style/Indent.js";
4
+ import { getPaddingClass } from "../style/Padding.js";
5
+ import { getWidthClass } from "../style/Width.js";
6
+ import { getClass, getModuleClass } from "../util/css.js";
7
+ import LAYOUT_CSS from "./CenteredLayout.module.css";
8
+ const LAYOUT_MAIN_CLASS = getModuleClass(LAYOUT_CSS, "main");
9
+ const LAYOUT_INNER_CLASS = getModuleClass(LAYOUT_CSS, "inner");
5
10
  /**
6
11
  * Layout that centres its content with no header/footer and a narrow max-width.
7
12
  * - Used for e.g. login/register/error/form pages where the content is the only focus.
@@ -9,8 +14,15 @@ import CENTERED_LAYOUT_CSS from "./CenteredLayout.module.css";
9
14
  * @kind component
10
15
  * @see https://shelving.cc/ui/CenteredLayout
11
16
  */
12
- export function CenteredLayout({ children, fullWidth = false }) {
17
+ export function CenteredLayout({ children, ...props }) {
13
18
  // Wrap the scrolling `<main>` in `<RouteCache>` so recently-visited pages stay mounted but hidden,
14
19
  // keeping their scroll position and state intact across back/forward navigation.
15
- return (_jsx(RouteCache, { children: _jsx("main", { className: getModuleClass(CENTERED_LAYOUT_CSS, "main"), children: _jsx("div", { className: getModuleClass(CENTERED_LAYOUT_CSS, "mainInner"), style: fullWidth ? { maxWidth: "none" } : undefined, children: children }) }) }));
20
+ //
21
+ // The `padding` / `indent` variants set the scroll area's block / inline padding on `<main>`, while the `width`
22
+ // variant sizes the centred `.inner` column. The column stays free of variant/block classes so its `margin: auto`
23
+ // (which does the vertical + horizontal centring) is never zeroed by the `:first-child` / `:last-child` margin
24
+ // collapses that `.block` carries in `@layer overrides`.
25
+ return (_jsx(RouteCache, { children: _jsx("main", { className: getClass(LAYOUT_MAIN_CLASS, //
26
+ getPaddingClass(props), getIndentClass(props)), children: _jsx("div", { className: getClass(LAYOUT_INNER_CLASS, //
27
+ getWidthClass(props)), children: children }) }) }));
16
28
  }
@@ -1,10 +1,11 @@
1
1
  # CenteredLayout
2
2
 
3
- A full-viewport layout that centres its content horizontally inside a narrow max-width column. Good for login, registration, error, and other focused single-purpose pages where the content is the only thing on screen.
3
+ A full-viewport layout that centres its content — vertically and horizontally inside a narrow max-width column. Good for login, registration, error, and other focused single-purpose pages where the content is the only thing on screen. When the content is taller than the viewport it scrolls vertically, pinned to the top so nothing is clipped.
4
4
 
5
5
  **Things to know:**
6
6
 
7
- - Pass `fullWidth` to drop the max-width constraint while keeping the centred positioning use it when the content itself needs to fill the width.
7
+ - Pass the `width` variant to resize the centred column (e.g. `width="normal"`, `width="wide"`, or `width="full"` to fill the available width).
8
+ - Pass the `padding` (block / top + bottom) and `indent` (inline / left + right) variants to change the space around the content.
8
9
  - Like the other full-viewport layouts it owns scroll, padding, and safe-area insets so individual pages don't have to.
9
10
 
10
11
  ## Usage
@@ -14,8 +15,8 @@ import { CenteredLayout, Section } from "shelving/ui";
14
15
 
15
16
  function LoginPage() {
16
17
  return (
17
- <CenteredLayout>
18
- <Section width="narrow">
18
+ <CenteredLayout width="narrow">
19
+ <Section>
19
20
  <LoginForm/>
20
21
  </Section>
21
22
  </CenteredLayout>
@@ -27,6 +28,13 @@ Layouts compose naturally as `<Router>` route values — wrap a group of routes
27
28
 
28
29
  ## Styling
29
30
 
30
- This layout exposes no own `--centered-layout-*` hooks. The inner column is capped at the global `--width-narrow` token (dropped when `fullWidth` is set), and the outer element owns its scroll, padding, and safe-area behaviour directly — reading the layout hooks `--layout-space`, `--layout-padding`, `--layout-body-bg`, and `--layout-inset-top` / `-bottom` / `-left` / `-right`.
31
+ | Variable | Styles | Default |
32
+ |---|---|---|
33
+ | `--centered-layout-width` | Width of the centred column (capped at 100%) | `var(--width-narrow)` |
34
+ | `--centered-layout-padding` | Block (top/bottom) padding of the scroll area | `var(--space-normal)` |
35
+ | `--centered-layout-indent` | Inline (left/right) padding of the scroll area | `var(--space-normal)` |
36
+ | `--centered-layout-background` | Page background while the layout is mounted | Unset — the `body` default from `Typography.module.css` shows |
31
37
 
32
- **Global tokens it reads** `--width-narrow`.
38
+ The column width and the scroll-area padding can also be set per-instance via the `width`, `padding`, and `indent` variant props. The outer element owns its scroll, padding, and safe-area behaviour directly — safe-area insets are applied as transparent borders so they stack with (rather than replace) the padding.
39
+
40
+ **Global tokens it reads** — `--width-narrow` and `--space-normal`.
@@ -1,24 +1,7 @@
1
1
  @import "../style/layers.css";
2
2
  @import "../style/Space.module.css";
3
- @import "../style/Tint.module.css";
4
3
  @import "../style/Width.module.css";
5
4
 
6
- /* Low-priority padding defaults live in `@layer defaults` so consumers can override them */
7
- /* with normal-specificity selectors without needing `:where()` tricks. */
8
- @layer defaults {
9
- .main {
10
- /* Top/bottom use `layout-space` and left/right use `layout-padding`, plus safe-area additions. */
11
- padding: var(--layout-space, var(--space-normal)) var(--layout-padding, var(--space-normal));
12
- padding-top: calc(var(--layout-space, var(--space-normal)) + max(var(--layout-inset-top, 0px), env(safe-area-inset-top, 0px)));
13
- padding-bottom: calc(var(--layout-space, var(--space-normal)) + max(var(--layout-inset-bottom, 0px), env(safe-area-inset-bottom, 0px)));
14
- padding-left: calc(var(--layout-padding, var(--space-normal)) + max(var(--layout-inset-left, 0px), env(safe-area-inset-left, 0px)));
15
- padding-right: calc(var(--layout-padding, var(--space-normal)) + max(var(--layout-inset-right, 0px), env(safe-area-inset-right, 0px)));
16
-
17
- /* Grow to fill the body. */
18
- flex: 1;
19
- }
20
- }
21
-
22
5
  @layer components {
23
6
  /*
24
7
  * Lock the document and let `.main` own the page scroll.
@@ -51,11 +34,22 @@
51
34
  position: fixed;
52
35
  inset: 0;
53
36
 
54
- /* Layout-specific bg hook. */
55
- background: var(--layout-body-bg, var(--tint-100));
37
+ /* Deliberately no fallback: when the hook is unset this declaration fails and the body default from Typography.module.css shows. */
38
+ background: var(--centered-layout-background);
56
39
  }
57
40
 
58
41
  .main {
42
+ /* Box */
43
+ border-top: env(safe-area-inset-top, 0px) solid transparent;
44
+ border-bottom: env(safe-area-inset-bottom, 0px) solid transparent;
45
+ border-left: env(safe-area-inset-left, 0px) solid transparent;
46
+ border-right: env(safe-area-inset-right, 0px) solid transparent;
47
+ padding-block: var(--centered-layout-padding, var(--space-normal));
48
+ padding-inline: var(--centered-layout-indent, var(--space-normal));
49
+
50
+ /* Grow to fill the body. */
51
+ flex: 1;
52
+
59
53
  /* Content — a flex column whose inner column centres itself with `margin: auto` (both axes). */
60
54
  display: flex;
61
55
  flex-direction: column;
@@ -70,9 +64,9 @@
70
64
  scroll-behavior: smooth;
71
65
  }
72
66
 
73
- .mainInner {
74
- width: 100%;
75
- max-width: var(--width-narrow);
67
+ .inner {
68
+ width: var(--centered-layout-width, var(--width-narrow));
69
+ max-width: 100%;
76
70
  margin: auto;
77
71
  }
78
72
  }
@@ -1,21 +1,22 @@
1
1
  import type { ReactElement } from "react";
2
2
  import { RouteCache } from "../router/RouteCache.js";
3
- import { getModuleClass } from "../util/css.js";
3
+ import { getIndentClass, type IndentVariants } from "../style/Indent.js";
4
+ import { getPaddingClass, type PaddingVariants } from "../style/Padding.js";
5
+ import { getWidthClass, type WidthVariants } from "../style/Width.js";
6
+ import { getClass, getModuleClass } from "../util/css.js";
4
7
  import type { OptionalChildProps } from "../util/props.js";
5
- import CENTERED_LAYOUT_CSS from "./CenteredLayout.module.css";
8
+ import LAYOUT_CSS from "./CenteredLayout.module.css";
9
+
10
+ const LAYOUT_MAIN_CLASS = getModuleClass(LAYOUT_CSS, "main");
11
+ const LAYOUT_INNER_CLASS = getModuleClass(LAYOUT_CSS, "inner");
6
12
 
7
13
  /**
8
- * Props for `<CenteredLayout>` — optional `children` and a `fullWidth` flag to drop the max-width.
14
+ * Props for `<CenteredLayout>` — optional `children` plus `width` (of the centred column) and `padding` / `indent`
15
+ * (block / inline padding of the scroll area) variants.
9
16
  *
10
17
  * @see https://shelving.cc/ui/CenteredLayoutProps
11
18
  */
12
- export interface CenteredLayoutProps extends OptionalChildProps {
13
- /**
14
- * Drop the narrow max-width and let content fill the width.
15
- * @default false
16
- */
17
- fullWidth?: boolean;
18
- }
19
+ export interface CenteredLayoutProps extends WidthVariants, PaddingVariants, IndentVariants, OptionalChildProps {}
19
20
 
20
21
  /**
21
22
  * Layout that centres its content with no header/footer and a narrow max-width.
@@ -24,13 +25,29 @@ export interface CenteredLayoutProps extends OptionalChildProps {
24
25
  * @kind component
25
26
  * @see https://shelving.cc/ui/CenteredLayout
26
27
  */
27
- export function CenteredLayout({ children, fullWidth = false }: CenteredLayoutProps): ReactElement {
28
+ export function CenteredLayout({ children, ...props }: CenteredLayoutProps): ReactElement {
28
29
  // Wrap the scrolling `<main>` in `<RouteCache>` so recently-visited pages stay mounted but hidden,
29
30
  // keeping their scroll position and state intact across back/forward navigation.
31
+ //
32
+ // The `padding` / `indent` variants set the scroll area's block / inline padding on `<main>`, while the `width`
33
+ // variant sizes the centred `.inner` column. The column stays free of variant/block classes so its `margin: auto`
34
+ // (which does the vertical + horizontal centring) is never zeroed by the `:first-child` / `:last-child` margin
35
+ // collapses that `.block` carries in `@layer overrides`.
30
36
  return (
31
37
  <RouteCache>
32
- <main className={getModuleClass(CENTERED_LAYOUT_CSS, "main")}>
33
- <div className={getModuleClass(CENTERED_LAYOUT_CSS, "mainInner")} style={fullWidth ? { maxWidth: "none" } : undefined}>
38
+ <main
39
+ className={getClass(
40
+ LAYOUT_MAIN_CLASS, //
41
+ getPaddingClass(props),
42
+ getIndentClass(props),
43
+ )}
44
+ >
45
+ <div
46
+ className={getClass(
47
+ LAYOUT_INNER_CLASS, //
48
+ getWidthClass(props),
49
+ )}
50
+ >
34
51
  {children}
35
52
  </div>
36
53
  </main>
@@ -5,7 +5,13 @@ import { Button } from "../button/Button.js";
5
5
  import { requireMetaURL } from "../misc/MetaContext.js";
6
6
  import { RouteCache } from "../router/RouteCache.js";
7
7
  import { getClass, getModuleClass } from "../util/css.js";
8
- import SIDEBAR_LAYOUT_CSS from "./SidebarLayout.module.css";
8
+ import LAYOUT_CSS from "./SidebarLayout.module.css";
9
+ const LAYOUT_SIDEBAR_CLASS = getModuleClass(LAYOUT_CSS, "sidebar");
10
+ const LAYOUT_CONTENT_CLASS = getModuleClass(LAYOUT_CSS, "content");
11
+ const LAYOUT_TOGGLE_CLASS = getModuleClass(LAYOUT_CSS, "toggle");
12
+ const LAYOUT_OVERLAY_CLASS = getModuleClass(LAYOUT_CSS, "overlay");
13
+ const LAYOUT_MAIN_CLASS = getModuleClass(LAYOUT_CSS, "main");
14
+ const LAYOUT_OPEN_CLASS = getModuleClass(LAYOUT_CSS, "open");
9
15
  /**
10
16
  * Layout with a fixed-width side column (typically navigation) next to a scrollable main content column.
11
17
  * - The sidebar is rendered as `<nav>` — it almost always contains the page's primary navigation.
@@ -25,12 +31,13 @@ export function SidebarLayout({ sidebar, children, right = false }) {
25
31
  if (path)
26
32
  setOpen(false);
27
33
  }, [path]);
28
- const sidebarEl = (_jsx("nav", { className: getClass(getModuleClass(SIDEBAR_LAYOUT_CSS, "sidebar"), open && getModuleClass(SIDEBAR_LAYOUT_CSS, "open")), children: sidebar }, "sidebar"));
34
+ const sidebarEl = (_jsx("nav", { className: getClass(LAYOUT_SIDEBAR_CLASS, //
35
+ open && LAYOUT_OPEN_CLASS), children: sidebar }, "sidebar"));
29
36
  // Wrap the scrolling content column in `<RouteCache>` so recently-visited pages stay mounted but hidden
30
37
  // — keeping the scroll position of this `.content` container (and all page state) intact across
31
38
  // back/forward navigation. The sidebar and drawer state stay outside the cache, so they are neither
32
39
  // duplicated nor remounted as the URL changes.
33
- const contentEl = (_jsx(RouteCache, { children: _jsxs("div", { className: getModuleClass(SIDEBAR_LAYOUT_CSS, "content"), children: [_jsx("div", { className: getModuleClass(SIDEBAR_LAYOUT_CSS, "toggle"), children: _jsx(Button, { title: open ? "Close menu" : "Show menu", onClick: () => setOpen(o => !o), children: open ? _jsx(XMarkIcon, {}) : _jsx(Bars3Icon, {}) }) }), _jsx("div", { className: getModuleClass(SIDEBAR_LAYOUT_CSS, "contentInner"), children: children })] }) }, "content"));
34
- const overlayEl = open && (_jsx("button", { type: "button", className: getModuleClass(SIDEBAR_LAYOUT_CSS, "overlay"), "aria-label": "Close menu", onClick: () => setOpen(false) }, "overlay"));
35
- return (_jsx("main", { className: getModuleClass(SIDEBAR_LAYOUT_CSS, "main"), children: right ? [contentEl, sidebarEl, overlayEl] : [sidebarEl, contentEl, overlayEl] }));
40
+ const contentEl = (_jsx(RouteCache, { children: _jsxs("div", { className: LAYOUT_CONTENT_CLASS, children: [_jsx("div", { className: LAYOUT_TOGGLE_CLASS, children: _jsx(Button, { title: open ? "Close menu" : "Show menu", onClick: () => setOpen(o => !o), children: open ? _jsx(XMarkIcon, {}) : _jsx(Bars3Icon, {}) }) }), children] }) }, "content"));
41
+ const overlayEl = open && (_jsx("button", { type: "button", className: LAYOUT_OVERLAY_CLASS, "aria-label": "Close menu", onClick: () => setOpen(false) }, "overlay"));
42
+ return _jsx("main", { className: LAYOUT_MAIN_CLASS, children: right ? [contentEl, sidebarEl, overlayEl] : [sidebarEl, contentEl, overlayEl] });
36
43
  }
@@ -48,11 +48,11 @@ useEffect(useSafeKeyboardArea, []);
48
48
  | Variable | Styles | Default |
49
49
  |---|---|---|
50
50
  | `--sidebar-layout-width` | Width of the side column (and drawer) | `17.5rem` |
51
- | `--sidebar-layout-background` | Page background while the layout is mounted | `var(--tint-100)` |
52
- | `--sidebar-layout-sidebar-background` | Sidebar column fill | `var(--tint-90)` |
53
- | `--sidebar-layout-content-background` | Main content column fill | `var(--tint-100)` |
51
+ | `--sidebar-layout-background` | Page background while the layout is mounted | Unset — the `body` default from `Typography.module.css` shows |
52
+ | `--sidebar-layout-sidebar-background` | Sidebar column fill | Unset — the page background shows |
53
+ | `--sidebar-layout-content-background` | Main content column fill | Unset — the page background shows |
54
54
  | `--sidebar-layout-border` | Divider between sidebar and content | `var(--stroke-normal) solid var(--tint-80)` |
55
55
 
56
56
  The sidebar and content columns own their own scroll behaviour directly (this layout no longer composes a shared `.layout` class). `useSafeKeyboardArea()` still writes `--layout-inset-bottom` for layouts that pad to the safe area.
57
57
 
58
- **Global tokens it reads** — the tint ladder `--tint-80` / `--tint-90` / `--tint-100`, plus `--space-normal`, `--stroke-normal`, `--duration-normal`, and `--color-shadow`.
58
+ **Global tokens it reads** — `--tint-80`, plus `--space-normal`, `--stroke-normal`, `--duration-normal`, and `--color-shadow`.
@@ -50,7 +50,8 @@
50
50
  inset: 0;
51
51
 
52
52
  /* Style */
53
- background: var(--sidebar-layout-background, var(--tint-100));
53
+ /* Deliberately no fallback: when the hook is unset this declaration fails and the body default from Typography.module.css shows. */
54
+ background: var(--sidebar-layout-background);
54
55
  }
55
56
 
56
57
  .main {
@@ -74,7 +75,8 @@
74
75
  padding: var(--space-normal);
75
76
 
76
77
  /* Style */
77
- background: var(--sidebar-layout-sidebar-background, var(--tint-90));
78
+ /* Deliberately no fallback: when the hook is unset this declaration fails and the page background shows. */
79
+ background: var(--sidebar-layout-sidebar-background);
78
80
  }
79
81
 
80
82
  .content {
@@ -91,13 +93,8 @@
91
93
  scroll-behavior: smooth;
92
94
 
93
95
  /* Style */
94
- background: var(--sidebar-layout-content-background, var(--tint-100));
95
- }
96
-
97
- .contentInner {
98
- width: 100%;
99
- padding: 0;
100
- margin: 0 auto;
96
+ /* Deliberately no fallback: when the hook is unset this declaration fails and the page background shows. */
97
+ background: var(--sidebar-layout-content-background);
101
98
  }
102
99
 
103
100
  /* Wrapper for the menu toggle button — hidden on wide viewports, shown on narrow ones (see media query). */