@charcuterie/ui 3.9.0 → 3.11.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/README.md +11 -0
- package/dist/Badge/Badge.d.ts +2 -37
- package/dist/Badge/Badge.d.ts.map +1 -1
- package/dist/Badge/Badge.js +15 -30
- package/dist/Badge/Badge.js.map +1 -1
- package/dist/Badge/useBadgeShape.d.ts +57 -0
- package/dist/Badge/useBadgeShape.d.ts.map +1 -0
- package/dist/Badge/useBadgeShape.js +35 -0
- package/dist/Badge/useBadgeShape.js.map +1 -0
- package/dist/BadgeButton/BadgeButton.d.ts +59 -0
- package/dist/BadgeButton/BadgeButton.d.ts.map +1 -0
- package/dist/BadgeButton/BadgeButton.js +64 -0
- package/dist/BadgeButton/BadgeButton.js.map +1 -0
- package/dist/Card/Card.d.ts +12 -1
- package/dist/Card/Card.d.ts.map +1 -1
- package/dist/Card/Card.js +14 -2
- package/dist/Card/Card.js.map +1 -1
- package/dist/Card/cardAccentEdge.d.ts +101 -0
- package/dist/Card/cardAccentEdge.d.ts.map +1 -0
- package/dist/Card/cardAccentEdge.js +92 -0
- package/dist/Card/cardAccentEdge.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/Badge/Badge.tsx +30 -113
- package/src/Badge/useBadgeShape.tsx +142 -0
- package/src/BadgeButton/BadgeButton.mdx +67 -0
- package/src/BadgeButton/BadgeButton.stories.tsx +199 -0
- package/src/BadgeButton/BadgeButton.test.tsx +164 -0
- package/src/BadgeButton/BadgeButton.tsx +129 -0
- package/src/Card/Card.mdx +21 -0
- package/src/Card/Card.stories.tsx +91 -0
- package/src/Card/Card.test.tsx +92 -1
- package/src/Card/Card.tsx +34 -0
- package/src/Card/cardAccentEdge.test.ts +105 -0
- package/src/Card/cardAccentEdge.ts +135 -0
- package/src/index.ts +4 -0
- package/src/sourceRules.test.ts +8 -1
package/src/Card/Card.test.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import { mountStory } from "../mountStory.testHelpers.ts"
|
|
|
6
6
|
import { expectAgentDrivable } from "../testing/index.ts"
|
|
7
7
|
import * as stories from "./Card.stories.tsx"
|
|
8
8
|
|
|
9
|
-
const { AllStates, Default, Interactive } =
|
|
9
|
+
const { AccentEdge, AllStates, Default, Interactive } =
|
|
10
10
|
composeStories(stories)
|
|
11
11
|
|
|
12
12
|
test("a heading turns the card into a named landmark", async () => {
|
|
@@ -54,3 +54,94 @@ test("region scoping disambiguates two identical buttons", async () => {
|
|
|
54
54
|
}),
|
|
55
55
|
).toHaveAttribute("aria-valuenow", "40")
|
|
56
56
|
})
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The bar overlays the entire card, links and buttons included, so
|
|
60
|
+
* `pointer-events: none` on it is the difference between a card you
|
|
61
|
+
* can use and one that swallows every click. Driven, not asserted
|
|
62
|
+
* off a class name — the class is what the fix looks like, and this
|
|
63
|
+
* is what it is for.
|
|
64
|
+
*/
|
|
65
|
+
test("the accent edge does not swallow a click", async () => {
|
|
66
|
+
const { canvas } = await mountStory(AccentEdge)
|
|
67
|
+
|
|
68
|
+
const open = expectAgentDrivable(canvas, {
|
|
69
|
+
name: "Open",
|
|
70
|
+
role: "button",
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
let isClicked = false
|
|
74
|
+
|
|
75
|
+
open.addEventListener("click", () => {
|
|
76
|
+
isClicked = true
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
open.click()
|
|
80
|
+
|
|
81
|
+
await expect(isClicked).toBe(true)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const readRadius = (card: Element, isBar: boolean) =>
|
|
85
|
+
globalThis.getComputedStyle(
|
|
86
|
+
card,
|
|
87
|
+
isBar ? "::before" : undefined,
|
|
88
|
+
).borderTopLeftRadius
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The radius is INHERITED rather than restated, which is what lets
|
|
92
|
+
* one treatment sit on `rounded-lg`, on `rounded-3xl`, and on
|
|
93
|
+
* whatever an app overrides it to. Computed styles, because a class
|
|
94
|
+
* name is the thing that can be right while the paint is wrong.
|
|
95
|
+
*/
|
|
96
|
+
test("the bar takes the card's own radius, whatever it is", async () => {
|
|
97
|
+
const { canvas } = await mountStory(AccentEdge)
|
|
98
|
+
|
|
99
|
+
const wider = expectAgentDrivable(canvas, {
|
|
100
|
+
name: "A wider radius",
|
|
101
|
+
role: "region",
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const stock = expectAgentDrivable(canvas, {
|
|
105
|
+
name: "Project 1",
|
|
106
|
+
role: "region",
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
await expect(readRadius(wider, true)).toBe(
|
|
110
|
+
readRadius(wider, false),
|
|
111
|
+
)
|
|
112
|
+
await expect(readRadius(stock, true)).toBe(
|
|
113
|
+
readRadius(stock, false),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
// And the two cards genuinely differ, or the assertions above
|
|
117
|
+
// would pass on a component that hardcoded one radius.
|
|
118
|
+
await expect(readRadius(wider, false)).not.toBe(
|
|
119
|
+
readRadius(stock, false),
|
|
120
|
+
)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* `Card` carries `shadow-low`. The accent edge is a second shadow,
|
|
125
|
+
* and the first version of this fix wrote it onto the card itself —
|
|
126
|
+
* where an unlayered rule outranks the utility and the elevation
|
|
127
|
+
* silently goes away.
|
|
128
|
+
*/
|
|
129
|
+
test("the accent edge leaves the card's elevation alone", async () => {
|
|
130
|
+
const { canvas } = await mountStory(AccentEdge)
|
|
131
|
+
|
|
132
|
+
const card = expectAgentDrivable(canvas, {
|
|
133
|
+
name: "Project 1",
|
|
134
|
+
role: "region",
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const cardShadow =
|
|
138
|
+
globalThis.getComputedStyle(card).boxShadow
|
|
139
|
+
const barShadow = globalThis.getComputedStyle(
|
|
140
|
+
card,
|
|
141
|
+
"::before",
|
|
142
|
+
).boxShadow
|
|
143
|
+
|
|
144
|
+
await expect(cardShadow).not.toBe("none")
|
|
145
|
+
await expect(cardShadow).not.toContain("inset")
|
|
146
|
+
await expect(barShadow).toContain("inset")
|
|
147
|
+
})
|
package/src/Card/Card.tsx
CHANGED
|
@@ -9,6 +9,11 @@ import type {
|
|
|
9
9
|
} from "react"
|
|
10
10
|
|
|
11
11
|
import { toClassName } from "../toClassName.ts"
|
|
12
|
+
import type { CardAccentEdge } from "./cardAccentEdge.ts"
|
|
13
|
+
import {
|
|
14
|
+
getAccentEdgeClassName,
|
|
15
|
+
getAccentEdgeStyle,
|
|
16
|
+
} from "./cardAccentEdge.ts"
|
|
12
17
|
|
|
13
18
|
export type CardSurface = Extract<
|
|
14
19
|
SurfaceRole,
|
|
@@ -18,6 +23,16 @@ export type CardSurface = Extract<
|
|
|
18
23
|
export type CardPadding = "lg" | "md" | "none" | "sm"
|
|
19
24
|
|
|
20
25
|
export type CardProps = ComponentPropsWithRef<"section"> & {
|
|
26
|
+
/**
|
|
27
|
+
* A coloured bar down the leading edge, following the card's own
|
|
28
|
+
* corners.
|
|
29
|
+
*
|
|
30
|
+
* `{ categorical: 3 }` for a colour a user picked; `{ color }`
|
|
31
|
+
* for one an app computed — a hue hashed from a repo's name, say,
|
|
32
|
+
* where ten indices are not enough. See `cardAccentEdge.ts` for
|
|
33
|
+
* why it is a pseudo-element and not a border.
|
|
34
|
+
*/
|
|
35
|
+
accentEdge?: CardAccentEdge
|
|
21
36
|
/** Buttons or a badge, laid out beside the heading. */
|
|
22
37
|
actions?: ReactNode
|
|
23
38
|
elevation?: ElevationStep
|
|
@@ -79,6 +94,7 @@ const PADDING_CLASS: Record<CardPadding, string> = {
|
|
|
79
94
|
* a layout fact rather than a density one.
|
|
80
95
|
*/
|
|
81
96
|
export const Card = ({
|
|
97
|
+
accentEdge,
|
|
82
98
|
actions,
|
|
83
99
|
children,
|
|
84
100
|
className,
|
|
@@ -87,11 +103,25 @@ export const Card = ({
|
|
|
87
103
|
heading,
|
|
88
104
|
headingLevel = 2,
|
|
89
105
|
padding = "md",
|
|
106
|
+
style,
|
|
90
107
|
surface = "raised",
|
|
91
108
|
...sectionProps
|
|
92
109
|
}: CardProps): ReactNode => {
|
|
93
110
|
const headingId = useUniqueId()
|
|
94
111
|
|
|
112
|
+
// The caller's own `style` wins. An accent colour is one custom
|
|
113
|
+
// property, and silently dropping whatever else the call site set
|
|
114
|
+
// would be a worse trade than letting it override this one.
|
|
115
|
+
const accentEdgeStyle =
|
|
116
|
+
accentEdge == null
|
|
117
|
+
? undefined
|
|
118
|
+
: getAccentEdgeStyle(accentEdge)
|
|
119
|
+
|
|
120
|
+
const mergedStyle =
|
|
121
|
+
accentEdgeStyle == null
|
|
122
|
+
? style
|
|
123
|
+
: { ...accentEdgeStyle, ...style }
|
|
124
|
+
|
|
95
125
|
const Heading = `h${headingLevel}` as const
|
|
96
126
|
|
|
97
127
|
return (
|
|
@@ -108,8 +138,12 @@ export const Card = ({
|
|
|
108
138
|
SURFACE_CLASS[surface],
|
|
109
139
|
ELEVATION_CLASS[elevation],
|
|
110
140
|
PADDING_CLASS[padding],
|
|
141
|
+
accentEdge == null
|
|
142
|
+
? undefined
|
|
143
|
+
: getAccentEdgeClassName(accentEdge),
|
|
111
144
|
className,
|
|
112
145
|
)}
|
|
146
|
+
style={mergedStyle}
|
|
113
147
|
>
|
|
114
148
|
{heading || actions ? (
|
|
115
149
|
<header className="flex flex-col gap-2 cq-sm:flex-row cq-sm:items-start cq-sm:justify-between">
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ACCENT_EDGE_BASE_CLASS,
|
|
5
|
+
ACCENT_EDGE_CATEGORICAL_CLASS,
|
|
6
|
+
ACCENT_EDGE_COLOR_CLASS,
|
|
7
|
+
ACCENT_EDGE_COLOR_PROPERTY,
|
|
8
|
+
getAccentEdgeClassName,
|
|
9
|
+
getAccentEdgeStyle,
|
|
10
|
+
} from "./cardAccentEdge.ts"
|
|
11
|
+
|
|
12
|
+
describe("getAccentEdgeClassName", () => {
|
|
13
|
+
test("reads a categorical index off the ten-wide family", () => {
|
|
14
|
+
const className = getAccentEdgeClassName({
|
|
15
|
+
categorical: 3,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
expect(className).toContain(ACCENT_EDGE_BASE_CLASS)
|
|
19
|
+
expect(className).toContain(
|
|
20
|
+
ACCENT_EDGE_CATEGORICAL_CLASS[3],
|
|
21
|
+
)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test("threads an app's own colour through one literal", () => {
|
|
25
|
+
const className = getAccentEdgeClassName({
|
|
26
|
+
color: "hsl(210 55% 52%)",
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
expect(className).toContain(ACCENT_EDGE_BASE_CLASS)
|
|
30
|
+
expect(className).toContain(ACCENT_EDGE_COLOR_CLASS)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The bar overlays the whole card, links and buttons included, so
|
|
35
|
+
* this is the difference between a card you can click and one you
|
|
36
|
+
* cannot.
|
|
37
|
+
*/
|
|
38
|
+
test("never eats a pointer", () => {
|
|
39
|
+
expect(
|
|
40
|
+
getAccentEdgeClassName({ categorical: 1 }),
|
|
41
|
+
).toContain("before:pointer-events-none")
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The card already has a radius; the bar must not restate it.
|
|
46
|
+
* `inherit` is what makes one component work on `rounded-lg`,
|
|
47
|
+
* `rounded-xl` and whatever an app overrides it to.
|
|
48
|
+
*/
|
|
49
|
+
test("takes the card's own radius rather than naming one", () => {
|
|
50
|
+
expect(
|
|
51
|
+
getAccentEdgeClassName({ categorical: 1 }),
|
|
52
|
+
).toContain("before:rounded-[inherit]")
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* `box-shadow` has no logical form. Without the `rtl:` twin the
|
|
57
|
+
* bar sits on the left of an Arabic page, which is the trailing
|
|
58
|
+
* edge there.
|
|
59
|
+
*/
|
|
60
|
+
test("carries the bar to the other side under rtl", () => {
|
|
61
|
+
for (const className of [
|
|
62
|
+
...Object.values(ACCENT_EDGE_CATEGORICAL_CLASS),
|
|
63
|
+
ACCENT_EDGE_COLOR_CLASS,
|
|
64
|
+
]) {
|
|
65
|
+
expect(className).toContain("rtl:before:shadow-")
|
|
66
|
+
expect(className).toContain("inset_-3px")
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Tailwind scans source text for COMPLETE class strings, so an
|
|
72
|
+
* interpolated index generates nothing, paints nothing and
|
|
73
|
+
* reports nothing. This is the cheap half of that guarantee;
|
|
74
|
+
* `tailwindCandidates.test.ts` is the half that compiles them.
|
|
75
|
+
*/
|
|
76
|
+
test("names every categorical index in full", () => {
|
|
77
|
+
for (const [index, className] of Object.entries(
|
|
78
|
+
ACCENT_EDGE_CATEGORICAL_CLASS,
|
|
79
|
+
)) {
|
|
80
|
+
expect(className).toContain(
|
|
81
|
+
`var(--color-categorical-${index}-solid)`,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe("getAccentEdgeStyle", () => {
|
|
88
|
+
test("writes one custom property for an app's own colour", () => {
|
|
89
|
+
expect(
|
|
90
|
+
getAccentEdgeStyle({ color: "hsl(210 55% 52%)" }),
|
|
91
|
+
).toStrictEqual({
|
|
92
|
+
[ACCENT_EDGE_COLOR_PROPERTY]: "hsl(210 55% 52%)",
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* `undefined`, not `{}`, so a categorical card renders no `style`
|
|
98
|
+
* attribute at all.
|
|
99
|
+
*/
|
|
100
|
+
test("writes nothing when the colour came from an index", () => {
|
|
101
|
+
expect(
|
|
102
|
+
getAccentEdgeStyle({ categorical: 7 }),
|
|
103
|
+
).toBeUndefined()
|
|
104
|
+
})
|
|
105
|
+
})
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A coloured bar down a card's leading edge — and the reason it is
|
|
3
|
+
* a pseudo-element rather than a border.
|
|
4
|
+
*
|
|
5
|
+
* Three apps had grown this shape independently and all three drew
|
|
6
|
+
* it as a straight border beside a rounded box, so the bar ran past
|
|
7
|
+
* the curve and stopped square while the card it belonged to was
|
|
8
|
+
* round. Folio put `border-inline-start` on a WRAPPER around the
|
|
9
|
+
* card; mail-sifter puts `borderInlineStartWidth: 4` on a
|
|
10
|
+
* `rounded-xl` anchor; spoolbuddy writes `rounded-lg border-l-3`.
|
|
11
|
+
* A border is painted on the border box, so none of them can follow
|
|
12
|
+
* a corner — the notch is not a bug in any one app, it is what that
|
|
13
|
+
* technique does.
|
|
14
|
+
*
|
|
15
|
+
* A pseudo-element overlays the card and takes
|
|
16
|
+
* `border-radius: inherit`, so the bar follows whatever radius the
|
|
17
|
+
* card already has and nobody states a radius twice.
|
|
18
|
+
*
|
|
19
|
+
* ⚠️ **The shadow goes on the pseudo-element, never on the card.**
|
|
20
|
+
* `Card` carries `shadow-low` from the elevation scale, and an app
|
|
21
|
+
* stylesheet is unlayered while Tailwind's utilities sit in
|
|
22
|
+
* `@layer utilities` — so a `box-shadow` written at the call site
|
|
23
|
+
* outranks the utility and takes the card's elevation away with it.
|
|
24
|
+
* That is not hypothetical; it is the version of this fix that was
|
|
25
|
+
* written first.
|
|
26
|
+
*
|
|
27
|
+
* ⚠️ **Not a 3px-wide pseudo-element with a rounded leading edge.**
|
|
28
|
+
* A `border-radius` of `0.5rem` on a 3px-wide box is clamped by the
|
|
29
|
+
* browser to fit, so it caps the bar instead of following the
|
|
30
|
+
* card's curve.
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ **`box-shadow` has no logical form**, so the offset is
|
|
33
|
+
* physical and the `rtl:` variant is what carries the bar to the
|
|
34
|
+
* other side. A single `inset 3px` would put it on the left of an
|
|
35
|
+
* Arabic page, which is the trailing edge there.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import type { CategoricalIndex } from "@charcuterie/tokens"
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Which colour the bar takes.
|
|
42
|
+
*
|
|
43
|
+
* Two arms, because the fleet has two answers and neither is wrong.
|
|
44
|
+
* Docket picks a **categorical index** — a colour a user chose for
|
|
45
|
+
* a project, from the ten-wide contrast-audited family. Folio and
|
|
46
|
+
* mail-sifter derive a **hue from a name hash**, so a repo added
|
|
47
|
+
* tomorrow already has a colour and nobody maintains a palette;
|
|
48
|
+
* that is 360 answers, not ten, and it cannot be an index.
|
|
49
|
+
*
|
|
50
|
+
* `intent` is deliberately absent. An intent is a claim the design
|
|
51
|
+
* system makes — `danger` says what happens if you press the thing
|
|
52
|
+
* — and no card in this fleet wants its edge to make one. When one
|
|
53
|
+
* does, that is a third arm and a decision record, not a guess
|
|
54
|
+
* made here.
|
|
55
|
+
*/
|
|
56
|
+
export type CardAccentEdge =
|
|
57
|
+
| { categorical: CategoricalIndex; color?: never }
|
|
58
|
+
| { categorical?: never; color: string }
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The custom property an explicit colour is threaded through.
|
|
62
|
+
*
|
|
63
|
+
* A property rather than an inline `box-shadow`, because the shadow
|
|
64
|
+
* string has to stay a COMPLETE literal in the source for Tailwind
|
|
65
|
+
* to generate it at all. One literal reading one variable covers
|
|
66
|
+
* every colour an app can compute; interpolating the colour into
|
|
67
|
+
* the class name would generate nothing, paint nothing, and report
|
|
68
|
+
* nothing.
|
|
69
|
+
*/
|
|
70
|
+
export const ACCENT_EDGE_COLOR_PROPERTY =
|
|
71
|
+
"--charcuterie-accent-edge"
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The overlay itself. Colour arrives separately.
|
|
75
|
+
*
|
|
76
|
+
* `pointer-events: none` is load-bearing rather than tidy: this box
|
|
77
|
+
* covers the whole card, including every link and button in it.
|
|
78
|
+
*/
|
|
79
|
+
export const ACCENT_EDGE_BASE_CLASS =
|
|
80
|
+
"relative before:pointer-events-none before:absolute before:inset-0 before:rounded-[inherit] before:content-['']"
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Ten indices, written out in full, for the reason
|
|
84
|
+
* `categoricalStyles.ts` opens with: Tailwind scans source text for
|
|
85
|
+
* COMPLETE class strings, so a template literal generates nothing
|
|
86
|
+
* at all and the bar renders invisible with no error and no failing
|
|
87
|
+
* test. `tailwindCandidates.test.ts` compiles every literal in this
|
|
88
|
+
* package through the real Tailwind, which is the something that
|
|
89
|
+
* checks.
|
|
90
|
+
*/
|
|
91
|
+
export const ACCENT_EDGE_CATEGORICAL_CLASS: Record<
|
|
92
|
+
CategoricalIndex,
|
|
93
|
+
string
|
|
94
|
+
> = {
|
|
95
|
+
1: "before:shadow-[inset_3px_0_0_var(--color-categorical-1-solid)] rtl:before:shadow-[inset_-3px_0_0_var(--color-categorical-1-solid)]",
|
|
96
|
+
2: "before:shadow-[inset_3px_0_0_var(--color-categorical-2-solid)] rtl:before:shadow-[inset_-3px_0_0_var(--color-categorical-2-solid)]",
|
|
97
|
+
3: "before:shadow-[inset_3px_0_0_var(--color-categorical-3-solid)] rtl:before:shadow-[inset_-3px_0_0_var(--color-categorical-3-solid)]",
|
|
98
|
+
4: "before:shadow-[inset_3px_0_0_var(--color-categorical-4-solid)] rtl:before:shadow-[inset_-3px_0_0_var(--color-categorical-4-solid)]",
|
|
99
|
+
5: "before:shadow-[inset_3px_0_0_var(--color-categorical-5-solid)] rtl:before:shadow-[inset_-3px_0_0_var(--color-categorical-5-solid)]",
|
|
100
|
+
6: "before:shadow-[inset_3px_0_0_var(--color-categorical-6-solid)] rtl:before:shadow-[inset_-3px_0_0_var(--color-categorical-6-solid)]",
|
|
101
|
+
7: "before:shadow-[inset_3px_0_0_var(--color-categorical-7-solid)] rtl:before:shadow-[inset_-3px_0_0_var(--color-categorical-7-solid)]",
|
|
102
|
+
8: "before:shadow-[inset_3px_0_0_var(--color-categorical-8-solid)] rtl:before:shadow-[inset_-3px_0_0_var(--color-categorical-8-solid)]",
|
|
103
|
+
9: "before:shadow-[inset_3px_0_0_var(--color-categorical-9-solid)] rtl:before:shadow-[inset_-3px_0_0_var(--color-categorical-9-solid)]",
|
|
104
|
+
10: "before:shadow-[inset_3px_0_0_var(--color-categorical-10-solid)] rtl:before:shadow-[inset_-3px_0_0_var(--color-categorical-10-solid)]",
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The same bar, reading the caller's own colour. */
|
|
108
|
+
export const ACCENT_EDGE_COLOR_CLASS =
|
|
109
|
+
"before:shadow-[inset_3px_0_0_var(--charcuterie-accent-edge)] rtl:before:shadow-[inset_-3px_0_0_var(--charcuterie-accent-edge)]"
|
|
110
|
+
|
|
111
|
+
export const getAccentEdgeClassName = (
|
|
112
|
+
accentEdge: CardAccentEdge,
|
|
113
|
+
): string =>
|
|
114
|
+
`${ACCENT_EDGE_BASE_CLASS} ${
|
|
115
|
+
accentEdge.categorical == null
|
|
116
|
+
? ACCENT_EDGE_COLOR_CLASS
|
|
117
|
+
: ACCENT_EDGE_CATEGORICAL_CLASS[
|
|
118
|
+
accentEdge.categorical
|
|
119
|
+
]
|
|
120
|
+
}`
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The inline style an explicit colour needs, and nothing when the
|
|
124
|
+
* colour came from an index.
|
|
125
|
+
*
|
|
126
|
+
* Returns `undefined` rather than `{}` so a card with a categorical
|
|
127
|
+
* edge and no `style` of its own renders no `style` attribute at
|
|
128
|
+
* all.
|
|
129
|
+
*/
|
|
130
|
+
export const getAccentEdgeStyle = (
|
|
131
|
+
accentEdge: CardAccentEdge,
|
|
132
|
+
): Record<string, string> | undefined =>
|
|
133
|
+
accentEdge.color == null
|
|
134
|
+
? undefined
|
|
135
|
+
: { [ACCENT_EDGE_COLOR_PROPERTY]: accentEdge.color }
|
package/src/index.ts
CHANGED
|
@@ -65,6 +65,9 @@ export type {
|
|
|
65
65
|
export { Alert } from "./Alert/Alert.tsx"
|
|
66
66
|
export type { BadgeProps } from "./Badge/Badge.tsx"
|
|
67
67
|
export { Badge } from "./Badge/Badge.tsx"
|
|
68
|
+
export type { BadgeShapeProps } from "./Badge/useBadgeShape.tsx"
|
|
69
|
+
export type { BadgeButtonProps } from "./BadgeButton/BadgeButton.tsx"
|
|
70
|
+
export { BadgeButton } from "./BadgeButton/BadgeButton.tsx"
|
|
68
71
|
export type {
|
|
69
72
|
BoardMove,
|
|
70
73
|
BoardProps,
|
|
@@ -95,6 +98,7 @@ export type {
|
|
|
95
98
|
CardSurface,
|
|
96
99
|
} from "./Card/Card.tsx"
|
|
97
100
|
export { Card } from "./Card/Card.tsx"
|
|
101
|
+
export type { CardAccentEdge } from "./Card/cardAccentEdge.ts"
|
|
98
102
|
export type { CheckboxProps } from "./Checkbox/Checkbox.tsx"
|
|
99
103
|
export { Checkbox } from "./Checkbox/Checkbox.tsx"
|
|
100
104
|
export type { ColorSchemeSwitcherProps } from "./ColorSchemeSwitcher/ColorSchemeSwitcher.tsx"
|
package/src/sourceRules.test.ts
CHANGED
|
@@ -452,7 +452,14 @@ test("the barrel is the only place components are re-exported", async () => {
|
|
|
452
452
|
// count by the `<Name>/<Name>.tsx` rule; it is not exported from the
|
|
453
453
|
// barrel either, because a consumer sets `min`/`max`/`step` and never
|
|
454
454
|
// does the arithmetic itself.
|
|
455
|
-
|
|
455
|
+
//
|
|
456
|
+
// `BadgeButton` — a `Badge` you can press — is +1 -> 53. It is to
|
|
457
|
+
// `Badge` what `ButtonLink` is to `Button`: a sibling element
|
|
458
|
+
// sharing one paint, here through `useBadgeShape`, which is a
|
|
459
|
+
// member of `Badge/` and so stays out of this count and out of the
|
|
460
|
+
// barrel as a component. Its PROPS type is exported, because a
|
|
461
|
+
// consumer writing a wrapper needs to name the visual half.
|
|
462
|
+
expect(componentNames.length).toBe(53)
|
|
456
463
|
|
|
457
464
|
for (const name of componentNames) {
|
|
458
465
|
expect(barrel).toContain(`export { ${name} }`)
|