@cerebruminc/cerebellum 20.3.1 → 20.4.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/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/components/Icons/IconComponentStyles.module.css +45 -0
- package/src/components/Icons/IconComponentStyles.tsx +34 -51
- package/src/components/Icons/IconList.module.css +38 -0
- package/src/components/Icons/IconList.tsx +8 -9
- package/src/components/Icons/Icons.test.tsx +222 -0
- package/src/components/Icons/types.tsx +5 -2
- package/src/index.ts +1 -0
- package/src/mantine/components/DataTable/DataTable.module.css +114 -4
- package/src/mantine/components/DataTable/DataTable.test.tsx +144 -2
- package/src/mantine/components/DataTable/DataTable.tsx +190 -17
- package/src/mantine/components/DataTable/index.ts +1 -0
- package/src/mantine/mantineTheme.test.tsx +75 -0
- package/src/mantine/mantineTheme.ts +9 -0
- package/src/mantine/stories/DataTable.stories.tsx +96 -0
- package/src/mantine/stories/DataTableStory.test.tsx +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# react-component-lib-boilerplate
|
|
2
2
|
|
|
3
|
+
## [20.4.0](https://github.com/cerebruminc/cerebellum/compare/v20.3.1...v20.4.0) (2026-09-04)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **datatable:** style columns from columnDef.meta ([e466fdf](https://github.com/cerebruminc/cerebellum/commit/e466fdf3cdef67a434541f8a2451b26ffae20a22))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **icons:** drop the styled-components dependency from icons ([394fa8e](https://github.com/cerebruminc/cerebellum/commit/394fa8e72c1d7fdce6933f907536b02ba0a416dc))
|
|
14
|
+
|
|
3
15
|
## [20.3.1](https://github.com/cerebruminc/cerebellum/compare/v20.3.0...v20.3.1) (2026-09-03)
|
|
4
16
|
|
|
5
17
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/Icons/IconComponentStyles.module.css
|
|
3
|
+
* @summary The three boxes an icon renders into, plus the svg itself.
|
|
4
|
+
* @remarks Per-instance values (box size, box colour, padding, scale) arrive as inline
|
|
5
|
+
* custom properties rather than generated classes, so the sheet stays static. Each one
|
|
6
|
+
* carries a fallback, so a primitive rendered without it behaves as it did before.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/* Unboxed icons: a full-bleed positioning context for the absolutely placed svg. */
|
|
10
|
+
.boringSpan {
|
|
11
|
+
display: block;
|
|
12
|
+
height: 100%;
|
|
13
|
+
position: relative;
|
|
14
|
+
width: 100%;
|
|
15
|
+
}
|
|
16
|
+
.boringSpan svg {
|
|
17
|
+
position: absolute;
|
|
18
|
+
height: 100%;
|
|
19
|
+
width: 100%;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Boxed and round icons get a fixed outer square instead. */
|
|
23
|
+
.iconBox {
|
|
24
|
+
display: block;
|
|
25
|
+
height: var(--cb-icon-box-size, auto);
|
|
26
|
+
width: var(--cb-icon-box-size, auto);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.innerIconBox {
|
|
30
|
+
display: block;
|
|
31
|
+
background-color: var(--cb-icon-box-color, transparent);
|
|
32
|
+
border-radius: 6px;
|
|
33
|
+
height: 100%;
|
|
34
|
+
padding: var(--cb-icon-box-padding, 28.4%);
|
|
35
|
+
width: 100%;
|
|
36
|
+
}
|
|
37
|
+
.innerIconBox[data-round] {
|
|
38
|
+
border-radius: 50%;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.svg {
|
|
42
|
+
height: 100%;
|
|
43
|
+
width: 100%;
|
|
44
|
+
transform: scale(var(--cb-icon-scale, 1));
|
|
45
|
+
}
|
|
@@ -1,56 +1,39 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CSSProperties, FC, PropsWithChildren, SVGProps } from "react";
|
|
2
2
|
|
|
3
|
+
import styles from "./IconComponentStyles.module.css";
|
|
3
4
|
import { IconBoxProps, InnerIconBoxProps, SvgProps } from "./types";
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
margin: 30px 20px;
|
|
8
|
-
width: 150px;
|
|
9
|
-
`;
|
|
5
|
+
// The $-prefixed prop names are a leftover of the styled-components transient-prop
|
|
6
|
+
// convention. They are kept so the 180-odd icon files that pass them need no change.
|
|
7
|
+
export const BoringSpan: FC<PropsWithChildren> = ({ children }) => <span className={styles.boringSpan}>{children}</span>;
|
|
10
8
|
|
|
11
|
-
export const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
export const IconBox: FC<PropsWithChildren<IconBoxProps>> = ({ $boxedSize, children }) => (
|
|
10
|
+
<span
|
|
11
|
+
className={styles.iconBox}
|
|
12
|
+
style={{ "--cb-icon-box-size": $boxedSize === undefined ? undefined : `${$boxedSize}px` } as CSSProperties}
|
|
13
|
+
>
|
|
14
|
+
{children}
|
|
15
|
+
</span>
|
|
16
|
+
);
|
|
17
17
|
|
|
18
|
-
export const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
export const InnerIconBox: FC<PropsWithChildren<InnerIconBoxProps>> = ({ $boxColor, $paddingString, $round, children }) => (
|
|
19
|
+
<span
|
|
20
|
+
className={styles.innerIconBox}
|
|
21
|
+
data-round={$round || undefined}
|
|
22
|
+
style={{ "--cb-icon-box-color": $boxColor, "--cb-icon-box-padding": $paddingString } as CSSProperties}
|
|
23
|
+
>
|
|
24
|
+
{children}
|
|
25
|
+
</span>
|
|
26
|
+
);
|
|
23
27
|
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
export const IconBox = styled.span<IconBoxProps>`
|
|
38
|
-
display: block;
|
|
39
|
-
height: ${({ $boxedSize }) => $boxedSize}px;
|
|
40
|
-
width: ${({ $boxedSize }) => $boxedSize}px;
|
|
41
|
-
`;
|
|
42
|
-
|
|
43
|
-
export const InnerIconBox = styled.span<InnerIconBoxProps>`
|
|
44
|
-
display: block;
|
|
45
|
-
background-color: ${({ $boxColor }) => $boxColor};
|
|
46
|
-
border-radius: ${({ $round }) => ($round ? "50%" : "6px")};
|
|
47
|
-
height: 100%;
|
|
48
|
-
padding: ${({ $paddingString }) => $paddingString || "28.4%"};
|
|
49
|
-
width: 100%;
|
|
50
|
-
`;
|
|
51
|
-
|
|
52
|
-
export const Svg = styled.svg<SvgProps>`
|
|
53
|
-
height: 100%;
|
|
54
|
-
width: 100%;
|
|
55
|
-
transform: ${(props) => `scale(${props.scale})`};
|
|
56
|
-
`;
|
|
28
|
+
// scale is pulled out of the spread so it never lands on the svg as an attribute, where
|
|
29
|
+
// it is not valid; the class reads it back as a custom property instead. className and
|
|
30
|
+
// style are pulled out for the same reason: rest is spread first so nothing can knock
|
|
31
|
+
// them off by accident, and a caller that supplies either one is then taken at its word
|
|
32
|
+
// — className replaces the default styling, and an icon's own transform (BackArrow's
|
|
33
|
+
// scaleX(-1)) outranks the class rule from its inline position.
|
|
34
|
+
export const Svg: FC<SVGProps<SVGSVGElement> & SvgProps> = ({ scale = 1, className, style, children, ...rest }) => (
|
|
35
|
+
// biome-ignore lint/a11y/noSvgWithoutTitle: each icon supplies its own title, or aria-hidden when decorative
|
|
36
|
+
<svg {...rest} className={className ?? styles.svg} style={{ ...style, "--cb-icon-scale": scale } as CSSProperties}>
|
|
37
|
+
{children}
|
|
38
|
+
</svg>
|
|
39
|
+
);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/Icons/IconList.module.css
|
|
3
|
+
* @summary A responsive grid of named icon tiles.
|
|
4
|
+
* @remarks The grid replicates the default rendering of the shared GridContainer,
|
|
5
|
+
* which IconList used before the icons moved off styled-components.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
.grid {
|
|
9
|
+
display: grid;
|
|
10
|
+
grid-template-columns: repeat(4, 1fr);
|
|
11
|
+
gap: 20px;
|
|
12
|
+
padding: 20px;
|
|
13
|
+
background-color: white;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@media all and (max-width: 1000px) {
|
|
17
|
+
.grid {
|
|
18
|
+
grid-template-columns: repeat(2, 1fr);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.iconContainer {
|
|
23
|
+
display: inline-block;
|
|
24
|
+
margin: 30px 20px;
|
|
25
|
+
width: 150px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.iconItem {
|
|
29
|
+
height: 67px;
|
|
30
|
+
margin: 10px auto;
|
|
31
|
+
width: 67px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.name {
|
|
35
|
+
font-size: 16px;
|
|
36
|
+
margin-top: 20px;
|
|
37
|
+
text-align: center;
|
|
38
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { FC } from "react";
|
|
2
|
-
import { GridContainer } from "./../../SharedStorybook/GridStyles";
|
|
3
2
|
import { colors } from "./../../const/colors";
|
|
4
|
-
import
|
|
3
|
+
import styles from "./IconList.module.css";
|
|
5
4
|
import { IconListProps } from "./types";
|
|
6
5
|
|
|
7
6
|
import * as iconList from ".";
|
|
@@ -25,15 +24,15 @@ export const IconList: FC<IconListProps> = (props: IconListProps) => {
|
|
|
25
24
|
let lastIndex = 0;
|
|
26
25
|
|
|
27
26
|
return (
|
|
28
|
-
<
|
|
27
|
+
<div className={styles.grid}>
|
|
29
28
|
{iconNames.map((name) => {
|
|
30
29
|
const colorIndex = getRandomInt(colorLib.length, lastIndex);
|
|
31
30
|
lastIndex = colorIndex;
|
|
32
31
|
// @ts-ignore
|
|
33
32
|
const IconComponent = iconList[name];
|
|
34
33
|
return (
|
|
35
|
-
<
|
|
36
|
-
<
|
|
34
|
+
<div className={styles.iconContainer} key={name}>
|
|
35
|
+
<div className={styles.iconItem}>
|
|
37
36
|
<IconComponent
|
|
38
37
|
fill={boxed || round ? colorLib[colorIndex].icon : "#444"}
|
|
39
38
|
boxed={boxed}
|
|
@@ -42,11 +41,11 @@ export const IconList: FC<IconListProps> = (props: IconListProps) => {
|
|
|
42
41
|
round={round}
|
|
43
42
|
title={`${name}: use a descriptive title, or don't include one if the icon is decorative`}
|
|
44
43
|
/>
|
|
45
|
-
</
|
|
46
|
-
<
|
|
47
|
-
</
|
|
44
|
+
</div>
|
|
45
|
+
<p className={styles.name}>{name}</p>
|
|
46
|
+
</div>
|
|
48
47
|
);
|
|
49
48
|
})}
|
|
50
|
-
</
|
|
49
|
+
</div>
|
|
51
50
|
);
|
|
52
51
|
};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { render } from "@testing-library/react";
|
|
4
|
+
|
|
5
|
+
import { Accessibility, BackArrow, IconProps, IconType } from ".";
|
|
6
|
+
import * as iconList from ".";
|
|
7
|
+
import { Cerebrum } from "../../assets/icons/Cerebrum";
|
|
8
|
+
import { Cognition } from "../../assets/icons/Cognition";
|
|
9
|
+
import { Cortex } from "../../assets/icons/Cortex";
|
|
10
|
+
import { Neuron } from "../../assets/icons/Neuron";
|
|
11
|
+
import { colors } from "../../const/colors";
|
|
12
|
+
import ResizeIcon from "../Textarea/ResizeIcon";
|
|
13
|
+
|
|
14
|
+
const boxes = (container: HTMLElement) => {
|
|
15
|
+
const outer = container.firstElementChild as HTMLElement;
|
|
16
|
+
const inner = outer.firstElementChild as HTMLElement;
|
|
17
|
+
const svg = inner.firstElementChild as SVGSVGElement;
|
|
18
|
+
return { outer, inner, svg };
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe("icon primitives", () => {
|
|
22
|
+
it("renders an unboxed icon as span > span > svg", () => {
|
|
23
|
+
const { outer, inner, svg } = boxes(render(<Accessibility />).container);
|
|
24
|
+
|
|
25
|
+
expect(outer.tagName).toBe("SPAN");
|
|
26
|
+
expect(outer).toHaveClass("boringSpan");
|
|
27
|
+
expect(inner.tagName).toBe("SPAN");
|
|
28
|
+
expect(inner).toHaveClass("boringSpan");
|
|
29
|
+
expect(svg.tagName).toBe("svg");
|
|
30
|
+
expect(svg).toHaveClass("svg");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("sizes and colours a boxed icon through custom properties", () => {
|
|
34
|
+
const { outer, inner } = boxes(render(<Accessibility boxed boxedSize={40} boxColor="#abcdef" />).container);
|
|
35
|
+
|
|
36
|
+
expect(outer).toHaveClass("iconBox");
|
|
37
|
+
expect(outer.style.getPropertyValue("--cb-icon-box-size")).toBe("40px");
|
|
38
|
+
expect(inner).toHaveClass("innerIconBox");
|
|
39
|
+
expect(inner.style.getPropertyValue("--cb-icon-box-color")).toBe("#abcdef");
|
|
40
|
+
expect(inner.style.getPropertyValue("--cb-icon-box-padding")).toBe("25.4%");
|
|
41
|
+
expect(inner).not.toHaveAttribute("data-round");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("falls back to the default box size when none is given", () => {
|
|
45
|
+
const { outer } = boxes(render(<Accessibility boxed />).container);
|
|
46
|
+
|
|
47
|
+
expect(outer.style.getPropertyValue("--cb-icon-box-size")).toBe("67px");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("marks a round icon with data-round and its wider padding", () => {
|
|
51
|
+
const { inner } = boxes(render(<Accessibility round />).container);
|
|
52
|
+
|
|
53
|
+
expect(inner).toHaveAttribute("data-round");
|
|
54
|
+
expect(inner.style.getPropertyValue("--cb-icon-box-padding")).toBe("35%");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("derives the box colour from fill when boxColor is omitted", () => {
|
|
58
|
+
const { inner } = boxes(render(<Accessibility boxed fill={colors.BLUE_100} />).container);
|
|
59
|
+
|
|
60
|
+
expect(inner.style.getPropertyValue("--cb-icon-box-color")).toBe(colors.BLUE_5);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("falls back to black for a fill boxColorLookup has no entry for", () => {
|
|
64
|
+
const { inner } = boxes(render(<Accessibility boxed fill="var(--cb-table-sort-indicator)" />).container);
|
|
65
|
+
|
|
66
|
+
expect(inner.style.getPropertyValue("--cb-icon-box-color")).toBe(colors.BLACK);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("passes scale to the svg as a custom property rather than an attribute", () => {
|
|
70
|
+
const { svg } = boxes(render(<Accessibility scale={2} />).container);
|
|
71
|
+
|
|
72
|
+
expect(svg.style.getPropertyValue("--cb-icon-scale")).toBe("2");
|
|
73
|
+
// styled-components used to forward scale onto the element, where it is not a
|
|
74
|
+
// valid SVG attribute and did nothing.
|
|
75
|
+
expect(svg).not.toHaveAttribute("scale");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("keeps an icon's own transform ahead of the scale custom property", () => {
|
|
79
|
+
const { svg } = boxes(render(<BackArrow scale={3} />).container);
|
|
80
|
+
|
|
81
|
+
expect(svg.style.transform).toBe("scaleX(-1)");
|
|
82
|
+
expect(svg.style.getPropertyValue("--cb-icon-scale")).toBe("3");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("leaks none of the transient-style props onto the DOM", () => {
|
|
86
|
+
const rendered = [
|
|
87
|
+
boxes(render(<Accessibility />).container),
|
|
88
|
+
boxes(render(<Accessibility boxed />).container),
|
|
89
|
+
boxes(render(<Accessibility round />).container),
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
for (const { outer, inner, svg } of rendered) {
|
|
93
|
+
for (const el of [outer, inner, svg]) {
|
|
94
|
+
for (const name of el.getAttributeNames()) {
|
|
95
|
+
expect(name).not.toContain("$");
|
|
96
|
+
expect(["boxedsize", "boxcolor", "paddingstring", "round", "scale"]).not.toContain(name);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("threads fill onto the drawn shapes", () => {
|
|
103
|
+
const { container } = render(<Accessibility fill="var(--cb-test-colour)" />);
|
|
104
|
+
|
|
105
|
+
const paths = Array.from(container.querySelectorAll("path"));
|
|
106
|
+
expect(paths.length).toBeGreaterThan(0);
|
|
107
|
+
for (const path of paths) {
|
|
108
|
+
expect(path).toHaveAttribute("fill", "var(--cb-test-colour)");
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("labels a titled icon and hides an untitled one", () => {
|
|
113
|
+
const { svg: titled, ...rest } = boxes(render(<Accessibility title="Accessibility" />).container);
|
|
114
|
+
const titleId = titled.getAttribute("aria-labelledby");
|
|
115
|
+
|
|
116
|
+
expect(titleId).toBeTruthy();
|
|
117
|
+
expect(rest.outer.querySelector(`title#${titleId}`)).toHaveTextContent("Accessibility");
|
|
118
|
+
expect(titled).not.toHaveAttribute("aria-hidden");
|
|
119
|
+
|
|
120
|
+
const { svg: untitled } = boxes(render(<Accessibility />).container);
|
|
121
|
+
expect(untitled).toHaveAttribute("aria-hidden", "true");
|
|
122
|
+
expect(untitled.querySelector("title")).toBeNull();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("every exported icon", () => {
|
|
127
|
+
const names = Object.keys(iconList);
|
|
128
|
+
const modes: Array<[string, IconProps]> = [
|
|
129
|
+
["plain", {}],
|
|
130
|
+
["boxed", { boxed: true }],
|
|
131
|
+
["round", { round: true }],
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
it.each(names.flatMap((name) => modes.map(([mode, props]): [string, string, IconProps] => [name, mode, props])))(
|
|
135
|
+
"%s renders an svg when %s",
|
|
136
|
+
(name, _mode, props) => {
|
|
137
|
+
const Icon = (iconList as Record<string, IconType>)[name];
|
|
138
|
+
|
|
139
|
+
const { container } = render(<Icon {...props} title={name} />);
|
|
140
|
+
expect(container.querySelector("svg")).toBeInTheDocument();
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// These live outside src/components/Icons but build on the same four primitives, so the
|
|
146
|
+
// barrel-driven sweep above never reaches them.
|
|
147
|
+
describe("the icons built on the primitives from elsewhere", () => {
|
|
148
|
+
it.each([
|
|
149
|
+
["Cerebrum", Cerebrum],
|
|
150
|
+
["Cortex", Cortex],
|
|
151
|
+
["Neuron", Neuron],
|
|
152
|
+
])("%s renders an svg", (name, Icon) => {
|
|
153
|
+
const { container } = render(<Icon boxed title={name} />);
|
|
154
|
+
expect(container.querySelector("svg")).toBeInTheDocument();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("Cognition renders an image rather than an svg", () => {
|
|
158
|
+
const { inner } = boxes(render(<Cognition boxed />).container);
|
|
159
|
+
|
|
160
|
+
expect(inner).toHaveClass("innerIconBox");
|
|
161
|
+
expect(inner.firstElementChild?.tagName).toBe("IMG");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("ResizeIcon leaves the box colour to the stylesheet fallback", () => {
|
|
165
|
+
const { inner } = boxes(render(<ResizeIcon boxed />).container);
|
|
166
|
+
|
|
167
|
+
expect(inner).toHaveClass("innerIconBox");
|
|
168
|
+
expect(inner.style.getPropertyValue("--cb-icon-box-color")).toBe("");
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("the icon stylesheets", () => {
|
|
173
|
+
const dir = __dirname;
|
|
174
|
+
const modules = ["IconComponentStyles.module.css", "IconList.module.css"];
|
|
175
|
+
const read = (file: string) => readFileSync(join(dir, file), "utf8");
|
|
176
|
+
const withoutComments = (css: string) => css.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
177
|
+
|
|
178
|
+
it.each(modules)("%s avoids native nesting", (file) => {
|
|
179
|
+
// Consumers compile these modules with their own pipeline, so nesting is unsafe.
|
|
180
|
+
// Nesting needs no "&", so the real check is a block inside a block — with at-rule
|
|
181
|
+
// blocks such as @media stripped first, since those legitimately wrap rules.
|
|
182
|
+
const css = withoutComments(read(file)).replace(/@[\w-]+[^{]*\{/g, "");
|
|
183
|
+
|
|
184
|
+
expect(css).not.toContain("&");
|
|
185
|
+
expect(css).not.toMatch(/\{[^{}]*\{/);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it.each(["IconComponentStyles", "IconList"])("%s only uses classes its stylesheet declares", (name) => {
|
|
189
|
+
const declared = new Set(Array.from(withoutComments(read(`${name}.module.css`)).matchAll(/\.([a-zA-Z][\w-]*)/g), (m) => m[1]));
|
|
190
|
+
const used = Array.from(readFileSync(join(dir, `${name}.tsx`), "utf8").matchAll(/\bstyles\.([\w$]+)/g), (m) => m[1]);
|
|
191
|
+
|
|
192
|
+
// Jest maps CSS to identity-obj-proxy, so a class missing from the stylesheet still
|
|
193
|
+
// renders its own name and every toHaveClass above would pass regardless.
|
|
194
|
+
expect(used.length).toBeGreaterThan(0);
|
|
195
|
+
for (const className of used) {
|
|
196
|
+
expect([...declared]).toContain(className);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("positions the svg absolutely only for unboxed icons", () => {
|
|
201
|
+
const css = withoutComments(read("IconComponentStyles.module.css"));
|
|
202
|
+
const boringSpanSvg = css.match(/\.boringSpan svg \{([^}]*)\}/);
|
|
203
|
+
|
|
204
|
+
expect(boringSpanSvg?.[1]).toContain("position: absolute");
|
|
205
|
+
expect(css.match(/position: absolute/g)).toHaveLength(1);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("keeps the icon directory free of styled-components", () => {
|
|
209
|
+
const sources = (function walk(from: string): string[] {
|
|
210
|
+
return readdirSync(from, { withFileTypes: true }).flatMap((entry) => {
|
|
211
|
+
const path = join(from, entry.name);
|
|
212
|
+
if (entry.isDirectory()) return walk(path);
|
|
213
|
+
return (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts")) && entry.name !== "Icons.test.tsx" ? [path] : [];
|
|
214
|
+
});
|
|
215
|
+
})(dir);
|
|
216
|
+
|
|
217
|
+
expect(sources.length).toBeGreaterThan(180);
|
|
218
|
+
for (const path of sources) {
|
|
219
|
+
expect(readFileSync(path, "utf8")).not.toMatch(/["']styled-components/);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -18,11 +18,14 @@ export interface IconListProps {
|
|
|
18
18
|
round?: boolean;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// Every prop is optional: BoringSpan stands in for both boxes on an unboxed icon, and
|
|
22
|
+
// ResizeIcon renders InnerIconBox without a box colour. The CSS carries a fallback for
|
|
23
|
+
// each one.
|
|
21
24
|
export interface IconBoxProps {
|
|
22
|
-
$boxedSize
|
|
25
|
+
$boxedSize?: number;
|
|
23
26
|
}
|
|
24
27
|
export interface InnerIconBoxProps {
|
|
25
|
-
$boxColor
|
|
28
|
+
$boxColor?: string;
|
|
26
29
|
$paddingString?: string;
|
|
27
30
|
$round?: boolean;
|
|
28
31
|
}
|
package/src/index.ts
CHANGED
|
@@ -167,7 +167,13 @@
|
|
|
167
167
|
gap: 6px;
|
|
168
168
|
letter-spacing: inherit;
|
|
169
169
|
padding: 0;
|
|
170
|
-
|
|
170
|
+
/*
|
|
171
|
+
* Only here to undo the UA's `text-align: center` on a button, which shows up
|
|
172
|
+
* when a header label wraps to a second line. `inherit` does that just as well
|
|
173
|
+
* as `left` did -- the th resolves to left unless it is aligned -- and picks up
|
|
174
|
+
* a `meta.align` for free instead of needing a fourth rule to override it.
|
|
175
|
+
*/
|
|
176
|
+
text-align: inherit;
|
|
171
177
|
}
|
|
172
178
|
|
|
173
179
|
.sortControl.sortControl:focus-visible {
|
|
@@ -206,16 +212,120 @@
|
|
|
206
212
|
/*
|
|
207
213
|
* A `meta.fixedWidth` column is the track, so the cell keeps none of the 16px
|
|
208
214
|
* inset the auto-width columns carry -- the old ColumnType.fixedWidth columns were
|
|
209
|
-
* exact grid tracks. The cell still sets its width inline (see
|
|
215
|
+
* exact grid tracks. The cell still sets its width inline (see cellProps),
|
|
210
216
|
* because a class cannot carry a per-column number.
|
|
211
217
|
*
|
|
212
218
|
* The compact rules above reach (0,5,0) and so does this, which is a tie -- broken
|
|
213
219
|
* by source order, which is decided here rather than by the consumer's bundler,
|
|
214
|
-
* since both rules live in this file. That is why this sits below them.
|
|
220
|
+
* since both rules live in this file. That is why this sits below them. Naming only
|
|
221
|
+
* the two sides is also what leaves the compact density's 8px top and bottom in
|
|
222
|
+
* place, rather than having to restate them.
|
|
215
223
|
*/
|
|
216
224
|
.table.table .td.td[data-fixed-width],
|
|
217
225
|
.table.table .th.th[data-fixed-width] {
|
|
218
|
-
padding: 0;
|
|
226
|
+
padding-left: 0;
|
|
227
|
+
padding-right: 0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/*
|
|
231
|
+
* A column's `meta.align`. The two halves are deliberately asymmetric, per the
|
|
232
|
+
* doubling rule at the top of this file: Mantine declares `text-align: left` on
|
|
233
|
+
* the th (and `right` under [dir="rtl"]) at (0,1,0), so that half is doubled to
|
|
234
|
+
* beat it; nothing declares it on the td, so that half is left undoubled.
|
|
235
|
+
*
|
|
236
|
+
* Note the attribute already carries a point of specificity, so even the
|
|
237
|
+
* undoubled half lands at (0,2,0) rather than a class's (0,1,0) -- a consumer
|
|
238
|
+
* overriding it needs at least that much, or the inline `style` the file header
|
|
239
|
+
* points them at. Undoubled still buys them the cheaper target of the two.
|
|
240
|
+
*
|
|
241
|
+
* Nothing above declares text-align on a cell, so unlike the fixed-width reset
|
|
242
|
+
* these do not depend on where in the file they sit.
|
|
243
|
+
*
|
|
244
|
+
* Physical left/right, not the logical properties: there is no RTL anywhere in
|
|
245
|
+
* this package, and the rest of the module is physical too.
|
|
246
|
+
*/
|
|
247
|
+
.td[data-align="left"],
|
|
248
|
+
.th.th[data-align="left"] {
|
|
249
|
+
text-align: left;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.td[data-align="center"],
|
|
253
|
+
.th.th[data-align="center"] {
|
|
254
|
+
text-align: center;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.td[data-align="right"],
|
|
258
|
+
.th.th[data-align="right"] {
|
|
259
|
+
text-align: right;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/*
|
|
263
|
+
* The sortable header needs its own pair, and text-align cannot stand in for it.
|
|
264
|
+
* .sortControl is a <button>, which sizes to fit-content under `width: auto` even
|
|
265
|
+
* with `display: flex` -- so it hugs its label at the cell's start edge, where
|
|
266
|
+
* text-align has no inline content left to move and justify-content has no slack
|
|
267
|
+
* to distribute. Giving it the cell's width supplies that slack.
|
|
268
|
+
*
|
|
269
|
+
* Gated on the aligned values rather than declared on the base: a full-width
|
|
270
|
+
* button would stretch every existing sortable header's focus ring from "hugs the
|
|
271
|
+
* label" to "spans the cell". Aligned headers accept that; unaligned ones should
|
|
272
|
+
* not have to.
|
|
273
|
+
*/
|
|
274
|
+
.th.th[data-align="center"] .sortControl {
|
|
275
|
+
justify-content: center;
|
|
276
|
+
width: 100%;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.th.th[data-align="right"] .sortControl {
|
|
280
|
+
justify-content: flex-end;
|
|
281
|
+
width: 100%;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/*
|
|
285
|
+
* A column's `meta.textStyle` -- the legacy BodyS* set, which was all 15px on a
|
|
286
|
+
* 22.5px line (theme.text.lineHeightSmall) and differed only in weight and colour.
|
|
287
|
+
*
|
|
288
|
+
* Single-class throughout: Mantine declares none of these on a td, and the value
|
|
289
|
+
* a cell would otherwise use is inherited from the table, which loses to any
|
|
290
|
+
* declared value whatever its specificity. So there is nothing here to outrank.
|
|
291
|
+
* The attribute still puts these at (0,2,0), above a bare class -- a consumer
|
|
292
|
+
* overriding them needs equal specificity or an inline `style`.
|
|
293
|
+
*
|
|
294
|
+
* Only `secondary` needs a custom property: COOL_GREY_100 and COOL_GREY_65 are
|
|
295
|
+
* gray[9] and gray[8], and both are already black in the high-contrast tuple, so
|
|
296
|
+
* they need no theme branch. COOL_GREY_75 has no shade -- see mantineTheme.ts.
|
|
297
|
+
*/
|
|
298
|
+
.td[data-text-style] {
|
|
299
|
+
font-size: 15px;
|
|
300
|
+
/*
|
|
301
|
+
* Declared, not inherited: every BodyS* carried its own weight, so `primary`,
|
|
302
|
+
* `secondary` and `tertiary` reset a bolder cell rather than picking it up.
|
|
303
|
+
* `emphasis` below ties this at (0,2,0) and wins on source order, which is this
|
|
304
|
+
* file's to decide since both rules live in it.
|
|
305
|
+
*/
|
|
306
|
+
font-weight: 400;
|
|
307
|
+
line-height: 22.5px;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.td[data-text-style="primary"] {
|
|
311
|
+
color: var(--mantine-color-gray-9);
|
|
312
|
+
letter-spacing: 0.36px;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.td[data-text-style="emphasis"] {
|
|
316
|
+
color: var(--mantine-color-gray-9);
|
|
317
|
+
font-weight: 700;
|
|
318
|
+
letter-spacing: 0.39px;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.td[data-text-style="secondary"] {
|
|
322
|
+
color: var(--cb-table-cell-secondary);
|
|
323
|
+
letter-spacing: 0.39px;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.td[data-text-style="tertiary"] {
|
|
327
|
+
color: var(--mantine-color-gray-8);
|
|
328
|
+
letter-spacing: 0.39px;
|
|
219
329
|
}
|
|
220
330
|
|
|
221
331
|
/* rowBorderRadius: 4. Only the outer cells round, so the row reads as one shape. */
|