@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.
Files changed (113) hide show
  1. package/README.md +10 -0
  2. package/dist/Badge/Badge.d.ts +33 -3
  3. package/dist/Badge/Badge.d.ts.map +1 -1
  4. package/dist/Badge/Badge.js +20 -2
  5. package/dist/Badge/Badge.js.map +1 -1
  6. package/dist/Board/Board.d.ts +119 -0
  7. package/dist/Board/Board.d.ts.map +1 -0
  8. package/dist/Board/Board.js +165 -0
  9. package/dist/Board/Board.js.map +1 -0
  10. package/dist/Board/BoardCard.d.ts +120 -0
  11. package/dist/Board/BoardCard.d.ts.map +1 -0
  12. package/dist/Board/BoardCard.js +77 -0
  13. package/dist/Board/BoardCard.js.map +1 -0
  14. package/dist/Board/BoardLaneList.d.ts +103 -0
  15. package/dist/Board/BoardLaneList.d.ts.map +1 -0
  16. package/dist/Board/BoardLaneList.js +69 -0
  17. package/dist/Board/BoardLaneList.js.map +1 -0
  18. package/dist/Board/boardMove.d.ts +116 -0
  19. package/dist/Board/boardMove.d.ts.map +1 -0
  20. package/dist/Board/boardMove.js +116 -0
  21. package/dist/Board/boardMove.js.map +1 -0
  22. package/dist/Board/useBoardDrag.d.ts +44 -0
  23. package/dist/Board/useBoardDrag.d.ts.map +1 -0
  24. package/dist/Board/useBoardDrag.js +237 -0
  25. package/dist/Board/useBoardDrag.js.map +1 -0
  26. package/dist/Checkbox/Checkbox.d.ts.map +1 -1
  27. package/dist/Checkbox/Checkbox.js +15 -4
  28. package/dist/Checkbox/Checkbox.js.map +1 -1
  29. package/dist/DataTable/DataTable.d.ts +159 -0
  30. package/dist/DataTable/DataTable.d.ts.map +1 -0
  31. package/dist/DataTable/DataTable.js +197 -0
  32. package/dist/DataTable/DataTable.js.map +1 -0
  33. package/dist/DatePicker/DateGrid.d.ts +79 -0
  34. package/dist/DatePicker/DateGrid.d.ts.map +1 -0
  35. package/dist/DatePicker/DateGrid.js +220 -0
  36. package/dist/DatePicker/DateGrid.js.map +1 -0
  37. package/dist/DatePicker/DatePicker.d.ts +138 -0
  38. package/dist/DatePicker/DatePicker.d.ts.map +1 -0
  39. package/dist/DatePicker/DatePicker.js +384 -0
  40. package/dist/DatePicker/DatePicker.js.map +1 -0
  41. package/dist/DatePicker/datePresets.d.ts +29 -0
  42. package/dist/DatePicker/datePresets.d.ts.map +1 -0
  43. package/dist/DatePicker/datePresets.js +28 -0
  44. package/dist/DatePicker/datePresets.js.map +1 -0
  45. package/dist/DatePicker/parseDateInput.d.ts +106 -0
  46. package/dist/DatePicker/parseDateInput.d.ts.map +1 -0
  47. package/dist/DatePicker/parseDateInput.js +419 -0
  48. package/dist/DatePicker/parseDateInput.js.map +1 -0
  49. package/dist/DatePicker/plainDate.d.ts +167 -0
  50. package/dist/DatePicker/plainDate.d.ts.map +1 -0
  51. package/dist/DatePicker/plainDate.js +272 -0
  52. package/dist/DatePicker/plainDate.js.map +1 -0
  53. package/dist/MarkdownEditor/MarkdownEditor.d.ts +147 -0
  54. package/dist/MarkdownEditor/MarkdownEditor.d.ts.map +1 -0
  55. package/dist/MarkdownEditor/MarkdownEditor.js +492 -0
  56. package/dist/MarkdownEditor/MarkdownEditor.js.map +1 -0
  57. package/dist/MarkdownEditor/markdownCommands.d.ts +202 -0
  58. package/dist/MarkdownEditor/markdownCommands.d.ts.map +1 -0
  59. package/dist/MarkdownEditor/markdownCommands.js +379 -0
  60. package/dist/MarkdownEditor/markdownCommands.js.map +1 -0
  61. package/dist/MarkdownEditor/markdownSpans.d.ts +92 -0
  62. package/dist/MarkdownEditor/markdownSpans.d.ts.map +1 -0
  63. package/dist/MarkdownEditor/markdownSpans.js +303 -0
  64. package/dist/MarkdownEditor/markdownSpans.js.map +1 -0
  65. package/dist/categoricalStyles.d.ts +53 -0
  66. package/dist/categoricalStyles.d.ts.map +1 -0
  67. package/dist/categoricalStyles.js +195 -0
  68. package/dist/categoricalStyles.js.map +1 -0
  69. package/dist/index.d.ts +22 -0
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +11 -0
  72. package/dist/index.js.map +1 -1
  73. package/package.json +2 -2
  74. package/src/Badge/Badge.mdx +46 -0
  75. package/src/Badge/Badge.stories.tsx +98 -4
  76. package/src/Badge/Badge.test.tsx +157 -2
  77. package/src/Badge/Badge.tsx +49 -3
  78. package/src/Board/Board.mdx +199 -0
  79. package/src/Board/Board.stories.tsx +707 -0
  80. package/src/Board/Board.test.tsx +310 -0
  81. package/src/Board/Board.tsx +333 -0
  82. package/src/Board/BoardCard.tsx +290 -0
  83. package/src/Board/BoardLaneList.tsx +275 -0
  84. package/src/Board/boardMove.test.ts +213 -0
  85. package/src/Board/boardMove.ts +225 -0
  86. package/src/Board/useBoardDrag.ts +413 -0
  87. package/src/Checkbox/Checkbox.tsx +34 -3
  88. package/src/DataTable/DataTable.mdx +123 -0
  89. package/src/DataTable/DataTable.stories.tsx +411 -0
  90. package/src/DataTable/DataTable.test.tsx +386 -0
  91. package/src/DataTable/DataTable.tsx +497 -0
  92. package/src/DatePicker/DateGrid.tsx +467 -0
  93. package/src/DatePicker/DatePicker.mdx +170 -0
  94. package/src/DatePicker/DatePicker.stories.tsx +268 -0
  95. package/src/DatePicker/DatePicker.test.tsx +455 -0
  96. package/src/DatePicker/DatePicker.tsx +852 -0
  97. package/src/DatePicker/datePresets.ts +34 -0
  98. package/src/DatePicker/parseDateInput.test.ts +187 -0
  99. package/src/DatePicker/parseDateInput.ts +629 -0
  100. package/src/DatePicker/plainDate.test.ts +375 -0
  101. package/src/DatePicker/plainDate.ts +465 -0
  102. package/src/MarkdownEditor/MarkdownEditor.mdx +189 -0
  103. package/src/MarkdownEditor/MarkdownEditor.stories.tsx +267 -0
  104. package/src/MarkdownEditor/MarkdownEditor.test.tsx +627 -0
  105. package/src/MarkdownEditor/MarkdownEditor.tsx +862 -0
  106. package/src/MarkdownEditor/markdownCommands.test.ts +384 -0
  107. package/src/MarkdownEditor/markdownCommands.ts +566 -0
  108. package/src/MarkdownEditor/markdownSpans.test.ts +217 -0
  109. package/src/MarkdownEditor/markdownSpans.ts +458 -0
  110. package/src/argTypes.storyHelpers.ts +39 -0
  111. package/src/categoricalStyles.ts +244 -0
  112. package/src/index.ts +89 -0
  113. 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,
@@ -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,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AACrD,OAAO,KAAK,EACV,qBAAqB,EACrB,SAAS,EACV,MAAM,OAAO,CAAA;AAEd,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,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,KAAK,GAAI,wFAUnB,UAAU,KAAG,SAyDf,CAAA"}
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"}
@@ -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], INTENT_APPEARANCE_CLASS[intent][appearance], className),
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
@@ -1 +1 @@
1
- {"version":3,"file":"Badge.js","sourceRoot":"","sources":["../../src/Badge/Badge.tsx"],"names":[],"mappings":";AAOA,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;AAqBxD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,EACpB,UAAU,GAAG,MAAM,EACnB,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,EACtB,uBAAuB,CAAC,MAAM,CAAC,CAAC,UAAU,CAAC,EAC3C,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"}
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":"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"}
@@ -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