shelving 1.287.0 → 1.289.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 +1 -1
- package/ui/button/Button.d.ts +19 -6
- package/ui/button/Button.js +6 -4
- package/ui/button/Button.md +59 -21
- package/ui/button/Button.module.css +37 -17
- package/ui/button/Button.tsx +21 -8
- package/ui/button/Clickable.d.ts +4 -2
- package/ui/button/Clickable.js +4 -4
- package/ui/button/Clickable.tsx +6 -1
- package/ui/button/SubmitButton.d.ts +3 -2
- package/ui/button/SubmitButton.js +3 -3
- package/ui/button/SubmitButton.tsx +4 -2
- package/ui/input/Input.d.ts +5 -5
- package/ui/input/Input.js +2 -2
- package/ui/input/Input.module.css +0 -8
- package/ui/input/Input.tsx +6 -6
package/package.json
CHANGED
package/ui/button/Button.d.ts
CHANGED
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
import type { ReactElement } from "react";
|
|
2
|
+
import { type BlockVariants } from "../style/Block.js";
|
|
2
3
|
import { type FlexVariants } from "../style/Flex.js";
|
|
3
4
|
import { type StatusVariants } from "../style/Status.js";
|
|
4
|
-
import { type TypographyVariants } from "../style/Typography.js";
|
|
5
5
|
import type { ClassProps } from "../util/props.js";
|
|
6
6
|
import { type ClickableProps } from "./Clickable.js";
|
|
7
7
|
/**
|
|
8
|
-
* Styling variants for a `Button
|
|
8
|
+
* Styling variants for a `Button`: the block variants (space, padding, indent, width, typography), flex, and status, plus button-specific toggles.
|
|
9
9
|
*
|
|
10
10
|
* @see https://shelving.cc/ui/ButtonVariants
|
|
11
11
|
*/
|
|
12
|
-
export interface ButtonVariants extends FlexVariants, StatusVariants
|
|
13
|
-
/**
|
|
12
|
+
export interface ButtonVariants extends BlockVariants, FlexVariants, StatusVariants {
|
|
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). */
|
|
@@ -20,7 +31,7 @@ export interface ButtonVariants extends FlexVariants, StatusVariants, Typography
|
|
|
20
31
|
/**
|
|
21
32
|
* Get the full combined `className` string for a button from its styling variants.
|
|
22
33
|
*
|
|
23
|
-
* @param variants The button styling variants (
|
|
34
|
+
* @param variants The button styling variants (block, flex, status, plus button toggles).
|
|
24
35
|
* @returns A space-separated `className` string combining all the resolved variant classes.
|
|
25
36
|
* @see https://shelving.cc/ui/getButtonClass
|
|
26
37
|
*/
|
|
@@ -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
|
-
* -
|
|
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
|
package/ui/button/Button.js
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { getBlockClass } from "../style/Block.js";
|
|
2
3
|
import { getFlexClass } from "../style/Flex.js";
|
|
3
4
|
import { getStatusClass } from "../style/Status.js";
|
|
4
|
-
import { getTypographyClass } from "../style/Typography.js";
|
|
5
5
|
import { getClass, getModuleClass } from "../util/css.js";
|
|
6
6
|
import BUTTON_CSS from "./Button.module.css";
|
|
7
7
|
import { Clickable } from "./Clickable.js";
|
|
8
8
|
/**
|
|
9
9
|
* Get the full combined `className` string for a button from its styling variants.
|
|
10
10
|
*
|
|
11
|
-
* @param variants The button styling variants (
|
|
11
|
+
* @param variants The button styling variants (block, flex, status, plus button toggles).
|
|
12
12
|
* @returns A space-separated `className` string combining all the resolved variant classes.
|
|
13
13
|
* @see https://shelving.cc/ui/getButtonClass
|
|
14
14
|
*/
|
|
15
15
|
export function getButtonClass(variants) {
|
|
16
|
-
return getClass(getModuleClass(BUTTON_CSS, "button", variants
|
|
16
|
+
return getClass(getBlockClass(variants), getModuleClass(BUTTON_CSS, "button", variants, variants.selected === false && "unselected"), getFlexClass(variants), getStatusClass(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
|
-
* -
|
|
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
|
package/ui/button/Button.md
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
# Button
|
|
2
2
|
|
|
3
|
-
A clickable styled as a
|
|
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
|
-
-
|
|
9
|
-
-
|
|
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.
|
|
17
|
+
- It takes the block variants, like any block: `space` sets its outer margin (`space="none"` removes it), `padding` and `indent` set its inner padding, and `width` sizes it. A `padding` variant sets the padding but not the minimum height, which `--button-padding` and `--button-height` set.
|
|
11
18
|
- `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
19
|
- `className` attaches an app class to one button, merged after the computed classes so an app stylesheet wins — see `ClassProps`.
|
|
13
20
|
|
|
@@ -18,9 +25,19 @@ A clickable styled as a solid button. Renders an `<a href="">` when given `href`
|
|
|
18
25
|
```tsx
|
|
19
26
|
import { Button } from "shelving/ui";
|
|
20
27
|
|
|
21
|
-
<Button onClick={save} color="primary">Save</Button>
|
|
28
|
+
<Button onClick={save} solid color="primary">Save</Button>
|
|
22
29
|
<Button href="/about">About</Button>
|
|
23
30
|
<Button onClick={remove} status="error">Delete</Button>
|
|
31
|
+
<Button onClick={share} outline>Share</Button>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Spacing and size
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { Button } from "shelving/ui";
|
|
38
|
+
|
|
39
|
+
// No outer margin: it sits flush with the content around it.
|
|
40
|
+
<Button full space="none" onClick={start}>Start</Button>
|
|
24
41
|
```
|
|
25
42
|
|
|
26
43
|
### A row of buttons
|
|
@@ -31,7 +48,21 @@ import { Row } from "shelving/ui";
|
|
|
31
48
|
|
|
32
49
|
<Row gap="small" right>
|
|
33
50
|
<Button plain onClick={cancel}>Cancel</Button>
|
|
34
|
-
<Button color="primary" onClick={submit}>Continue</Button>
|
|
51
|
+
<Button solid color="primary" onClick={submit}>Continue</Button>
|
|
52
|
+
</Row>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Tabs
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import { Button, Row } from "shelving/ui";
|
|
59
|
+
|
|
60
|
+
<Row gap="xsmall">
|
|
61
|
+
{SECTIONS.map(({ key, label }) => (
|
|
62
|
+
<Button key={key} small solid selected={key === section} onClick={() => setSection(key)}>
|
|
63
|
+
{label}
|
|
64
|
+
</Button>
|
|
65
|
+
))}
|
|
35
66
|
</Row>
|
|
36
67
|
```
|
|
37
68
|
|
|
@@ -50,7 +81,7 @@ import { Button } from "shelving/ui";
|
|
|
50
81
|
import { getButtonClass } from "shelving/ui";
|
|
51
82
|
|
|
52
83
|
// Style an arbitrary element as a button.
|
|
53
|
-
<label className={getButtonClass({ color: "primary", small: true })}>
|
|
84
|
+
<label className={getButtonClass({ color: "primary", solid: true, small: true })}>
|
|
54
85
|
Upload<input type="file" hidden />
|
|
55
86
|
</label>
|
|
56
87
|
```
|
|
@@ -63,19 +94,21 @@ import { getButtonClass } from "shelving/ui";
|
|
|
63
94
|
|
|
64
95
|
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.
|
|
65
96
|
|
|
66
|
-
|
|
97
|
+
The `--button-*` colour hooks without a look in their name paint the default look. `solid` has its own `--button-solid-*` colour hooks.
|
|
98
|
+
|
|
99
|
+
`--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.
|
|
67
100
|
|
|
68
|
-
`plain`
|
|
101
|
+
`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`.
|
|
69
102
|
|
|
70
103
|
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.
|
|
71
104
|
|
|
72
105
|
| Variable | Styles | Default |
|
|
73
106
|
|---|---|---|
|
|
74
|
-
| `--button-background` | Surface fill | `var(--tint-
|
|
75
|
-
| `--button-hover-background` | Surface fill on hover / focus | `var(--tint-
|
|
107
|
+
| `--button-background` | Surface fill | `var(--tint-90)` |
|
|
108
|
+
| `--button-hover-background` | Surface fill on hover / focus | `var(--tint-85)` |
|
|
76
109
|
| `--button-hover-border` | Border on hover / focus | `var(--button-stroke) solid transparent` |
|
|
77
110
|
| `--button-hover-transform` | Transform on hover / focus | `none` |
|
|
78
|
-
| `--button-text` | Label colour | `var(--tint-
|
|
111
|
+
| `--button-text` | Label colour | `var(--tint-50)` |
|
|
79
112
|
| `--button-border` | Border shorthand | `var(--button-stroke) solid transparent` |
|
|
80
113
|
| `--button-stroke` | Border / outline thickness | `var(--stroke-normal)` (2px) |
|
|
81
114
|
| `--button-radius` | Corner radius | `var(--radius-xsmall)` (8px) |
|
|
@@ -101,14 +134,20 @@ Backgrounds paint to the button's true edge: `background-origin` is set to `bord
|
|
|
101
134
|
| `--button-transition` | Transition | `all var(--duration-fast)` (150ms) |
|
|
102
135
|
| `--button-focus-border` | Focus outline | `var(--stroke-focus) solid var(--color-focus)` |
|
|
103
136
|
| `--button-disabled-opacity` | Opacity when disabled | `0.5` |
|
|
104
|
-
| `--button-
|
|
137
|
+
| `--button-solid-background` | Surface fill when `solid` | `var(--tint-50)` |
|
|
138
|
+
| `--button-solid-text` | Label colour when `solid` | `var(--tint-100)` (white) |
|
|
139
|
+
| `--button-solid-hover-background` | Surface fill on hover / focus when `solid` | `var(--tint-55)` |
|
|
140
|
+
| `--button-solid-active-background` | Surface fill while pressed when `solid` | `var(--button-solid-hover-background)` |
|
|
141
|
+
| `--button-plain-text` | Label colour when `plain` or `outline` | `var(--tint-50)` |
|
|
105
142
|
| `--button-plain-border` | Resting border when `plain` | `var(--button-stroke) solid transparent` |
|
|
106
|
-
| `--button-
|
|
107
|
-
| `--button-
|
|
108
|
-
| `--button-plain-
|
|
109
|
-
| `--button-plain-
|
|
143
|
+
| `--button-outline-border` | Resting border when `outline` | `var(--button-stroke) solid var(--tint-80)` |
|
|
144
|
+
| `--button-unselected-background` | Resting fill when `selected={false}` | `transparent` |
|
|
145
|
+
| `--button-plain-hover-background` | Fill on hover / focus when `plain` or `outline` | `var(--button-hover-background)` |
|
|
146
|
+
| `--button-plain-hover-border` | Border on hover / focus when `plain` or `outline` | `var(--button-hover-border)` (transparent) |
|
|
147
|
+
| `--button-plain-active-background` | Fill while pressed when `plain` or `outline` | `var(--button-plain-hover-background)` |
|
|
148
|
+
| `--button-plain-active-border` | Border while pressed when `plain` or `outline` | `var(--button-plain-hover-border)` |
|
|
110
149
|
|
|
111
|
-
**Global tokens it reads:** the tint ladder `--tint-50` / `--tint-55` / `--tint-
|
|
150
|
+
**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`.
|
|
112
151
|
|
|
113
152
|
```css
|
|
114
153
|
/* Theme: pill-shaped buttons, with roomier inline padding. */
|
|
@@ -119,15 +158,14 @@ Backgrounds paint to the button's true edge: `background-origin` is set to `bord
|
|
|
119
158
|
```
|
|
120
159
|
|
|
121
160
|
```css
|
|
122
|
-
/* Theme:
|
|
161
|
+
/* Theme: outline buttons use the label colour for their edge. */
|
|
123
162
|
:root {
|
|
124
|
-
--button-
|
|
125
|
-
--button-plain-hover-border: var(--stroke-normal) solid var(--tint-80);
|
|
163
|
+
--button-outline-border: var(--stroke-normal) solid var(--tint-50);
|
|
126
164
|
}
|
|
127
165
|
```
|
|
128
166
|
|
|
129
167
|
```css
|
|
130
|
-
/* Theme: buttons are raised and press down flat — `plain`
|
|
168
|
+
/* Theme: buttons are raised and press down flat — `plain` and `outline` press down too but never casts a shadow. */
|
|
131
169
|
:root {
|
|
132
170
|
--button-shadow: 0 0.25rem 0 var(--tint-30);
|
|
133
171
|
--button-active-transform: translateY(0.2rem);
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
align-items: center;
|
|
41
41
|
|
|
42
42
|
/* Style. */
|
|
43
|
-
background: var(--button-background, var(--tint-
|
|
44
|
-
color: var(--button-text, var(--tint-
|
|
43
|
+
background: var(--button-background, var(--tint-90));
|
|
44
|
+
color: var(--button-text, var(--tint-50));
|
|
45
45
|
box-shadow: var(--button-shadow, none);
|
|
46
46
|
transition: var(--button-transition, all var(--duration-fast));
|
|
47
47
|
cursor: pointer;
|
|
@@ -66,14 +66,14 @@
|
|
|
66
66
|
&:any-link:hover,
|
|
67
67
|
&:focus:not(:focus-visible) {
|
|
68
68
|
border: var(--button-hover-border, var(--button-stroke, var(--stroke-normal)) solid transparent);
|
|
69
|
-
background: var(--button-hover-background, var(--tint-
|
|
69
|
+
background: var(--button-hover-background, var(--tint-85));
|
|
70
70
|
transform: var(--button-hover-transform, none);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
&:enabled:active,
|
|
74
74
|
&:any-link:active {
|
|
75
75
|
border: var(--button-active-border, var(--button-hover-border, var(--button-stroke, var(--stroke-normal)) solid transparent));
|
|
76
|
-
background: var(--button-active-background, var(--button-hover-background, var(--tint-
|
|
76
|
+
background: var(--button-active-background, var(--button-hover-background, var(--tint-85)));
|
|
77
77
|
transform: var(--button-active-transform, var(--button-hover-transform, none));
|
|
78
78
|
box-shadow: var(--button-active-shadow, var(--button-shadow, none));
|
|
79
79
|
}
|
|
@@ -105,15 +105,32 @@
|
|
|
105
105
|
inline-size: 100%;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
&.
|
|
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) {
|
|
109
126
|
color: var(--button-plain-text, var(--tint-50));
|
|
110
127
|
|
|
111
|
-
/*
|
|
128
|
+
/* Includes plain `:focus` so a keyboard-focused button paints its fill and stays legible. */
|
|
112
129
|
&:enabled:hover,
|
|
113
130
|
&:any-link:hover,
|
|
114
131
|
&:focus {
|
|
115
132
|
border: var(--button-plain-hover-border, var(--button-hover-border, var(--button-stroke, var(--stroke-normal)) solid transparent));
|
|
116
|
-
background: var(--button-plain-hover-background, var(--tint-
|
|
133
|
+
background: var(--button-plain-hover-background, var(--button-hover-background, var(--tint-85)));
|
|
117
134
|
}
|
|
118
135
|
|
|
119
136
|
&:enabled:active,
|
|
@@ -122,7 +139,10 @@
|
|
|
122
139
|
--button-plain-active-border,
|
|
123
140
|
var(--button-plain-hover-border, var(--button-hover-border, var(--button-stroke, var(--stroke-normal)) solid transparent))
|
|
124
141
|
);
|
|
125
|
-
background: var(
|
|
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
|
+
);
|
|
126
146
|
}
|
|
127
147
|
}
|
|
128
148
|
}
|
|
@@ -133,21 +153,13 @@
|
|
|
133
153
|
/* The border is transparent by default, so background images/gradients must size to the border box to reach the button's edge. Lives in the overrides layer because every state's `background` shorthand resets the origin to `padding-box`. */
|
|
134
154
|
background-origin: border-box;
|
|
135
155
|
|
|
136
|
-
&:first-child {
|
|
137
|
-
margin-block-start: 0;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
&:last-child {
|
|
141
|
-
margin-block-end: 0;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
156
|
/* Pseudo-classes */
|
|
145
157
|
&:not(:focus-visible) {
|
|
146
158
|
outline-color: transparent;
|
|
147
159
|
}
|
|
148
160
|
|
|
149
161
|
/* Variants */
|
|
150
|
-
|
|
162
|
+
&:is(.plain, .outline, .unselected) {
|
|
151
163
|
box-shadow: none;
|
|
152
164
|
|
|
153
165
|
&:not(:enabled:hover, :any-link:hover, :active, :focus) {
|
|
@@ -155,5 +167,13 @@
|
|
|
155
167
|
background: transparent;
|
|
156
168
|
}
|
|
157
169
|
}
|
|
170
|
+
|
|
171
|
+
&.unselected:not(:enabled:hover, :any-link:hover, :active, :focus) {
|
|
172
|
+
background: var(--button-unselected-background, transparent);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
&.outline:not(:enabled:hover, :any-link:hover, :active, :focus) {
|
|
176
|
+
border: var(--button-outline-border, var(--button-stroke, var(--stroke-normal)) solid var(--tint-80));
|
|
177
|
+
}
|
|
158
178
|
}
|
|
159
179
|
}
|
package/ui/button/Button.tsx
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
import type { ReactElement } from "react";
|
|
2
|
+
import { type BlockVariants, getBlockClass } from "../style/Block.js";
|
|
2
3
|
import { type FlexVariants, getFlexClass } from "../style/Flex.js";
|
|
3
4
|
import { getStatusClass, type StatusVariants } from "../style/Status.js";
|
|
4
|
-
import { getTypographyClass, type TypographyVariants } from "../style/Typography.js";
|
|
5
5
|
import { getClass, getModuleClass } from "../util/css.js";
|
|
6
6
|
import type { ClassProps } from "../util/props.js";
|
|
7
7
|
import BUTTON_CSS from "./Button.module.css";
|
|
8
8
|
import { Clickable, type ClickableProps } from "./Clickable.js";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Styling variants for a `Button
|
|
11
|
+
* Styling variants for a `Button`: the block variants (space, padding, indent, width, typography), flex, and status, plus button-specific toggles.
|
|
12
12
|
*
|
|
13
13
|
* @see https://shelving.cc/ui/ButtonVariants
|
|
14
14
|
*/
|
|
15
|
-
export interface ButtonVariants extends FlexVariants, StatusVariants
|
|
16
|
-
/**
|
|
15
|
+
export interface ButtonVariants extends BlockVariants, FlexVariants, StatusVariants {
|
|
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). */
|
|
@@ -24,16 +35,16 @@ export interface ButtonVariants extends FlexVariants, StatusVariants, Typography
|
|
|
24
35
|
/**
|
|
25
36
|
* Get the full combined `className` string for a button from its styling variants.
|
|
26
37
|
*
|
|
27
|
-
* @param variants The button styling variants (
|
|
38
|
+
* @param variants The button styling variants (block, flex, status, plus button toggles).
|
|
28
39
|
* @returns A space-separated `className` string combining all the resolved variant classes.
|
|
29
40
|
* @see https://shelving.cc/ui/getButtonClass
|
|
30
41
|
*/
|
|
31
42
|
export function getButtonClass(variants: ButtonVariants): string {
|
|
32
43
|
return getClass(
|
|
33
|
-
|
|
44
|
+
getBlockClass(variants),
|
|
45
|
+
getModuleClass(BUTTON_CSS, "button", variants, variants.selected === false && "unselected"),
|
|
34
46
|
getFlexClass(variants),
|
|
35
47
|
getStatusClass(variants),
|
|
36
|
-
getTypographyClass(variants),
|
|
37
48
|
);
|
|
38
49
|
}
|
|
39
50
|
|
|
@@ -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
|
-
* -
|
|
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
|
package/ui/button/Clickable.d.ts
CHANGED
|
@@ -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
|
*
|
package/ui/button/Clickable.js
CHANGED
|
@@ -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) {
|
package/ui/button/Clickable.tsx
CHANGED
|
@@ -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
|
);
|
package/ui/input/Input.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { ReactElement } from "react";
|
|
2
|
-
import { type
|
|
2
|
+
import { type BlockVariants } from "../style/Block.js";
|
|
3
3
|
import type { ChildProps, ClassProps } from "../util/props.js";
|
|
4
4
|
/**
|
|
5
|
-
* Styling variants shared by every form input —
|
|
6
|
-
* -
|
|
7
|
-
* - Designed to grow: new cross-cutting input styling props
|
|
5
|
+
* Styling variants shared by every form input — the block variants: `space`, `padding`, `indent`, `width`, and typography.
|
|
6
|
+
* - Any input can be spaced and sized like a block (e.g. `<TextInput space="none">`, `<CheckboxInput width="fit">`).
|
|
7
|
+
* - Designed to grow: new cross-cutting input styling props should be added here so every input picks them up consistently.
|
|
8
8
|
*
|
|
9
9
|
* @see https://shelving.cc/ui/InputVariants
|
|
10
10
|
*/
|
|
11
|
-
export interface InputVariants extends
|
|
11
|
+
export interface InputVariants extends BlockVariants {
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
14
|
* Build the shared base `className` for a form input from its styling variants — the base input class plus any `InputVariants`.
|
package/ui/input/Input.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { LOADING } from "../misc/Loading.js";
|
|
3
|
+
import { getBlockClass } from "../style/Block.js";
|
|
3
4
|
import { getFlexClass } from "../style/Flex.js";
|
|
4
|
-
import { getWidthClass } from "../style/Width.js";
|
|
5
5
|
import { getClass, getModuleClass } from "../util/css.js";
|
|
6
6
|
import INPUT_CSS from "./Input.module.css";
|
|
7
7
|
/**
|
|
@@ -12,7 +12,7 @@ import INPUT_CSS from "./Input.module.css";
|
|
|
12
12
|
* @see https://shelving.cc/ui/getInputClass
|
|
13
13
|
*/
|
|
14
14
|
export function getInputClass(props) {
|
|
15
|
-
return getClass(getModuleClass(INPUT_CSS, "input")
|
|
15
|
+
return getClass(getBlockClass(props), getModuleClass(INPUT_CSS, "input"));
|
|
16
16
|
}
|
|
17
17
|
/** Input that is loading. */
|
|
18
18
|
export const LOADING_INPUT = _jsx("div", { className: getClass(getInputClass({}), getFlexClass({})), children: LOADING });
|
package/ui/input/Input.tsx
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import type { ReactElement } from "react";
|
|
2
2
|
import { LOADING } from "../misc/Loading.js";
|
|
3
|
+
import { type BlockVariants, getBlockClass } from "../style/Block.js";
|
|
3
4
|
import { getFlexClass } from "../style/Flex.js";
|
|
4
|
-
import { getWidthClass, type WidthVariants } from "../style/Width.js";
|
|
5
5
|
import { getClass, getModuleClass } from "../util/css.js";
|
|
6
6
|
import type { ChildProps, ClassProps } from "../util/props.js";
|
|
7
7
|
import INPUT_CSS from "./Input.module.css";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Styling variants shared by every form input —
|
|
11
|
-
* -
|
|
12
|
-
* - Designed to grow: new cross-cutting input styling props
|
|
10
|
+
* Styling variants shared by every form input — the block variants: `space`, `padding`, `indent`, `width`, and typography.
|
|
11
|
+
* - Any input can be spaced and sized like a block (e.g. `<TextInput space="none">`, `<CheckboxInput width="fit">`).
|
|
12
|
+
* - Designed to grow: new cross-cutting input styling props should be added here so every input picks them up consistently.
|
|
13
13
|
*
|
|
14
14
|
* @see https://shelving.cc/ui/InputVariants
|
|
15
15
|
*/
|
|
16
|
-
export interface InputVariants extends
|
|
16
|
+
export interface InputVariants extends BlockVariants {}
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Build the shared base `className` for a form input from its styling variants — the base input class plus any `InputVariants`.
|
|
@@ -23,7 +23,7 @@ export interface InputVariants extends WidthVariants {}
|
|
|
23
23
|
* @see https://shelving.cc/ui/getInputClass
|
|
24
24
|
*/
|
|
25
25
|
export function getInputClass(props: InputVariants): string {
|
|
26
|
-
return getClass(getModuleClass(INPUT_CSS, "input")
|
|
26
|
+
return getClass(getBlockClass(props), getModuleClass(INPUT_CSS, "input"));
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|