shelving 1.286.1 → 1.288.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.1",
3
+ "version": "1.288.0",
4
4
  "author": "Dave Houlbrooke <dave@shax.com>",
5
5
  "repository": {
6
6
  "type": "git",
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 */
@@ -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;
@@ -10,8 +10,19 @@ import { type ClickableProps } from "./Clickable.js";
10
10
  * @see https://shelving.cc/ui/ButtonVariants
11
11
  */
12
12
  export interface ButtonVariants extends FlexVariants, StatusVariants, TypographyVariants {
13
- /** Add plain styling (no background or border until hover or focus). */
13
+ /** Solid styling: a strong fill of the tint colour with white text. Use it for the main action. */
14
+ solid?: boolean | undefined;
15
+ /** Plain styling: no fill or border until hover or focus. */
14
16
  plain?: boolean | undefined;
17
+ /** Outline styling: like `plain`, but with a border until hover or focus. */
18
+ outline?: boolean | undefined;
19
+ /**
20
+ * Whether the button is the selected one in a group, such as a set of tabs.
21
+ * - `true` sets `aria-pressed` (or `aria-current` on a link) and keeps the button's normal look.
22
+ * - `false` also drops the fill until hover or focus, so the selected button stands out.
23
+ * - `undefined` (the default) means the button is not part of a group.
24
+ */
25
+ selected?: boolean | undefined;
15
26
  /** Make the button appear smaller. */
16
27
  small?: boolean | undefined;
17
28
  /** Fill the available width instead of sizing to content (buttons are content-width by default). */
@@ -35,7 +46,9 @@ export interface ButtonProps extends ButtonVariants, ClickableProps, ClassProps
35
46
  /**
36
47
  * Render either a `<button>` or an `<a href="">` styled as a button, based on whether an `onClick` or `href` prop is provided.
37
48
  * - Content-width by default (never grows); it won't shrink below its label. Pass `full` to fill the available width.
38
- * - Filled by default — use `color=` / `status=` for emphasis, or `plain` to de-emphasise.
49
+ * - Light by default (a pale fill with dark text). Use `solid` for the main action, or `plain` / `outline` to de-emphasise.
50
+ * - `color=` / `status=` set the colour of every look.
51
+ * - Pass `selected` to make a group of buttons (such as tabs): the selected one keeps its look, the others drop their fill.
39
52
  * - Accepts all `ButtonVariants` styling props plus the `ClickableProps` (`onClick`, `href`, `disabled`, etc.).
40
53
  *
41
54
  * @kind component
@@ -13,12 +13,14 @@ import { Clickable } from "./Clickable.js";
13
13
  * @see https://shelving.cc/ui/getButtonClass
14
14
  */
15
15
  export function getButtonClass(variants) {
16
- return getClass(getModuleClass(BUTTON_CSS, "button", variants), getFlexClass(variants), getStatusClass(variants), getTypographyClass(variants));
16
+ return getClass(getModuleClass(BUTTON_CSS, "button", variants, variants.selected === false && "unselected"), getFlexClass(variants), getStatusClass(variants), getTypographyClass(variants));
17
17
  }
18
18
  /**
19
19
  * Render either a `<button>` or an `<a href="">` styled as a button, based on whether an `onClick` or `href` prop is provided.
20
20
  * - Content-width by default (never grows); it won't shrink below its label. Pass `full` to fill the available width.
21
- * - Filled by default — use `color=` / `status=` for emphasis, or `plain` to de-emphasise.
21
+ * - Light by default (a pale fill with dark text). Use `solid` for the main action, or `plain` / `outline` to de-emphasise.
22
+ * - `color=` / `status=` set the colour of every look.
23
+ * - Pass `selected` to make a group of buttons (such as tabs): the selected one keeps its look, the others drop their fill.
22
24
  * - Accepts all `ButtonVariants` styling props plus the `ClickableProps` (`onClick`, `href`, `disabled`, etc.).
23
25
  *
24
26
  * @kind component
@@ -1,12 +1,18 @@
1
1
  # Button
2
2
 
3
- A clickable styled as a solid button. Renders an `<a href="">` when given `href`, or a `<button>` when given `onClick` — the shared `<Clickable>` primitive picks the element, so a button is always the right semantics for what it does.
3
+ A clickable styled as a button. Renders an `<a href="">` when given `href`, or a `<button>` when given `onClick` — the shared `<Clickable>` primitive picks the element, so a button is always the right semantics for what it does.
4
4
 
5
5
  **Things to know:**
6
6
 
7
7
  - Content-width by default: it sizes to its label and never grows. Pass `full` to fill the available width (it then shrinks to share a row, down to the content floor).
8
- - Every button is filled. Emphasis comes from colour: `color=` / `status=` move the tint anchor, so `color="primary"` marks the main action and a colourless button stays a neutral grey.
9
- - `plain` de-emphasises — no fill or border until hover/focus, for chrome-level actions like breadcrumbs and a dialog's close button. Set `--button-plain-border` to give plain buttons a resting edge when they need to hold their shape.
8
+ - There are four looks:
9
+ - **Default** — a pale fill of the tint colour with the tint colour as text. Use it for most actions.
10
+ - **`solid`** — a strong fill of the tint colour with white text. Use it for the main action on a screen, such as a form's submit button.
11
+ - **`plain`** — no fill or border until hover or focus. Use it for chrome-level actions, such as breadcrumbs and a dialog's close button.
12
+ - **`outline`** — like `plain`, but with a border until hover or focus.
13
+ - On hover, `plain` and `outline` take the same fill as a hovered default button.
14
+ - `color=` / `status=` move the tint anchor, so they set the colour of every look. A colourless button stays a neutral grey.
15
+ - `selected` makes a group of buttons, such as tabs. `selected={true}` sets `aria-pressed` (or `aria-current` on a link) and keeps the button's normal look. `selected={false}` also drops the fill until hover or focus, like `plain`, so the selected button stands out. Leave it `undefined` for a button that is not in a group.
10
16
  - `small` tightens the padding.
11
17
  - `getButtonClass(variants)` returns the same `className` the component composes — use it to style a non-`<button>` element as a button when `Button` itself doesn't fit.
12
18
  - `className` attaches an app class to one button, merged after the computed classes so an app stylesheet wins — see `ClassProps`.
@@ -18,9 +24,10 @@ A clickable styled as a solid button. Renders an `<a href="">` when given `href`
18
24
  ```tsx
19
25
  import { Button } from "shelving/ui";
20
26
 
21
- <Button onClick={save} color="primary">Save</Button>
27
+ <Button onClick={save} solid color="primary">Save</Button>
22
28
  <Button href="/about">About</Button>
23
29
  <Button onClick={remove} status="error">Delete</Button>
30
+ <Button onClick={share} outline>Share</Button>
24
31
  ```
25
32
 
26
33
  ### A row of buttons
@@ -31,7 +38,21 @@ import { Row } from "shelving/ui";
31
38
 
32
39
  <Row gap="small" right>
33
40
  <Button plain onClick={cancel}>Cancel</Button>
34
- <Button color="primary" onClick={submit}>Continue</Button>
41
+ <Button solid color="primary" onClick={submit}>Continue</Button>
42
+ </Row>
43
+ ```
44
+
45
+ ### Tabs
46
+
47
+ ```tsx
48
+ import { Button, Row } from "shelving/ui";
49
+
50
+ <Row gap="xsmall">
51
+ {SECTIONS.map(({ key, label }) => (
52
+ <Button key={key} small solid selected={key === section} onClick={() => setSection(key)}>
53
+ {label}
54
+ </Button>
55
+ ))}
35
56
  </Row>
36
57
  ```
37
58
 
@@ -50,7 +71,7 @@ import { Button } from "shelving/ui";
50
71
  import { getButtonClass } from "shelving/ui";
51
72
 
52
73
  // Style an arbitrary element as a button.
53
- <label className={getButtonClass({ color: "primary", small: true })}>
74
+ <label className={getButtonClass({ color: "primary", solid: true, small: true })}>
54
75
  Upload<input type="file" hidden />
55
76
  </label>
56
77
  ```
@@ -59,26 +80,35 @@ import { getButtonClass } from "shelving/ui";
59
80
 
60
81
  `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
82
 
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)`).
83
+ `--button-padding` sets the top and bottom padding, and `--button-indent` the left and right. Each takes one length, not a shorthand.
84
+
85
+ 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.
86
+
87
+ The `--button-*` colour hooks without a look in their name paint the default look. `solid` has its own `--button-solid-*` colour hooks.
63
88
 
64
- `--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.
89
+ `--button-shadow`, `--button-hover-transform` and the `--button-active-*` pressed-state hooks are static and apply to every button, with one exception: `plain` and `outline` never paint a box shadow in any state — they have no fill until hover, so a raised edge under them reads broken. The hover and pressed transforms still apply to them, so all buttons move together. `--button-transition` already covers animating the press and release.
65
90
 
66
- `plain` carries its own hooks for where it differs: `--button-plain-text` recolours the label, `--button-plain-hover-background` / `--button-plain-hover-border` paint the hover and focus state, the `--button-plain-active-*` pair paints the pressed state (falling back to the plain hover hooks), and `--button-plain-border` sets the resting border — transparent by default, so a theme where plain buttons should keep a visible edge (an "outline" button) sets it once. The hover border falls back through `--button-hover-border`, so a theme that borders every hovered button also borders hovered plain ones.
91
+ `plain`, `outline` and `selected={false}` share the `--button-plain-*` hooks: `--button-plain-text` recolours the label, `--button-plain-hover-background` / `--button-plain-hover-border` paint the hover and focus state, and the `--button-plain-active-*` pair paints the pressed state. The hover fill falls back to `--button-hover-background`, so plain and outline buttons always hover like a default button. `--button-plain-border` sets the resting border of `plain` (transparent by default), and `--button-outline-border` sets the resting border of `outline`.
67
92
 
68
93
  Backgrounds paint to the button's true edge: `background-origin` is set to `border-box`, so a gradient or image background in any state reaches through the transparent border instead of stopping 2px short at the padding box.
69
94
 
70
95
  | Variable | Styles | Default |
71
96
  |---|---|---|
72
- | `--button-background` | Surface fill | `var(--tint-50)` |
73
- | `--button-hover-background` | Surface fill on hover / focus | `var(--tint-55)` |
97
+ | `--button-background` | Surface fill | `var(--tint-90)` |
98
+ | `--button-hover-background` | Surface fill on hover / focus | `var(--tint-85)` |
74
99
  | `--button-hover-border` | Border on hover / focus | `var(--button-stroke) solid transparent` |
75
100
  | `--button-hover-transform` | Transform on hover / focus | `none` |
76
- | `--button-text` | Label colour | `var(--tint-100)` |
101
+ | `--button-text` | Label colour | `var(--tint-50)` |
77
102
  | `--button-border` | Border shorthand | `var(--button-stroke) solid transparent` |
78
103
  | `--button-stroke` | Border / outline thickness | `var(--stroke-normal)` (2px) |
79
104
  | `--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) |
105
+ | `--button-padding` | Top and bottom padding (one length) | `var(--space-small)` (12px) |
106
+ | `--button-indent` | Left and right padding (one length) | `var(--space-small)` (12px) |
107
+ | `--button-small-padding` | Top and bottom padding when `small` (one length) | `var(--space-xxsmall)` (4px) |
108
+ | `--button-small-indent` | Left and right padding when `small` (one length) | `var(--space-xxsmall)` (4px) |
109
+ | `--button-icon-size` | Icon size, and the base of the minimum height | `var(--size-icon)` (24px) |
110
+ | `--button-height` | Minimum height | `--button-icon-size` + 2 × `--button-padding` + 2 × `--button-stroke` (52px) |
111
+ | `--button-small-height` | Minimum height when `small` | `--button-icon-size` + 2 × `--button-small-padding` + 2 × `--button-stroke` (36px) |
82
112
  | `--button-gap` | Gap between icon and label | `var(--space-small)` (12px) |
83
113
  | `--button-small-gap` | Gap between icon and label when `small` | `var(--space-xxsmall)` (4px) |
84
114
  | `--button-space` | Outer block margin | `var(--space-small)` (12px) |
@@ -94,33 +124,38 @@ Backgrounds paint to the button's true edge: `background-origin` is set to `bord
94
124
  | `--button-transition` | Transition | `all var(--duration-fast)` (150ms) |
95
125
  | `--button-focus-border` | Focus outline | `var(--stroke-focus) solid var(--color-focus)` |
96
126
  | `--button-disabled-opacity` | Opacity when disabled | `0.5` |
97
- | `--button-plain-text` | Label colour when `plain` | `var(--tint-50)` |
127
+ | `--button-solid-background` | Surface fill when `solid` | `var(--tint-50)` |
128
+ | `--button-solid-text` | Label colour when `solid` | `var(--tint-100)` (white) |
129
+ | `--button-solid-hover-background` | Surface fill on hover / focus when `solid` | `var(--tint-55)` |
130
+ | `--button-solid-active-background` | Surface fill while pressed when `solid` | `var(--button-solid-hover-background)` |
131
+ | `--button-plain-text` | Label colour when `plain` or `outline` | `var(--tint-50)` |
98
132
  | `--button-plain-border` | Resting border when `plain` | `var(--button-stroke) solid transparent` |
99
- | `--button-plain-hover-background` | Fill on hover / focus when `plain` | `var(--tint-95)` |
100
- | `--button-plain-hover-border` | Border on hover / focus when `plain` | `var(--button-hover-border)` (transparent) |
101
- | `--button-plain-active-background` | Fill while pressed when `plain` | `var(--button-plain-hover-background)` |
102
- | `--button-plain-active-border` | Border while pressed when `plain` | `var(--button-plain-hover-border)` |
133
+ | `--button-outline-border` | Resting border when `outline` | `var(--button-stroke) solid var(--tint-80)` |
134
+ | `--button-unselected-background` | Resting fill when `selected={false}` | `transparent` |
135
+ | `--button-plain-hover-background` | Fill on hover / focus when `plain` or `outline` | `var(--button-hover-background)` |
136
+ | `--button-plain-hover-border` | Border on hover / focus when `plain` or `outline` | `var(--button-hover-border)` (transparent) |
137
+ | `--button-plain-active-background` | Fill while pressed when `plain` or `outline` | `var(--button-plain-hover-background)` |
138
+ | `--button-plain-active-border` | Border while pressed when `plain` or `outline` | `var(--button-plain-hover-border)` |
103
139
 
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`.
140
+ **Global tokens it reads:** the tint ladder `--tint-50` / `--tint-55` / `--tint-80` / `--tint-85` / `--tint-90` / `--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
141
 
106
142
  ```css
107
143
  /* Theme: pill-shaped buttons, with roomier inline padding. */
108
144
  :root {
109
145
  --button-radius: 999px;
110
- --button-padding: var(--space-small) var(--space-normal);
146
+ --button-indent: var(--space-normal);
111
147
  }
112
148
  ```
113
149
 
114
150
  ```css
115
- /* Theme: plain buttons keep an edge, so a quiet button holds its shape next to a filled one. */
151
+ /* Theme: outline buttons use the label colour for their edge. */
116
152
  :root {
117
- --button-plain-border: var(--stroke-normal) solid var(--tint-80);
118
- --button-plain-hover-border: var(--stroke-normal) solid var(--tint-80);
153
+ --button-outline-border: var(--stroke-normal) solid var(--tint-50);
119
154
  }
120
155
  ```
121
156
 
122
157
  ```css
123
- /* Theme: buttons are raised and press down flat — `plain` presses down too but never casts a shadow. */
158
+ /* Theme: buttons are raised and press down flat — `plain` and `outline` press down too but never casts a shadow. */
124
159
  :root {
125
160
  --button-shadow: 0 0.25rem 0 var(--tint-30);
126
161
  --button-active-transform: translateY(0.2rem);
@@ -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
 
@@ -29,8 +40,8 @@
29
40
  align-items: center;
30
41
 
31
42
  /* Style. */
32
- background: var(--button-background, var(--tint-50));
33
- color: var(--button-text, var(--tint-100));
43
+ background: var(--button-background, var(--tint-90));
44
+ color: var(--button-text, var(--tint-50));
34
45
  box-shadow: var(--button-shadow, none);
35
46
  transition: var(--button-transition, all var(--duration-fast));
36
47
  cursor: pointer;
@@ -55,14 +66,14 @@
55
66
  &:any-link:hover,
56
67
  &:focus:not(:focus-visible) {
57
68
  border: var(--button-hover-border, var(--button-stroke, var(--stroke-normal)) solid transparent);
58
- background: var(--button-hover-background, var(--tint-55));
69
+ background: var(--button-hover-background, var(--tint-85));
59
70
  transform: var(--button-hover-transform, none);
60
71
  }
61
72
 
62
73
  &:enabled:active,
63
74
  &:any-link:active {
64
75
  border: var(--button-active-border, var(--button-hover-border, var(--button-stroke, var(--stroke-normal)) solid transparent));
65
- background: var(--button-active-background, var(--button-hover-background, var(--tint-55)));
76
+ background: var(--button-active-background, var(--button-hover-background, var(--tint-85)));
66
77
  transform: var(--button-active-transform, var(--button-hover-transform, none));
67
78
  box-shadow: var(--button-active-shadow, var(--button-shadow, none));
68
79
  }
@@ -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 {
@@ -78,15 +105,32 @@
78
105
  inline-size: 100%;
79
106
  }
80
107
 
81
- &.plain {
108
+ &.solid {
109
+ background: var(--button-solid-background, var(--tint-50));
110
+ color: var(--button-solid-text, var(--tint-100));
111
+
112
+ &:enabled:hover,
113
+ &:any-link:hover,
114
+ &:focus:not(:focus-visible) {
115
+ background: var(--button-solid-hover-background, var(--tint-55));
116
+ }
117
+
118
+ &:enabled:active,
119
+ &:any-link:active {
120
+ background: var(--button-solid-active-background, var(--button-solid-hover-background, var(--tint-55)));
121
+ }
122
+ }
123
+
124
+ /* Plain, outline and unselected share one look: no fill until hover, then the default button's hover fill. */
125
+ &:is(.plain, .outline, .unselected) {
82
126
  color: var(--button-plain-text, var(--tint-50));
83
127
 
84
- /* Plain includes plain `:focus` so a keyboard-focused button paints its fill and stays legible. */
128
+ /* Includes plain `:focus` so a keyboard-focused button paints its fill and stays legible. */
85
129
  &:enabled:hover,
86
130
  &:any-link:hover,
87
131
  &:focus {
88
132
  border: var(--button-plain-hover-border, var(--button-hover-border, var(--button-stroke, var(--stroke-normal)) solid transparent));
89
- background: var(--button-plain-hover-background, var(--tint-95));
133
+ background: var(--button-plain-hover-background, var(--button-hover-background, var(--tint-85)));
90
134
  }
91
135
 
92
136
  &:enabled:active,
@@ -95,7 +139,10 @@
95
139
  --button-plain-active-border,
96
140
  var(--button-plain-hover-border, var(--button-hover-border, var(--button-stroke, var(--stroke-normal)) solid transparent))
97
141
  );
98
- background: var(--button-plain-active-background, var(--button-plain-hover-background, var(--tint-95)));
142
+ background: var(
143
+ --button-plain-active-background,
144
+ var(--button-plain-hover-background, var(--button-active-background, var(--button-hover-background, var(--tint-85))))
145
+ );
99
146
  }
100
147
  }
101
148
  }
@@ -120,7 +167,7 @@
120
167
  }
121
168
 
122
169
  /* Variants */
123
- &.plain {
170
+ &:is(.plain, .outline, .unselected) {
124
171
  box-shadow: none;
125
172
 
126
173
  &:not(:enabled:hover, :any-link:hover, :active, :focus) {
@@ -128,5 +175,13 @@
128
175
  background: transparent;
129
176
  }
130
177
  }
178
+
179
+ &.unselected:not(:enabled:hover, :any-link:hover, :active, :focus) {
180
+ background: var(--button-unselected-background, transparent);
181
+ }
182
+
183
+ &.outline:not(:enabled:hover, :any-link:hover, :active, :focus) {
184
+ border: var(--button-outline-border, var(--button-stroke, var(--stroke-normal)) solid var(--tint-80));
185
+ }
131
186
  }
132
187
  }
@@ -13,8 +13,19 @@ import { Clickable, type ClickableProps } from "./Clickable.js";
13
13
  * @see https://shelving.cc/ui/ButtonVariants
14
14
  */
15
15
  export interface ButtonVariants extends FlexVariants, StatusVariants, TypographyVariants {
16
- /** Add plain styling (no background or border until hover or focus). */
16
+ /** Solid styling: a strong fill of the tint colour with white text. Use it for the main action. */
17
+ solid?: boolean | undefined;
18
+ /** Plain styling: no fill or border until hover or focus. */
17
19
  plain?: boolean | undefined;
20
+ /** Outline styling: like `plain`, but with a border until hover or focus. */
21
+ outline?: boolean | undefined;
22
+ /**
23
+ * Whether the button is the selected one in a group, such as a set of tabs.
24
+ * - `true` sets `aria-pressed` (or `aria-current` on a link) and keeps the button's normal look.
25
+ * - `false` also drops the fill until hover or focus, so the selected button stands out.
26
+ * - `undefined` (the default) means the button is not part of a group.
27
+ */
28
+ selected?: boolean | undefined;
18
29
  /** Make the button appear smaller. */
19
30
  small?: boolean | undefined;
20
31
  /** Fill the available width instead of sizing to content (buttons are content-width by default). */
@@ -30,7 +41,7 @@ export interface ButtonVariants extends FlexVariants, StatusVariants, Typography
30
41
  */
31
42
  export function getButtonClass(variants: ButtonVariants): string {
32
43
  return getClass(
33
- getModuleClass(BUTTON_CSS, "button", variants),
44
+ getModuleClass(BUTTON_CSS, "button", variants, variants.selected === false && "unselected"),
34
45
  getFlexClass(variants),
35
46
  getStatusClass(variants),
36
47
  getTypographyClass(variants),
@@ -47,7 +58,9 @@ export interface ButtonProps extends ButtonVariants, ClickableProps, ClassProps
47
58
  /**
48
59
  * Render either a `<button>` or an `<a href="">` styled as a button, based on whether an `onClick` or `href` prop is provided.
49
60
  * - Content-width by default (never grows); it won't shrink below its label. Pass `full` to fill the available width.
50
- * - Filled by default — use `color=` / `status=` for emphasis, or `plain` to de-emphasise.
61
+ * - Light by default (a pale fill with dark text). Use `solid` for the main action, or `plain` / `outline` to de-emphasise.
62
+ * - `color=` / `status=` set the colour of every look.
63
+ * - Pass `selected` to make a group of buttons (such as tabs): the selected one keeps its look, the others drop their fill.
51
64
  * - Accepts all `ButtonVariants` styling props plus the `ClickableProps` (`onClick`, `href`, `disabled`, etc.).
52
65
  *
53
66
  * @kind component
@@ -30,6 +30,8 @@ export interface ClickableProps extends OptionalChildProps {
30
30
  download?: string | undefined;
31
31
  /** Title shown on hover. */
32
32
  title?: string | undefined;
33
+ /** Whether this is the selected item in a group. Sets `aria-pressed` on a `<button>`, or `aria-current` on an `<a>`. */
34
+ selected?: boolean | undefined;
33
35
  }
34
36
  /**
35
37
  * Props for a clickable that also accepts a `className` for styling.
@@ -57,7 +59,7 @@ export declare function Clickable(props: StylableClickableProps): ReactElement;
57
59
  * @example <LinkClickable href="/about" className="link">About</LinkClickable>
58
60
  * @see https://shelving.cc/ui/LinkClickable
59
61
  */
60
- export declare function LinkClickable({ href, disabled, target, download, title, children, className, }: StylableClickableProps): ReactElement;
62
+ export declare function LinkClickable({ href, disabled, target, download, title, selected, children, className, }: StylableClickableProps): ReactElement;
61
63
  /**
62
64
  * Render a `<button>` element that runs its `onClick` handler through a `BusyStore` and shows a loading spinner while busy.
63
65
  * - Notifies the user of the handler's returned value (success) or thrown value (error).
@@ -66,7 +68,7 @@ export declare function LinkClickable({ href, disabled, target, download, title,
66
68
  * @example <ButtonClickable onClick={save} className="btn">Save</ButtonClickable>
67
69
  * @see https://shelving.cc/ui/ButtonClickable
68
70
  */
69
- export declare function ButtonClickable({ onClick, disabled, title, children, className, }: StylableClickableProps): ReactElement;
71
+ export declare function ButtonClickable({ onClick, disabled, title, selected, children, className, }: StylableClickableProps): ReactElement;
70
72
  /**
71
73
  * Render a non-interactive `<span>` element, used as the fallback when neither `href` nor `onClick` is provided.
72
74
  *
@@ -29,13 +29,13 @@ export function Clickable(props) {
29
29
  * @example <LinkClickable href="/about" className="link">About</LinkClickable>
30
30
  * @see https://shelving.cc/ui/LinkClickable
31
31
  */
32
- export function LinkClickable({ href, disabled = !href, target, download, title, children = "Go", className, }) {
32
+ export function LinkClickable({ href, disabled = !href, target, download, title, selected, children = "Go", className, }) {
33
33
  // Resolve `href` against the current page URL and site root so site-absolute paths (`/foo`) honour the base subfolder.
34
34
  const { url, root } = requireMeta();
35
35
  const link = disabled ? undefined : getLink(href, url, root);
36
36
  // Is this link "active" compared to the current URL?
37
37
  const active = isURLActive(link, url);
38
- return (_jsx("a", { href: link?.href, title: title, download: download, target: target, className: getClass(className) || undefined, "aria-current": active ? "page" : undefined, children: children }));
38
+ return (_jsx("a", { href: link?.href, title: title, download: download, target: target, className: getClass(className) || undefined, "aria-current": active ? "page" : selected ? "true" : undefined, children: children }));
39
39
  }
40
40
  /**
41
41
  * Render a `<button>` element that runs its `onClick` handler through a `BusyStore` and shows a loading spinner while busy.
@@ -45,12 +45,12 @@ export function LinkClickable({ href, disabled = !href, target, download, title,
45
45
  * @example <ButtonClickable onClick={save} className="btn">Save</ButtonClickable>
46
46
  * @see https://shelving.cc/ui/ButtonClickable
47
47
  */
48
- export function ButtonClickable({ onClick, disabled = !onClick, title, children = "Click", className, }) {
48
+ export function ButtonClickable({ onClick, disabled = !onClick, title, selected, children = "Click", className, }) {
49
49
  // Create a `BusyStore<undefined>` to keep track of the `onClick` call and any thrown errors.
50
50
  const store = useInstance(BusyStore, undefined);
51
51
  // Track the `busy/unbusy` state of the button to show a loading spinner appropriately.
52
52
  const busy = useStore(store.busy).value;
53
- return (_jsx("button", { type: "button", title: title, disabled: busy || disabled, className: getClass(className) || undefined, onClick: disabled
53
+ return (_jsx("button", { type: "button", title: title, "aria-pressed": selected, disabled: busy || disabled, className: getClass(className) || undefined, onClick: disabled
54
54
  ? undefined
55
55
  : e => {
56
56
  if (!store.busy.value && onClick) {
@@ -41,6 +41,8 @@ export interface ClickableProps extends OptionalChildProps {
41
41
  download?: string | undefined;
42
42
  /** Title shown on hover. */
43
43
  title?: string | undefined;
44
+ /** Whether this is the selected item in a group. Sets `aria-pressed` on a `<button>`, or `aria-current` on an `<a>`. */
45
+ selected?: boolean | undefined;
44
46
  }
45
47
 
46
48
  /**
@@ -84,6 +86,7 @@ export function LinkClickable({
84
86
  target,
85
87
  download,
86
88
  title,
89
+ selected,
87
90
  children = "Go",
88
91
  className,
89
92
  }: StylableClickableProps): ReactElement {
@@ -101,7 +104,7 @@ export function LinkClickable({
101
104
  download={download}
102
105
  target={target}
103
106
  className={getClass(className) || undefined}
104
- aria-current={active ? "page" : undefined}
107
+ aria-current={active ? "page" : selected ? "true" : undefined}
105
108
  >
106
109
  {children}
107
110
  </a>
@@ -120,6 +123,7 @@ export function ButtonClickable({
120
123
  onClick,
121
124
  disabled = !onClick,
122
125
  title,
126
+ selected,
123
127
  children = "Click",
124
128
  className,
125
129
  }: StylableClickableProps): ReactElement {
@@ -133,6 +137,7 @@ export function ButtonClickable({
133
137
  <button //
134
138
  type="button"
135
139
  title={title}
140
+ aria-pressed={selected}
136
141
  disabled={busy || disabled}
137
142
  className={getClass(className) || undefined}
138
143
  onClick={
@@ -6,6 +6,7 @@ import { type ButtonVariants } from "./Button.js";
6
6
  *
7
7
  * @property children - The content of the button. Defaults to `"Save"` with a right-pointing arrow icon.
8
8
  * @property color - The color variant of the button. Defaults to `"primary"`
9
+ * @property solid - Solid styling. Defaults to `true`
9
10
  *
10
11
  * @see https://shelving.cc/ui/SubmitButtonProps
11
12
  */
@@ -13,10 +14,10 @@ export interface SubmitButtonProps extends ButtonVariants, OptionalChildProps, C
13
14
  }
14
15
  /**
15
16
  * Submit button for a form that disables itself and shows a spinner while the form is busy.
16
- * - Defaults to full-width, primary styling and a "Save" label.
17
+ * - Defaults to full-width, solid primary styling and a "Save" label.
17
18
  *
18
19
  * @returns A `<button type="submit">` element bound to the current form.
19
20
  * @example <SubmitButton>Save changes</SubmitButton>
20
21
  * @see https://shelving.cc/ui/SubmitButton
21
22
  */
22
- export declare function SubmitButton({ children, color, full, className, ...props }: SubmitButtonProps): ReactElement;
23
+ export declare function SubmitButton({ children, color, solid, full, className, ...props }: SubmitButtonProps): ReactElement;
@@ -8,14 +8,14 @@ import { getButtonClass } from "./Button.js";
8
8
  const _SUBMIT_CHILDREN = (_jsxs(_Fragment, { children: ["Save", _jsx(ArrowRightIcon, {})] }));
9
9
  /**
10
10
  * Submit button for a form that disables itself and shows a spinner while the form is busy.
11
- * - Defaults to full-width, primary styling and a "Save" label.
11
+ * - Defaults to full-width, solid primary styling and a "Save" label.
12
12
  *
13
13
  * @returns A `<button type="submit">` element bound to the current form.
14
14
  * @example <SubmitButton>Save changes</SubmitButton>
15
15
  * @see https://shelving.cc/ui/SubmitButton
16
16
  */
17
- export function SubmitButton({ children = _SUBMIT_CHILDREN, color = "primary", full = true, className, ...props }) {
17
+ export function SubmitButton({ children = _SUBMIT_CHILDREN, color = "primary", solid = true, full = true, className, ...props }) {
18
18
  const form = requireForm();
19
19
  const busy = useStore(form.busy).value;
20
- return (_jsx("button", { type: "submit", disabled: busy, className: getClass(getButtonClass({ color, full, ...props }), className), children: busy ? LOADING : children }));
20
+ return (_jsx("button", { type: "submit", disabled: busy, className: getClass(getButtonClass({ color, solid, full, ...props }), className), children: busy ? LOADING : children }));
21
21
  }
@@ -12,6 +12,7 @@ import { type ButtonVariants, getButtonClass } from "./Button.js";
12
12
  *
13
13
  * @property children - The content of the button. Defaults to `"Save"` with a right-pointing arrow icon.
14
14
  * @property color - The color variant of the button. Defaults to `"primary"`
15
+ * @property solid - Solid styling. Defaults to `true`
15
16
  *
16
17
  * @see https://shelving.cc/ui/SubmitButtonProps
17
18
  */
@@ -26,7 +27,7 @@ const _SUBMIT_CHILDREN = (
26
27
 
27
28
  /**
28
29
  * Submit button for a form that disables itself and shows a spinner while the form is busy.
29
- * - Defaults to full-width, primary styling and a "Save" label.
30
+ * - Defaults to full-width, solid primary styling and a "Save" label.
30
31
  *
31
32
  * @returns A `<button type="submit">` element bound to the current form.
32
33
  * @example <SubmitButton>Save changes</SubmitButton>
@@ -35,6 +36,7 @@ const _SUBMIT_CHILDREN = (
35
36
  export function SubmitButton({
36
37
  children = _SUBMIT_CHILDREN,
37
38
  color = "primary",
39
+ solid = true,
38
40
  full = true,
39
41
  className,
40
42
  ...props
@@ -42,7 +44,7 @@ export function SubmitButton({
42
44
  const form = requireForm();
43
45
  const busy = useStore(form.busy).value;
44
46
  return (
45
- <button type="submit" disabled={busy} className={getClass(getButtonClass({ color, full, ...props }), className)}>
47
+ <button type="submit" disabled={busy} className={getClass(getButtonClass({ color, solid, full, ...props }), className)}>
46
48
  {busy ? LOADING : children}
47
49
  </button>
48
50
  );
@@ -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