@charcuterie/ui 0.1.1 → 1.0.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 (115) hide show
  1. package/README.md +9 -0
  2. package/dist/Accordion/Accordion.d.ts +82 -0
  3. package/dist/Accordion/Accordion.d.ts.map +1 -0
  4. package/dist/Accordion/Accordion.js +108 -0
  5. package/dist/Accordion/Accordion.js.map +1 -0
  6. package/dist/Accordion/AccordionSection.d.ts +28 -0
  7. package/dist/Accordion/AccordionSection.d.ts.map +1 -0
  8. package/dist/Accordion/AccordionSection.js +53 -0
  9. package/dist/Accordion/AccordionSection.js.map +1 -0
  10. package/dist/Field/Field.d.ts +104 -0
  11. package/dist/Field/Field.d.ts.map +1 -0
  12. package/dist/Field/Field.js +115 -0
  13. package/dist/Field/Field.js.map +1 -0
  14. package/dist/Field/FieldGroup.d.ts +58 -0
  15. package/dist/Field/FieldGroup.d.ts.map +1 -0
  16. package/dist/Field/FieldGroup.js +65 -0
  17. package/dist/Field/FieldGroup.js.map +1 -0
  18. package/dist/FileDropZone/FileDropZone.d.ts +63 -0
  19. package/dist/FileDropZone/FileDropZone.d.ts.map +1 -0
  20. package/dist/FileDropZone/FileDropZone.js +122 -0
  21. package/dist/FileDropZone/FileDropZone.js.map +1 -0
  22. package/dist/LogViewer/LogViewer.d.ts +90 -0
  23. package/dist/LogViewer/LogViewer.d.ts.map +1 -0
  24. package/dist/LogViewer/LogViewer.js +165 -0
  25. package/dist/LogViewer/LogViewer.js.map +1 -0
  26. package/dist/Menu/Menu.d.ts +81 -0
  27. package/dist/Menu/Menu.d.ts.map +1 -0
  28. package/dist/Menu/Menu.js +199 -0
  29. package/dist/Menu/Menu.js.map +1 -0
  30. package/dist/Menu/MenuAction.d.ts +24 -0
  31. package/dist/Menu/MenuAction.d.ts.map +1 -0
  32. package/dist/Menu/MenuAction.js +37 -0
  33. package/dist/Menu/MenuAction.js.map +1 -0
  34. package/dist/Select/Select.d.ts +87 -0
  35. package/dist/Select/Select.d.ts.map +1 -0
  36. package/dist/Select/Select.js +51 -0
  37. package/dist/Select/Select.js.map +1 -0
  38. package/dist/SortableTableHeader/SortableTableHeader.d.ts +61 -0
  39. package/dist/SortableTableHeader/SortableTableHeader.d.ts.map +1 -0
  40. package/dist/SortableTableHeader/SortableTableHeader.js +56 -0
  41. package/dist/SortableTableHeader/SortableTableHeader.js.map +1 -0
  42. package/dist/Toast/Toast.d.ts +54 -0
  43. package/dist/Toast/Toast.d.ts.map +1 -0
  44. package/dist/Toast/Toast.js +121 -0
  45. package/dist/Toast/Toast.js.map +1 -0
  46. package/dist/Toast/ToastRegion.d.ts +57 -0
  47. package/dist/Toast/ToastRegion.d.ts.map +1 -0
  48. package/dist/Toast/ToastRegion.js +41 -0
  49. package/dist/Toast/ToastRegion.js.map +1 -0
  50. package/dist/Toast/toastLifecycle.d.ts +34 -0
  51. package/dist/Toast/toastLifecycle.d.ts.map +1 -0
  52. package/dist/Toast/toastLifecycle.js +25 -0
  53. package/dist/Toast/toastLifecycle.js.map +1 -0
  54. package/dist/Tooltip/Tooltip.d.ts +82 -0
  55. package/dist/Tooltip/Tooltip.d.ts.map +1 -0
  56. package/dist/Tooltip/Tooltip.js +124 -0
  57. package/dist/Tooltip/Tooltip.js.map +1 -0
  58. package/dist/index.d.ts +26 -0
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +13 -0
  61. package/dist/index.js.map +1 -1
  62. package/dist/mountStory.testHelpers.d.ts +0 -11
  63. package/dist/mountStory.testHelpers.d.ts.map +1 -1
  64. package/dist/mountStory.testHelpers.js +27 -0
  65. package/dist/mountStory.testHelpers.js.map +1 -1
  66. package/dist/slotProps.d.ts +110 -0
  67. package/dist/slotProps.d.ts.map +1 -0
  68. package/dist/slotProps.js +39 -0
  69. package/dist/slotProps.js.map +1 -0
  70. package/package.json +3 -3
  71. package/src/Accordion/Accordion.mdx +83 -0
  72. package/src/Accordion/Accordion.stories.tsx +146 -0
  73. package/src/Accordion/Accordion.test.tsx +163 -0
  74. package/src/Accordion/Accordion.tsx +182 -0
  75. package/src/Accordion/AccordionSection.tsx +163 -0
  76. package/src/Field/Field.mdx +144 -0
  77. package/src/Field/Field.stories.tsx +280 -0
  78. package/src/Field/Field.test.tsx +344 -0
  79. package/src/Field/Field.tsx +212 -0
  80. package/src/Field/FieldGroup.tsx +138 -0
  81. package/src/FileDropZone/FileDropZone.mdx +78 -0
  82. package/src/FileDropZone/FileDropZone.stories.tsx +136 -0
  83. package/src/FileDropZone/FileDropZone.test.tsx +230 -0
  84. package/src/FileDropZone/FileDropZone.tsx +234 -0
  85. package/src/LogViewer/LogViewer.mdx +123 -0
  86. package/src/LogViewer/LogViewer.stories.tsx +169 -0
  87. package/src/LogViewer/LogViewer.test.tsx +236 -0
  88. package/src/LogViewer/LogViewer.tsx +261 -0
  89. package/src/Menu/Menu.mdx +103 -0
  90. package/src/Menu/Menu.stories.tsx +161 -0
  91. package/src/Menu/Menu.test.tsx +202 -0
  92. package/src/Menu/Menu.tsx +300 -0
  93. package/src/Menu/MenuAction.tsx +95 -0
  94. package/src/Select/Select.mdx +74 -0
  95. package/src/Select/Select.stories.tsx +126 -0
  96. package/src/Select/Select.test.tsx +129 -0
  97. package/src/Select/Select.tsx +200 -0
  98. package/src/SortableTableHeader/SortableTableHeader.mdx +67 -0
  99. package/src/SortableTableHeader/SortableTableHeader.stories.tsx +207 -0
  100. package/src/SortableTableHeader/SortableTableHeader.test.tsx +180 -0
  101. package/src/SortableTableHeader/SortableTableHeader.tsx +131 -0
  102. package/src/Toast/Toast.mdx +84 -0
  103. package/src/Toast/Toast.stories.tsx +192 -0
  104. package/src/Toast/Toast.test.tsx +217 -0
  105. package/src/Toast/Toast.tsx +224 -0
  106. package/src/Toast/ToastRegion.tsx +98 -0
  107. package/src/Toast/toastLifecycle.ts +45 -0
  108. package/src/Tooltip/Tooltip.mdx +69 -0
  109. package/src/Tooltip/Tooltip.stories.tsx +104 -0
  110. package/src/Tooltip/Tooltip.test.tsx +138 -0
  111. package/src/Tooltip/Tooltip.tsx +193 -0
  112. package/src/index.ts +47 -0
  113. package/src/mountStory.testHelpers.ts +30 -0
  114. package/src/slotProps.ts +143 -0
  115. package/src/sourceRules.test.ts +221 -30
package/dist/index.js CHANGED
@@ -30,26 +30,39 @@
30
30
  * `@charcuterie/tokens/theme.css` by its own name, which is what
31
31
  * this package's README has always told it to do.
32
32
  */
33
+ export { Accordion } from "./Accordion/Accordion.js";
33
34
  export { Alert } from "./Alert/Alert.js";
34
35
  export { Badge } from "./Badge/Badge.js";
35
36
  export { Button } from "./Button/Button.js";
36
37
  export { Card } from "./Card/Card.js";
37
38
  export { BADGE_SIZE_CLASS, CONTROL_SIZE_CLASS, DOT_SIZE_CLASS, ICON_CONTROL_SIZE_CLASS, MIN_TOUCH_TARGET_CLASS, SPINNER_SIZE_CLASS, } from "./controlStyles.js";
38
39
  export { EmptyState } from "./EmptyState/EmptyState.js";
40
+ export { Field } from "./Field/Field.js";
41
+ export { FieldGroup } from "./Field/FieldGroup.js";
42
+ export { FileDropZone } from "./FileDropZone/FileDropZone.js";
39
43
  export { IconButton } from "./IconButton/IconButton.js";
40
44
  export { DISABLED_CLASS, FOCUS_RING_CLASS, INTENT_APPEARANCE_CLASS, INTENT_CONTENT_CLASS, INTENT_HOVER_CLASS, INTENT_SOLID_FILL_CLASS, } from "./intentStyles.js";
41
45
  export { LiveStatusIndicator } from "./LiveStatusIndicator/LiveStatusIndicator.js";
46
+ export { LogViewer } from "./LogViewer/LogViewer.js";
42
47
  export { MediaTile } from "./MediaTile/MediaTile.js";
43
48
  export { mediaTransitions } from "./MediaTile/mediaStatus.js";
49
+ export { Menu } from "./Menu/Menu.js";
44
50
  export { Modal } from "./Modal/Modal.js";
45
51
  export { Popover } from "./Popover/Popover.js";
46
52
  export { ProgressBar } from "./ProgressBar/ProgressBar.js";
47
53
  export { getProgressIntent, toProgressPercent, toProgressValue, } from "./ProgressBar/progressValue.js";
48
54
  export { SegmentedControl } from "./SegmentedControl/SegmentedControl.js";
55
+ export { Select } from "./Select/Select.js";
49
56
  export { Skeleton } from "./Skeleton/Skeleton.js";
57
+ export { SortableTableHeader } from "./SortableTableHeader/SortableTableHeader.js";
50
58
  export { Spinner } from "./Spinner/Spinner.js";
59
+ export { mergeSlotProps } from "./slotProps.js";
51
60
  export { getAsyncIntent, getAsyncLabel, getConnectionIntent, getConnectionLabel, getIsConnectionBusy, } from "./statusIntent.js";
52
61
  export { Tabs } from "./Tabs/Tabs.js";
62
+ export { Toast } from "./Toast/Toast.js";
63
+ export { ToastRegion } from "./Toast/ToastRegion.js";
64
+ export { toastTransitions } from "./Toast/toastLifecycle.js";
65
+ export { Tooltip } from "./Tooltip/Tooltip.js";
53
66
  export { toClassName } from "./toClassName.js";
54
67
  export { VisuallyHidden } from "./VisuallyHidden/VisuallyHidden.js";
55
68
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAMH,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAEzC,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAEzC,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAqB,CAAA;AAM5C,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAiB,CAAA;AAEtC,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,EACd,uBAAuB,EACvB,sBAAsB,EACtB,kBAAkB,GACnB,MAAM,oBAAoB,CAAA;AAK3B,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA6B,CAAA;AAExD,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA6B,CAAA;AAExD,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,uBAAuB,EACvB,oBAAoB,EACpB,kBAAkB,EAClB,uBAAuB,GACxB,MAAM,mBAAmB,CAAA;AAK1B,OAAO,EAAE,mBAAmB,EAAE,MAAM,8CAA+C,CAAA;AAKnF,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA2B,CAAA;AAErD,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAK7D,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAEzC,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAuB,CAAA;AAK/C,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA+B,CAAA;AAE3D,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,GAChB,MAAM,gCAAgC,CAAA;AAKvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,wCAAyC,CAAA;AAK1E,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAyB,CAAA;AAElD,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAuB,CAAA;AAC/C,OAAO,EACL,cAAc,EACd,aAAa,EACb,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,mBAAmB,CAAA;AAO1B,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAiB,CAAA;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAE9C,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAqC,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAOH,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA2B,CAAA;AAKrD,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAEzC,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAEzC,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAqB,CAAA;AAM5C,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAiB,CAAA;AAEtC,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,EACd,uBAAuB,EACvB,sBAAsB,EACtB,kBAAkB,GACnB,MAAM,oBAAoB,CAAA;AAK3B,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA6B,CAAA;AAExD,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAEzC,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAwB,CAAA;AAEnD,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAiC,CAAA;AAE9D,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA6B,CAAA;AAExD,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,uBAAuB,EACvB,oBAAoB,EACpB,kBAAkB,EAClB,uBAAuB,GACxB,MAAM,mBAAmB,CAAA;AAK1B,OAAO,EAAE,mBAAmB,EAAE,MAAM,8CAA+C,CAAA;AAKnF,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA2B,CAAA;AAKrD,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA2B,CAAA;AAErD,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAK7D,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAiB,CAAA;AAKtC,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAEzC,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAuB,CAAA;AAK/C,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA+B,CAAA;AAE3D,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,GAChB,MAAM,gCAAgC,CAAA;AAKvC,OAAO,EAAE,gBAAgB,EAAE,MAAM,wCAAyC,CAAA;AAO1E,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAqB,CAAA;AAK5C,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAyB,CAAA;AAKlD,OAAO,EAAE,mBAAmB,EAAE,MAAM,8CAA+C,CAAA;AAEnF,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAuB,CAAA;AAE/C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,EACL,cAAc,EACd,aAAa,EACb,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,mBAAmB,CAAA;AAO1B,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAiB,CAAA;AAEtC,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAKzC,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAyB,CAAA;AAErD,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAA;AAE5D,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAuB,CAAA;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAE9C,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAqC,CAAA"}
@@ -11,17 +11,6 @@ type MountableStory = {
11
11
  canvasElement: HTMLElement;
12
12
  }) => Promise<void>;
13
13
  };
14
- /**
15
- * Mount a composed story into a fresh canvas and hand back the same
16
- * two things a `play` function used to receive.
17
- *
18
- * `run()` — rather than rendering the component directly — is what
19
- * keeps these tests honest after the story/test split: the subject
20
- * is the story a reader sees, with its args, its decorators, and its
21
- * loaders, not a second assembly of the same component that can
22
- * drift from it. It is also what triggers the a11y addon's
23
- * `afterEach`, so every mount here is an axe run.
24
- */
25
14
  export declare const mountStory: (story: MountableStory) => Promise<{
26
15
  canvas: Canvas;
27
16
  canvasElement: HTMLElement;
@@ -1 +1 @@
1
- {"version":3,"file":"mountStory.testHelpers.d.ts","sourceRoot":"","sources":["../src/mountStory.testHelpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAEvC,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAA;AAEvC;;;;;GAKG;AACH,KAAK,cAAc,GAAG;IACpB,GAAG,EAAE,CAAC,OAAO,EAAE;QACb,aAAa,EAAE,WAAW,CAAA;KAC3B,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACpB,CAAA;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,UAAU,GACrB,OAAO,cAAc,KACpB,OAAO,CAAC;IACT,MAAM,EAAE,MAAM,CAAA;IACd,aAAa,EAAE,WAAW,CAAA;CAC3B,CAQA,CAAA"}
1
+ {"version":3,"file":"mountStory.testHelpers.d.ts","sourceRoot":"","sources":["../src/mountStory.testHelpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAEvC,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAA;AAEvC;;;;;GAKG;AACH,KAAK,cAAc,GAAG;IACpB,GAAG,EAAE,CAAC,OAAO,EAAE;QACb,aAAa,EAAE,WAAW,CAAA;KAC3B,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CACpB,CAAA;AAqCD,eAAO,MAAM,UAAU,GACrB,OAAO,cAAc,KACpB,OAAO,CAAC;IACT,MAAM,EAAE,MAAM,CAAA;IACd,aAAa,EAAE,WAAW,CAAA;CAC3B,CAcA,CAAA"}
@@ -10,9 +10,36 @@ import { within } from "storybook/test";
10
10
  * drift from it. It is also what triggers the a11y addon's
11
11
  * `afterEach`, so every mount here is an axe run.
12
12
  */
13
+ /**
14
+ * Every canvas this helper has created, so the next mount can take
15
+ * the previous ones out of the document.
16
+ *
17
+ * They used to accumulate — one detached-but-attached `<div>` per
18
+ * test, for the whole file — and nothing noticed for three
19
+ * milestones, because every component up to M6 lays out **in
20
+ * flow**: a stale copy sits harmlessly below the live one and every
21
+ * query is scoped to its own canvas anyway.
22
+ *
23
+ * `ToastRegion` is the first `position: fixed` component in the
24
+ * library, and it broke that immediately. A previous test's toast
25
+ * stack is pinned to the same corner of the viewport as the live
26
+ * one, so `userEvent.click` — which clicks by **coordinates** —
27
+ * drove the wrong element while every query still returned the
28
+ * right one. The test failed with "the toast did not go away", one
29
+ * test after the toast it was actually clicking.
30
+ *
31
+ * The stale tree is removed rather than unmounted because portable
32
+ * stories expose no unmount. Detaching is enough: it takes the node
33
+ * out of the layout, which is the entire problem.
34
+ */
35
+ const mountedCanvases = [];
13
36
  export const mountStory = async (story) => {
37
+ for (const previous of mountedCanvases.splice(0)) {
38
+ previous.remove();
39
+ }
14
40
  const canvasElement = document.createElement("div");
15
41
  document.body.append(canvasElement);
42
+ mountedCanvases.push(canvasElement);
16
43
  await story.run({ canvasElement });
17
44
  return { canvas: within(canvasElement), canvasElement };
18
45
  };
@@ -1 +1 @@
1
- {"version":3,"file":"mountStory.testHelpers.js","sourceRoot":"","sources":["../src/mountStory.testHelpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAgBvC;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,EAC7B,KAAqB,EAIpB,EAAE;IACH,MAAM,aAAa,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAA;IAEnD,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,CAAA;IAEnC,MAAM,KAAK,CAAC,GAAG,CAAC,EAAE,aAAa,EAAE,CAAC,CAAA;IAElC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,aAAa,CAAC,EAAE,aAAa,EAAE,CAAA;AACzD,CAAC,CAAA"}
1
+ {"version":3,"file":"mountStory.testHelpers.js","sourceRoot":"","sources":["../src/mountStory.testHelpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAgBvC;;;;;;;;;;GAUG;AACH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,eAAe,GAAkB,EAAE,CAAA;AAEzC,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,EAC7B,KAAqB,EAIpB,EAAE;IACH,KAAK,MAAM,QAAQ,IAAI,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;QACjD,QAAQ,CAAC,MAAM,EAAE,CAAA;IACnB,CAAC;IAED,MAAM,aAAa,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAA;IAEnD,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,CAAA;IAEnC,eAAe,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;IAEnC,MAAM,KAAK,CAAC,GAAG,CAAC,EAAE,aAAa,EAAE,CAAC,CAAA;IAElC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,aAAa,CAAC,EAAE,aAAa,EAAE,CAAA;AACzD,CAAC,CAAA"}
@@ -0,0 +1,110 @@
1
+ import type { InputHTMLAttributes } from "react";
2
+ /**
3
+ * The props a **cloning ancestor** injects into the control below it.
4
+ *
5
+ * `Field` and `Tooltip` both take one child and `cloneElement` onto
6
+ * it — "cloned, not wrapped", which is what lets a `Field` go around
7
+ * an `<input>`, a `Select`, or an app's own control without changing
8
+ * that control's layout. The contract has a hole in it the moment two
9
+ * of them are nested:
10
+ *
11
+ * ```tsx
12
+ * <Field label="Pattern" isRequired>
13
+ * <Tooltip label="A JavaScript regular expression">
14
+ * <input />
15
+ * </Tooltip>
16
+ * </Field>
17
+ * ```
18
+ *
19
+ * `Field` clones onto the **`Tooltip` element**, so `id`,
20
+ * `aria-describedby`, `aria-invalid` and `required` are handed to a
21
+ * component that declares none of them — and `cloneElement` does not
22
+ * care. React drops them without a warning, TypeScript never sees
23
+ * them (`Children.only` returns a `ReactElement` with `any` props),
24
+ * every test still passes, and the render is pixel-identical. The
25
+ * `<label>` then points at an `id` that is nowhere in the document.
26
+ *
27
+ * **Silent prop-dropping is the worst version of this**, so the rule
28
+ * is: *a slot component is a pass-through*. Anything in `SlotProps`
29
+ * that arrives from above is forwarded to the child this component
30
+ * clones onto, so a chain of slots behaves like the one control at
31
+ * the bottom of it.
32
+ *
33
+ * ### The list is closed, the forwarding is not
34
+ *
35
+ * Worth being exact, because the two are easy to conflate. This type
36
+ * is a closed list of five keys rather than an index signature: it is
37
+ * exactly the wiring `Field` writes plus the wiring `Tooltip` writes,
38
+ * it is greppable, and it is the set whose **merge semantics are
39
+ * defined** — which for four of them is last-write-wins and for
40
+ * `aria-describedby` is a join.
41
+ *
42
+ * The *runtime* forwards everything. Both components collect their
43
+ * received props with a rest spread and `mergeSlotProps` starts from
44
+ * `...receivedProps`, so an ancestor's `onPointerEnter`, its
45
+ * `onFocus`, its `ref` — none of which are named here — reach the
46
+ * control at the bottom too. That is not an oversight, it is load
47
+ * bearing: a `Tooltip` around a `Field` hands down `useHover`,
48
+ * `useFocus`, `useDismiss` and `refs.setReference`, and a `Field`
49
+ * that forwarded only the five keys below would leave the tip with no
50
+ * trigger and no anchor. A closed *runtime* filter would be
51
+ * mux-magic's `FieldTooltip` defect rebuilt inside the fix for it.
52
+ *
53
+ * The five keys, and who writes them:
54
+ *
55
+ * | Key | Written by |
56
+ * | --- | --- |
57
+ * | `aria-describedby` | `Field` (its description and its error) **and** `Tooltip` (its tip) — the one key that is a list, and the reason `mergeSlotProps` exists |
58
+ * | `aria-invalid` | `Field`, derived from `error` being present |
59
+ * | `aria-required` | `Field`, from `isRequired` — the announced constraint, for a composite control the browser does not validate |
60
+ * | `id` | `Field`, so its `<label htmlFor>` has something to point at. A caller may also pass one directly, which is how a consumer pins a stable id for a deep link or an autofill hint |
61
+ * | `required` | `Field`, from `isRequired` — the constraint the browser validates |
62
+ *
63
+ * ### Why these are `Pick`ed and not written out
64
+ *
65
+ * Three of the five are booleans whose names cannot start with `is`
66
+ * or `has`, because they are not our names — they are the DOM's, and
67
+ * they are spread onto a DOM node verbatim. The house rule has
68
+ * [no carve-out](../../../docs/decisions/2026-07-29-is-has-rule-has-no-external-api-carve-out.md)
69
+ * for that: a *type property* is a shape we own, an object literal
70
+ * key is someone else's contract, and no `eslint-disable` is
71
+ * available because unused disable directives are themselves an
72
+ * error.
73
+ *
74
+ * So this type does not declare a shape. It **selects** one out of
75
+ * React's own `InputHTMLAttributes`, which is the honest description
76
+ * of what it is: five keys of the props React already types for an
77
+ * `<input>`, forwarded unchanged. The same reasoning as the
78
+ * `Record<string, boolean>` in `createReactAdapter` — we are not
79
+ * describing React's shape, we are naming the keys of it we touch —
80
+ * and it has the side benefit that `aria-invalid` gets React's real
81
+ * type (`"grammar"` and `"spelling"` are legal values) rather than
82
+ * the `boolean` a hand-written version would have narrowed it to.
83
+ */
84
+ export type SlotProps = Pick<InputHTMLAttributes<HTMLElement>, "aria-describedby" | "aria-invalid" | "aria-required" | "id" | "required">;
85
+ /**
86
+ * Merge what arrived from above with what this component writes
87
+ * itself.
88
+ *
89
+ * Everything here is a single value and last-write-wins is correct
90
+ * for it — **except `aria-describedby`, which is a list**. Two slot
91
+ * components each writing their own onto one control is the entire
92
+ * nesting problem in one attribute: a `Field` naming its description
93
+ * and its error, and a `Tooltip` naming its tip. A plain spread keeps
94
+ * one of them and loses the other, which is the same silent drop by a
95
+ * shorter route.
96
+ *
97
+ * **Outer first.** A screen reader reads the list in order, and the
98
+ * field's own description and error are the control's primary
99
+ * explanation; a tip is supplementary by construction. The ordering
100
+ * inside a single `Field` — description, then error — is `Field`'s
101
+ * own concern and is unaffected.
102
+ *
103
+ * `undefined` in `ownProps` is *absence*, not an instruction to
104
+ * clear. Both callers spell their optional wiring as
105
+ * `isRequired || undefined`, so a plain spread would let an inner
106
+ * `Field` with no error erase an outer one's `aria-invalid` — the
107
+ * same silent drop, one level down.
108
+ */
109
+ export declare const mergeSlotProps: <OwnProps extends Record<string, unknown>>(receivedProps: SlotProps, ownProps: OwnProps) => OwnProps & SlotProps;
110
+ //# sourceMappingURL=slotProps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slotProps.d.ts","sourceRoot":"","sources":["../src/slotProps.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,OAAO,CAAA;AAEhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiFG;AACH,MAAM,MAAM,SAAS,GAAG,IAAI,CAC1B,mBAAmB,CAAC,WAAW,CAAC,EAC9B,kBAAkB,GAClB,cAAc,GACd,eAAe,GACf,IAAI,GACJ,UAAU,CACb,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,cAAc,GACzB,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAExC,eAAe,SAAS,EACxB,UAAU,QAAQ,KACjB,QAAQ,GAAG,SAoBb,CAAA"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Merge what arrived from above with what this component writes
3
+ * itself.
4
+ *
5
+ * Everything here is a single value and last-write-wins is correct
6
+ * for it — **except `aria-describedby`, which is a list**. Two slot
7
+ * components each writing their own onto one control is the entire
8
+ * nesting problem in one attribute: a `Field` naming its description
9
+ * and its error, and a `Tooltip` naming its tip. A plain spread keeps
10
+ * one of them and loses the other, which is the same silent drop by a
11
+ * shorter route.
12
+ *
13
+ * **Outer first.** A screen reader reads the list in order, and the
14
+ * field's own description and error are the control's primary
15
+ * explanation; a tip is supplementary by construction. The ordering
16
+ * inside a single `Field` — description, then error — is `Field`'s
17
+ * own concern and is unaffected.
18
+ *
19
+ * `undefined` in `ownProps` is *absence*, not an instruction to
20
+ * clear. Both callers spell their optional wiring as
21
+ * `isRequired || undefined`, so a plain spread would let an inner
22
+ * `Field` with no error erase an outer one's `aria-invalid` — the
23
+ * same silent drop, one level down.
24
+ */
25
+ export const mergeSlotProps = (receivedProps, ownProps) => {
26
+ const definedOwnProps = Object.fromEntries(Object.entries(ownProps).filter(([, value]) => value !== undefined));
27
+ const describedBy = [
28
+ receivedProps["aria-describedby"],
29
+ ownProps["aria-describedby"],
30
+ ]
31
+ .filter(Boolean)
32
+ .join(" ");
33
+ return {
34
+ ...receivedProps,
35
+ ...definedOwnProps,
36
+ "aria-describedby": describedBy === "" ? undefined : describedBy,
37
+ };
38
+ };
39
+ //# sourceMappingURL=slotProps.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slotProps.js","sourceRoot":"","sources":["../src/slotProps.ts"],"names":[],"mappings":"AA6FA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAG5B,aAAwB,EACxB,QAAkB,EACI,EAAE;IACxB,MAAM,eAAe,GAAG,MAAM,CAAC,WAAW,CACxC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,MAAM,CAC7B,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,CACnC,CACU,CAAA;IAEb,MAAM,WAAW,GAAG;QAClB,aAAa,CAAC,kBAAkB,CAAC;QACjC,QAAQ,CAAC,kBAAkB,CAAC;KAC7B;SACE,MAAM,CAAC,OAAO,CAAC;SACf,IAAI,CAAC,GAAG,CAAC,CAAA;IAEZ,OAAO;QACL,GAAG,aAAa;QAChB,GAAG,eAAe;QAClB,kBAAkB,EAChB,WAAW,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW;KAC/C,CAAA;AACH,CAAC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@charcuterie/ui",
3
- "version": "0.1.1",
3
+ "version": "1.0.0",
4
4
  "description": "The Charcuterie components. Tokens for look, @charcuterie/logic for state, ARIA that agents can actually drive.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -43,8 +43,8 @@
43
43
  "typecheck": "tsc --noEmit"
44
44
  },
45
45
  "dependencies": {
46
- "@charcuterie/logic": "0.1.0",
47
- "@charcuterie/tokens": "0.2.0",
46
+ "@charcuterie/logic": "1.0.0",
47
+ "@charcuterie/tokens": "1.0.0",
48
48
  "@floating-ui/react": "^0.27.20"
49
49
  },
50
50
  "peerDependencies": {
@@ -0,0 +1,83 @@
1
+ import { Canvas, Controls, Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ import * as AccordionStories from "./Accordion.stories";
4
+
5
+ <Meta of={AccordionStories} />
6
+
7
+ # Accordion
8
+
9
+ Eleven sites in the fleet. The plan called it "nearly free once `Tabs` exists" — it is, the
10
+ exclusive mode is `VisibilityGroup` and nothing else — but the saving is not why it ships.
11
+
12
+ <Canvas of={AccordionStories.Default} />
13
+
14
+ ## Props
15
+
16
+ <Controls />
17
+
18
+ ## The two-owners bug, live, in the app this milestone migrates
19
+
20
+ mux-magic's `JobStepsDisclosure` is built on `<details>`, and `<details>` **owns `open`**. It
21
+ also wants the state in a Jotai atom so a job can be expanded from elsewhere. Reconciling
22
+ the two takes three separate mechanisms:
23
+
24
+ ```tsx
25
+ const detailsRef = useRef<HTMLDetailsElement>(null)
26
+ const skipNextToggleRef = useRef(isOpen)
27
+
28
+ useEffect(() => {
29
+ if (detailsRef.current) detailsRef.current.open = isOpen
30
+ }, [isOpen])
31
+
32
+ const handleToggle = (event) => {
33
+ if (skipNextToggleRef.current) {
34
+ skipNextToggleRef.current = false
35
+ return // ← swallow the toggle our own write just fired
36
+ }
37
+ setStepsOpen(…)
38
+ }
39
+ ```
40
+
41
+ A ref to reach past React, an effect to push state into the DOM, and a guard to stop the
42
+ DOM's echo coming back. Every one of the three exists only because **two things hold one
43
+ fact** — the Radix argument from `Popover`, aimed at the platform this time.
44
+
45
+ So this is a `<button aria-expanded>` and a `role="region"`, with `VisibilityGroup` holding
46
+ the state alone.
47
+
48
+ ## Why not `<details name="…">`
49
+
50
+ The platform *does* now ship an exclusive accordion, and it was still rejected:
51
+
52
+ | | `<details name>` | This |
53
+ | --- | --- | --- |
54
+ | Who owns `open` | the DOM | `VisibilityGroup` |
55
+ | Disabled section | **impossible** — a `<summary>` cannot be disabled | not registered, so not a member |
56
+ | Driven from elsewhere | ref + effect + echo guard | pass `expandedKeys` |
57
+ | Panel keeps scroll/subscriptions | yes | yes (`hidden`, not unmounted) |
58
+
59
+ The second row is the one that settles it: four of the eleven sites have a section that is
60
+ unreachable until a job produces it.
61
+
62
+ <Canvas of={AccordionStories.AllVariants} />
63
+
64
+ ## Two kinds, one component
65
+
66
+ `isMultiple` picks between `VisibilityGroup` and `MultiplePicker`. Hooks cannot be
67
+ conditional, so **both are created and one is consulted** — two empty stores and no DOM.
68
+
69
+ The alternative is reimplementing exclusivity on top of `MultiplePicker` by deselecting the
70
+ others, which is precisely the logic `VisibilityGroup` exists to own once.
71
+
72
+ ## Registration is membership
73
+
74
+ A disabled section never registers, so nothing can expand it — including a consumer passing
75
+ its key in `expandedKeys`. `AllStates` does exactly that, on purpose.
76
+
77
+ <Canvas of={AccordionStories.AllStates} />
78
+
79
+ ## The heading level is the caller's
80
+
81
+ An accordion inside a `Card` that already has an `<h2>` needs `headingLevel={3}`. Getting it
82
+ wrong is a document-outline break that **no gate in this repo can see** — it depends
83
+ entirely on the page the accordion lands on, so it is a prop rather than a constant.
@@ -0,0 +1,146 @@
1
+ import type { Meta, StoryObj } from "@storybook/react"
2
+
3
+ import { Badge } from "../Badge/Badge.tsx"
4
+ import {
5
+ StoryCell,
6
+ StoryGrid,
7
+ } from "../board.storyHelpers.tsx"
8
+ import type { AccordionItem } from "./Accordion.tsx"
9
+ import { Accordion } from "./Accordion.tsx"
10
+
11
+ const JOB_SECTIONS: AccordionItem[] = [
12
+ {
13
+ content: (
14
+ <p>Blade Runner (1982) · 9 titles · MakeMKV 1.17.8</p>
15
+ ),
16
+ key: "disc",
17
+ label: "Disc",
18
+ },
19
+ {
20
+ content: (
21
+ <p className="font-mono text-xs">
22
+ 14:02:11 AACS handshake complete, reading…
23
+ </p>
24
+ ),
25
+ key: "log",
26
+ label: "Log",
27
+ },
28
+ {
29
+ content: (
30
+ <div className="flex gap-2">
31
+ <Badge intent="warning" size="sm">
32
+ 1 retry
33
+ </Badge>
34
+
35
+ <Badge intent="info" size="sm">
36
+ AACS
37
+ </Badge>
38
+ </div>
39
+ ),
40
+ key: "flags",
41
+ label: "Flags",
42
+ },
43
+ ]
44
+
45
+ const STATE_SECTIONS: AccordionItem[] = [
46
+ ...JOB_SECTIONS,
47
+ {
48
+ content: <p>Unreachable — no verdict yet.</p>,
49
+ isDisabled: true,
50
+ key: "verdict",
51
+ label: "Verdict",
52
+ },
53
+ ]
54
+
55
+ const meta = {
56
+ title: "Components/Accordion",
57
+ component: Accordion,
58
+ parameters: { layout: "padded" },
59
+ args: {
60
+ headingLevel: 3,
61
+ isMultiple: false,
62
+ items: JOB_SECTIONS,
63
+ },
64
+ } satisfies Meta<typeof Accordion>
65
+
66
+ export default meta
67
+
68
+ type Story = StoryObj<typeof meta>
69
+
70
+ export const Default: Story = {
71
+ args: { expandedKeys: ["disc"], items: JOB_SECTIONS },
72
+ }
73
+
74
+ /**
75
+ * Exclusive is `VisibilityGroup`; `isMultiple` is
76
+ * `MultiplePicker`. Both hooks are created and one is consulted,
77
+ * because hooks cannot be conditional — two empty stores and no DOM,
78
+ * which is cheaper than reimplementing exclusivity on top of the
79
+ * multiple kind.
80
+ */
81
+ export const AllVariants: Story = {
82
+ args: { items: JOB_SECTIONS },
83
+ render: () => (
84
+ <StoryGrid columns={2}>
85
+ <StoryCell label="exclusive (default)">
86
+ <Accordion
87
+ expandedKeys={["disc"]}
88
+ items={JOB_SECTIONS}
89
+ />
90
+ </StoryCell>
91
+
92
+ <StoryCell label="multiple">
93
+ <Accordion
94
+ expandedKeys={["disc", "flags"]}
95
+ isMultiple
96
+ items={JOB_SECTIONS}
97
+ />
98
+ </StoryCell>
99
+
100
+ <StoryCell label="all collapsed">
101
+ <Accordion items={JOB_SECTIONS} />
102
+ </StoryCell>
103
+
104
+ {/*
105
+ Its own item set, with names that appear nowhere else on the
106
+ board. Four accordions sharing three labels means
107
+ `getByRole("heading", { name: "Log" })` matches four
108
+ elements, and `expectAgentDrivable` treats ambiguity as a
109
+ failure — correctly, since an agent would be picking one at
110
+ random.
111
+ */}
112
+ <StoryCell label="heading level 2">
113
+ <Accordion
114
+ expandedKeys={["steps"]}
115
+ headingLevel={2}
116
+ items={[
117
+ {
118
+ content: <p>4 of 9 titles complete.</p>,
119
+ key: "steps",
120
+ label: "Steps",
121
+ },
122
+ {
123
+ content: <p>No errors.</p>,
124
+ key: "errors",
125
+ label: "Errors",
126
+ },
127
+ ]}
128
+ />
129
+ </StoryCell>
130
+ </StoryGrid>
131
+ ),
132
+ }
133
+
134
+ /**
135
+ * A disabled section is **not a member of the group**. Registration
136
+ * is membership, so nothing can expand it — including a consumer
137
+ * passing its key in `expandedKeys`. That is the behaviour a
138
+ * `<summary>` cannot have at all, which is one of the two reasons
139
+ * this is not built on `<details>`.
140
+ */
141
+ export const AllStates: Story = {
142
+ args: {
143
+ expandedKeys: ["verdict"],
144
+ items: STATE_SECTIONS,
145
+ },
146
+ }