shelving 1.283.0 → 1.284.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.md +18 -0
- package/ui/button/Button.module.css +23 -5
- package/ui/util/css.d.ts +1 -1
- package/ui/util/css.js +2 -2
- package/ui/util/css.test.ts +29 -0
- package/ui/util/css.ts +2 -2
package/package.json
CHANGED
package/ui/button/Button.md
CHANGED
|
@@ -60,11 +60,14 @@ import { getButtonClass } from "shelving/ui";
|
|
|
60
60
|
|
|
61
61
|
`--button-padding` and `--button-small-padding` set the `padding` shorthand, so a single value pads both axes equally and a two-value override pads block and inline separately (e.g. `var(--space-small) var(--space-normal)`).
|
|
62
62
|
|
|
63
|
+
`--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.
|
|
64
|
+
|
|
63
65
|
| Variable | Styles | Default |
|
|
64
66
|
|---|---|---|
|
|
65
67
|
| `--button-background` | Surface fill | `var(--tint-90)` |
|
|
66
68
|
| `--button-hover-background` | Surface fill on hover / focus | `var(--tint-95)` |
|
|
67
69
|
| `--button-hover-border` | Border on hover / focus | `var(--button-stroke) solid var(--tint-90)` |
|
|
70
|
+
| `--button-hover-transform` | Transform on hover / focus | `none` |
|
|
68
71
|
| `--button-text` | Label colour | `var(--tint-50)` |
|
|
69
72
|
| `--button-border` | Border shorthand | `var(--button-stroke) solid var(--tint-80)` |
|
|
70
73
|
| `--button-stroke` | Border / outline thickness | `var(--stroke-normal)` (2px) |
|
|
@@ -78,6 +81,11 @@ import { getButtonClass } from "shelving/ui";
|
|
|
78
81
|
| `--button-weight` | Font weight | `var(--weight-strong)` (700) |
|
|
79
82
|
| `--button-size` | Font size | `var(--size-normal)` |
|
|
80
83
|
| `--button-leading` | Line height | `var(--leading)` |
|
|
84
|
+
| `--button-shadow` | Box shadow (never on `plain` / `outline`) | `none` |
|
|
85
|
+
| `--button-active-background` | Surface fill while pressed | `var(--button-hover-background)` |
|
|
86
|
+
| `--button-active-border` | Border while pressed | `var(--button-hover-border)` |
|
|
87
|
+
| `--button-active-shadow` | Box shadow while pressed | `var(--button-shadow)` |
|
|
88
|
+
| `--button-active-transform` | Transform while pressed | `var(--button-hover-transform)` |
|
|
81
89
|
| `--button-transition` | Transition | `all var(--duration-fast)` (150ms) |
|
|
82
90
|
| `--button-focus-border` | Focus outline | `var(--stroke-focus) solid var(--color-focus)` |
|
|
83
91
|
| `--button-disabled-opacity` | Opacity when disabled | `0.5` |
|
|
@@ -94,3 +102,13 @@ import { getButtonClass } from "shelving/ui";
|
|
|
94
102
|
--button-padding: var(--space-small) var(--space-normal);
|
|
95
103
|
}
|
|
96
104
|
```
|
|
105
|
+
|
|
106
|
+
```css
|
|
107
|
+
/* Theme: buttons are raised and press down flat — `plain` and `outline` press down too but never cast a shadow. */
|
|
108
|
+
:root {
|
|
109
|
+
--button-shadow: 0 0.25rem 0 var(--tint-80);
|
|
110
|
+
--button-active-transform: translateY(0.2rem);
|
|
111
|
+
--button-active-shadow: 0 0.05rem 0 var(--tint-80);
|
|
112
|
+
--button-transition: all var(--duration-fast) cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
113
|
+
}
|
|
114
|
+
```
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
/* Style. */
|
|
32
32
|
background: var(--button-background, var(--tint-90));
|
|
33
33
|
color: var(--button-text, var(--tint-50));
|
|
34
|
+
box-shadow: var(--button-shadow, none);
|
|
34
35
|
transition: var(--button-transition, all var(--duration-fast));
|
|
35
36
|
cursor: pointer;
|
|
36
37
|
outline: var(--button-focus-border, var(--stroke-focus) solid var(--color-focus));
|
|
@@ -55,6 +56,15 @@
|
|
|
55
56
|
&:focus:not(:focus-visible) {
|
|
56
57
|
border: var(--button-hover-border, var(--button-stroke, var(--stroke-normal)) solid var(--tint-90));
|
|
57
58
|
background: var(--button-hover-background, var(--tint-95));
|
|
59
|
+
transform: var(--button-hover-transform, none);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
&:enabled:active,
|
|
63
|
+
&:any-link:active {
|
|
64
|
+
border: var(--button-active-border, var(--button-hover-border, var(--button-stroke, var(--stroke-normal)) solid var(--tint-90)));
|
|
65
|
+
background: var(--button-active-background, var(--button-hover-background, var(--tint-95)));
|
|
66
|
+
transform: var(--button-active-transform, var(--button-hover-transform, none));
|
|
67
|
+
box-shadow: var(--button-active-shadow, var(--button-shadow, none));
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
/* Variants */
|
|
@@ -101,13 +111,21 @@
|
|
|
101
111
|
border-color: transparent;
|
|
102
112
|
}
|
|
103
113
|
|
|
104
|
-
&.plain
|
|
105
|
-
|
|
106
|
-
|
|
114
|
+
&.plain {
|
|
115
|
+
box-shadow: none;
|
|
116
|
+
|
|
117
|
+
&:not(:enabled:hover, :any-link:hover, :focus) {
|
|
118
|
+
border-color: transparent;
|
|
119
|
+
background: transparent;
|
|
120
|
+
}
|
|
107
121
|
}
|
|
108
122
|
|
|
109
|
-
&.outline
|
|
110
|
-
|
|
123
|
+
&.outline {
|
|
124
|
+
box-shadow: none;
|
|
125
|
+
|
|
126
|
+
&:not(:enabled:hover, :any-link:hover, :focus) {
|
|
127
|
+
background: transparent;
|
|
128
|
+
}
|
|
111
129
|
}
|
|
112
130
|
}
|
|
113
131
|
}
|
package/ui/util/css.d.ts
CHANGED
|
@@ -47,7 +47,7 @@ export declare function getClass(...classes: unknown[]): string;
|
|
|
47
47
|
* - This allows this situation to be handled gracefully and classes will be silently ignored in this environment.
|
|
48
48
|
*
|
|
49
49
|
* @param classes Class keys/values to merge.
|
|
50
|
-
* @returns The merged string classname, or `undefined` when `module` is a string (unprocessed CSS module).
|
|
50
|
+
* @returns The merged string classname, or `undefined` when `module` is a string (unprocessed CSS module) or no classes match.
|
|
51
51
|
* @see https://shelving.cc/ui/getModuleClass
|
|
52
52
|
*/
|
|
53
53
|
export declare function getModuleClass(module: CSSModule | string, ...classes: unknown[]): string | undefined;
|
package/ui/util/css.js
CHANGED
|
@@ -42,12 +42,12 @@ function* getClasses(classes) {
|
|
|
42
42
|
* - This allows this situation to be handled gracefully and classes will be silently ignored in this environment.
|
|
43
43
|
*
|
|
44
44
|
* @param classes Class keys/values to merge.
|
|
45
|
-
* @returns The merged string classname, or `undefined` when `module` is a string (unprocessed CSS module).
|
|
45
|
+
* @returns The merged string classname, or `undefined` when `module` is a string (unprocessed CSS module) or no classes match.
|
|
46
46
|
* @see https://shelving.cc/ui/getModuleClass
|
|
47
47
|
*/
|
|
48
48
|
export function getModuleClass(module, ...classes) {
|
|
49
49
|
if (isDictionary(module))
|
|
50
|
-
return Array.from(getModuleClasses(module, classes)).join(" ");
|
|
50
|
+
return Array.from(getModuleClasses(module, classes)).join(" ") || undefined;
|
|
51
51
|
}
|
|
52
52
|
/** Yield the items in a list of possible `className` strings that match a `CSSModule` dictionary. */
|
|
53
53
|
function* getModuleClasses(module, classes) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { getClass, getModuleClass } from "shelving/ui";
|
|
3
|
+
|
|
4
|
+
describe("getClass", () => {
|
|
5
|
+
test("joins strings, arrays, and true-valued variant keys", () => {
|
|
6
|
+
expect(getClass("a", ["b", "c"], { d: true, e: false })).toBe("a b c d");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("ignores `null` and `undefined`", () => {
|
|
10
|
+
expect(getClass("a", null, undefined, "b")).toBe("a b");
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("getModuleClass", () => {
|
|
15
|
+
test("maps class keys through the module dictionary", () => {
|
|
16
|
+
expect(getModuleClass({ track: "abc123" }, "track")).toBe("abc123");
|
|
17
|
+
expect(getModuleClass({ track: "abc123", spin: "def456" }, "track", "spin")).toBe("abc123 def456");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("returns `undefined` when the module is a string (unprocessed CSS module)", () => {
|
|
21
|
+
expect(getModuleClass("./Loading.module.css", "track")).toBeUndefined();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('returns `undefined` when no classes match, so no empty `class=""` attribute renders', () => {
|
|
25
|
+
// Some environments (e.g. `bun test` v1.4+) import a `.module.css` as an empty object.
|
|
26
|
+
expect(getModuleClass({}, "track")).toBeUndefined();
|
|
27
|
+
expect(getModuleClass({ track: "abc123" }, "missing")).toBeUndefined();
|
|
28
|
+
});
|
|
29
|
+
});
|
package/ui/util/css.ts
CHANGED
|
@@ -77,11 +77,11 @@ function* getClasses(classes: unknown): Iterable<string> {
|
|
|
77
77
|
* - This allows this situation to be handled gracefully and classes will be silently ignored in this environment.
|
|
78
78
|
*
|
|
79
79
|
* @param classes Class keys/values to merge.
|
|
80
|
-
* @returns The merged string classname, or `undefined` when `module` is a string (unprocessed CSS module).
|
|
80
|
+
* @returns The merged string classname, or `undefined` when `module` is a string (unprocessed CSS module) or no classes match.
|
|
81
81
|
* @see https://shelving.cc/ui/getModuleClass
|
|
82
82
|
*/
|
|
83
83
|
export function getModuleClass(module: CSSModule | string, ...classes: unknown[]): string | undefined {
|
|
84
|
-
if (isDictionary(module)) return Array.from(getModuleClasses(module, classes)).join(" ");
|
|
84
|
+
if (isDictionary(module)) return Array.from(getModuleClasses(module, classes)).join(" ") || undefined;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
/** Yield the items in a list of possible `className` strings that match a `CSSModule` dictionary. */
|