shelving 1.286.0 → 1.287.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shelving",
3
- "version": "1.286.0",
3
+ "version": "1.287.0",
4
4
  "author": "Dave Houlbrooke <dave@shax.com>",
5
5
  "repository": {
6
6
  "type": "git",
@@ -9,12 +9,12 @@
9
9
  "main": "./index.js",
10
10
  "module": "./index.js",
11
11
  "devDependencies": {
12
- "@biomejs/biome": "^2.5.14",
12
+ "@biomejs/biome": "^2.5.15",
13
13
  "@heroicons/react": "^2.2.0",
14
14
  "@types/bun": "^1.4.2",
15
15
  "@types/react": "^19.3.0",
16
16
  "@types/react-dom": "^19.3.0",
17
- "stylelint": "^17.15.0",
17
+ "stylelint": "^17.16.0",
18
18
  "stylelint-config-standard": "^40.0.0",
19
19
  "typescript": "^7.0.2"
20
20
  },
package/ui/block/Card.md CHANGED
@@ -66,7 +66,8 @@ import { Card, Subheading } from "shelving/ui";
66
66
  | `--card-border` | Border shorthand | `var(--card-stroke) solid var(--tint-80)` |
67
67
  | `--card-stroke` | Border thickness — set it (e.g. `var(--stroke-normal)`) to show the border | `0` |
68
68
  | `--card-radius` | Corner radius | `var(--radius-normal)` (16px) |
69
- | `--card-padding` | Inner padding | `var(--space-normal)` (16px) |
69
+ | `--card-padding` | Inner top and bottom padding (one length) | `var(--space-normal)` (16px) |
70
+ | `--card-indent` | Inner left and right padding (one length) | `var(--space-normal)` (16px) |
70
71
  | `--card-space` | Outer block margin (top + bottom) | `var(--space-paragraph)` (16px) |
71
72
  | `--card-shadow` | Drop shadow | `none` |
72
73
  | `--card-transition` | Transition | `all var(--duration-fast)` (150ms) |
@@ -24,7 +24,8 @@
24
24
  margin-inline: 0;
25
25
  margin-block: var(--card-space, var(--space-paragraph));
26
26
  border: var(--card-border, var(--card-stroke, 0) solid var(--tint-80));
27
- padding: var(--card-padding, var(--space-normal));
27
+ padding-block: var(--card-padding, var(--space-normal));
28
+ padding-inline: var(--card-indent, var(--space-normal));
28
29
  border-radius: var(--card-radius, var(--radius-normal));
29
30
 
30
31
  /* Style */
@@ -1,22 +1,27 @@
1
1
  import type { ReactElement, ReactNode } from "react";
2
2
  import { type BlockVariants } from "../style/Block.js";
3
3
  import type { ClassProps } from "../util/props.js";
4
- /** Props for `DetailsItem` — a single collapsible disclosure. */
4
+ /**
5
+ * Props for `<Details>` — the summary title, the revealed content, and the open state.
6
+ *
7
+ * @see https://shelving.cc/ui/DetailsProps
8
+ */
5
9
  export interface DetailsProps extends BlockVariants, ClassProps {
6
10
  /** Content of the always-visible summary (e.g. a question). */
7
11
  title: ReactNode;
8
- /** Whether the item starts expanded. */
12
+ /** Whether the panel starts expanded. */
9
13
  open?: boolean | undefined;
10
- /** Shared group name — items with the same `name` open exclusively (only one at a time). */
14
+ /** Shared group name — panels with the same `name` open exclusively (only one at a time). */
11
15
  name?: string | undefined;
12
- /** Content revealed when the item is expanded. */
16
+ /** Content revealed when the panel is expanded. */
13
17
  children: ReactNode;
14
18
  }
15
19
  /**
16
- * A single collapsible panel within an `Details`, built on native `<details>` and `<summary>`
17
- * - Panel animates to its true height (where `interpolate-size` + `::details-content` are supported),
18
- * - Give sibling items a shared `name` to make them open exclusively.
20
+ * A collapsible panel with a title that is always visible, built on native `<details>` and `<summary>`.
21
+ * - The panel animates to its true height (where `interpolate-size` and `::details-content` are supported).
22
+ * - Give sibling panels a shared `name` to make them open exclusively.
19
23
  *
20
24
  * @kind component
25
+ * @see https://shelving.cc/ui/Details
21
26
  */
22
27
  export declare function Details({ title, open, name, children, className, ...props }: DetailsProps): ReactElement;
@@ -1,16 +1,18 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { ChevronUpIcon } from "@heroicons/react/24/outline";
2
+ import { ChevronUpIcon } from "@heroicons/react/24/solid";
3
3
  import { getBlockClass } from "../style/Block.js";
4
- import { getClass } from "../util/css.js";
4
+ import { getFlexClass } from "../style/Flex.js";
5
+ import { getClass, getModuleClass } from "../util/css.js";
5
6
  import DETAILS_CSS from "./Details.module.css";
6
- const DETAILS_CLASS = DETAILS_CSS.details;
7
- const DETAILS_SUMMARY_CLASS = DETAILS_CSS.summary;
7
+ const DETAILS_CLASS = getModuleClass(DETAILS_CSS, "details");
8
+ const DETAILS_SUMMARY_CLASS = getClass(getModuleClass(DETAILS_CSS, "summary"), getFlexClass({ between: true, gap: "normal" }));
8
9
  /**
9
- * A single collapsible panel within an `Details`, built on native `<details>` and `<summary>`
10
- * - Panel animates to its true height (where `interpolate-size` + `::details-content` are supported),
11
- * - Give sibling items a shared `name` to make them open exclusively.
10
+ * A collapsible panel with a title that is always visible, built on native `<details>` and `<summary>`.
11
+ * - The panel animates to its true height (where `interpolate-size` and `::details-content` are supported).
12
+ * - Give sibling panels a shared `name` to make them open exclusively.
12
13
  *
13
14
  * @kind component
15
+ * @see https://shelving.cc/ui/Details
14
16
  */
15
17
  export function Details({ title, open = false, name, children, className, ...props }) {
16
18
  return (_jsxs("details", { className: getClass(DETAILS_CLASS, getBlockClass(props), className), open: open, name: name, children: [_jsxs("summary", { className: DETAILS_SUMMARY_CLASS, children: [_jsx("span", { children: title }), _jsx(ChevronUpIcon, {})] }), children] }));
@@ -0,0 +1,63 @@
1
+ # Details
2
+
3
+ A collapsible panel. The title is always visible. The content shows when the user opens the panel. Built on the native `<details>` and `<summary>` elements, so it works with the keyboard and without JavaScript.
4
+
5
+ **Things to know:**
6
+
7
+ - A chevron on the right of the title points down when the panel is closed and up when it is open.
8
+ - The panel animates to its true height where the browser supports `interpolate-size` and `::details-content`. Other browsers open it at once.
9
+ - Give sibling panels the same `name` to make them open exclusively: when one opens, the others close.
10
+ - Two or more panels next to each other get a divider line between them.
11
+ - A raw `<details>` inside `.prose` gets the same spacing and divider, but it keeps the browser's own marker in place of the chevron.
12
+
13
+ ## Usage
14
+
15
+ ### Single panel
16
+
17
+ ```tsx
18
+ import { Details, Paragraph } from "shelving/ui";
19
+
20
+ <Details title="What is shelving?">
21
+ <Paragraph>A TypeScript data toolkit.</Paragraph>
22
+ </Details>
23
+ ```
24
+
25
+ ### Open by default
26
+
27
+ ```tsx
28
+ import { Details, Paragraph } from "shelving/ui";
29
+
30
+ <Details title="Release notes" open>
31
+ <Paragraph>Bug fixes and small improvements.</Paragraph>
32
+ </Details>
33
+ ```
34
+
35
+ ### Exclusive group (FAQ)
36
+
37
+ ```tsx
38
+ import { Details, Paragraph } from "shelving/ui";
39
+
40
+ // Only one answer is open at a time.
41
+ <Details name="faq" title="Is it free?">
42
+ <Paragraph>Yes.</Paragraph>
43
+ </Details>
44
+ <Details name="faq" title="Does it work with React?">
45
+ <Paragraph>Yes, through `shelving/ui`.</Paragraph>
46
+ </Details>
47
+ ```
48
+
49
+ ## Styling
50
+
51
+ `Details` paints from the [tint ladder](/ui/TINT_CLASS). Override these hooks at `:root` (or any ancestor scope) to retheme. Apply `color=` / `status=` to an ancestor scope to recolour the chevron and divider together.
52
+
53
+ | Variable | Styles | Default |
54
+ |---|---|---|
55
+ | `--details-space` | Outer block margin, and the padding above a divider | `var(--space-paragraph)` (16px) |
56
+ | `--details-border` | Divider between panels next to each other | `var(--stroke-normal) solid var(--tint-90)` |
57
+ | `--details-marker-color` | Colour of the browser's marker on a raw `<details>` in `.prose` | `var(--tint-80)` |
58
+ | `--details-radius` | Corner radius of the title's focus ring | `var(--radius-xsmall)` (8px) |
59
+ | `--details-icon-color` | Chevron colour | `var(--tint-50)` |
60
+ | `--details-transition` | Open/close animation and chevron turn | `all var(--duration-fast)` (150ms) |
61
+ | `--details-gap` | Space between the title and the content | `var(--space-paragraph)` (16px) |
62
+
63
+ **Global tokens it reads:** the tint ladder `--tint-50` / `--tint-80` / `--tint-90`, plus `--space-paragraph`, `--space-normal`, `--radius-xsmall`, `--stroke-normal`, `--stroke-focus`, `--color-focus`, `--duration-fast` and `--size-icon` (chevron size, from `getFlexClass`).
@@ -48,26 +48,11 @@
48
48
  }
49
49
  }
50
50
 
51
- /* Title (with chevron icon) */
52
- .summary {
53
- /* Box */
54
- display: flex;
55
-
56
- /* Contents */
57
- align-items: center;
58
- justify-content: space-between;
59
- gap: var(--space-normal);
60
-
61
- /* Children */
62
- [data-slot="icon"] {
63
- width: var(--size-icon);
64
- height: var(--size-icon);
65
- color: var(--color-gray);
66
- transform: rotate(180deg);
67
- transition: var(--details-transition, all var(--duration-fast));
68
- border-radius: var(--radius-xxsmall);
69
- flex: none;
70
- }
51
+ /* Title chevron (the summary's flex class sets its size). Points down while closed. */
52
+ .summary [data-slot="icon"] {
53
+ color: var(--details-icon-color, var(--tint-50));
54
+ transform: rotate(180deg);
55
+ transition: var(--details-transition, all var(--duration-fast));
71
56
  }
72
57
 
73
58
  /* Content */
@@ -96,10 +81,8 @@
96
81
 
97
82
  /* States */
98
83
  &[open] {
99
- .summary {
100
- [data-slot="icon"] {
101
- transform: rotate(0deg);
102
- }
84
+ .summary [data-slot="icon"] {
85
+ transform: rotate(0deg);
103
86
  }
104
87
 
105
88
  &::details-content {
@@ -1,31 +1,37 @@
1
- import { ChevronUpIcon } from "@heroicons/react/24/outline";
1
+ import { ChevronUpIcon } from "@heroicons/react/24/solid";
2
2
  import type { ReactElement, ReactNode } from "react";
3
3
  import { type BlockVariants, getBlockClass } from "../style/Block.js";
4
- import { getClass } from "../util/css.js";
4
+ import { getFlexClass } from "../style/Flex.js";
5
+ import { getClass, getModuleClass } from "../util/css.js";
5
6
  import type { ClassProps } from "../util/props.js";
6
7
  import DETAILS_CSS from "./Details.module.css";
7
8
 
8
- const DETAILS_CLASS = DETAILS_CSS.details;
9
- const DETAILS_SUMMARY_CLASS = DETAILS_CSS.summary;
9
+ const DETAILS_CLASS = getModuleClass(DETAILS_CSS, "details");
10
+ const DETAILS_SUMMARY_CLASS = getClass(getModuleClass(DETAILS_CSS, "summary"), getFlexClass({ between: true, gap: "normal" }));
10
11
 
11
- /** Props for `DetailsItem` — a single collapsible disclosure. */
12
+ /**
13
+ * Props for `<Details>` — the summary title, the revealed content, and the open state.
14
+ *
15
+ * @see https://shelving.cc/ui/DetailsProps
16
+ */
12
17
  export interface DetailsProps extends BlockVariants, ClassProps {
13
18
  /** Content of the always-visible summary (e.g. a question). */
14
19
  title: ReactNode;
15
- /** Whether the item starts expanded. */
20
+ /** Whether the panel starts expanded. */
16
21
  open?: boolean | undefined;
17
- /** Shared group name — items with the same `name` open exclusively (only one at a time). */
22
+ /** Shared group name — panels with the same `name` open exclusively (only one at a time). */
18
23
  name?: string | undefined;
19
- /** Content revealed when the item is expanded. */
24
+ /** Content revealed when the panel is expanded. */
20
25
  children: ReactNode;
21
26
  }
22
27
 
23
28
  /**
24
- * A single collapsible panel within an `Details`, built on native `<details>` and `<summary>`
25
- * - Panel animates to its true height (where `interpolate-size` + `::details-content` are supported),
26
- * - Give sibling items a shared `name` to make them open exclusively.
29
+ * A collapsible panel with a title that is always visible, built on native `<details>` and `<summary>`.
30
+ * - The panel animates to its true height (where `interpolate-size` and `::details-content` are supported).
31
+ * - Give sibling panels a shared `name` to make them open exclusively.
27
32
  *
28
33
  * @kind component
34
+ * @see https://shelving.cc/ui/Details
29
35
  */
30
36
  export function Details({ title, open = false, name, children, className, ...props }: DetailsProps): ReactElement {
31
37
  return (
@@ -15,7 +15,8 @@
15
15
  margin-inline: 0;
16
16
  margin-block: var(--preformatted-space, var(--space-paragraph));
17
17
  border: var(--preformatted-border, var(--preformatted-stroke, var(--stroke-normal)) solid var(--tint-80));
18
- padding: var(--preformatted-padding, var(--space-normal));
18
+ padding-block: var(--preformatted-padding, var(--space-normal));
19
+ padding-inline: var(--preformatted-indent, var(--space-normal));
19
20
  border-radius: var(--preformatted-radius, var(--radius-normal));
20
21
  width: 100%;
21
22
  box-sizing: border-box;
@@ -14,7 +14,8 @@
14
14
  margin-block: var(--video-space, var(--space-paragraph));
15
15
  border: var(--video-border, none);
16
16
  border-radius: var(--video-radius, var(--radius-normal));
17
- padding: var(--video-padding, var(--space-normal));
17
+ padding-block: var(--video-padding, var(--space-normal));
18
+ padding-inline: var(--video-indent, var(--space-normal));
18
19
 
19
20
  /* Contents */
20
21
  display: flex;
@@ -59,7 +59,9 @@ import { getButtonClass } from "shelving/ui";
59
59
 
60
60
  `Button` paints from the [tint ladder](/ui/TINT_CLASS). Override these hooks at `:root` or any ancestor scope; apply `color=` / `status=` (on the button or an ancestor scope) to recolour the whole button, or use a per-property hook for one change.
61
61
 
62
- `--button-padding` and `--button-small-padding` set the `padding` shorthand, so a single value pads both axes equally and a two-value override pads block and inline separately (e.g. `var(--space-small) var(--space-normal)`).
62
+ `--button-padding` sets the top and bottom padding, and `--button-indent` the left and right. Each takes one length, not a shorthand.
63
+
64
+ Every button is at least as tall as a button with an icon, so buttons line up whether they have an icon or not, and at every text size. The minimum height is `--button-icon-size` plus two `--button-padding` plus two `--button-stroke`. It reads those hooks, so it stays correct when a theme changes them. Set `--button-height` to replace it. The `small` variant has its own minimum, `--button-small-height`. Inputs use the same formula (`--input-height`), so an input and a button sit at the same height by default.
63
65
 
64
66
  `--button-shadow`, `--button-hover-transform` and the `--button-active-*` pressed-state hooks are static and apply to every button, with one exception: `plain` never paints a box shadow in any state — it has no fill until hover, so a raised edge under it reads broken. The hover and pressed transforms still apply to it, so all buttons move together. `--button-transition` already covers animating the press and release.
65
67
 
@@ -77,8 +79,13 @@ Backgrounds paint to the button's true edge: `background-origin` is set to `bord
77
79
  | `--button-border` | Border shorthand | `var(--button-stroke) solid transparent` |
78
80
  | `--button-stroke` | Border / outline thickness | `var(--stroke-normal)` (2px) |
79
81
  | `--button-radius` | Corner radius | `var(--radius-xsmall)` (8px) |
80
- | `--button-padding` | Inner padding | `var(--space-small)` (12px) |
81
- | `--button-small-padding` | Inner padding when `small` | `var(--space-xxsmall)` (4px) |
82
+ | `--button-padding` | Top and bottom padding (one length) | `var(--space-small)` (12px) |
83
+ | `--button-indent` | Left and right padding (one length) | `var(--space-small)` (12px) |
84
+ | `--button-small-padding` | Top and bottom padding when `small` (one length) | `var(--space-xxsmall)` (4px) |
85
+ | `--button-small-indent` | Left and right padding when `small` (one length) | `var(--space-xxsmall)` (4px) |
86
+ | `--button-icon-size` | Icon size, and the base of the minimum height | `var(--size-icon)` (24px) |
87
+ | `--button-height` | Minimum height | `--button-icon-size` + 2 × `--button-padding` + 2 × `--button-stroke` (52px) |
88
+ | `--button-small-height` | Minimum height when `small` | `--button-icon-size` + 2 × `--button-small-padding` + 2 × `--button-stroke` (36px) |
82
89
  | `--button-gap` | Gap between icon and label | `var(--space-small)` (12px) |
83
90
  | `--button-small-gap` | Gap between icon and label when `small` | `var(--space-xxsmall)` (4px) |
84
91
  | `--button-space` | Outer block margin | `var(--space-small)` (12px) |
@@ -101,13 +108,13 @@ Backgrounds paint to the button's true edge: `background-origin` is set to `bord
101
108
  | `--button-plain-active-background` | Fill while pressed when `plain` | `var(--button-plain-hover-background)` |
102
109
  | `--button-plain-active-border` | Border while pressed when `plain` | `var(--button-plain-hover-border)` |
103
110
 
104
- **Global tokens it reads:** the tint ladder `--tint-50` / `--tint-55` / `--tint-95` / `--tint-100`, plus `--space-small`, `--space-xxsmall`, `--radius-xsmall`, `--stroke-normal`, `--stroke-focus`, `--color-focus`, `--font-body`, `--weight-normal`, `--size-normal`, `--leading`, and `--duration-fast`.
111
+ **Global tokens it reads:** the tint ladder `--tint-50` / `--tint-55` / `--tint-95` / `--tint-100`, plus `--size-icon`, `--space-small`, `--space-xxsmall`, `--radius-xsmall`, `--stroke-normal`, `--stroke-focus`, `--color-focus`, `--font-body`, `--weight-normal`, `--size-normal`, `--leading`, and `--duration-fast`.
105
112
 
106
113
  ```css
107
114
  /* Theme: pill-shaped buttons, with roomier inline padding. */
108
115
  :root {
109
116
  --button-radius: 999px;
110
- --button-padding: var(--space-small) var(--space-normal);
117
+ --button-indent: var(--space-normal);
111
118
  }
112
119
  ```
113
120
 
@@ -17,7 +17,18 @@
17
17
  max-inline-size: 100%;
18
18
  border-radius: var(--button-radius, var(--radius-xsmall));
19
19
  border: var(--button-border, var(--button-stroke, var(--stroke-normal)) solid transparent);
20
- padding: var(--button-padding, var(--space-small));
20
+ padding-block: var(--button-padding, var(--space-small));
21
+ padding-inline: var(--button-indent, var(--space-small));
22
+ min-block-size: var(
23
+ --button-height,
24
+ calc(
25
+ var(--button-icon-size, var(--size-icon)) +
26
+ 2 *
27
+ var(--button-padding, var(--space-small)) +
28
+ 2 *
29
+ var(--button-stroke, var(--stroke-normal))
30
+ )
31
+ );
21
32
  margin-inline: 0;
22
33
  margin-block: var(--button-space, var(--space-small));
23
34
 
@@ -70,7 +81,23 @@
70
81
  /* Variants */
71
82
  &.small {
72
83
  gap: var(--button-small-gap, var(--space-xxsmall));
73
- padding: var(--button-small-padding, var(--space-xxsmall));
84
+ padding-block: var(--button-small-padding, var(--space-xxsmall));
85
+ padding-inline: var(--button-small-indent, var(--space-xxsmall));
86
+ min-block-size: var(
87
+ --button-small-height,
88
+ calc(
89
+ var(--button-icon-size, var(--size-icon)) +
90
+ 2 *
91
+ var(--button-small-padding, var(--space-xxsmall)) +
92
+ 2 *
93
+ var(--button-stroke, var(--stroke-normal))
94
+ )
95
+ );
96
+ }
97
+
98
+ /* Icons follow `--button-icon-size`, which also sets the minimum height. */
99
+ > [data-slot="icon"] {
100
+ font-size: var(--button-icon-size, var(--size-icon));
74
101
  }
75
102
 
76
103
  &.full {
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { ArrowsPointingInIcon, ArrowsPointingOutIcon } from "@heroicons/react/16/solid";
2
+ import { ArrowsPointingInIcon, ArrowsPointingOutIcon } from "@heroicons/react/24/solid";
3
3
  import { useEffect, useState } from "react";
4
4
  import { Button } from "./Button.js";
5
5
  /**
@@ -1,4 +1,4 @@
1
- import { ArrowsPointingInIcon, ArrowsPointingOutIcon } from "@heroicons/react/16/solid";
1
+ import { ArrowsPointingInIcon, ArrowsPointingOutIcon } from "@heroicons/react/24/solid";
2
2
  import { type ReactElement, useEffect, useState } from "react";
3
3
  import type { ClassProps } from "../util/props.js";
4
4
  import { Button, type ButtonVariants } from "./Button.js";
@@ -1,5 +1,5 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { ArrowPathIcon } from "@heroicons/react/16/solid";
2
+ import { ArrowPathIcon } from "@heroicons/react/24/solid";
3
3
  import { createContext, use } from "react";
4
4
  import { Button } from "./Button.js";
5
5
  /**
@@ -1,4 +1,4 @@
1
- import { ArrowPathIcon } from "@heroicons/react/16/solid";
1
+ import { ArrowPathIcon } from "@heroicons/react/24/solid";
2
2
  import { createContext, type ReactElement, use } from "react";
3
3
  import type { Callback } from "../../util/function.js";
4
4
  import type { ClassProps, OptionalChildProps } from "../util/index.js";
@@ -68,7 +68,8 @@ function DeleteButton({ onConfirm }: { onConfirm: () => void }) {
68
68
 
69
69
  | Variable | Styles | Default |
70
70
  |---|---|---|
71
- | `--dialog-padding` | Padding around the centred content | `var(--space-normal)` (16px) |
71
+ | `--dialog-padding` | Top and bottom padding around the centred content (one length) | `var(--space-normal)` (16px) |
72
+ | `--dialog-indent` | Left and right padding around the centred content (one length) | `var(--space-normal)` (16px) |
72
73
  | `--dialog-width` | Width of the centred content, and so of a centred `<Modal>` | `var(--width-narrow)` (36rem) |
73
74
  | `--dialog-background` | Overlay fill behind the content | `var(--shadow-color)` |
74
75
  | `--dialog-color` | Text colour directly on the overlay | `var(--tint-100)` (white) |
@@ -21,7 +21,8 @@
21
21
 
22
22
  /* A scroll that reaches the top or bottom of the dialog stops there, and does not pass on to the page. */
23
23
  overscroll-behavior: contain;
24
- padding: var(--dialog-padding, var(--space-normal));
24
+ padding-block: var(--dialog-padding, var(--space-normal));
25
+ padding-inline: var(--dialog-indent, var(--space-normal));
25
26
  border: none;
26
27
 
27
28
  /* Style */
@@ -105,7 +105,8 @@ A link click inside a `<Dialog>` closes it, so the menu slides out as the page c
105
105
  | `--modal-border` | Border shorthand | `var(--modal-stroke)` solid `--tint-80` |
106
106
  | `--modal-radius` | Corner radius (the `radius` variant wins over it) | `var(--radius-normal)` (16px) |
107
107
  | `--modal-background` | Surface fill | `var(--tint-100)` |
108
- | `--modal-padding` | Inner padding (the `padding` variant overrides the top and bottom, and `indent` the left and right) | `var(--space-normal)` (16px) |
108
+ | `--modal-padding` | Inner top and bottom padding (one length; the `padding` variant overrides it) | `var(--space-normal)` (16px) |
109
+ | `--modal-indent` | Inner left and right padding (one length; the `indent` variant overrides it) | `var(--space-normal)` (16px) |
109
110
  | `--modal-color` | Text colour | `var(--tint-00)` |
110
111
  | `--modal-max-height` | Maximum height of a `top` or `bottom` panel (it scrolls past this) | `100%` |
111
112
  | `--modal-transition-duration` | Length of the slide for a pinned panel. Keep it the same as `--fade-transition-duration`, so the panel and the `<Dialog>` overlay finish together | `var(--duration-fast)` (150ms) |
@@ -16,7 +16,8 @@
16
16
  border: var(--modal-border, var(--modal-stroke, 0) solid var(--tint-80));
17
17
  border-radius: var(--modal-radius, var(--radius-normal));
18
18
  background: var(--modal-background, var(--tint-100));
19
- padding: var(--modal-padding, var(--space-normal));
19
+ padding-block: var(--modal-padding, var(--space-normal));
20
+ padding-inline: var(--modal-indent, var(--space-normal));
20
21
 
21
22
  /* Style */
22
23
  color: var(--modal-color, var(--tint-00));
package/ui/inline/Code.md CHANGED
@@ -45,7 +45,7 @@ import { Code } from "shelving/ui";
45
45
  | `--code-weight` | Font weight | `var(--weight-code)` |
46
46
  | `--code-size` | Font size | `var(--size-smaller)` |
47
47
  | `--code-leading` | Line height | `var(--leading)` |
48
- | `--code-padding` | Inline padding (non-`plain`) | `var(--space-xxsmall)` |
48
+ | `--code-indent` | Left and right padding (one length; non-`plain`) | `var(--space-xxsmall)` |
49
49
  | `--code-radius` | Corner radius (non-`plain`) | `var(--radius-xxsmall)` |
50
50
  | `--code-background` | Background fill (non-`plain`) | `var(--tint-90)` |
51
51
  | `--code-color` | Text colour (non-`plain`) | `var(--tint-00)` |
@@ -19,7 +19,7 @@
19
19
  .prose :is(code, kbd, samp, var):not(pre *) {
20
20
  /* Box */
21
21
  display: inline-block;
22
- padding-inline: var(--code-padding, var(--space-xxsmall));
22
+ padding-inline: var(--code-indent, var(--space-xxsmall));
23
23
  border-radius: var(--code-radius, var(--radius-xxsmall));
24
24
  vertical-align: baseline;
25
25
 
package/ui/inline/Mark.md CHANGED
@@ -27,7 +27,7 @@ import { Mark } from "shelving/ui";
27
27
  | `--mark-tint` | Highlight hue, mixed 50% with `transparent` for the background | `var(--color-yellow)` |
28
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
- | `--mark-padding` | Inline padding | `var(--space-xxsmall)` |
30
+ | `--mark-indent` | Left and right padding (one length) | `var(--space-xxsmall)` |
31
31
  | `--mark-radius` | Corner radius | `var(--radius-xxsmall)` |
32
32
  | `--mark-weight` | Font weight | `var(--weight-strong)` |
33
33
 
@@ -9,7 +9,7 @@
9
9
  .prose mark {
10
10
  /* Box */
11
11
  display: inline-block;
12
- padding-inline: var(--mark-padding, var(--space-xxsmall));
12
+ padding-inline: var(--mark-indent, var(--space-xxsmall));
13
13
  border-radius: var(--mark-radius, var(--radius-xxsmall));
14
14
  vertical-align: baseline;
15
15
 
@@ -16,7 +16,20 @@
16
16
  margin-block: var(--input-space, var(--space-small));
17
17
  border-radius: var(--input-radius, var(--radius-xsmall));
18
18
  border: var(--input-border, var(--input-stroke, var(--stroke-normal)) solid var(--tint-80));
19
- padding: var(--input-padding, var(--space-small));
19
+ padding-block: var(--input-padding, var(--space-small));
20
+ padding-inline: var(--input-indent, var(--space-small));
21
+
22
+ /* Match `--button-height`, so an input and a button with an icon sit at the same height. */
23
+ min-block-size: var(
24
+ --input-height,
25
+ calc(
26
+ var(--input-icon-size, var(--size-icon)) +
27
+ 2 *
28
+ var(--input-padding, var(--space-small)) +
29
+ 2 *
30
+ var(--input-stroke, var(--stroke-normal))
31
+ )
32
+ );
20
33
 
21
34
  /* Style */
22
35
  background: var(--input-background, var(--tint-100));
@@ -45,7 +58,7 @@
45
58
  background-repeat: no-repeat;
46
59
  background-size: var(--input-icon-size, var(--size-icon));
47
60
  padding-right: 2.5em;
48
- background-position: right var(--input-padding, var(--space-small)) center;
61
+ background-position: right var(--input-indent, var(--space-small)) center;
49
62
  background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iY3VycmVudENvbG9yIj48cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMS40NyA0LjcyYS43NS43NSAwIDAgMSAxLjA2IDBsMy43NSAzLjc1YS43NS43NSAwIDAgMS0xLjA2IDEuMDZMMTIgNi4zMSA4Ljc4IDkuNTNhLjc1Ljc1IDAgMCAxLTEuMDYtMS4wNmwzLjc1LTMuNzVabS0zLjc1IDkuNzVhLjc1Ljc1IDAgMCAxIDEuMDYgMEwxMiAxNy42OWwzLjIyLTMuMjJhLjc1Ljc1IDAgMSAxIDEuMDYgMS4wNmwtMy43NSAzLjc1YS43NS43NSAwIDAgMS0xLjA2IDBsLTMuNzUtMy43NWEuNzUuNzUgMCAwIDEgMC0xLjA2WiIgY2xpcC1ydWxlPSJldmVub2RkIiAvPjwvc3ZnPgo=");
50
63
 
51
64
  /* Placeholder colour if contains checked empty option. */
@@ -130,45 +143,48 @@
130
143
 
131
144
  /* Children */
132
145
  > .input {
133
- /* Nested inputs don't double up border or padding. */
146
+ /* Nested inputs don't double up border, padding, or minimum height. */
134
147
  padding: 0;
135
148
  border: 0;
136
149
  margin: 0;
137
150
  outline: 0;
151
+ min-block-size: 0;
138
152
  }
139
153
 
140
154
  &.wrapper {
141
155
  position: relative;
142
156
 
143
- /* Absolutely position any slotted element and center it vertically. */
157
+ /* Absolutely position any slotted element as an icon-sized square, centred vertically. Auto block margins centre it, which also works for a bare `<svg>`. */
144
158
  & > [data-slot] {
145
159
  position: absolute;
146
160
  inset-block: 0;
161
+ margin-block: auto;
147
162
  display: flex;
148
163
  align-items: center;
149
164
  justify-content: center;
150
- inline-size: calc(var(--input-padding, var(--space-small)) + var(--input-icon-size, var(--size-icon)));
165
+ inline-size: var(--input-icon-size, var(--size-icon));
166
+ block-size: var(--input-icon-size, var(--size-icon));
151
167
  pointer-events: none;
152
168
  }
153
169
 
154
- /* First [data-slot] child anchors to the left. */
170
+ /* First [data-slot] child sits one indent from the start, where the text of a plain input starts. */
155
171
  & > [data-slot]:first-child {
156
- inset-inline-start: 0;
172
+ inset-inline-start: var(--input-indent, var(--space-small));
157
173
  }
158
174
 
159
- /* Last [data-slot] child anchors to the right. */
175
+ /* Last [data-slot] child sits one indent from the end. */
160
176
  & > [data-slot]:last-child {
161
- inset-inline-end: 0;
177
+ inset-inline-end: var(--input-indent, var(--space-small));
162
178
  }
163
179
 
164
- /* Push the inner .input left padding to clear the left slot. */
180
+ /* Push the inner .input text past the start slot, with one indent gap. */
165
181
  &:has(> [data-slot]:first-child) > .input {
166
- padding-inline-start: var(--input-icon-size, var(--size-icon));
182
+ padding-inline-start: calc(var(--input-icon-size, var(--size-icon)) + var(--input-indent, var(--space-small)));
167
183
  }
168
184
 
169
- /* Push the inner .input right padding to clear the right slot. */
185
+ /* Push the inner .input text clear of the end slot, with one indent gap. */
170
186
  &:has(> [data-slot]:last-child) > .input {
171
- padding-inline-end: var(--input-icon-size, var(--size-icon));
187
+ padding-inline-end: calc(var(--input-icon-size, var(--size-icon)) + var(--input-indent, var(--space-small)));
172
188
  }
173
189
  }
174
190
  }
@@ -17,7 +17,8 @@
17
17
  max-height: var(--popover-height, 30vh);
18
18
  border-radius: var(--popover-radius, var(--radius-xsmall));
19
19
  border: var(--popover-border, var(--popover-stroke, var(--stroke-normal)) solid var(--tint-80));
20
- padding: var(--popover-padding, var(--space-small));
20
+ padding-block: var(--popover-padding, var(--space-small));
21
+ padding-inline: var(--popover-indent, var(--space-small));
21
22
 
22
23
  /* Scrolling */
23
24
  overflow: hidden auto;
package/ui/menu/Menu.md CHANGED
@@ -35,7 +35,8 @@ import { Menu, MenuItem } from "shelving/ui";
35
35
  | `--menu-leading` | Line height | `var(--leading)` |
36
36
  | `--menu-color` | Text colour | `var(--tint-00)` |
37
37
  | `--menu-nested-space` | Block margin around a nested submenu | `var(--space-xxsmall)` |
38
- | `--menu-padding` | Item link padding (also insets the nested border) | `var(--space-xsmall)` |
38
+ | `--menu-padding` | Item link top and bottom padding (one length) | `var(--space-xsmall)` |
39
+ | `--menu-indent` | Item link left and right padding (one length; also insets the nested border) | `var(--space-xsmall)` |
39
40
  | `--menu-nested-border` | Nested submenu left-border width | `var(--stroke-focus)` |
40
41
  | `--menu-nested-color-border` | Nested submenu left-border colour | `var(--tint-50)` |
41
42
  | `--menu-nested-indent` | Nested submenu left padding | `var(--space-xsmall)` |
@@ -26,7 +26,7 @@
26
26
  margin-block: var(--menu-nested-space, var(--space-xxsmall));
27
27
 
28
28
  /* Inset the border by the link's inline padding so it lines up with the parent item's label. */
29
- margin-inline-start: var(--menu-padding, var(--space-xsmall));
29
+ margin-inline-start: var(--menu-indent, var(--space-xsmall));
30
30
  border-inline-start: var(--menu-nested-border, var(--stroke-focus)) solid var(--menu-nested-color-border, var(--tint-50));
31
31
  padding-inline-start: var(--menu-nested-indent, var(--space-xsmall));
32
32
  }
@@ -41,7 +41,8 @@
41
41
  display: block;
42
42
  box-sizing: border-box;
43
43
  inline-size: 100%;
44
- padding: var(--menu-padding, var(--space-xsmall));
44
+ padding-block: var(--menu-padding, var(--space-xsmall));
45
+ padding-inline: var(--menu-indent, var(--space-xsmall));
45
46
  border: none;
46
47
  border-radius: var(--menu-radius, var(--radius-xsmall));
47
48
 
@@ -31,7 +31,8 @@ The item link's hooks (defined in `Menu.module.css`):
31
31
 
32
32
  | Variable | Styles | Default |
33
33
  |---|---|---|
34
- | `--menu-padding` | Link inner padding | `var(--space-xsmall)` |
34
+ | `--menu-padding` | Link top and bottom padding (one length) | `var(--space-xsmall)` |
35
+ | `--menu-indent` | Link left and right padding (one length) | `var(--space-xsmall)` |
35
36
  | `--menu-radius` | Link corner radius | `var(--radius-xsmall)` |
36
37
  | `--menu-focus-border` | Focus outline | `var(--stroke-focus) solid var(--color-focus)` |
37
38
  | `--menu-hover-background` | Link fill on hover/focus | `var(--tint-90)` |
package/ui/misc/Tag.md CHANGED
@@ -27,7 +27,8 @@ import { Tag } from "shelving/ui";
27
27
  | `--tag-background` | Surface fill | `var(--tint-50)` |
28
28
  | `--tag-hover-background` | Surface fill when an interactive tag is hovered | `var(--tint-55)` |
29
29
  | `--tag-color` | Text colour | `var(--tint-100)` |
30
- | `--tag-padding` | Inner padding | `0 var(--space-xxsmall)` |
30
+ | `--tag-padding` | Inner top and bottom padding (one length) | `0` |
31
+ | `--tag-indent` | Inner left and right padding (one length) | `var(--space-xxsmall)` (4px) |
31
32
  | `--tag-border` | Border shorthand | `0` |
32
33
  | `--tag-radius` | Corner radius | `var(--radius-xxsmall)` |
33
34
  | `--tag-font` | Font family | `var(--font-body)` |
@@ -15,7 +15,8 @@
15
15
  inline-size: fit-content;
16
16
  min-inline-size: fit-content;
17
17
  max-inline-size: 100%;
18
- padding: var(--tag-padding, 0 var(--space-xxsmall));
18
+ padding-block: var(--tag-padding, 0);
19
+ padding-inline: var(--tag-indent, var(--space-xxsmall));
19
20
  border: var(--tag-border, 0);
20
21
  border-radius: var(--tag-radius, var(--radius-xxsmall));
21
22
 
@@ -37,7 +37,8 @@ import { LOADING_NOTICE } from "shelving/ui";
37
37
  | `--notice-border` | Border shorthand | `var(--notice-stroke) solid var(--tint-80)` |
38
38
  | `--notice-stroke` | Border thickness | `var(--stroke-normal)` (2px) |
39
39
  | `--notice-radius` | Corner radius | `var(--radius-xsmall)` |
40
- | `--notice-padding` | Inner padding | `var(--space-small)` |
40
+ | `--notice-padding` | Inner top and bottom padding (one length) | `var(--space-small)` (12px) |
41
+ | `--notice-indent` | Inner left and right padding (one length) | `var(--space-small)` (12px) |
41
42
  | `--notice-space` | Outer block margin (top + bottom) | `var(--space-paragraph)` |
42
43
  | `--notice-size` | Font size | `var(--size-normal)` |
43
44
  | `--notice-weight` | Font weight | `var(--weight-strong)` |
@@ -10,7 +10,8 @@
10
10
  margin-block: var(--notice-space, var(--space-paragraph));
11
11
  border-radius: var(--notice-radius, var(--radius-xsmall));
12
12
  border: var(--notice-border, var(--notice-stroke, var(--stroke-normal)) solid var(--tint-80));
13
- padding: var(--notice-padding, var(--space-small));
13
+ padding-block: var(--notice-padding, var(--space-small));
14
+ padding-inline: var(--notice-indent, var(--space-small));
14
15
 
15
16
  /* Style */
16
17
  background: var(--notice-background, var(--tint-90));
package/ui/table/Table.md CHANGED
@@ -34,12 +34,13 @@ import { Table } from "shelving/ui";
34
34
 
35
35
  `Table` exposes hooks for its rhythm, cell padding, and border; it paints no surface of its own, so it inherits the surrounding tint.
36
36
 
37
- `--table-padding` sets the `padding` shorthand on each cell, so a single value pads both axes equally and a two-value override pads block and inline separately (e.g. `var(--space-xxsmall) var(--space-small)`). The outermost columns still drop their outer inline padding, and `<thead>` / `<tfoot>` their outer block padding, so the table stays flush with its container.
37
+ `--table-padding` sets the top and bottom padding of each cell, and `--table-indent` the left and right. Each takes one length. The outermost columns still drop their outer inline padding, and `<thead>` / `<tfoot>` their outer block padding, so the table stays flush with its container.
38
38
 
39
39
  | Variable | Styles | Default |
40
40
  |---|---|---|
41
41
  | `--table-space` | Outer block margin (top + bottom) | `var(--space-paragraph)` (16px) |
42
- | `--table-padding` | Cell padding | `var(--space-xsmall)` |
42
+ | `--table-padding` | Cell top and bottom padding (one length) | `var(--space-xsmall)` (8px) |
43
+ | `--table-indent` | Cell left and right padding (one length) | `var(--space-xsmall)` (8px) |
43
44
  | `--table-border` | Cell border shorthand | `var(--table-stroke)` |
44
45
  | `--table-stroke` | Cell border thickness/colour | `var(--stroke-normal) solid var(--tint-80)` |
45
46
  | `--table-header-weight` | Weight of `<thead>` / `<tfoot>` / `<tbody> <th>` cells | `var(--weight-strong)` |
@@ -20,7 +20,8 @@
20
20
  /* Children */
21
21
  th,
22
22
  td {
23
- padding: var(--table-padding, var(--space-xsmall));
23
+ padding-block: var(--table-padding, var(--space-xsmall));
24
+ padding-inline: var(--table-indent, var(--space-xsmall));
24
25
  border-block: var(--table-border, var(--table-stroke, var(--stroke-normal) solid var(--tint-80)));
25
26
  }
26
27