@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/README.md
CHANGED
|
@@ -48,6 +48,16 @@ Moving a card takes **no drag-and-drop dependency**: one handle per card, a `Men
|
|
|
48
48
|
other lanes as the primary path, Pointer-Event dragging at 1.4 KB gzip beside it
|
|
49
49
|
([decision](../../docs/decisions/2026-08-19-the-board-owns-the-move-and-takes-no-drag-and-drop-dependency.md)).
|
|
50
50
|
|
|
51
|
+
**`BadgeButton` is a `Badge` you can press**, and it came from a consumer rather than from a
|
|
52
|
+
plan: QueuePilot has six pill-shaped controls — per-entry setting tags, an Edit chip, two
|
|
53
|
+
start-point chips, a group chip, a pool's Exclude chip — that were hand-rolled
|
|
54
|
+
`<button className="badge …">` because `Badge` is a `<span>`. A sibling component, exactly
|
|
55
|
+
as `ButtonLink` is to `Button`: one paint through `useBadgeShape`, compared as **computed**
|
|
56
|
+
styles in the test, and a real `<button>` for focus, Enter, Space, `:disabled` and a role a
|
|
57
|
+
screen reader announces. `onClick` is required, so a pill nobody can press is a `Badge` by
|
|
58
|
+
construction
|
|
59
|
+
([decision](../../docs/decisions/2026-08-22-a-pressable-badge-is-a-sibling-component-not-a-prop.md)).
|
|
60
|
+
|
|
51
61
|
**The unified app shell adds four**: `Shell`, `Header`, `Rail`, `Main` — the fleet's largest
|
|
52
62
|
single duplication, where ten of twelve UI repos hand-roll the page chrome and three of them
|
|
53
63
|
have a file named `AppShell.tsx` (two of those headers are a *byte-identical* class string,
|
|
@@ -136,6 +146,7 @@ an hour of confusion.
|
|
|
136
146
|
| Type | `text-sm`/`text-md`/`text-lg`, which are **ours**: `theme.css` bridges `--text-*` onto the density-scaled `--font-size-*` |
|
|
137
147
|
| Motion | `styles.css`, at `--duration-loop-*`, switched off under `prefers-reduced-motion` |
|
|
138
148
|
| Scrollbar | `styles.css` — add `charcuterie-scrollbar` to any scrolling element for a token-tinted bar (rounded thumb on Chromium/Safari; thin standard match on Firefox) that flips with `[data-scheme]` |
|
|
149
|
+
| Accent edge | `Card/cardAccentEdge.ts` — `<Card accentEdge={{ categorical }} \| {{ color }}>` draws a bar down the leading edge from a pseudo-element that **inherits the card's radius**. A border cannot follow a corner, which is why three apps had a square bar on a round card |
|
|
139
150
|
| State | `@charcuterie/logic` — `useUniqueId` for label wiring, `useStatus` for `MediaTile`'s three image states |
|
|
140
151
|
|
|
141
152
|
**Class names are never interpolated.** `` `bg-intent-${intent}-solid` `` generates nothing
|
package/dist/Badge/Badge.d.ts
CHANGED
|
@@ -1,41 +1,6 @@
|
|
|
1
|
-
import type { CategoricalIndex, IntentName } from "@charcuterie/tokens";
|
|
2
1
|
import type { ComponentPropsWithRef, ReactNode } from "react";
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
export type BadgeProps = ComponentPropsWithRef<"span"> & {
|
|
6
|
-
appearance?: Exclude<IntentAppearance, "ghost">;
|
|
7
|
-
/**
|
|
8
|
-
* A numbered, **non-semantic** colour: 1–10, no meaning attached.
|
|
9
|
-
*
|
|
10
|
-
* For a badge whose colour the user picked — a Docket label, a
|
|
11
|
-
* project, a chart series — where `intent` would be a lie. A
|
|
12
|
-
* "Homelab" label is not a `danger`, and the palette is curated
|
|
13
|
-
* and contrast-gated in both schemes precisely so the user cannot
|
|
14
|
-
* pick one that is unreadable. `getCategoricalIndex(key)` from
|
|
15
|
-
* `@charcuterie/tokens` is the stable fallback for a row nobody
|
|
16
|
-
* has chosen a colour for yet.
|
|
17
|
-
*
|
|
18
|
-
* Mutually exclusive with `intent` **in the type**: a badge is one
|
|
19
|
-
* colour, and passing both is a question with no answer rather
|
|
20
|
-
* than a precedence rule to remember.
|
|
21
|
-
*/
|
|
22
|
-
categorical?: CategoricalIndex;
|
|
23
|
-
children: ReactNode;
|
|
24
|
-
icon?: ReactNode;
|
|
25
|
-
intent?: IntentName;
|
|
26
|
-
/**
|
|
27
|
-
* What happens when the label is wider than the space it is given.
|
|
28
|
-
*
|
|
29
|
-
* - `truncate` — one line, capped at the container, ellipsis. The
|
|
30
|
-
* default, because a status pill that changes its row's height
|
|
31
|
-
* when a message gets longer breaks the table it sits in.
|
|
32
|
-
* - `wrap` — the pill grows taller and shows everything. For the
|
|
33
|
-
* kiosk and any touch context, where the hover readout that
|
|
34
|
-
* `truncate` relies on does not exist.
|
|
35
|
-
*/
|
|
36
|
-
overflow?: "truncate" | "wrap";
|
|
37
|
-
size?: BadgeSize;
|
|
38
|
-
} & ({
|
|
2
|
+
import type { BadgeShapeProps } from "./useBadgeShape.tsx";
|
|
3
|
+
export type BadgeProps = ComponentPropsWithRef<"span"> & BadgeShapeProps & {} & ({
|
|
39
4
|
categorical?: never;
|
|
40
5
|
} | {
|
|
41
6
|
intent?: never;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Badge.d.ts","sourceRoot":"","sources":["../../src/Badge/Badge.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,
|
|
1
|
+
{"version":3,"file":"Badge.d.ts","sourceRoot":"","sources":["../../src/Badge/Badge.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,qBAAqB,EACrB,SAAS,EACV,MAAM,OAAO,CAAA;AAEd,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAG1D,MAAM,MAAM,UAAU,GAAG,qBAAqB,CAAC,MAAM,CAAC,GACpD,eAAe,GAAG,EAQjB,GAAG,CAAC;IAAE,WAAW,CAAC,EAAE,KAAK,CAAA;CAAE,GAAG;IAAE,MAAM,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC,CAAA;AAEpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,eAAO,MAAM,KAAK,GAAI,qGAWnB,UAAU,KAAG,SAsBf,CAAA"}
|
package/dist/Badge/Badge.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
2
|
-
import {
|
|
3
|
-
import { BADGE_SIZE_CLASS } from "../controlStyles.js";
|
|
4
|
-
import { INTENT_APPEARANCE_CLASS } from "../intentStyles.js";
|
|
5
|
-
import { toClassName } from "../toClassName.js";
|
|
6
|
-
import { useIsTextClipped } from "./useIsTextClipped.js";
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useBadgeShape } from "./useBadgeShape.js";
|
|
7
3
|
/**
|
|
8
4
|
* Four repos, and rip-deck declares the **identical** `TONE_CLASS`
|
|
9
5
|
* map twice — `VerdictBadge.tsx` and `TowerAlerts.tsx`, same five
|
|
@@ -44,29 +40,18 @@ import { useIsTextClipped } from "./useIsTextClipped.js";
|
|
|
44
40
|
* across the next cell. `max-inline-size: 100%` is what stops it, and
|
|
45
41
|
* `overflow` is what happens next.
|
|
46
42
|
*/
|
|
47
|
-
export const Badge = ({ appearance
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// where both arrive.
|
|
61
|
-
categorical === undefined
|
|
62
|
-
? INTENT_APPEARANCE_CLASS[intent][appearance]
|
|
63
|
-
: CATEGORICAL_APPEARANCE_CLASS[categorical][appearance], className),
|
|
64
|
-
// Only when something is actually hidden. A tooltip on every
|
|
65
|
-
// short pill in a bay list is noise, and a `title` that
|
|
66
|
-
// duplicates fully-visible text is a screen-reader duplicate
|
|
67
|
-
// for no gain.
|
|
68
|
-
title: title ?? clippedText, children: [icon ? (_jsx("span", { "aria-hidden": "true", className: "contents shrink-0", children: icon })) : null, _jsx("span", { className: overflow === "wrap"
|
|
69
|
-
? "min-w-0"
|
|
70
|
-
: "min-w-0 overflow-hidden text-ellipsis whitespace-nowrap", ref: labelRef, children: children })] }));
|
|
43
|
+
export const Badge = ({ appearance, categorical, children, className, icon, intent, overflow, size, title, ...spanProps }) => {
|
|
44
|
+
const shape = useBadgeShape({
|
|
45
|
+
appearance,
|
|
46
|
+
categorical,
|
|
47
|
+
children,
|
|
48
|
+
className,
|
|
49
|
+
icon,
|
|
50
|
+
intent,
|
|
51
|
+
overflow,
|
|
52
|
+
size,
|
|
53
|
+
title,
|
|
54
|
+
});
|
|
55
|
+
return (_jsx("span", { ...spanProps, className: shape.className, title: shape.title, children: shape.content }));
|
|
71
56
|
};
|
|
72
57
|
//# sourceMappingURL=Badge.js.map
|
package/dist/Badge/Badge.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Badge.js","sourceRoot":"","sources":["../../src/Badge/Badge.tsx"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"Badge.js","sourceRoot":"","sources":["../../src/Badge/Badge.tsx"],"names":[],"mappings":";AAMA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAqB,CAAA;AAanD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,EACpB,UAAU,EACV,WAAW,EACX,QAAQ,EACR,SAAS,EACT,IAAI,EACJ,MAAM,EACN,QAAQ,EACR,IAAI,EACJ,KAAK,EACL,GAAG,SAAS,EACD,EAAa,EAAE;IAC1B,MAAM,KAAK,GAAG,aAAa,CAAC;QAC1B,UAAU;QACV,WAAW;QACX,QAAQ;QACR,SAAS;QACT,IAAI;QACJ,MAAM;QACN,QAAQ;QACR,IAAI;QACJ,KAAK;KACN,CAAC,CAAA;IAEF,OAAO,CACL,kBACM,SAAS,EACb,SAAS,EAAE,KAAK,CAAC,SAAS,EAC1B,KAAK,EAAE,KAAK,CAAC,KAAK,YAEjB,KAAK,CAAC,OAAO,GACT,CACR,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { CategoricalIndex, IntentName } from "@charcuterie/tokens";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import type { BadgeSize } from "../controlStyles.ts";
|
|
4
|
+
import type { IntentAppearance } from "../intentStyles.ts";
|
|
5
|
+
/**
|
|
6
|
+
* The visual half of a badge, shared by the two elements that wear
|
|
7
|
+
* it.
|
|
8
|
+
*
|
|
9
|
+
* `Badge` is a `<span>` and `BadgeButton` is a `<button>`, and the
|
|
10
|
+
* pill is supposed to be indistinguishable between them — so the
|
|
11
|
+
* class list, the icon slot, the truncation and the
|
|
12
|
+
* only-when-clipped `title` are computed **here**, once, exactly as
|
|
13
|
+
* `Button` and `ButtonLink` both call `getControlClassName`. A
|
|
14
|
+
* second copy of this arithmetic is how the two drift: the wrapping
|
|
15
|
+
* pill's `rounded-2xl` and the label's `min-w-0` are each one line,
|
|
16
|
+
* and each is load-bearing.
|
|
17
|
+
*/
|
|
18
|
+
export type BadgeShapeProps = {
|
|
19
|
+
appearance?: Exclude<IntentAppearance, "ghost">;
|
|
20
|
+
/**
|
|
21
|
+
* A numbered, **non-semantic** colour: 1–10, no meaning attached.
|
|
22
|
+
*
|
|
23
|
+
* For a badge whose colour the user picked — a Docket label, a
|
|
24
|
+
* project, a chart series — where `intent` would be a lie.
|
|
25
|
+
* `getCategoricalIndex(key)` from `@charcuterie/tokens` is the
|
|
26
|
+
* stable fallback for a row nobody has chosen a colour for yet.
|
|
27
|
+
*
|
|
28
|
+
* Mutually exclusive with `intent` **in the type**: a badge is one
|
|
29
|
+
* colour, and passing both is a question with no answer rather
|
|
30
|
+
* than a precedence rule to remember.
|
|
31
|
+
*/
|
|
32
|
+
categorical?: CategoricalIndex;
|
|
33
|
+
children: ReactNode;
|
|
34
|
+
icon?: ReactNode;
|
|
35
|
+
intent?: IntentName;
|
|
36
|
+
/**
|
|
37
|
+
* What happens when the label is wider than the space it is given.
|
|
38
|
+
*
|
|
39
|
+
* - `truncate` — one line, capped at the container, ellipsis. The
|
|
40
|
+
* default, because a status pill that changes its row's height
|
|
41
|
+
* when a message gets longer breaks the table it sits in.
|
|
42
|
+
* - `wrap` — the pill grows taller and shows everything. For the
|
|
43
|
+
* kiosk and any touch context, where the hover readout that
|
|
44
|
+
* `truncate` relies on does not exist.
|
|
45
|
+
*/
|
|
46
|
+
overflow?: "truncate" | "wrap";
|
|
47
|
+
size?: BadgeSize;
|
|
48
|
+
};
|
|
49
|
+
export declare const useBadgeShape: ({ appearance, categorical, children, className, icon, intent, overflow, size, title, }: BadgeShapeProps & {
|
|
50
|
+
className?: string;
|
|
51
|
+
title?: string;
|
|
52
|
+
}) => {
|
|
53
|
+
className: string;
|
|
54
|
+
content: ReactNode;
|
|
55
|
+
title?: string;
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=useBadgeShape.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useBadgeShape.d.ts","sourceRoot":"","sources":["../../src/Badge/useBadgeShape.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,UAAU,EACX,MAAM,qBAAqB,CAAA;AAC5B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAEpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAK1D;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,UAAU,CAAC,EAAE,OAAO,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAA;IAC/C;;;;;;;;;;;OAWG;IACH,WAAW,CAAC,EAAE,gBAAgB,CAAA;IAC9B,QAAQ,EAAE,SAAS,CAAA;IACnB,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,UAAU,GAAG,MAAM,CAAA;IAC9B,IAAI,CAAC,EAAE,SAAS,CAAA;CACjB,CAAA;AAED,eAAO,MAAM,aAAa,GAAI,wFAU3B,eAAe,GAAG;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,KAAG;IACF,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,SAAS,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;CAkEf,CAAA"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CATEGORICAL_APPEARANCE_CLASS } from "../categoricalStyles.js";
|
|
3
|
+
import { BADGE_SIZE_CLASS } from "../controlStyles.js";
|
|
4
|
+
import { INTENT_APPEARANCE_CLASS } from "../intentStyles.js";
|
|
5
|
+
import { toClassName } from "../toClassName.js";
|
|
6
|
+
import { useIsTextClipped } from "./useIsTextClipped.js";
|
|
7
|
+
export const useBadgeShape = ({ appearance = "soft", categorical, children, className, icon, intent = "neutral", overflow = "truncate", size = "md", title, }) => {
|
|
8
|
+
const [labelRef, clippedText] = useIsTextClipped();
|
|
9
|
+
return {
|
|
10
|
+
className: toClassName("inline-flex shrink-0 items-center border font-medium",
|
|
11
|
+
// The cap. Without it every other rule here is decoration.
|
|
12
|
+
"max-w-full", overflow === "wrap"
|
|
13
|
+
? // A stadium end-cap on a three-line box reads as a
|
|
14
|
+
// rendering fault rather than a badge, so the wrapping
|
|
15
|
+
// pill relaxes to a rounded rectangle.
|
|
16
|
+
"rounded-2xl"
|
|
17
|
+
: "rounded-full", BADGE_SIZE_CLASS[size],
|
|
18
|
+
// A ternary rather than a merged map: the two are indexed
|
|
19
|
+
// by different keys, and `undefined` is the only honest
|
|
20
|
+
// discriminant once the type has already refused the case
|
|
21
|
+
// where both arrive.
|
|
22
|
+
categorical === undefined
|
|
23
|
+
? INTENT_APPEARANCE_CLASS[intent][appearance]
|
|
24
|
+
: CATEGORICAL_APPEARANCE_CLASS[categorical][appearance], className),
|
|
25
|
+
content: (_jsxs(_Fragment, { children: [icon ? (_jsx("span", { "aria-hidden": "true", className: "contents shrink-0", children: icon })) : null, _jsx("span", { className: overflow === "wrap"
|
|
26
|
+
? "min-w-0"
|
|
27
|
+
: "min-w-0 overflow-hidden text-ellipsis whitespace-nowrap", ref: labelRef, children: children })] })),
|
|
28
|
+
// Only when something is actually hidden. A tooltip on every
|
|
29
|
+
// short pill in a bay list is noise, and a `title` that
|
|
30
|
+
// duplicates fully-visible text is a screen-reader duplicate
|
|
31
|
+
// for no gain.
|
|
32
|
+
title: title ?? clippedText,
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=useBadgeShape.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useBadgeShape.js","sourceRoot":"","sources":["../../src/Badge/useBadgeShape.tsx"],"names":[],"mappings":";AAMA,OAAO,EAAE,4BAA4B,EAAE,MAAM,yBAAyB,CAAA;AAEtE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAEtD,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAA;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AA+CxD,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,EAC5B,UAAU,GAAG,MAAM,EACnB,WAAW,EACX,QAAQ,EACR,SAAS,EACT,IAAI,EACJ,MAAM,GAAG,SAAS,EAClB,QAAQ,GAAG,UAAU,EACrB,IAAI,GAAG,IAAI,EACX,KAAK,GAIN,EAIC,EAAE;IACF,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,gBAAgB,EAAE,CAAA;IAElD,OAAO;QACL,SAAS,EAAE,WAAW,CACpB,sDAAsD;QACtD,2DAA2D;QAC3D,YAAY,EACZ,QAAQ,KAAK,MAAM;YACjB,CAAC,CAAC,mDAAmD;gBACnD,uDAAuD;gBACvD,uCAAuC;gBACvC,aAAa;YACf,CAAC,CAAC,cAAc,EAClB,gBAAgB,CAAC,IAAI,CAAC;QACtB,0DAA0D;QAC1D,wDAAwD;QACxD,0DAA0D;QAC1D,qBAAqB;QACrB,WAAW,KAAK,SAAS;YACvB,CAAC,CAAC,uBAAuB,CAAC,MAAM,CAAC,CAAC,UAAU,CAAC;YAC7C,CAAC,CAAC,4BAA4B,CAAC,WAAW,CAAC,CACvC,UAAU,CACX,EACL,SAAS,CACV;QACD,OAAO,EAAE,CACP,8BACG,IAAI,CAAC,CAAC,CAAC,CACN,8BACc,MAAM,EAClB,SAAS,EAAC,mBAAmB,YAE5B,IAAI,GACA,CACR,CAAC,CAAC,CAAC,IAAI,EAYR,eACE,SAAS,EACP,QAAQ,KAAK,MAAM;wBACjB,CAAC,CAAC,SAAS;wBACX,CAAC,CAAC,yDAAyD,EAE/D,GAAG,EAAE,QAAQ,YAEZ,QAAQ,GACJ,IACN,CACJ;QACD,6DAA6D;QAC7D,wDAAwD;QACxD,6DAA6D;QAC7D,eAAe;QACf,KAAK,EAAE,KAAK,IAAI,WAAW;KAC5B,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ComponentPropsWithRef, ReactNode } from "react";
|
|
2
|
+
import type { BadgeShapeProps } from "../Badge/useBadgeShape.tsx";
|
|
3
|
+
export type BadgeButtonProps = Omit<ComponentPropsWithRef<"button">, "disabled"> & BadgeShapeProps & {
|
|
4
|
+
/**
|
|
5
|
+
* Required, and that is the component's argument in one line: a
|
|
6
|
+
* pill nobody can press is a `Badge`, and a pill that looks
|
|
7
|
+
* pressable and is not is the defect this exists to stop.
|
|
8
|
+
*/
|
|
9
|
+
onClick: NonNullable<ComponentPropsWithRef<"button">["onClick"]>;
|
|
10
|
+
/**
|
|
11
|
+
* `isDisabled`, not `disabled` — the same prop `Button` keeps
|
|
12
|
+
* instead of the native one, so a control's disabled state is
|
|
13
|
+
* spelled one way across the library.
|
|
14
|
+
*/
|
|
15
|
+
isDisabled?: boolean;
|
|
16
|
+
} & ({
|
|
17
|
+
categorical?: never;
|
|
18
|
+
} | {
|
|
19
|
+
intent?: never;
|
|
20
|
+
});
|
|
21
|
+
/**
|
|
22
|
+
* A badge you can press.
|
|
23
|
+
*
|
|
24
|
+
* A sibling component rather than a prop, exactly as `ButtonLink` is
|
|
25
|
+
* to `Button`: the paint is shared (both call `useBadgeShape`, so
|
|
26
|
+
* there is one class list and `BadgeButton.test.tsx` compares the
|
|
27
|
+
* two elements' *computed* styles rather than trusting that), and
|
|
28
|
+
* what differs is the element and everything the platform hangs off
|
|
29
|
+
* it.
|
|
30
|
+
*
|
|
31
|
+
* **Why not `asChild`, and why not `Badge` growing an `onClick`.**
|
|
32
|
+
* This library has no polymorphism pattern and has not needed one;
|
|
33
|
+
* adding it here would mean every future component answering "can it
|
|
34
|
+
* be something else?" instead of "what is it?". And an element type
|
|
35
|
+
* that changes as a *side effect* of passing a handler is a footgun
|
|
36
|
+
* with no error: forget the handler and a control silently ships as
|
|
37
|
+
* a `<span>` — not focusable, not in the tab order, invisible to
|
|
38
|
+
* `getByRole("button")` and to every agent driving the app.
|
|
39
|
+
*
|
|
40
|
+
* **What the element buys, and none of it is paint.** Focus, the tab
|
|
41
|
+
* order, Enter and Space, `:disabled`, form participation, and a
|
|
42
|
+
* `button` role a screen reader announces as pressable. QueuePilot
|
|
43
|
+
* had six pills doing all of this with a hand-rolled
|
|
44
|
+
* `<button className="badge …">`, which is where this came from —
|
|
45
|
+
* six chips that open an editor, change a start point, or drop a
|
|
46
|
+
* title from a pool.
|
|
47
|
+
*
|
|
48
|
+
* **`type="button"` by default.** These chips sit inside forms —
|
|
49
|
+
* QueuePilot's entry sheet is one — and the platform's default of
|
|
50
|
+
* `submit` would make a "change the start point" chip save the whole
|
|
51
|
+
* dialog. Overridable, because a badge-shaped submit is a real thing
|
|
52
|
+
* and the default is a safe floor rather than a ban.
|
|
53
|
+
*
|
|
54
|
+
* **`ghost` is still excluded**, inherited from `Badge`: a pill that
|
|
55
|
+
* paints nothing until hovered has no pill left, and a control that
|
|
56
|
+
* only advertises itself on hover cannot be found by touch.
|
|
57
|
+
*/
|
|
58
|
+
export declare const BadgeButton: ({ appearance, categorical, children, className, icon, intent, isDisabled, overflow, size, title, type, ...buttonProps }: BadgeButtonProps) => ReactNode;
|
|
59
|
+
//# sourceMappingURL=BadgeButton.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BadgeButton.d.ts","sourceRoot":"","sources":["../../src/BadgeButton/BadgeButton.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,qBAAqB,EACrB,SAAS,EACV,MAAM,OAAO,CAAA;AAEd,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AAUjE,MAAM,MAAM,gBAAgB,GAAG,IAAI,CACjC,qBAAqB,CAAC,QAAQ,CAAC,EAC/B,UAAU,CACX,GACC,eAAe,GAAG;IAChB;;;;OAIG;IACH,OAAO,EAAE,WAAW,CAClB,qBAAqB,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,CAC3C,CAAA;IACD;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;CAKrB,GAAG,CAAC;IAAE,WAAW,CAAC,EAAE,KAAK,CAAA;CAAE,GAAG;IAAE,MAAM,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC,CAAA;AAEpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,WAAW,GAAI,yHAazB,gBAAgB,KAAG,SAsCrB,CAAA"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useBadgeShape } from "../Badge/useBadgeShape.js";
|
|
3
|
+
import { CATEGORICAL_HOVER_CLASS } from "../categoricalStyles.js";
|
|
4
|
+
import { DISABLED_CLASS, FOCUS_RING_CLASS, INTENT_HOVER_CLASS, } from "../intentStyles.js";
|
|
5
|
+
import { toClassName } from "../toClassName.js";
|
|
6
|
+
/**
|
|
7
|
+
* A badge you can press.
|
|
8
|
+
*
|
|
9
|
+
* A sibling component rather than a prop, exactly as `ButtonLink` is
|
|
10
|
+
* to `Button`: the paint is shared (both call `useBadgeShape`, so
|
|
11
|
+
* there is one class list and `BadgeButton.test.tsx` compares the
|
|
12
|
+
* two elements' *computed* styles rather than trusting that), and
|
|
13
|
+
* what differs is the element and everything the platform hangs off
|
|
14
|
+
* it.
|
|
15
|
+
*
|
|
16
|
+
* **Why not `asChild`, and why not `Badge` growing an `onClick`.**
|
|
17
|
+
* This library has no polymorphism pattern and has not needed one;
|
|
18
|
+
* adding it here would mean every future component answering "can it
|
|
19
|
+
* be something else?" instead of "what is it?". And an element type
|
|
20
|
+
* that changes as a *side effect* of passing a handler is a footgun
|
|
21
|
+
* with no error: forget the handler and a control silently ships as
|
|
22
|
+
* a `<span>` — not focusable, not in the tab order, invisible to
|
|
23
|
+
* `getByRole("button")` and to every agent driving the app.
|
|
24
|
+
*
|
|
25
|
+
* **What the element buys, and none of it is paint.** Focus, the tab
|
|
26
|
+
* order, Enter and Space, `:disabled`, form participation, and a
|
|
27
|
+
* `button` role a screen reader announces as pressable. QueuePilot
|
|
28
|
+
* had six pills doing all of this with a hand-rolled
|
|
29
|
+
* `<button className="badge …">`, which is where this came from —
|
|
30
|
+
* six chips that open an editor, change a start point, or drop a
|
|
31
|
+
* title from a pool.
|
|
32
|
+
*
|
|
33
|
+
* **`type="button"` by default.** These chips sit inside forms —
|
|
34
|
+
* QueuePilot's entry sheet is one — and the platform's default of
|
|
35
|
+
* `submit` would make a "change the start point" chip save the whole
|
|
36
|
+
* dialog. Overridable, because a badge-shaped submit is a real thing
|
|
37
|
+
* and the default is a safe floor rather than a ban.
|
|
38
|
+
*
|
|
39
|
+
* **`ghost` is still excluded**, inherited from `Badge`: a pill that
|
|
40
|
+
* paints nothing until hovered has no pill left, and a control that
|
|
41
|
+
* only advertises itself on hover cannot be found by touch.
|
|
42
|
+
*/
|
|
43
|
+
export const BadgeButton = ({ appearance = "soft", categorical, children, className, icon, intent = "neutral", isDisabled = false, overflow, size, title, type = "button", ...buttonProps }) => {
|
|
44
|
+
const shape = useBadgeShape({
|
|
45
|
+
appearance,
|
|
46
|
+
categorical,
|
|
47
|
+
children,
|
|
48
|
+
icon,
|
|
49
|
+
intent,
|
|
50
|
+
overflow,
|
|
51
|
+
size,
|
|
52
|
+
title,
|
|
53
|
+
});
|
|
54
|
+
return (_jsx("button", { ...buttonProps, className: toClassName(shape.className, "cursor-pointer",
|
|
55
|
+
// The three states the `<span>` has no business carrying,
|
|
56
|
+
// keyed the same way the appearance above was.
|
|
57
|
+
categorical === undefined
|
|
58
|
+
? INTENT_HOVER_CLASS[intent][appearance]
|
|
59
|
+
: CATEGORICAL_HOVER_CLASS[categorical][appearance], FOCUS_RING_CLASS, DISABLED_CLASS,
|
|
60
|
+
// Last, so a caller's layout beats the defaults above — the
|
|
61
|
+
// same order `getControlClassName` uses.
|
|
62
|
+
className), disabled: isDisabled, title: shape.title, type: type, children: shape.content }));
|
|
63
|
+
};
|
|
64
|
+
//# sourceMappingURL=BadgeButton.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BadgeButton.js","sourceRoot":"","sources":["../../src/BadgeButton/BadgeButton.tsx"],"names":[],"mappings":";AAMA,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA4B,CAAA;AAC1D,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAA;AACjE,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AA2B/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,EAC1B,UAAU,GAAG,MAAM,EACnB,WAAW,EACX,QAAQ,EACR,SAAS,EACT,IAAI,EACJ,MAAM,GAAG,SAAS,EAClB,UAAU,GAAG,KAAK,EAClB,QAAQ,EACR,IAAI,EACJ,KAAK,EACL,IAAI,GAAG,QAAQ,EACf,GAAG,WAAW,EACG,EAAa,EAAE;IAChC,MAAM,KAAK,GAAG,aAAa,CAAC;QAC1B,UAAU;QACV,WAAW;QACX,QAAQ;QACR,IAAI;QACJ,MAAM;QACN,QAAQ;QACR,IAAI;QACJ,KAAK;KACN,CAAC,CAAA;IAEF,OAAO,CACL,oBACM,WAAW,EACf,SAAS,EAAE,WAAW,CACpB,KAAK,CAAC,SAAS,EACf,gBAAgB;QAChB,0DAA0D;QAC1D,+CAA+C;QAC/C,WAAW,KAAK,SAAS;YACvB,CAAC,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,UAAU,CAAC;YACxC,CAAC,CAAC,uBAAuB,CAAC,WAAW,CAAC,CAClC,UAAU,CACX,EACL,gBAAgB,EAChB,cAAc;QACd,4DAA4D;QAC5D,yCAAyC;QACzC,SAAS,CACV,EACD,QAAQ,EAAE,UAAU,EACpB,KAAK,EAAE,KAAK,CAAC,KAAK,EAClB,IAAI,EAAE,IAAI,YAET,KAAK,CAAC,OAAO,GACP,CACV,CAAA;AACH,CAAC,CAAA"}
|
package/dist/Card/Card.d.ts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import type { ElevationStep, SurfaceRole } from "@charcuterie/tokens";
|
|
2
2
|
import type { ComponentPropsWithRef, ReactNode } from "react";
|
|
3
|
+
import type { CardAccentEdge } from "./cardAccentEdge.ts";
|
|
3
4
|
export type CardSurface = Extract<SurfaceRole, "base" | "raised" | "sunken">;
|
|
4
5
|
export type CardPadding = "lg" | "md" | "none" | "sm";
|
|
5
6
|
export type CardProps = ComponentPropsWithRef<"section"> & {
|
|
7
|
+
/**
|
|
8
|
+
* A coloured bar down the leading edge, following the card's own
|
|
9
|
+
* corners.
|
|
10
|
+
*
|
|
11
|
+
* `{ categorical: 3 }` for a colour a user picked; `{ color }`
|
|
12
|
+
* for one an app computed — a hue hashed from a repo's name, say,
|
|
13
|
+
* where ten indices are not enough. See `cardAccentEdge.ts` for
|
|
14
|
+
* why it is a pseudo-element and not a border.
|
|
15
|
+
*/
|
|
16
|
+
accentEdge?: CardAccentEdge;
|
|
6
17
|
/** Buttons or a badge, laid out beside the heading. */
|
|
7
18
|
actions?: ReactNode;
|
|
8
19
|
elevation?: ElevationStep;
|
|
@@ -40,5 +51,5 @@ export declare const ELEVATION_CLASS: Record<ElevationStep, string>;
|
|
|
40
51
|
* because a card wrapping a poster grid genuinely has none, which is
|
|
41
52
|
* a layout fact rather than a density one.
|
|
42
53
|
*/
|
|
43
|
-
export declare const Card: ({ actions, children, className, elevation, footer, heading, headingLevel, padding, surface, ...sectionProps }: CardProps) => ReactNode;
|
|
54
|
+
export declare const Card: ({ accentEdge, actions, children, className, elevation, footer, heading, headingLevel, padding, style, surface, ...sectionProps }: CardProps) => ReactNode;
|
|
44
55
|
//# sourceMappingURL=Card.d.ts.map
|
package/dist/Card/Card.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Card.d.ts","sourceRoot":"","sources":["../../src/Card/Card.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,aAAa,EACb,WAAW,EACZ,MAAM,qBAAqB,CAAA;AAC5B,OAAO,KAAK,EACV,qBAAqB,EACrB,SAAS,EACV,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"Card.d.ts","sourceRoot":"","sources":["../../src/Card/Card.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,aAAa,EACb,WAAW,EACZ,MAAM,qBAAqB,CAAA;AAC5B,OAAO,KAAK,EACV,qBAAqB,EACrB,SAAS,EACV,MAAM,OAAO,CAAA;AAGd,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAMzD,MAAM,MAAM,WAAW,GAAG,OAAO,CAC/B,WAAW,EACX,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAC7B,CAAA;AAED,MAAM,MAAM,WAAW,GAAG,IAAI,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAAA;AAErD,MAAM,MAAM,SAAS,GAAG,qBAAqB,CAAC,SAAS,CAAC,GAAG;IACzD;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,cAAc,CAAA;IAC3B,uDAAuD;IACvD,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,SAAS,CAAC,EAAE,aAAa,CAAA;IACzB,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IACxB,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,OAAO,CAAC,EAAE,WAAW,CAAA;CACtB,CAAA;AAQD;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAClC,aAAa,EACb,MAAM,CAMP,CAAA;AASD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,IAAI,GAAI,kIAalB,SAAS,KAAG,SAmEd,CAAA"}
|
package/dist/Card/Card.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useUniqueId } from "@charcuterie/logic";
|
|
3
3
|
import { toClassName } from "../toClassName.js";
|
|
4
|
+
import { getAccentEdgeClassName, getAccentEdgeStyle, } from "./cardAccentEdge.js";
|
|
4
5
|
const SURFACE_CLASS = {
|
|
5
6
|
base: "bg-surface-base",
|
|
6
7
|
raised: "bg-surface-raised",
|
|
@@ -45,8 +46,17 @@ const PADDING_CLASS = {
|
|
|
45
46
|
* because a card wrapping a poster grid genuinely has none, which is
|
|
46
47
|
* a layout fact rather than a density one.
|
|
47
48
|
*/
|
|
48
|
-
export const Card = ({ actions, children, className, elevation = "low", footer, heading, headingLevel = 2, padding = "md", surface = "raised", ...sectionProps }) => {
|
|
49
|
+
export const Card = ({ accentEdge, actions, children, className, elevation = "low", footer, heading, headingLevel = 2, padding = "md", style, surface = "raised", ...sectionProps }) => {
|
|
49
50
|
const headingId = useUniqueId();
|
|
51
|
+
// The caller's own `style` wins. An accent colour is one custom
|
|
52
|
+
// property, and silently dropping whatever else the call site set
|
|
53
|
+
// would be a worse trade than letting it override this one.
|
|
54
|
+
const accentEdgeStyle = accentEdge == null
|
|
55
|
+
? undefined
|
|
56
|
+
: getAccentEdgeStyle(accentEdge);
|
|
57
|
+
const mergedStyle = accentEdgeStyle == null
|
|
58
|
+
? style
|
|
59
|
+
: { ...accentEdgeStyle, ...style };
|
|
50
60
|
const Heading = `h${headingLevel}`;
|
|
51
61
|
return (_jsxs("section", { ...sectionProps, "aria-labelledby": heading ? headingId : undefined, className: toClassName(
|
|
52
62
|
// `@container` here, not on a wrapper, is what lets the
|
|
@@ -54,6 +64,8 @@ export const Card = ({ actions, children, className, elevation = "low", footer,
|
|
|
54
64
|
// a card in a sidebar and the same card full-bleed are the
|
|
55
65
|
// case a media query cannot tell apart, and the fleet's
|
|
56
66
|
// poster grids get it wrong at intermediate widths today.
|
|
57
|
-
"@container flex flex-col gap-3 rounded-lg border border-border-subtle text-content-primary", SURFACE_CLASS[surface], ELEVATION_CLASS[elevation], PADDING_CLASS[padding],
|
|
67
|
+
"@container flex flex-col gap-3 rounded-lg border border-border-subtle text-content-primary", SURFACE_CLASS[surface], ELEVATION_CLASS[elevation], PADDING_CLASS[padding], accentEdge == null
|
|
68
|
+
? undefined
|
|
69
|
+
: getAccentEdgeClassName(accentEdge), className), style: mergedStyle, children: [heading || actions ? (_jsxs("header", { className: "flex flex-col gap-2 cq-sm:flex-row cq-sm:items-start cq-sm:justify-between", children: [heading ? (_jsx(Heading, { className: "font-semibold text-md leading-tight", id: headingId, children: heading })) : null, actions ? (_jsx("div", { className: "flex shrink-0 items-center gap-1", children: actions })) : null] })) : null, children, footer ? (_jsx("footer", { className: "border-t border-border-subtle pt-3 text-content-secondary text-sm", children: footer })) : null] }));
|
|
58
70
|
};
|
|
59
71
|
//# sourceMappingURL=Card.js.map
|
package/dist/Card/Card.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Card.js","sourceRoot":"","sources":["../../src/Card/Card.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAUhD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"Card.js","sourceRoot":"","sources":["../../src/Card/Card.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAUhD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAE/C,OAAO,EACL,sBAAsB,EACtB,kBAAkB,GACnB,MAAM,qBAAqB,CAAA;AA8B5B,MAAM,aAAa,GAAgC;IACjD,IAAI,EAAE,iBAAiB;IACvB,MAAM,EAAE,mBAAmB;IAC3B,MAAM,EAAE,mBAAmB;CAC5B,CAAA;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,eAAe,GAGxB;IACF,IAAI,EAAE,aAAa;IACnB,GAAG,EAAE,YAAY;IACjB,MAAM,EAAE,eAAe;IACvB,IAAI,EAAE,aAAa;CACpB,CAAA;AAED,MAAM,aAAa,GAAgC;IACjD,IAAI,EAAE,KAAK;IACX,EAAE,EAAE,KAAK;IACT,EAAE,EAAE,KAAK;IACT,EAAE,EAAE,KAAK;CACV,CAAA;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,EACnB,UAAU,EACV,OAAO,EACP,QAAQ,EACR,SAAS,EACT,SAAS,GAAG,KAAK,EACjB,MAAM,EACN,OAAO,EACP,YAAY,GAAG,CAAC,EAChB,OAAO,GAAG,IAAI,EACd,KAAK,EACL,OAAO,GAAG,QAAQ,EAClB,GAAG,YAAY,EACL,EAAa,EAAE;IACzB,MAAM,SAAS,GAAG,WAAW,EAAE,CAAA;IAE/B,gEAAgE;IAChE,kEAAkE;IAClE,4DAA4D;IAC5D,MAAM,eAAe,GACnB,UAAU,IAAI,IAAI;QAChB,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAA;IAEpC,MAAM,WAAW,GACf,eAAe,IAAI,IAAI;QACrB,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,EAAE,GAAG,eAAe,EAAE,GAAG,KAAK,EAAE,CAAA;IAEtC,MAAM,OAAO,GAAG,IAAI,YAAY,EAAW,CAAA;IAE3C,OAAO,CACL,sBACM,YAAY,qBACC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,EAChD,SAAS,EAAE,WAAW;QACpB,wDAAwD;QACxD,2DAA2D;QAC3D,2DAA2D;QAC3D,wDAAwD;QACxD,0DAA0D;QAC1D,4FAA4F,EAC5F,aAAa,CAAC,OAAO,CAAC,EACtB,eAAe,CAAC,SAAS,CAAC,EAC1B,aAAa,CAAC,OAAO,CAAC,EACtB,UAAU,IAAI,IAAI;YAChB,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,sBAAsB,CAAC,UAAU,CAAC,EACtC,SAAS,CACV,EACD,KAAK,EAAE,WAAW,aAEjB,OAAO,IAAI,OAAO,CAAC,CAAC,CAAC,CACpB,kBAAQ,SAAS,EAAC,4EAA4E,aAC3F,OAAO,CAAC,CAAC,CAAC,CACT,KAAC,OAAO,IACN,SAAS,EAAC,qCAAqC,EAC/C,EAAE,EAAE,SAAS,YAEZ,OAAO,GACA,CACX,CAAC,CAAC,CAAC,IAAI,EAEP,OAAO,CAAC,CAAC,CAAC,CACT,cAAK,SAAS,EAAC,kCAAkC,YAC9C,OAAO,GACJ,CACP,CAAC,CAAC,CAAC,IAAI,IACD,CACV,CAAC,CAAC,CAAC,IAAI,EAEP,QAAQ,EAER,MAAM,CAAC,CAAC,CAAC,CACR,iBAAQ,SAAS,EAAC,mEAAmE,YAClF,MAAM,GACA,CACV,CAAC,CAAC,CAAC,IAAI,IACA,CACX,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
import type { CategoricalIndex } from "@charcuterie/tokens";
|
|
38
|
+
/**
|
|
39
|
+
* Which colour the bar takes.
|
|
40
|
+
*
|
|
41
|
+
* Two arms, because the fleet has two answers and neither is wrong.
|
|
42
|
+
* Docket picks a **categorical index** — a colour a user chose for
|
|
43
|
+
* a project, from the ten-wide contrast-audited family. Folio and
|
|
44
|
+
* mail-sifter derive a **hue from a name hash**, so a repo added
|
|
45
|
+
* tomorrow already has a colour and nobody maintains a palette;
|
|
46
|
+
* that is 360 answers, not ten, and it cannot be an index.
|
|
47
|
+
*
|
|
48
|
+
* `intent` is deliberately absent. An intent is a claim the design
|
|
49
|
+
* system makes — `danger` says what happens if you press the thing
|
|
50
|
+
* — and no card in this fleet wants its edge to make one. When one
|
|
51
|
+
* does, that is a third arm and a decision record, not a guess
|
|
52
|
+
* made here.
|
|
53
|
+
*/
|
|
54
|
+
export type CardAccentEdge = {
|
|
55
|
+
categorical: CategoricalIndex;
|
|
56
|
+
color?: never;
|
|
57
|
+
} | {
|
|
58
|
+
categorical?: never;
|
|
59
|
+
color: string;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* The custom property an explicit colour is threaded through.
|
|
63
|
+
*
|
|
64
|
+
* A property rather than an inline `box-shadow`, because the shadow
|
|
65
|
+
* string has to stay a COMPLETE literal in the source for Tailwind
|
|
66
|
+
* to generate it at all. One literal reading one variable covers
|
|
67
|
+
* every colour an app can compute; interpolating the colour into
|
|
68
|
+
* the class name would generate nothing, paint nothing, and report
|
|
69
|
+
* nothing.
|
|
70
|
+
*/
|
|
71
|
+
export declare const ACCENT_EDGE_COLOR_PROPERTY = "--charcuterie-accent-edge";
|
|
72
|
+
/**
|
|
73
|
+
* The overlay itself. Colour arrives separately.
|
|
74
|
+
*
|
|
75
|
+
* `pointer-events: none` is load-bearing rather than tidy: this box
|
|
76
|
+
* covers the whole card, including every link and button in it.
|
|
77
|
+
*/
|
|
78
|
+
export declare const ACCENT_EDGE_BASE_CLASS = "relative before:pointer-events-none before:absolute before:inset-0 before:rounded-[inherit] before:content-['']";
|
|
79
|
+
/**
|
|
80
|
+
* Ten indices, written out in full, for the reason
|
|
81
|
+
* `categoricalStyles.ts` opens with: Tailwind scans source text for
|
|
82
|
+
* COMPLETE class strings, so a template literal generates nothing
|
|
83
|
+
* at all and the bar renders invisible with no error and no failing
|
|
84
|
+
* test. `tailwindCandidates.test.ts` compiles every literal in this
|
|
85
|
+
* package through the real Tailwind, which is the something that
|
|
86
|
+
* checks.
|
|
87
|
+
*/
|
|
88
|
+
export declare const ACCENT_EDGE_CATEGORICAL_CLASS: Record<CategoricalIndex, string>;
|
|
89
|
+
/** The same bar, reading the caller's own colour. */
|
|
90
|
+
export declare const ACCENT_EDGE_COLOR_CLASS = "before:shadow-[inset_3px_0_0_var(--charcuterie-accent-edge)] rtl:before:shadow-[inset_-3px_0_0_var(--charcuterie-accent-edge)]";
|
|
91
|
+
export declare const getAccentEdgeClassName: (accentEdge: CardAccentEdge) => string;
|
|
92
|
+
/**
|
|
93
|
+
* The inline style an explicit colour needs, and nothing when the
|
|
94
|
+
* colour came from an index.
|
|
95
|
+
*
|
|
96
|
+
* Returns `undefined` rather than `{}` so a card with a categorical
|
|
97
|
+
* edge and no `style` of its own renders no `style` attribute at
|
|
98
|
+
* all.
|
|
99
|
+
*/
|
|
100
|
+
export declare const getAccentEdgeStyle: (accentEdge: CardAccentEdge) => Record<string, string> | undefined;
|
|
101
|
+
//# sourceMappingURL=cardAccentEdge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cardAccentEdge.d.ts","sourceRoot":"","sources":["../../src/Card/cardAccentEdge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAE3D;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,cAAc,GACtB;IAAE,WAAW,EAAE,gBAAgB,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,GAChD;IAAE,WAAW,CAAC,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAE1C;;;;;;;;;GASG;AACH,eAAO,MAAM,0BAA0B,8BACV,CAAA;AAE7B;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,oHACgF,CAAA;AAEnH;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B,EAAE,MAAM,CAChD,gBAAgB,EAChB,MAAM,CAYP,CAAA;AAED,qDAAqD;AACrD,eAAO,MAAM,uBAAuB,mIAC8F,CAAA;AAElI,eAAO,MAAM,sBAAsB,GACjC,YAAY,cAAc,KACzB,MAOC,CAAA;AAEJ;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,GAC7B,YAAY,cAAc,KACzB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAG4B,CAAA"}
|