@charcuterie/ui 3.0.0 → 3.1.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 +10 -0
- package/dist/Badge/Badge.d.ts +33 -3
- package/dist/Badge/Badge.d.ts.map +1 -1
- package/dist/Badge/Badge.js +20 -2
- package/dist/Badge/Badge.js.map +1 -1
- package/dist/Board/Board.d.ts +119 -0
- package/dist/Board/Board.d.ts.map +1 -0
- package/dist/Board/Board.js +165 -0
- package/dist/Board/Board.js.map +1 -0
- package/dist/Board/BoardCard.d.ts +120 -0
- package/dist/Board/BoardCard.d.ts.map +1 -0
- package/dist/Board/BoardCard.js +77 -0
- package/dist/Board/BoardCard.js.map +1 -0
- package/dist/Board/BoardLaneList.d.ts +103 -0
- package/dist/Board/BoardLaneList.d.ts.map +1 -0
- package/dist/Board/BoardLaneList.js +69 -0
- package/dist/Board/BoardLaneList.js.map +1 -0
- package/dist/Board/boardMove.d.ts +116 -0
- package/dist/Board/boardMove.d.ts.map +1 -0
- package/dist/Board/boardMove.js +116 -0
- package/dist/Board/boardMove.js.map +1 -0
- package/dist/Board/useBoardDrag.d.ts +44 -0
- package/dist/Board/useBoardDrag.d.ts.map +1 -0
- package/dist/Board/useBoardDrag.js +237 -0
- package/dist/Board/useBoardDrag.js.map +1 -0
- package/dist/Checkbox/Checkbox.d.ts.map +1 -1
- package/dist/Checkbox/Checkbox.js +15 -4
- package/dist/Checkbox/Checkbox.js.map +1 -1
- package/dist/DataTable/DataTable.d.ts +159 -0
- package/dist/DataTable/DataTable.d.ts.map +1 -0
- package/dist/DataTable/DataTable.js +197 -0
- package/dist/DataTable/DataTable.js.map +1 -0
- package/dist/DatePicker/DateGrid.d.ts +79 -0
- package/dist/DatePicker/DateGrid.d.ts.map +1 -0
- package/dist/DatePicker/DateGrid.js +220 -0
- package/dist/DatePicker/DateGrid.js.map +1 -0
- package/dist/DatePicker/DatePicker.d.ts +138 -0
- package/dist/DatePicker/DatePicker.d.ts.map +1 -0
- package/dist/DatePicker/DatePicker.js +384 -0
- package/dist/DatePicker/DatePicker.js.map +1 -0
- package/dist/DatePicker/datePresets.d.ts +29 -0
- package/dist/DatePicker/datePresets.d.ts.map +1 -0
- package/dist/DatePicker/datePresets.js +28 -0
- package/dist/DatePicker/datePresets.js.map +1 -0
- package/dist/DatePicker/parseDateInput.d.ts +106 -0
- package/dist/DatePicker/parseDateInput.d.ts.map +1 -0
- package/dist/DatePicker/parseDateInput.js +419 -0
- package/dist/DatePicker/parseDateInput.js.map +1 -0
- package/dist/DatePicker/plainDate.d.ts +167 -0
- package/dist/DatePicker/plainDate.d.ts.map +1 -0
- package/dist/DatePicker/plainDate.js +272 -0
- package/dist/DatePicker/plainDate.js.map +1 -0
- package/dist/MarkdownEditor/MarkdownEditor.d.ts +147 -0
- package/dist/MarkdownEditor/MarkdownEditor.d.ts.map +1 -0
- package/dist/MarkdownEditor/MarkdownEditor.js +492 -0
- package/dist/MarkdownEditor/MarkdownEditor.js.map +1 -0
- package/dist/MarkdownEditor/markdownCommands.d.ts +202 -0
- package/dist/MarkdownEditor/markdownCommands.d.ts.map +1 -0
- package/dist/MarkdownEditor/markdownCommands.js +379 -0
- package/dist/MarkdownEditor/markdownCommands.js.map +1 -0
- package/dist/MarkdownEditor/markdownSpans.d.ts +92 -0
- package/dist/MarkdownEditor/markdownSpans.d.ts.map +1 -0
- package/dist/MarkdownEditor/markdownSpans.js +303 -0
- package/dist/MarkdownEditor/markdownSpans.js.map +1 -0
- package/dist/categoricalStyles.d.ts +53 -0
- package/dist/categoricalStyles.d.ts.map +1 -0
- package/dist/categoricalStyles.js +195 -0
- package/dist/categoricalStyles.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/Badge/Badge.mdx +46 -0
- package/src/Badge/Badge.stories.tsx +98 -4
- package/src/Badge/Badge.test.tsx +157 -2
- package/src/Badge/Badge.tsx +49 -3
- package/src/Board/Board.mdx +199 -0
- package/src/Board/Board.stories.tsx +707 -0
- package/src/Board/Board.test.tsx +310 -0
- package/src/Board/Board.tsx +333 -0
- package/src/Board/BoardCard.tsx +290 -0
- package/src/Board/BoardLaneList.tsx +275 -0
- package/src/Board/boardMove.test.ts +213 -0
- package/src/Board/boardMove.ts +225 -0
- package/src/Board/useBoardDrag.ts +413 -0
- package/src/Checkbox/Checkbox.tsx +34 -3
- package/src/DataTable/DataTable.mdx +123 -0
- package/src/DataTable/DataTable.stories.tsx +411 -0
- package/src/DataTable/DataTable.test.tsx +386 -0
- package/src/DataTable/DataTable.tsx +497 -0
- package/src/DatePicker/DateGrid.tsx +467 -0
- package/src/DatePicker/DatePicker.mdx +170 -0
- package/src/DatePicker/DatePicker.stories.tsx +268 -0
- package/src/DatePicker/DatePicker.test.tsx +455 -0
- package/src/DatePicker/DatePicker.tsx +852 -0
- package/src/DatePicker/datePresets.ts +34 -0
- package/src/DatePicker/parseDateInput.test.ts +187 -0
- package/src/DatePicker/parseDateInput.ts +629 -0
- package/src/DatePicker/plainDate.test.ts +375 -0
- package/src/DatePicker/plainDate.ts +465 -0
- package/src/MarkdownEditor/MarkdownEditor.mdx +189 -0
- package/src/MarkdownEditor/MarkdownEditor.stories.tsx +267 -0
- package/src/MarkdownEditor/MarkdownEditor.test.tsx +627 -0
- package/src/MarkdownEditor/MarkdownEditor.tsx +862 -0
- package/src/MarkdownEditor/markdownCommands.test.ts +384 -0
- package/src/MarkdownEditor/markdownCommands.ts +566 -0
- package/src/MarkdownEditor/markdownSpans.test.ts +217 -0
- package/src/MarkdownEditor/markdownSpans.ts +458 -0
- package/src/argTypes.storyHelpers.ts +39 -0
- package/src/categoricalStyles.ts +244 -0
- package/src/index.ts +89 -0
- package/src/sourceRules.test.ts +52 -1
package/README.md
CHANGED
|
@@ -38,6 +38,16 @@ The router is **injected** rather than depended on — `RouterLinkProvider` at t
|
|
|
38
38
|
with `@charcuterie/ui/react-router` as an optional subpath adapter and a plain `<a>` when
|
|
39
39
|
nothing is provided. Recipe: **Guides/Routing** in Storybook.
|
|
40
40
|
|
|
41
|
+
**`Board` is the first component whose own operation is a write**, and the first to declare
|
|
42
|
+
two nested containers. Lanes with honest counts, priority bars, per-card footers for a live
|
|
43
|
+
run line, real empty states and a `+ n more` overflow — the board's own box decides how many
|
|
44
|
+
lanes are on screen, each lane's box decides whether a card is two lines, one line, or a card.
|
|
45
|
+
There is **no media query in it**, because a lane in a three-up board is ~500px on a maximised
|
|
46
|
+
1600px window and a browser at 175% zoom reports ~860 effective pixels for a 1500px one.
|
|
47
|
+
Moving a card takes **no drag-and-drop dependency**: one handle per card, a `Menu` of the
|
|
48
|
+
other lanes as the primary path, Pointer-Event dragging at 1.4 KB gzip beside it
|
|
49
|
+
([decision](../../docs/decisions/2026-08-19-the-board-owns-the-move-and-takes-no-drag-and-drop-dependency.md)).
|
|
50
|
+
|
|
41
51
|
**The unified app shell adds four**: `Shell`, `Header`, `Rail`, `Main` — the fleet's largest
|
|
42
52
|
single duplication, where ten of twelve UI repos hand-roll the page chrome and three of them
|
|
43
53
|
have a file named `AppShell.tsx` (two of those headers are a *byte-identical* class string,
|
package/dist/Badge/Badge.d.ts
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
|
-
import type { IntentName } from "@charcuterie/tokens";
|
|
1
|
+
import type { CategoricalIndex, IntentName } from "@charcuterie/tokens";
|
|
2
2
|
import type { ComponentPropsWithRef, ReactNode } from "react";
|
|
3
3
|
import type { BadgeSize } from "../controlStyles.ts";
|
|
4
4
|
import type { IntentAppearance } from "../intentStyles.ts";
|
|
5
5
|
export type BadgeProps = ComponentPropsWithRef<"span"> & {
|
|
6
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;
|
|
7
23
|
children: ReactNode;
|
|
8
24
|
icon?: ReactNode;
|
|
9
25
|
intent?: IntentName;
|
|
@@ -19,7 +35,11 @@ export type BadgeProps = ComponentPropsWithRef<"span"> & {
|
|
|
19
35
|
*/
|
|
20
36
|
overflow?: "truncate" | "wrap";
|
|
21
37
|
size?: BadgeSize;
|
|
22
|
-
}
|
|
38
|
+
} & ({
|
|
39
|
+
categorical?: never;
|
|
40
|
+
} | {
|
|
41
|
+
intent?: never;
|
|
42
|
+
});
|
|
23
43
|
/**
|
|
24
44
|
* Four repos, and rip-deck declares the **identical** `TONE_CLASS`
|
|
25
45
|
* map twice — `VerdictBadge.tsx` and `TowerAlerts.tsx`, same five
|
|
@@ -44,11 +64,21 @@ export type BadgeProps = ComponentPropsWithRef<"span"> & {
|
|
|
44
64
|
* asserts the text is queryable, which is what an agent actually
|
|
45
65
|
* matches on.
|
|
46
66
|
*
|
|
67
|
+
* **`intent` or `categorical`, never both.** The two families are
|
|
68
|
+
* both seven roles wide and both paint a pill, and they answer
|
|
69
|
+
* opposite questions: an intent is a *claim* the design system
|
|
70
|
+
* makes (`danger` says what happens if you press the thing), and a
|
|
71
|
+
* categorical index is a claim about nothing at all — it is a
|
|
72
|
+
* colour a user picked for a label. Which is why the exclusivity is
|
|
73
|
+
* in the type rather than in a precedence rule: a badge is one
|
|
74
|
+
* colour, and `<Badge intent="danger" categorical={3}>` is a
|
|
75
|
+
* question with no answer.
|
|
76
|
+
*
|
|
47
77
|
* **It is capped at its container.** `shrink-0` and
|
|
48
78
|
* `whitespace-nowrap` alone let a long label paint straight through
|
|
49
79
|
* the neighbouring column — no clipping, no error, just a pill lying
|
|
50
80
|
* across the next cell. `max-inline-size: 100%` is what stops it, and
|
|
51
81
|
* `overflow` is what happens next.
|
|
52
82
|
*/
|
|
53
|
-
export declare const Badge: ({ appearance, children, className, icon, intent, overflow, size, title, ...spanProps }: BadgeProps) => ReactNode;
|
|
83
|
+
export declare const Badge: ({ appearance, categorical, children, className, icon, intent, overflow, size, title, ...spanProps }: BadgeProps) => ReactNode;
|
|
54
84
|
//# sourceMappingURL=Badge.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Badge.d.ts","sourceRoot":"","sources":["../../src/Badge/Badge.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"Badge.d.ts","sourceRoot":"","sources":["../../src/Badge/Badge.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,UAAU,EACX,MAAM,qBAAqB,CAAA;AAC5B,OAAO,KAAK,EACV,qBAAqB,EACrB,SAAS,EACV,MAAM,OAAO,CAAA;AAGd,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAEpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAK1D,MAAM,MAAM,UAAU,GAAG,qBAAqB,CAAC,MAAM,CAAC,GAAG;IACvD,UAAU,CAAC,EAAE,OAAO,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAA;IAC/C;;;;;;;;;;;;;;OAcG;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;CAQjB,GAAG,CAAC;IAAE,WAAW,CAAC,EAAE,KAAK,CAAA;CAAE,GAAG;IAAE,MAAM,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC,CAAA;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,eAAO,MAAM,KAAK,GAAI,qGAWnB,UAAU,KAAG,SAiEf,CAAA"}
|
package/dist/Badge/Badge.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CATEGORICAL_APPEARANCE_CLASS } from "../categoricalStyles.js";
|
|
2
3
|
import { BADGE_SIZE_CLASS } from "../controlStyles.js";
|
|
3
4
|
import { INTENT_APPEARANCE_CLASS } from "../intentStyles.js";
|
|
4
5
|
import { toClassName } from "../toClassName.js";
|
|
@@ -27,13 +28,23 @@ import { useIsTextClipped } from "./useIsTextClipped.js";
|
|
|
27
28
|
* asserts the text is queryable, which is what an agent actually
|
|
28
29
|
* matches on.
|
|
29
30
|
*
|
|
31
|
+
* **`intent` or `categorical`, never both.** The two families are
|
|
32
|
+
* both seven roles wide and both paint a pill, and they answer
|
|
33
|
+
* opposite questions: an intent is a *claim* the design system
|
|
34
|
+
* makes (`danger` says what happens if you press the thing), and a
|
|
35
|
+
* categorical index is a claim about nothing at all — it is a
|
|
36
|
+
* colour a user picked for a label. Which is why the exclusivity is
|
|
37
|
+
* in the type rather than in a precedence rule: a badge is one
|
|
38
|
+
* colour, and `<Badge intent="danger" categorical={3}>` is a
|
|
39
|
+
* question with no answer.
|
|
40
|
+
*
|
|
30
41
|
* **It is capped at its container.** `shrink-0` and
|
|
31
42
|
* `whitespace-nowrap` alone let a long label paint straight through
|
|
32
43
|
* the neighbouring column — no clipping, no error, just a pill lying
|
|
33
44
|
* across the next cell. `max-inline-size: 100%` is what stops it, and
|
|
34
45
|
* `overflow` is what happens next.
|
|
35
46
|
*/
|
|
36
|
-
export const Badge = ({ appearance = "soft", children, className, icon, intent = "neutral", overflow = "truncate", size = "md", title, ...spanProps }) => {
|
|
47
|
+
export const Badge = ({ appearance = "soft", categorical, children, className, icon, intent = "neutral", overflow = "truncate", size = "md", title, ...spanProps }) => {
|
|
37
48
|
const [labelRef, clippedText] = useIsTextClipped();
|
|
38
49
|
return (_jsxs("span", { ...spanProps, className: toClassName("inline-flex shrink-0 items-center rounded-full border font-medium",
|
|
39
50
|
// The cap. Without it every other rule here is decoration.
|
|
@@ -42,7 +53,14 @@ export const Badge = ({ appearance = "soft", children, className, icon, intent =
|
|
|
42
53
|
// rendering fault rather than a badge, so the wrapping
|
|
43
54
|
// pill relaxes to a rounded rectangle.
|
|
44
55
|
"rounded-2xl"
|
|
45
|
-
: "rounded-full", BADGE_SIZE_CLASS[size],
|
|
56
|
+
: "rounded-full", BADGE_SIZE_CLASS[size],
|
|
57
|
+
// A ternary rather than a merged map: the two are indexed
|
|
58
|
+
// by different keys, and `undefined` is the only honest
|
|
59
|
+
// discriminant once the type has already refused the case
|
|
60
|
+
// where both arrive.
|
|
61
|
+
categorical === undefined
|
|
62
|
+
? INTENT_APPEARANCE_CLASS[intent][appearance]
|
|
63
|
+
: CATEGORICAL_APPEARANCE_CLASS[categorical][appearance], className),
|
|
46
64
|
// Only when something is actually hidden. A tooltip on every
|
|
47
65
|
// short pill in a bay list is noise, and a `title` that
|
|
48
66
|
// duplicates fully-visible text is a screen-reader duplicate
|
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":";AASA,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;AA4CxD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,EACpB,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,EACL,GAAG,SAAS,EACD,EAAa,EAAE;IAC1B,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,gBAAgB,EAAE,CAAA;IAElD,OAAO,CACL,mBACM,SAAS,EACb,SAAS,EAAE,WAAW,CACpB,mEAAmE;QACnE,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,6DAA6D;QAC7D,wDAAwD;QACxD,6DAA6D;QAC7D,eAAe;QACf,KAAK,EAAE,KAAK,IAAI,WAAW,aAE1B,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;oBACjB,CAAC,CAAC,SAAS;oBACX,CAAC,CAAC,yDAAyD,EAE/D,GAAG,EAAE,QAAQ,YAEZ,QAAQ,GACJ,IACF,CACR,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { BoardLane } from "./BoardLaneList.tsx";
|
|
3
|
+
export type BoardMove = {
|
|
4
|
+
/** Where the card was, in `fromLane.items`. */
|
|
5
|
+
fromIndex: number;
|
|
6
|
+
fromLaneKey: string;
|
|
7
|
+
itemKey: string;
|
|
8
|
+
/**
|
|
9
|
+
* Where it goes, **already corrected for its own removal** — so a
|
|
10
|
+
* consumer's handler is `splice(fromIndex, 1)` then
|
|
11
|
+
* `splice(toIndex, 0, item)` with no arithmetic of its own. A
|
|
12
|
+
* within-lane move quoted against the un-spliced list is one too
|
|
13
|
+
* high for every position below the origin, and that off-by-one is
|
|
14
|
+
* the single most common bug in a hand-rolled board.
|
|
15
|
+
*/
|
|
16
|
+
toIndex: number;
|
|
17
|
+
toLaneKey: string;
|
|
18
|
+
};
|
|
19
|
+
export type BoardProps = {
|
|
20
|
+
className?: string;
|
|
21
|
+
/**
|
|
22
|
+
* What level the lane headings are. The caller's, because only the
|
|
23
|
+
* caller knows what the board sits under: a board directly below a
|
|
24
|
+
* page `<h1>` wants `2`, and one inside an already-deep section
|
|
25
|
+
* wants `4`. Getting it wrong is an axe `heading-order` failure and
|
|
26
|
+
* a broken screen-reader outline.
|
|
27
|
+
*/
|
|
28
|
+
headingLevel?: 2 | 3 | 4 | 5;
|
|
29
|
+
/** The board's accessible name. Required — it is a landmark. */
|
|
30
|
+
label: string;
|
|
31
|
+
lanes: readonly BoardLane[];
|
|
32
|
+
/**
|
|
33
|
+
* What the move handle shows instead of the word "Move". The
|
|
34
|
+
* library ships no icons and no symbol glyphs in a default —
|
|
35
|
+
* they render as nothing where the font lacks them, which
|
|
36
|
+
* includes this sandbox's chromium, the kiosk image and the
|
|
37
|
+
* ePaper build — so the default is a word and an app that owns a
|
|
38
|
+
* glyph set passes one. On a dense board it is worth ~55px a row.
|
|
39
|
+
*/
|
|
40
|
+
moveIcon?: ReactNode;
|
|
41
|
+
/**
|
|
42
|
+
* Which lane is on screen below `cq-lg`. **Initial only** — the
|
|
43
|
+
* segmented control owns it from then on, matching every other
|
|
44
|
+
* uncontrolled control in this library.
|
|
45
|
+
*/
|
|
46
|
+
narrowLaneKey?: string;
|
|
47
|
+
/**
|
|
48
|
+
* The one thing that makes a board a board. **Absent means
|
|
49
|
+
* read-only**: no handles, no menus, no drag, and nothing
|
|
50
|
+
* announcing itself. A board with no `onMove` is a legitimate
|
|
51
|
+
* thing — a dashboard, an ePaper render — and it should not be
|
|
52
|
+
* carrying affordances that do nothing.
|
|
53
|
+
*/
|
|
54
|
+
onMove?: (move: BoardMove) => void;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Lanes of cards, sized by their **container**, moved by keyboard or
|
|
58
|
+
* by pointer.
|
|
59
|
+
*
|
|
60
|
+
* ### The container rule, which is the whole point
|
|
61
|
+
*
|
|
62
|
+
* There is no media query in this component, and adding one would be
|
|
63
|
+
* a defect rather than a shortcut. Two facts make window width
|
|
64
|
+
* useless here and they compound:
|
|
65
|
+
*
|
|
66
|
+
* - **A lane in a three-up board is narrow on any monitor.** Three
|
|
67
|
+
* lanes across 1600px is ~500px each, which is a phone's worth of
|
|
68
|
+
* room, on a 4K display, in a maximised window.
|
|
69
|
+
* - **The owner browses zoomed in.** A 1500px window at 175% zoom
|
|
70
|
+
* is ~860 effective CSS pixels, so the number a media query reads
|
|
71
|
+
* is not the number of pixels anything actually has.
|
|
72
|
+
*
|
|
73
|
+
* So there are **two nested containers**, and they answer two
|
|
74
|
+
* different questions:
|
|
75
|
+
*
|
|
76
|
+
* - The board's own box decides *how many lanes are on screen* —
|
|
77
|
+
* three-up at `cq-lg` and wider, one lane plus a segmented
|
|
78
|
+
* control below it.
|
|
79
|
+
* - Each lane's list decides *what shape a card is* — two lines,
|
|
80
|
+
* one line, or its own card. See `BoardCard`.
|
|
81
|
+
*
|
|
82
|
+
* Both are `@container`, and neither is queried by the element that
|
|
83
|
+
* declares it, because a container query matches descendants only.
|
|
84
|
+
*
|
|
85
|
+
* ### There is no horizontal scroll, at any width
|
|
86
|
+
*
|
|
87
|
+
* *"No horizontal scroll, ever. The board becomes a segmented
|
|
88
|
+
* control, never a pan surface."* A board that pans sideways hides
|
|
89
|
+
* lanes behind a gesture with no affordance, and on a trackpad it
|
|
90
|
+
* fights the browser's own back-navigation. So the narrow layout
|
|
91
|
+
* shows one lane at a time and names the others in a `radiogroup`
|
|
92
|
+
* that says how many cards each holds.
|
|
93
|
+
*
|
|
94
|
+
* ### Moving a card is a first-class operation, and it is not a drag
|
|
95
|
+
*
|
|
96
|
+
* The move is a **`Menu` on every card**, listing the other lanes.
|
|
97
|
+
* That is the primary path, not an accessibility fallback: it is the
|
|
98
|
+
* only one that works from the keyboard, the only one a screen
|
|
99
|
+
* reader can drive, and the only one available in the Narrow View
|
|
100
|
+
* where the other lanes are not on screen to drop onto. Pointer
|
|
101
|
+
* dragging is layered on the same handle and commits through the
|
|
102
|
+
* same callback.
|
|
103
|
+
*
|
|
104
|
+
* **No drag-and-drop dependency is taken.** The reasoning — licence,
|
|
105
|
+
* provenance, bundle cost, and what a general DnD library would
|
|
106
|
+
* actually be buying for one card going into one lane — is in
|
|
107
|
+
* [the decision](../../../docs/decisions/2026-08-19-the-board-owns-the-move-and-takes-no-drag-and-drop-dependency.md).
|
|
108
|
+
*
|
|
109
|
+
* ### What this component deliberately does not do
|
|
110
|
+
*
|
|
111
|
+
* Lanes, counts, truncation, empty states and the move; **not**
|
|
112
|
+
* data, not virtualisation, and not the cross-lane "needs attention"
|
|
113
|
+
* summary. A board is a view of a list the consumer owns, the lane
|
|
114
|
+
* truncates by construction so there is nothing to virtualise, and a
|
|
115
|
+
* panel that reaches across every lane is not a lane — it is an
|
|
116
|
+
* `Alert` above the board, which is how the docs page composes it.
|
|
117
|
+
*/
|
|
118
|
+
export declare const Board: ({ className, headingLevel, label, lanes, moveIcon, narrowLaneKey, onMove, }: BoardProps) => ReactNode;
|
|
119
|
+
//# sourceMappingURL=Board.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Board.d.ts","sourceRoot":"","sources":["../../src/Board/Board.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAMtC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AASpD,MAAM,MAAM,SAAS,GAAG;IACtB,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;IACf;;;;;;;OAOG;IACH,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAC5B,gEAAgE;IAChE,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,SAAS,SAAS,EAAE,CAAA;IAC3B;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAA;IACpB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAA;CACnC,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AACH,eAAO,MAAM,KAAK,GAAI,6EAQnB,UAAU,KAAG,SAgMf,CAAA"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useState } from "react";
|
|
3
|
+
import { SegmentedControl } from "../SegmentedControl/SegmentedControl.js";
|
|
4
|
+
import { toClassName } from "../toClassName.js";
|
|
5
|
+
import { VisuallyHidden } from "../VisuallyHidden/VisuallyHidden.js";
|
|
6
|
+
import { BoardLaneList } from "./BoardLaneList.js";
|
|
7
|
+
import { describeMove, getIsMoveMeaningful, toSettledIndex, } from "./boardMove.js";
|
|
8
|
+
import { useBoardDrag } from "./useBoardDrag.js";
|
|
9
|
+
/**
|
|
10
|
+
* Lanes of cards, sized by their **container**, moved by keyboard or
|
|
11
|
+
* by pointer.
|
|
12
|
+
*
|
|
13
|
+
* ### The container rule, which is the whole point
|
|
14
|
+
*
|
|
15
|
+
* There is no media query in this component, and adding one would be
|
|
16
|
+
* a defect rather than a shortcut. Two facts make window width
|
|
17
|
+
* useless here and they compound:
|
|
18
|
+
*
|
|
19
|
+
* - **A lane in a three-up board is narrow on any monitor.** Three
|
|
20
|
+
* lanes across 1600px is ~500px each, which is a phone's worth of
|
|
21
|
+
* room, on a 4K display, in a maximised window.
|
|
22
|
+
* - **The owner browses zoomed in.** A 1500px window at 175% zoom
|
|
23
|
+
* is ~860 effective CSS pixels, so the number a media query reads
|
|
24
|
+
* is not the number of pixels anything actually has.
|
|
25
|
+
*
|
|
26
|
+
* So there are **two nested containers**, and they answer two
|
|
27
|
+
* different questions:
|
|
28
|
+
*
|
|
29
|
+
* - The board's own box decides *how many lanes are on screen* —
|
|
30
|
+
* three-up at `cq-lg` and wider, one lane plus a segmented
|
|
31
|
+
* control below it.
|
|
32
|
+
* - Each lane's list decides *what shape a card is* — two lines,
|
|
33
|
+
* one line, or its own card. See `BoardCard`.
|
|
34
|
+
*
|
|
35
|
+
* Both are `@container`, and neither is queried by the element that
|
|
36
|
+
* declares it, because a container query matches descendants only.
|
|
37
|
+
*
|
|
38
|
+
* ### There is no horizontal scroll, at any width
|
|
39
|
+
*
|
|
40
|
+
* *"No horizontal scroll, ever. The board becomes a segmented
|
|
41
|
+
* control, never a pan surface."* A board that pans sideways hides
|
|
42
|
+
* lanes behind a gesture with no affordance, and on a trackpad it
|
|
43
|
+
* fights the browser's own back-navigation. So the narrow layout
|
|
44
|
+
* shows one lane at a time and names the others in a `radiogroup`
|
|
45
|
+
* that says how many cards each holds.
|
|
46
|
+
*
|
|
47
|
+
* ### Moving a card is a first-class operation, and it is not a drag
|
|
48
|
+
*
|
|
49
|
+
* The move is a **`Menu` on every card**, listing the other lanes.
|
|
50
|
+
* That is the primary path, not an accessibility fallback: it is the
|
|
51
|
+
* only one that works from the keyboard, the only one a screen
|
|
52
|
+
* reader can drive, and the only one available in the Narrow View
|
|
53
|
+
* where the other lanes are not on screen to drop onto. Pointer
|
|
54
|
+
* dragging is layered on the same handle and commits through the
|
|
55
|
+
* same callback.
|
|
56
|
+
*
|
|
57
|
+
* **No drag-and-drop dependency is taken.** The reasoning — licence,
|
|
58
|
+
* provenance, bundle cost, and what a general DnD library would
|
|
59
|
+
* actually be buying for one card going into one lane — is in
|
|
60
|
+
* [the decision](../../../docs/decisions/2026-08-19-the-board-owns-the-move-and-takes-no-drag-and-drop-dependency.md).
|
|
61
|
+
*
|
|
62
|
+
* ### What this component deliberately does not do
|
|
63
|
+
*
|
|
64
|
+
* Lanes, counts, truncation, empty states and the move; **not**
|
|
65
|
+
* data, not virtualisation, and not the cross-lane "needs attention"
|
|
66
|
+
* summary. A board is a view of a list the consumer owns, the lane
|
|
67
|
+
* truncates by construction so there is nothing to virtualise, and a
|
|
68
|
+
* panel that reaches across every lane is not a lane — it is an
|
|
69
|
+
* `Alert` above the board, which is how the docs page composes it.
|
|
70
|
+
*/
|
|
71
|
+
export const Board = ({ className, headingLevel = 3, label, lanes, moveIcon, narrowLaneKey, onMove, }) => {
|
|
72
|
+
const [visibleLaneKey, setVisibleLaneKey] = useState(narrowLaneKey ?? lanes[0]?.key ?? "");
|
|
73
|
+
/**
|
|
74
|
+
* What the live region last said. A string rather than a boolean
|
|
75
|
+
* plus a lookup, because the region's job is to have the sentence
|
|
76
|
+
* ready the moment it changes — and because a move announced as
|
|
77
|
+
* "moved" with no destination tells a screen-reader user strictly
|
|
78
|
+
* less than watching the card land tells everyone else.
|
|
79
|
+
*/
|
|
80
|
+
const [moveAnnouncement, setMoveAnnouncement] = useState("");
|
|
81
|
+
const commitMove = useCallback((move) => {
|
|
82
|
+
const toLane = lanes.find((one) => one.key === move.toLaneKey);
|
|
83
|
+
if (!toLane) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const item = lanes
|
|
87
|
+
.flatMap((one) => one.items)
|
|
88
|
+
.find((one) => one.key === move.itemKey);
|
|
89
|
+
setMoveAnnouncement(describeMove({
|
|
90
|
+
index: move.toIndex,
|
|
91
|
+
laneLabel: toLane.label,
|
|
92
|
+
laneSize: move.fromLaneKey === move.toLaneKey
|
|
93
|
+
? (toLane.itemCount ?? toLane.items.length)
|
|
94
|
+
: (toLane.itemCount ?? toLane.items.length) +
|
|
95
|
+
1,
|
|
96
|
+
title: item?.title ?? move.itemKey,
|
|
97
|
+
}));
|
|
98
|
+
onMove?.(move);
|
|
99
|
+
}, [lanes, onMove]);
|
|
100
|
+
const { drag, registerLane, startDrag } = useBoardDrag({
|
|
101
|
+
onDrop: (origin, target) => {
|
|
102
|
+
if (!getIsMoveMeaningful({
|
|
103
|
+
fromIndex: origin.fromIndex,
|
|
104
|
+
fromLaneKey: origin.fromLaneKey,
|
|
105
|
+
toIndex: target.index,
|
|
106
|
+
toLaneKey: target.laneKey,
|
|
107
|
+
})) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
commitMove({
|
|
111
|
+
fromIndex: origin.fromIndex,
|
|
112
|
+
fromLaneKey: origin.fromLaneKey,
|
|
113
|
+
itemKey: origin.itemKey,
|
|
114
|
+
toIndex: toSettledIndex({
|
|
115
|
+
fromIndex: origin.fromIndex,
|
|
116
|
+
fromLaneKey: origin.fromLaneKey,
|
|
117
|
+
toIndex: target.index,
|
|
118
|
+
toLaneKey: target.laneKey,
|
|
119
|
+
}),
|
|
120
|
+
toLaneKey: target.laneKey,
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
return (_jsxs("section", { "aria-label": label, className: toClassName("@container", className), children: [_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx(SegmentedControl, { className: "max-w-full flex-wrap cq-lg:hidden", items: lanes.map((lane) => ({
|
|
125
|
+
// Stacked rather than `Todo (6)`, because the count is
|
|
126
|
+
// the reason to look: a lane picker that reads
|
|
127
|
+
// "In Progress (5)" on one line is a long label with a
|
|
128
|
+
// number in it, and at 15rem three of them do not fit
|
|
129
|
+
// on any line at all.
|
|
130
|
+
label: (_jsxs("span", { className: "flex flex-col items-center leading-tight", children: [_jsx("span", { children: lane.label }), _jsx("span", { className: "font-normal text-content-muted text-xs", children: String(lane.itemCount ?? lane.items.length) })] })),
|
|
131
|
+
value: lane.key,
|
|
132
|
+
})), label: `${label} lanes`, onChange: (value) => {
|
|
133
|
+
setVisibleLaneKey(value ?? "");
|
|
134
|
+
}, selectedValue: visibleLaneKey }), _jsx("div", { className: "flex flex-col gap-4 cq-lg:flex-row cq-lg:items-start", children: lanes.map((lane) => (_jsx(BoardLaneList, { headingLevel: headingLevel, dropIndicatorOffset: drag?.target?.laneKey === lane.key
|
|
135
|
+
? drag.indicatorOffset
|
|
136
|
+
: null, isVisibleWhenNarrow: lane.key === visibleLaneKey, lane: lane, moveIcon: moveIcon, moveTargets: lanes
|
|
137
|
+
.filter((one) => one.key !== lane.key)
|
|
138
|
+
.map((one) => ({
|
|
139
|
+
key: one.key,
|
|
140
|
+
label: one.label,
|
|
141
|
+
})), onMoveToLane: onMove
|
|
142
|
+
? (itemKey, toLaneKey) => {
|
|
143
|
+
const toLane = lanes.find((one) => one.key === toLaneKey);
|
|
144
|
+
commitMove({
|
|
145
|
+
fromIndex: lane.items.findIndex((one) => one.key === itemKey),
|
|
146
|
+
fromLaneKey: lane.key,
|
|
147
|
+
itemKey,
|
|
148
|
+
// A menu move appends. There is no pointer
|
|
149
|
+
// to read a position off, and "the end of
|
|
150
|
+
// the lane you just sent it to" is the one
|
|
151
|
+
// answer that needs no second question.
|
|
152
|
+
toIndex: toLane?.items.length ?? 0,
|
|
153
|
+
toLaneKey,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
: undefined, onStartDrag: onMove
|
|
157
|
+
? (pointerEvent, origin) => {
|
|
158
|
+
startDrag(pointerEvent, {
|
|
159
|
+
...origin,
|
|
160
|
+
fromLaneKey: lane.key,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
: undefined, registerListElement: registerLane(lane.key) }, lane.key))) })] }), _jsx(VisuallyHidden, { "aria-label": `${label} activity`, role: "status", children: moveAnnouncement })] }));
|
|
164
|
+
};
|
|
165
|
+
//# sourceMappingURL=Board.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Board.js","sourceRoot":"","sources":["../../src/Board/Board.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAE7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,yCAA0C,CAAA;AAC3E,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAsC,CAAA;AAErE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAqB,CAAA;AACnD,OAAO,EACL,YAAY,EACZ,mBAAmB,EACnB,cAAc,GACf,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAyDhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,EACpB,SAAS,EACT,YAAY,GAAG,CAAC,EAChB,KAAK,EACL,KAAK,EACL,QAAQ,EACR,aAAa,EACb,MAAM,GACK,EAAa,EAAE;IAC1B,MAAM,CAAC,cAAc,EAAE,iBAAiB,CAAC,GAAG,QAAQ,CAClD,aAAa,IAAI,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,CACrC,CAAA;IAED;;;;;;OAMG;IACH,MAAM,CAAC,gBAAgB,EAAE,mBAAmB,CAAC,GAC3C,QAAQ,CAAC,EAAE,CAAC,CAAA;IAEd,MAAM,UAAU,GAAG,WAAW,CAC5B,CAAC,IAAe,EAAE,EAAE;QAClB,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CACvB,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,IAAI,CAAC,SAAS,CACpC,CAAA;QAED,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAM;QACR,CAAC;QAED,MAAM,IAAI,GAAG,KAAK;aACf,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC;aAC3B,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,IAAI,CAAC,OAAO,CAAC,CAAA;QAE1C,mBAAmB,CACjB,YAAY,CAAC;YACX,KAAK,EAAE,IAAI,CAAC,OAAO;YACnB,SAAS,EAAE,MAAM,CAAC,KAAK;YACvB,QAAQ,EACN,IAAI,CAAC,WAAW,KAAK,IAAI,CAAC,SAAS;gBACjC,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;gBAC3C,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;oBACzC,CAAC;YACP,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,IAAI,CAAC,OAAO;SACnC,CAAC,CACH,CAAA;QAED,MAAM,EAAE,CAAC,IAAI,CAAC,CAAA;IAChB,CAAC,EACD,CAAC,KAAK,EAAE,MAAM,CAAC,CAChB,CAAA;IAED,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,EAAE,GAAG,YAAY,CAAC;QACrD,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE;YACzB,IACE,CAAC,mBAAmB,CAAC;gBACnB,SAAS,EAAE,MAAM,CAAC,SAAS;gBAC3B,WAAW,EAAE,MAAM,CAAC,WAAW;gBAC/B,OAAO,EAAE,MAAM,CAAC,KAAK;gBACrB,SAAS,EAAE,MAAM,CAAC,OAAO;aAC1B,CAAC,EACF,CAAC;gBACD,OAAM;YACR,CAAC;YAED,UAAU,CAAC;gBACT,SAAS,EAAE,MAAM,CAAC,SAAS;gBAC3B,WAAW,EAAE,MAAM,CAAC,WAAW;gBAC/B,OAAO,EAAE,MAAM,CAAC,OAAO;gBACvB,OAAO,EAAE,cAAc,CAAC;oBACtB,SAAS,EAAE,MAAM,CAAC,SAAS;oBAC3B,WAAW,EAAE,MAAM,CAAC,WAAW;oBAC/B,OAAO,EAAE,MAAM,CAAC,KAAK;oBACrB,SAAS,EAAE,MAAM,CAAC,OAAO;iBAC1B,CAAC;gBACF,SAAS,EAAE,MAAM,CAAC,OAAO;aAC1B,CAAC,CAAA;QACJ,CAAC;KACF,CAAC,CAAA;IAEF,OAAO,CACL,iCACc,KAAK,EACjB,SAAS,EAAE,WAAW,CAAC,YAAY,EAAE,SAAS,CAAC,aAE/C,eAAK,SAAS,EAAC,qBAAqB,aAUlC,KAAC,gBAAgB,IACf,SAAS,EAAC,mCAAmC,EAC7C,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;4BAC1B,uDAAuD;4BACvD,+CAA+C;4BAC/C,uDAAuD;4BACvD,sDAAsD;4BACtD,sBAAsB;4BACtB,KAAK,EAAE,CACL,gBAAM,SAAS,EAAC,0CAA0C,aACxD,yBAAO,IAAI,CAAC,KAAK,GAAQ,EAEzB,eAAM,SAAS,EAAC,wCAAwC,YACrD,MAAM,CACL,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,CACpC,GACI,IACF,CACR;4BACD,KAAK,EAAE,IAAI,CAAC,GAAG;yBAChB,CAAC,CAAC,EACH,KAAK,EAAE,GAAG,KAAK,QAAQ,EACvB,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE;4BAClB,iBAAiB,CAAC,KAAK,IAAI,EAAE,CAAC,CAAA;wBAChC,CAAC,EACD,aAAa,EAAE,cAAc,GAC7B,EAEF,cAAK,SAAS,EAAC,sDAAsD,YAClE,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CACnB,KAAC,aAAa,IACZ,YAAY,EAAE,YAAY,EAC1B,mBAAmB,EACjB,IAAI,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG;gCAChC,CAAC,CAAC,IAAI,CAAC,eAAe;gCACtB,CAAC,CAAC,IAAI,EAEV,mBAAmB,EACjB,IAAI,CAAC,GAAG,KAAK,cAAc,EAG7B,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,QAAQ,EAClB,WAAW,EAAE,KAAK;iCACf,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,IAAI,CAAC,GAAG,CAAC;iCACrC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;gCACb,GAAG,EAAE,GAAG,CAAC,GAAG;gCACZ,KAAK,EAAE,GAAG,CAAC,KAAK;6BACjB,CAAC,CAAC,EACL,YAAY,EACV,MAAM;gCACJ,CAAC,CAAC,CAAC,OAAO,EAAE,SAAS,EAAE,EAAE;oCACrB,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CACvB,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,SAAS,CAC/B,CAAA;oCAED,UAAU,CAAC;wCACT,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,CAC7B,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,KAAK,OAAO,CAC7B;wCACD,WAAW,EAAE,IAAI,CAAC,GAAG;wCACrB,OAAO;wCACP,2CAA2C;wCAC3C,0CAA0C;wCAC1C,2CAA2C;wCAC3C,wCAAwC;wCACxC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC;wCAClC,SAAS;qCACV,CAAC,CAAA;gCACJ,CAAC;gCACH,CAAC,CAAC,SAAS,EAEf,WAAW,EACT,MAAM;gCACJ,CAAC,CAAC,CAAC,YAAY,EAAE,MAAM,EAAE,EAAE;oCACvB,SAAS,CAAC,YAAY,EAAE;wCACtB,GAAG,MAAM;wCACT,WAAW,EAAE,IAAI,CAAC,GAAG;qCACtB,CAAC,CAAA;gCACJ,CAAC;gCACH,CAAC,CAAC,SAAS,EAEf,mBAAmB,EAAE,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,IA1CtC,IAAI,CAAC,GAAG,CA2Cb,CACH,CAAC,GACE,IACF,EAQN,KAAC,cAAc,kBACD,GAAG,KAAK,WAAW,EAC/B,IAAI,EAAC,QAAQ,YAEZ,gBAAgB,GACF,IACT,CACX,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { IntentName } from "@charcuterie/tokens";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
export type BoardItem = {
|
|
4
|
+
/**
|
|
5
|
+
* The leading colour bar. Priority, in the consumer this was built
|
|
6
|
+
* for — *"Priority is styled as a leading colour bar, and **never**
|
|
7
|
+
* a coloured word before the title."*
|
|
8
|
+
*
|
|
9
|
+
* Pair it with `accentLabel`. A bar is colour and nothing else, so
|
|
10
|
+
* on its own it fails WCAG 1.4.1 outright and is invisible to
|
|
11
|
+
* every screen reader; the label rides along in a
|
|
12
|
+
* `VisuallyHidden` so the same fact reaches both channels. Docket's
|
|
13
|
+
* first board painted these `--color-danger-9` — a Radix-style
|
|
14
|
+
* scale this token set has never had — so every bar was
|
|
15
|
+
* transparent while every "is it rendered" assertion passed. The
|
|
16
|
+
* families here are `--color-intent-<intent>-<role>`; there is no
|
|
17
|
+
* numeric scale to reach for.
|
|
18
|
+
*/
|
|
19
|
+
accentIntent?: IntentName;
|
|
20
|
+
/** What the bar means, in words. "Priority 0". */
|
|
21
|
+
accentLabel?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Under the title and metadata, spanning the card: a live run
|
|
24
|
+
* line, a gate explanation, a due date. A `ProgressBar` goes here.
|
|
25
|
+
*/
|
|
26
|
+
footer?: ReactNode;
|
|
27
|
+
/** Where the title navigates. Routed through `RouterLinkProvider`. */
|
|
28
|
+
href?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Visibly present but not actionable — Docket's *gated*: a task
|
|
31
|
+
* whose phase has not opened yet. Paint only; it does not disable
|
|
32
|
+
* the title, because being able to read a thing you cannot start
|
|
33
|
+
* yet is the entire point of showing it.
|
|
34
|
+
*/
|
|
35
|
+
isDimmed?: boolean;
|
|
36
|
+
key: string;
|
|
37
|
+
/**
|
|
38
|
+
* Small status marks rendered immediately before the title —
|
|
39
|
+
* running, blocked, stale. Each one needs its own accessible name;
|
|
40
|
+
* a bare coloured dot is the same WCAG 1.4.1 failure as the bar.
|
|
41
|
+
*/
|
|
42
|
+
marks?: ReactNode;
|
|
43
|
+
/**
|
|
44
|
+
* Chips. Under the title when the lane is narrow, right-aligned
|
|
45
|
+
* beside it when there is room. `Badge` is what goes here.
|
|
46
|
+
*/
|
|
47
|
+
meta?: ReactNode;
|
|
48
|
+
/** Fires when there is no `href` — for a board that opens a modal. */
|
|
49
|
+
onSelect?: () => void;
|
|
50
|
+
title: string;
|
|
51
|
+
/** The trailing cluster: an assignee avatar, an elapsed-time chip. */
|
|
52
|
+
trailing?: ReactNode;
|
|
53
|
+
};
|
|
54
|
+
export type BoardCardProps = {
|
|
55
|
+
item: BoardItem;
|
|
56
|
+
laneLabel: string;
|
|
57
|
+
/**
|
|
58
|
+
* Shown on the move handle instead of the word "Move". The
|
|
59
|
+
* library ships no icons, so the default is words — a `⋮` renders
|
|
60
|
+
* as nothing where the font lacks it, which includes the kiosk
|
|
61
|
+
* image and the ePaper build. An app that already owns a glyph
|
|
62
|
+
* set passes one and buys back the ~55px per row that a word
|
|
63
|
+
* costs, which in a 500px lane is the difference between twenty
|
|
64
|
+
* characters of title and thirty.
|
|
65
|
+
*/
|
|
66
|
+
moveIcon?: ReactNode;
|
|
67
|
+
/** The lanes this card can be sent to — never its own. */
|
|
68
|
+
moveTargets: readonly {
|
|
69
|
+
key: string;
|
|
70
|
+
label: string;
|
|
71
|
+
}[];
|
|
72
|
+
/** Absent on a read-only board, and then so is the handle. */
|
|
73
|
+
onMoveToLane?: (toLaneKey: string) => void;
|
|
74
|
+
onStartDrag?: (pointerEvent: {
|
|
75
|
+
clientX: number;
|
|
76
|
+
clientY: number;
|
|
77
|
+
currentTarget: EventTarget & Element;
|
|
78
|
+
pointerId: number;
|
|
79
|
+
}) => void;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* One row on the board — and *row* is only what it is at some
|
|
83
|
+
* widths.
|
|
84
|
+
*
|
|
85
|
+
* ### The three shapes are one component, chosen by the lane
|
|
86
|
+
*
|
|
87
|
+
* The requirement this was built against is unusually specific, and
|
|
88
|
+
* it is specific because the owner answered "which row style?" with
|
|
89
|
+
* *"all three, depending"*:
|
|
90
|
+
*
|
|
91
|
+
* | The lane is | The card is |
|
|
92
|
+
* | --- | --- |
|
|
93
|
+
* | narrower than `cq-sm` (24rem) | **two lines** — title clamped to two, metadata underneath, only the trailing slot beside it |
|
|
94
|
+
* | `cq-sm` to `cq-lg` | **one line** — title ellipsised, metadata right-aligned |
|
|
95
|
+
* | `cq-lg` (48rem) and wider | **a card** — its own border, surface and elevation, instead of a divider in a list |
|
|
96
|
+
*
|
|
97
|
+
* Every one of those is a **container** query against the lane's
|
|
98
|
+
* list, never a media query, and the difference is not pedantry: a
|
|
99
|
+
* lane in a three-up board is narrow on a 4K display, and the owner
|
|
100
|
+
* browses zoomed in, so a 1500px window at 175% zoom is ~860
|
|
101
|
+
* effective CSS pixels. Window width is not a proxy for the space
|
|
102
|
+
* this card has. A `@media (max-width: …)` anywhere in this file
|
|
103
|
+
* would be a defect.
|
|
104
|
+
*
|
|
105
|
+
* It also dissolved a bug rather than only satisfying a preference:
|
|
106
|
+
* a *fixed* two-line row collapsed its title to `a…` inside a narrow
|
|
107
|
+
* lane, because the metadata beside it could not shrink. A card
|
|
108
|
+
* sized by its container has no such state to be in.
|
|
109
|
+
*
|
|
110
|
+
* ### The move handle is a button first and a drag handle second
|
|
111
|
+
*
|
|
112
|
+
* One control, two drivers. Pressing it opens a `Menu` of the other
|
|
113
|
+
* lanes — the keyboard and screen-reader path, which also works on
|
|
114
|
+
* touch, and works in the Narrow View where the other lanes are not
|
|
115
|
+
* on screen at all. Dragging from it moves the card directly. Both
|
|
116
|
+
* commit through the same callback, so there is no second code path
|
|
117
|
+
* to keep honest and neither is a fallback for the other.
|
|
118
|
+
*/
|
|
119
|
+
export declare const BoardCard: ({ item, laneLabel, moveIcon, moveTargets, onMoveToLane, onStartDrag, }: BoardCardProps) => ReactNode;
|
|
120
|
+
//# sourceMappingURL=BoardCard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BoardCard.d.ts","sourceRoot":"","sources":["../../src/Board/BoardCard.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAatC,MAAM,MAAM,SAAS,GAAG;IACtB;;;;;;;;;;;;;;OAcG;IACH,YAAY,CAAC,EAAE,UAAU,CAAA;IACzB,kDAAkD;IAClD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB,sEAAsE;IACtE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,GAAG,EAAE,MAAM,CAAA;IACX;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB;;;OAGG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,sEAAsE;IACtE,QAAQ,CAAC,EAAE,SAAS,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,SAAS,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAA;IACpB,0DAA0D;IAC1D,WAAW,EAAE,SAAS;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACtD,8DAA8D;IAC9D,YAAY,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1C,WAAW,CAAC,EAAE,CAAC,YAAY,EAAE;QAC3B,OAAO,EAAE,MAAM,CAAA;QACf,OAAO,EAAE,MAAM,CAAA;QACf,aAAa,EAAE,WAAW,GAAG,OAAO,CAAA;QACpC,SAAS,EAAE,MAAM,CAAA;KAClB,KAAK,IAAI,CAAA;CACX,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,eAAO,MAAM,SAAS,GAAI,wEAOvB,cAAc,KAAG,SAyJnB,CAAA"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Button } from "../Button/Button.js";
|
|
4
|
+
import { FOCUS_RING_CLASS, INTENT_SOLID_FILL_CLASS, } from "../intentStyles.js";
|
|
5
|
+
import { Menu } from "../Menu/Menu.js";
|
|
6
|
+
import { TextLink } from "../TextLink/TextLink.js";
|
|
7
|
+
import { toClassName } from "../toClassName.js";
|
|
8
|
+
import { VisuallyHidden } from "../VisuallyHidden/VisuallyHidden.js";
|
|
9
|
+
/**
|
|
10
|
+
* One row on the board — and *row* is only what it is at some
|
|
11
|
+
* widths.
|
|
12
|
+
*
|
|
13
|
+
* ### The three shapes are one component, chosen by the lane
|
|
14
|
+
*
|
|
15
|
+
* The requirement this was built against is unusually specific, and
|
|
16
|
+
* it is specific because the owner answered "which row style?" with
|
|
17
|
+
* *"all three, depending"*:
|
|
18
|
+
*
|
|
19
|
+
* | The lane is | The card is |
|
|
20
|
+
* | --- | --- |
|
|
21
|
+
* | narrower than `cq-sm` (24rem) | **two lines** — title clamped to two, metadata underneath, only the trailing slot beside it |
|
|
22
|
+
* | `cq-sm` to `cq-lg` | **one line** — title ellipsised, metadata right-aligned |
|
|
23
|
+
* | `cq-lg` (48rem) and wider | **a card** — its own border, surface and elevation, instead of a divider in a list |
|
|
24
|
+
*
|
|
25
|
+
* Every one of those is a **container** query against the lane's
|
|
26
|
+
* list, never a media query, and the difference is not pedantry: a
|
|
27
|
+
* lane in a three-up board is narrow on a 4K display, and the owner
|
|
28
|
+
* browses zoomed in, so a 1500px window at 175% zoom is ~860
|
|
29
|
+
* effective CSS pixels. Window width is not a proxy for the space
|
|
30
|
+
* this card has. A `@media (max-width: …)` anywhere in this file
|
|
31
|
+
* would be a defect.
|
|
32
|
+
*
|
|
33
|
+
* It also dissolved a bug rather than only satisfying a preference:
|
|
34
|
+
* a *fixed* two-line row collapsed its title to `a…` inside a narrow
|
|
35
|
+
* lane, because the metadata beside it could not shrink. A card
|
|
36
|
+
* sized by its container has no such state to be in.
|
|
37
|
+
*
|
|
38
|
+
* ### The move handle is a button first and a drag handle second
|
|
39
|
+
*
|
|
40
|
+
* One control, two drivers. Pressing it opens a `Menu` of the other
|
|
41
|
+
* lanes — the keyboard and screen-reader path, which also works on
|
|
42
|
+
* touch, and works in the Narrow View where the other lanes are not
|
|
43
|
+
* on screen at all. Dragging from it moves the card directly. Both
|
|
44
|
+
* commit through the same callback, so there is no second code path
|
|
45
|
+
* to keep honest and neither is a fallback for the other.
|
|
46
|
+
*/
|
|
47
|
+
export const BoardCard = ({ item, laneLabel, moveIcon, moveTargets, onMoveToLane, onStartDrag, }) => {
|
|
48
|
+
const [isMenuVisible, setIsMenuVisible] = useState(false);
|
|
49
|
+
const titleContent = (_jsx("span", { className: "line-clamp-2 cq-sm:line-clamp-none cq-sm:block cq-sm:truncate", children: item.title }));
|
|
50
|
+
return (_jsxs("li", { className: toClassName("group relative flex gap-2 border-border-subtle border-b py-2 pe-1 last:border-b-0",
|
|
51
|
+
// At `cq-lg` the list stops being a list of rows: the
|
|
52
|
+
// divider goes and each card grows its own box. The *list*
|
|
53
|
+
// cannot do this to itself — it is the container, and a
|
|
54
|
+
// container query never matches its own container.
|
|
55
|
+
"cq-lg:mb-2 cq-lg:rounded-lg cq-lg:border cq-lg:bg-surface-raised cq-lg:p-3 cq-lg:shadow-low", item.isDimmed && "opacity-60"), "data-board-card": item.key, children: [item.accentIntent ? (_jsx("span", { className: toClassName("w-1 shrink-0 self-stretch rounded-full", INTENT_SOLID_FILL_CLASS[item.accentIntent]), children: item.accentLabel ? (_jsx(VisuallyHidden, { children: item.accentLabel })) : null })) : null, _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [_jsxs("div", { className: "flex min-w-0 items-start gap-2", children: [_jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1 cq-sm:flex-row cq-sm:items-center cq-sm:gap-2", children: [_jsxs("div", { className: "flex min-w-0 flex-1 items-start gap-1.5 text-sm leading-snug", children: [item.marks ? (_jsx("span", { className: "flex shrink-0 items-center gap-1 pt-1", children: item.marks })) : null, item.href ? (_jsx(TextLink, { appearance: "standalone", className: "min-w-0 max-w-full font-normal", href: item.href, intent: "neutral", children: titleContent })) : item.onSelect ? (_jsx("button", { className: toClassName("min-w-0 max-w-full cursor-pointer rounded-xs text-start text-intent-neutral-content hover:underline", FOCUS_RING_CLASS), onClick: item.onSelect, type: "button", children: titleContent })) : (titleContent)] }), item.meta ? (_jsx("div", { className: "flex min-w-0 flex-wrap items-center gap-1 cq-sm:shrink-0 cq-sm:flex-nowrap cq-sm:justify-end", children: item.meta })) : null] }), item.trailing || onMoveToLane ? (_jsxs("div", { className: "flex shrink-0 items-center gap-1", children: [item.trailing, onMoveToLane && moveTargets.length > 0 ? (_jsx(Menu, { isVisible: isMenuVisible, items: moveTargets.map((target) => ({
|
|
56
|
+
key: target.key,
|
|
57
|
+
label: target.label,
|
|
58
|
+
onSelect: () => {
|
|
59
|
+
onMoveToLane(target.key);
|
|
60
|
+
},
|
|
61
|
+
})), onDismiss: () => {
|
|
62
|
+
setIsMenuVisible(false);
|
|
63
|
+
}, placement: "bottom-end", trigger: _jsxs(Button, { appearance: "ghost", intent: "neutral",
|
|
64
|
+
// `touch-none` so a touch drag is a drag
|
|
65
|
+
// rather than a page scroll. Only the handle
|
|
66
|
+
// needs it; the rest of the gesture is covered
|
|
67
|
+
// by `preventDefault` on the captured
|
|
68
|
+
// `pointermove`.
|
|
69
|
+
className: "touch-none opacity-70 group-hover:opacity-100", onClick: () => {
|
|
70
|
+
setIsMenuVisible((isVisible) => !isVisible);
|
|
71
|
+
}, onPointerDown: (pointerEvent) => {
|
|
72
|
+
onStartDrag?.(pointerEvent);
|
|
73
|
+
}, size: "sm", sizing: moveIcon ? "icon" : "control", children: [moveIcon ?? "Move", _jsx(VisuallyHidden, { children: moveIcon
|
|
74
|
+
? `Move ${item.title}, currently in ${laneLabel}`
|
|
75
|
+
: ` ${item.title}, currently in ${laneLabel}` })] }) })) : null] })) : null] }), item.footer ? (_jsx("div", { className: "min-w-0 text-content-secondary text-xs", children: item.footer })) : null] })] }));
|
|
76
|
+
};
|
|
77
|
+
//# sourceMappingURL=BoardCard.js.map
|