@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
@@ -0,0 +1,65 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useUniqueId } from "@charcuterie/logic";
3
+ import { toClassName } from "../toClassName.js";
4
+ /**
5
+ * A label over **several** controls.
6
+ *
7
+ * `Field` mints one id and clones it onto one child, which is the
8
+ * right shape for a text input and the wrong shape the moment there
9
+ * are two of them: an `id` can only name one element, and a
10
+ * `<label htmlFor>` can only point at one. Six of mux-magic's sixteen
11
+ * field components are in that position — `RegexWithFlagsField`
12
+ * (pattern, flags, sample, and a checkbox), `NumberWithLookupField`
13
+ * (three inputs), `LanguageCodeField`, `LanguageCodesField`,
14
+ * `SubtitleTypesField` (a tag input beside a dropdown) and
15
+ * `RenameRegexField` (a row per rule) — and every one of them renders
16
+ * a `FieldLabel` whose `htmlFor` names, at best, one of them.
17
+ *
18
+ * ### `<fieldset>` and `<legend>`, not a `<label>`
19
+ *
20
+ * This is the one place in the library where `<fieldset>` is right.
21
+ * `AccordionSection` rejected it for its panel and said why — it is a
22
+ * *form-control grouping*, and dragging `<legend>` semantics onto
23
+ * prose is wrong. Here the content **is** a form-control grouping,
24
+ * so the element means what it says, `<legend>` names the group
25
+ * natively, and no `role="group"` and no `aria-labelledby` are
26
+ * needed.
27
+ *
28
+ * A `<label>` here would be the mux-magic defect deliberately: a
29
+ * label pointing at one control, or at none.
30
+ *
31
+ * ### What a group cannot carry
32
+ *
33
+ * `aria-invalid` has no group form — it belongs on the control that
34
+ * is actually invalid, and this component does not know which one
35
+ * that is. So `error` here is *described*, not *asserted*: it joins
36
+ * `aria-describedby` on the `<fieldset>`, which screen readers read
37
+ * as focus enters the group. A control inside that is itself invalid
38
+ * should say so itself, in its own `Field`.
39
+ *
40
+ * That is a real limitation and it is stated rather than papered
41
+ * over — the alternative was to clone `aria-invalid` onto every
42
+ * child, which marks the valid ones invalid.
43
+ */
44
+ export const FieldGroup = ({ children, className, description, error, isRequired = false, label, }) => {
45
+ const baseId = useUniqueId();
46
+ const descriptionId = `${baseId}-description`;
47
+ const errorId = `${baseId}-error`;
48
+ // Description first, error second, for the reason `Field` gives:
49
+ // a screen reader reads the list in order and the two sentences
50
+ // are not interchangeable.
51
+ const describedBy = [
52
+ description === undefined ? null : descriptionId,
53
+ error === undefined ? null : errorId,
54
+ ]
55
+ .filter(Boolean)
56
+ .join(" ");
57
+ return (_jsxs("fieldset", { "aria-describedby": describedBy === "" ? undefined : describedBy, className: toClassName(
58
+ // A `<fieldset>` arrives with a border, an inline margin, a
59
+ // block padding, and `min-inline-size: min-content` — the
60
+ // last of which makes it refuse to shrink inside a flex or
61
+ // grid parent, which is the reason design systems avoid the
62
+ // element. All four are cleared here, once.
63
+ "m-0 flex min-w-0 flex-col gap-1.5 border-0 p-0", className), children: [_jsxs("legend", { className: "mb-1.5 p-0 font-medium text-content-primary text-sm", children: [label, isRequired ? (_jsx("span", { "aria-hidden": "true", className: "ms-1 text-intent-danger-content", children: "*" })) : null] }), children, description === undefined ? null : (_jsx("p", { className: "text-content-secondary text-xs", id: descriptionId, children: description })), error === undefined ? null : (_jsx("p", { className: "text-intent-danger-content text-xs", id: errorId, children: error }))] }));
64
+ };
65
+ //# sourceMappingURL=FieldGroup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FieldGroup.js","sourceRoot":"","sources":["../../src/Field/FieldGroup.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAGhD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAkB/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,EACzB,QAAQ,EACR,SAAS,EACT,WAAW,EACX,KAAK,EACL,UAAU,GAAG,KAAK,EAClB,KAAK,GACW,EAAa,EAAE;IAC/B,MAAM,MAAM,GAAG,WAAW,EAAE,CAAA;IAE5B,MAAM,aAAa,GAAG,GAAG,MAAM,cAAc,CAAA;IAE7C,MAAM,OAAO,GAAG,GAAG,MAAM,QAAQ,CAAA;IAEjC,iEAAiE;IACjE,gEAAgE;IAChE,2BAA2B;IAC3B,MAAM,WAAW,GAAG;QAClB,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa;QAChD,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO;KACrC;SACE,MAAM,CAAC,OAAO,CAAC;SACf,IAAI,CAAC,GAAG,CAAC,CAAA;IAEZ,OAAO,CACL,wCAEI,WAAW,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,EAE9C,SAAS,EAAE,WAAW;QACpB,4DAA4D;QAC5D,0DAA0D;QAC1D,2DAA2D;QAC3D,4DAA4D;QAC5D,4CAA4C;QAC5C,gDAAgD,EAChD,SAAS,CACV,aAED,kBAAQ,SAAS,EAAC,qDAAqD,aACpE,KAAK,EAEL,UAAU,CAAC,CAAC,CAAC,CACZ,8BAIc,MAAM,EAClB,SAAS,EAAC,iCAAiC,kBAGtC,CACR,CAAC,CAAC,CAAC,IAAI,IACD,EAER,QAAQ,EAER,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAClC,YACE,SAAS,EAAC,gCAAgC,EAC1C,EAAE,EAAE,aAAa,YAEhB,WAAW,GACV,CACL,EAEA,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAC5B,YACE,SAAS,EAAC,oCAAoC,EAC9C,EAAE,EAAE,OAAO,YAEV,KAAK,GACJ,CACL,IACQ,CACZ,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,63 @@
1
+ import type { ReactNode } from "react";
2
+ export type FileDropZoneProps = {
3
+ /** An `accept` list, passed straight to the input. */
4
+ accept?: string;
5
+ className?: string;
6
+ /** Secondary line — the formats taken, a size cap. */
7
+ description?: ReactNode;
8
+ isDisabled?: boolean;
9
+ isMultiple?: boolean;
10
+ /** The control's accessible name, and its visible prompt. */
11
+ label: string;
12
+ onDropFiles: (files: File[]) => void;
13
+ /**
14
+ * Dropped text, when there is any. A browser hands over a dragged
15
+ * link as `text/plain` and no files at all, which is what
16
+ * gallery-downloader's page is built entirely around — it takes
17
+ * `dataTransfer.getData("text")` and never looks at `.files`.
18
+ */
19
+ onDropText?: (text: string) => void;
20
+ };
21
+ /**
22
+ * The one drag target in the fleet is gallery-downloader's, and it
23
+ * gets the hard part right: `dragenter`/`dragleave` fire once per
24
+ * *element* the pointer crosses, not once per zone, so a naive
25
+ * implementation flickers over every child — and that page keeps a
26
+ * `dragDepth` counter, which is the correct fix. It is reproduced
27
+ * below with credit.
28
+ *
29
+ * What it has no answer for is the part that cannot be fixed by
30
+ * counting:
31
+ *
32
+ * ### Dragging is pointer-only, so the zone is a label around an input
33
+ *
34
+ * There is **no keyboard gesture for drag-and-drop**. WCAG 2.5.7
35
+ * (AA since 2.2) requires a single-pointer alternative for any
36
+ * dragging movement, and the honest one has existed since 1995: a
37
+ * real `<input type="file">`. So this component is a `<label>`
38
+ * wrapping a visually-hidden input — the whole zone is the label's
39
+ * click target, Tab reaches the input, Space and Enter open the
40
+ * picker, and the drop handlers are an *enhancement* on top of a
41
+ * control that already works without them.
42
+ *
43
+ * That ordering is the entire design. Building the drop zone first
44
+ * and bolting on a "or browse" link is how the fleet's version ended
45
+ * up with an `alert()` as its error channel and no keyboard path at
46
+ * all.
47
+ *
48
+ * ### `dragover` must be prevented, and the reason is a footgun
49
+ *
50
+ * A browser's default for `dragover` is *reject the drop*. Not
51
+ * preventing it means `drop` never fires — nothing errors, the
52
+ * cursor shows a "no entry" sign, and the zone looks like it has a
53
+ * broken handler when it in fact has no handler running at all.
54
+ *
55
+ * ### It reports text as well as files
56
+ *
57
+ * Dragging a link gives you `text/plain` and an empty `files` list.
58
+ * A zone that only reads `.files` silently ignores the single most
59
+ * common thing a user drags into a downloader, which is why
60
+ * `onDropText` is here rather than being someone's follow-up.
61
+ */
62
+ export declare const FileDropZone: ({ accept, className, description, isDisabled, isMultiple, label, onDropFiles, onDropText, }: FileDropZoneProps) => ReactNode;
63
+ //# sourceMappingURL=FileDropZone.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FileDropZone.d.ts","sourceRoot":"","sources":["../../src/FileDropZone/FileDropZone.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAa,SAAS,EAAE,MAAM,OAAO,CAAA;AAKjD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sDAAsD;IACtD,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAA;IACpC;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CACpC,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,eAAO,MAAM,YAAY,GAAI,6FAS1B,iBAAiB,KAAG,SA6JtB,CAAA"}
@@ -0,0 +1,122 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useUniqueId } from "@charcuterie/logic";
3
+ import { useRef, useState } from "react";
4
+ import { toClassName } from "../toClassName.js";
5
+ /**
6
+ * The one drag target in the fleet is gallery-downloader's, and it
7
+ * gets the hard part right: `dragenter`/`dragleave` fire once per
8
+ * *element* the pointer crosses, not once per zone, so a naive
9
+ * implementation flickers over every child — and that page keeps a
10
+ * `dragDepth` counter, which is the correct fix. It is reproduced
11
+ * below with credit.
12
+ *
13
+ * What it has no answer for is the part that cannot be fixed by
14
+ * counting:
15
+ *
16
+ * ### Dragging is pointer-only, so the zone is a label around an input
17
+ *
18
+ * There is **no keyboard gesture for drag-and-drop**. WCAG 2.5.7
19
+ * (AA since 2.2) requires a single-pointer alternative for any
20
+ * dragging movement, and the honest one has existed since 1995: a
21
+ * real `<input type="file">`. So this component is a `<label>`
22
+ * wrapping a visually-hidden input — the whole zone is the label's
23
+ * click target, Tab reaches the input, Space and Enter open the
24
+ * picker, and the drop handlers are an *enhancement* on top of a
25
+ * control that already works without them.
26
+ *
27
+ * That ordering is the entire design. Building the drop zone first
28
+ * and bolting on a "or browse" link is how the fleet's version ended
29
+ * up with an `alert()` as its error channel and no keyboard path at
30
+ * all.
31
+ *
32
+ * ### `dragover` must be prevented, and the reason is a footgun
33
+ *
34
+ * A browser's default for `dragover` is *reject the drop*. Not
35
+ * preventing it means `drop` never fires — nothing errors, the
36
+ * cursor shows a "no entry" sign, and the zone looks like it has a
37
+ * broken handler when it in fact has no handler running at all.
38
+ *
39
+ * ### It reports text as well as files
40
+ *
41
+ * Dragging a link gives you `text/plain` and an empty `files` list.
42
+ * A zone that only reads `.files` silently ignores the single most
43
+ * common thing a user drags into a downloader, which is why
44
+ * `onDropText` is here rather than being someone's follow-up.
45
+ */
46
+ export const FileDropZone = ({ accept, className, description, isDisabled = false, isMultiple = false, label, onDropFiles, onDropText, }) => {
47
+ const baseId = useUniqueId();
48
+ const inputId = `${baseId}-input`;
49
+ const labelId = `${baseId}-label`;
50
+ const descriptionId = `${baseId}-description`;
51
+ const [isDragActive, setIsDragActive] = useState(false);
52
+ /**
53
+ * The counter gallery-downloader's page already keeps, and the
54
+ * reason it has to exist: `dragenter` fires again for every child
55
+ * element the pointer crosses, and `dragleave` fires for the one
56
+ * it left — so a plain boolean turns off halfway across the zone
57
+ * and the highlight strobes.
58
+ *
59
+ * A ref rather than state: nothing renders from the depth itself,
60
+ * only from whether it is above zero, and a re-render per crossed
61
+ * child during a drag is exactly the jank this is avoiding.
62
+ */
63
+ const dragDepth = useRef(0);
64
+ const endDrag = () => {
65
+ dragDepth.current = 0;
66
+ setIsDragActive(false);
67
+ };
68
+ const handleDrop = (dropEvent) => {
69
+ dropEvent.preventDefault();
70
+ endDrag();
71
+ if (isDisabled) {
72
+ return;
73
+ }
74
+ const files = Array.from(dropEvent.dataTransfer.files);
75
+ if (files.length > 0) {
76
+ onDropFiles(isMultiple ? files : files.slice(0, 1));
77
+ return;
78
+ }
79
+ const text = dropEvent.dataTransfer.getData("text");
80
+ if (text !== "") {
81
+ onDropText?.(text);
82
+ }
83
+ };
84
+ return (_jsxs("label", { className: toClassName("flex cursor-pointer flex-col items-center gap-1 rounded-md border-2 border-border-default border-dashed p-6 text-center transition-colors duration-(--duration-fast) ease-standard", isDragActive
85
+ ? "border-intent-accent-border bg-intent-accent-surface"
86
+ : "bg-surface-sunken hover:border-border-strong", isDisabled &&
87
+ "cursor-not-allowed border-border-subtle text-content-disabled hover:border-border-subtle",
88
+ // The ring is on the label, driven by the input inside it —
89
+ // the input is what actually takes focus, and it is
90
+ // visually hidden, so a ring on the input paints nothing.
91
+ "focus-within:outline-solid focus-within:outline-(length:--focus-ring-width) focus-within:outline-offset-(--focus-ring-offset) focus-within:outline-focus-ring", className), htmlFor: inputId, onDragEnter: (dragEvent) => {
92
+ dragEvent.preventDefault();
93
+ dragDepth.current += 1;
94
+ if (dragDepth.current === 1) {
95
+ setIsDragActive(true);
96
+ }
97
+ }, onDragLeave: (dragEvent) => {
98
+ dragEvent.preventDefault();
99
+ dragDepth.current -= 1;
100
+ if (dragDepth.current === 0) {
101
+ setIsDragActive(false);
102
+ }
103
+ }, onDragOver: (dragEvent) => {
104
+ // Not optional. The default action for `dragover` is to
105
+ // reject the drop, so without this `onDrop` below never runs
106
+ // and the zone looks broken rather than unhandled.
107
+ dragEvent.preventDefault();
108
+ }, onDrop: handleDrop, children: [_jsx("input", { accept: accept, "aria-describedby": description === undefined
109
+ ? undefined
110
+ : descriptionId, "aria-labelledby": labelId, className: "sr-only", disabled: isDisabled, id: inputId, multiple: isMultiple, onChange: (changeEvent) => {
111
+ const files = Array.from(changeEvent.currentTarget.files ?? []);
112
+ if (files.length > 0) {
113
+ onDropFiles(files);
114
+ }
115
+ // Cleared so choosing the *same* file twice fires again.
116
+ // A file input keeps its value, so the second pick is not
117
+ // a change and the handler never runs — which reads as
118
+ // "it ignored me" and is the oldest bug in this control.
119
+ changeEvent.currentTarget.value = "";
120
+ }, type: "file" }), _jsx("span", { className: "font-medium text-content-primary text-sm", id: labelId, children: label }), description === undefined ? null : (_jsx("span", { className: "text-content-secondary text-xs", id: descriptionId, children: description }))] }));
121
+ };
122
+ //# sourceMappingURL=FileDropZone.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FileDropZone.js","sourceRoot":"","sources":["../../src/FileDropZone/FileDropZone.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,90 @@
1
+ import type { IntentName } from "@charcuterie/tokens";
2
+ import type { ReactNode } from "react";
3
+ export type LogLine = {
4
+ intent?: IntentName;
5
+ key: string;
6
+ text: string;
7
+ };
8
+ export type LogViewerProps = {
9
+ className?: string;
10
+ /**
11
+ * Announce new lines to a screen reader. **Off by default**, and
12
+ * that is a considered choice rather than an oversight — see
13
+ * below.
14
+ */
15
+ isAnnounced?: boolean;
16
+ /** The pane's accessible name. Required. */
17
+ label: string;
18
+ lines: LogLine[];
19
+ /**
20
+ * Oldest lines past this are dropped from the DOM. A rip in
21
+ * progress emits a line every few hundred milliseconds for an
22
+ * hour, and the browser keeps every node.
23
+ */
24
+ maxLines?: number;
25
+ };
26
+ /**
27
+ * Three repos render a scrolling log pane —
28
+ * mux-magic's `JobLogsDisclosure` and `StepLogs`, rip-deck's
29
+ * `LogModal` — and between them they have four defects that this
30
+ * component exists to not have. A fifth, below the effects, is not
31
+ * theirs: it is defect 1 coming back through the front door, in this
32
+ * component, found by the first app to consume it.
33
+ *
34
+ * ### 1. The one that follows nothing
35
+ *
36
+ * ```tsx
37
+ * useEffect(() => {
38
+ * const pane = paneRef.current
39
+ * if (pane) pane.scrollTop = pane.scrollHeight
40
+ * }, []) // ← empty deps
41
+ * ```
42
+ *
43
+ * That is mux-magic's auto-scroll, and it runs **once**, on mount,
44
+ * when the pane is empty and `scrollHeight` is `clientHeight`. Every
45
+ * line after it arrives below the fold. It reads as correct, it is
46
+ * the shape everyone writes, and the log simply never follows.
47
+ *
48
+ * ### 2. Following that cannot be escaped
49
+ *
50
+ * The naive fix — scroll to the bottom on every render — is worse:
51
+ * a user scrolling back to read the line that mentioned an error is
52
+ * yanked to the end twice a second. Following has to be *pinned to
53
+ * the user's own position*, which is what `isFollowing` below is: it
54
+ * turns off the moment the user scrolls away and back on when they
55
+ * return to the bottom, and there is a button for when they do not
56
+ * want to scroll all that way.
57
+ *
58
+ * ### 3. `data-log-id`
59
+ *
60
+ * mux-magic's pane carries one. It is a `data-testid` under a
61
+ * different name — a handle only the test suite can see — and this
62
+ * package's `sourceRules.test.ts` bans the practice rather than the
63
+ * spelling. The pane here is found by its `label`, the same query
64
+ * Playwright makes.
65
+ *
66
+ * ### 4. Announcing an hour of build output
67
+ *
68
+ * `role="log"` is the correct role and `aria-live` is off unless
69
+ * asked for. A live log pane emitting a line every 300 ms is a
70
+ * screen reader that cannot be interrupted and a user who cannot
71
+ * read anything else on the page — the accessible answer for a
72
+ * long-running job is a `LiveStatusIndicator` announcing *state*
73
+ * and a log that is there to be read when wanted. `isAnnounced`
74
+ * exists for the short case: a five-line result panel where the
75
+ * lines *are* the outcome.
76
+ *
77
+ * ### Not every boolean is a state kind
78
+ *
79
+ * `isFollowing` is `useState`, not `useVisibility`, and this is the
80
+ * first component in the library to say so out loud. The kinds in
81
+ * `@charcuterie/logic` earn their keep when state is **registered**
82
+ * (members join a group), **composed** (focus and selection
83
+ * disagreeing), or **shared** (a trigger and a panel at opposite
84
+ * ends of a tree). None of that is true here: one component reads
85
+ * one boolean it wrote itself. Reaching for `useVisibility` because
86
+ * it is also a boolean would make `isVisible` mean "is following",
87
+ * which is how a state layer stops meaning anything.
88
+ */
89
+ export declare const LogViewer: ({ className, isAnnounced, label, lines, maxLines, }: LogViewerProps) => ReactNode;
90
+ //# sourceMappingURL=LogViewer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LogViewer.d.ts","sourceRoot":"","sources":["../../src/LogViewer/LogViewer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAOtC,MAAM,MAAM,OAAO,GAAG;IACpB,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,OAAO,EAAE,CAAA;IAChB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AACH,eAAO,MAAM,SAAS,GAAI,qDAMvB,cAAc,KAAG,SA8JnB,CAAA"}
@@ -0,0 +1,165 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { Button } from "../Button/Button.js";
4
+ import { INTENT_CONTENT_CLASS } from "../intentStyles.js";
5
+ import { toClassName } from "../toClassName.js";
6
+ /**
7
+ * Three repos render a scrolling log pane —
8
+ * mux-magic's `JobLogsDisclosure` and `StepLogs`, rip-deck's
9
+ * `LogModal` — and between them they have four defects that this
10
+ * component exists to not have. A fifth, below the effects, is not
11
+ * theirs: it is defect 1 coming back through the front door, in this
12
+ * component, found by the first app to consume it.
13
+ *
14
+ * ### 1. The one that follows nothing
15
+ *
16
+ * ```tsx
17
+ * useEffect(() => {
18
+ * const pane = paneRef.current
19
+ * if (pane) pane.scrollTop = pane.scrollHeight
20
+ * }, []) // ← empty deps
21
+ * ```
22
+ *
23
+ * That is mux-magic's auto-scroll, and it runs **once**, on mount,
24
+ * when the pane is empty and `scrollHeight` is `clientHeight`. Every
25
+ * line after it arrives below the fold. It reads as correct, it is
26
+ * the shape everyone writes, and the log simply never follows.
27
+ *
28
+ * ### 2. Following that cannot be escaped
29
+ *
30
+ * The naive fix — scroll to the bottom on every render — is worse:
31
+ * a user scrolling back to read the line that mentioned an error is
32
+ * yanked to the end twice a second. Following has to be *pinned to
33
+ * the user's own position*, which is what `isFollowing` below is: it
34
+ * turns off the moment the user scrolls away and back on when they
35
+ * return to the bottom, and there is a button for when they do not
36
+ * want to scroll all that way.
37
+ *
38
+ * ### 3. `data-log-id`
39
+ *
40
+ * mux-magic's pane carries one. It is a `data-testid` under a
41
+ * different name — a handle only the test suite can see — and this
42
+ * package's `sourceRules.test.ts` bans the practice rather than the
43
+ * spelling. The pane here is found by its `label`, the same query
44
+ * Playwright makes.
45
+ *
46
+ * ### 4. Announcing an hour of build output
47
+ *
48
+ * `role="log"` is the correct role and `aria-live` is off unless
49
+ * asked for. A live log pane emitting a line every 300 ms is a
50
+ * screen reader that cannot be interrupted and a user who cannot
51
+ * read anything else on the page — the accessible answer for a
52
+ * long-running job is a `LiveStatusIndicator` announcing *state*
53
+ * and a log that is there to be read when wanted. `isAnnounced`
54
+ * exists for the short case: a five-line result panel where the
55
+ * lines *are* the outcome.
56
+ *
57
+ * ### Not every boolean is a state kind
58
+ *
59
+ * `isFollowing` is `useState`, not `useVisibility`, and this is the
60
+ * first component in the library to say so out loud. The kinds in
61
+ * `@charcuterie/logic` earn their keep when state is **registered**
62
+ * (members join a group), **composed** (focus and selection
63
+ * disagreeing), or **shared** (a trigger and a panel at opposite
64
+ * ends of a tree). None of that is true here: one component reads
65
+ * one boolean it wrote itself. Reaching for `useVisibility` because
66
+ * it is also a boolean would make `isVisible` mean "is following",
67
+ * which is how a state layer stops meaning anything.
68
+ */
69
+ export const LogViewer = ({ className, isAnnounced = false, label, lines, maxLines = 500, }) => {
70
+ const paneRef = useRef(null);
71
+ const [isFollowing, setIsFollowing] = useState(true);
72
+ const shownLines = lines.length > maxLines
73
+ ? lines.slice(lines.length - maxLines)
74
+ : lines;
75
+ // biome-ignore lint/correctness/useExhaustiveDependencies: `shownLines` is read through the DOM, not through the closure — `pane.scrollHeight` is a *consequence* of the lines having rendered, so the linter sees a dependency it cannot trace and calls it unnecessary. Removing it is not a style change: the effect then runs only when `isFollowing` flips, which is mux-magic's `}, [])` bug rebuilt exactly. `yarn lint` runs `--write --unsafe`, so this was applied silently once already and the log stopped following.
76
+ useEffect(() => {
77
+ const pane = paneRef.current;
78
+ if (!pane || !isFollowing) {
79
+ return;
80
+ }
81
+ pane.scrollTop = pane.scrollHeight;
82
+ // `shownLines` rather than an empty array — this is the whole fix
83
+ // for defect 1, and the dependency *is* the fix.
84
+ }, [isFollowing, shownLines]);
85
+ /**
86
+ * Defect 1 again, and the effect above cannot see it: **a pane can
87
+ * be measured before it has a box.**
88
+ *
89
+ * `AccordionSection` renders its panel with `hidden` rather than
90
+ * unmounting it, deliberately — "a panel that unmounts loses a
91
+ * scroll position, a partially typed form, and any subscription its
92
+ * content opened — and the fleet's log panes are exactly that." A
93
+ * `hidden` subtree has no layout, so the effect above runs on mount
94
+ * with `scrollHeight 0` and writes `scrollTop = 0`, and then never
95
+ * runs again: neither `isFollowing` nor `shownLines` changes when
96
+ * the section is opened. Measured in mux-magic on a 60-line pane:
97
+ *
98
+ * ```
99
+ * while collapsed : scrollTop 0 scrollHeight 0 clientHeight 0
100
+ * after expanding : scrollTop 0 scrollHeight 976 clientHeight 254
101
+ * ```
102
+ *
103
+ * That is this component's own `}, [])` bug, rebuilt out of two
104
+ * components whose individual decisions are both right, and
105
+ * invisible to both of their test suites — `LogViewer`'s mounted
106
+ * visible, `Accordion`'s holding content that never measures
107
+ * itself. mux-magic hit it in its first week as a consumer and
108
+ * worked around it downstream, in a `DisclosedLogViewer` that
109
+ * withheld the pane until the section had been opened once. This is
110
+ * that workaround coming into the library so it can be deleted.
111
+ *
112
+ * ### Why the box and not the viewport
113
+ *
114
+ * An `IntersectionObserver` answers "is it on screen", which is a
115
+ * different question with two wrong answers here: a pane below the
116
+ * fold on a long page is *not* intersecting and has perfectly good
117
+ * layout, and scrolling the page later would re-fire for no reason.
118
+ * `ResizeObserver` answers "does it have a box, and is that box
119
+ * still the same size" — which is the precondition the measurement
120
+ * above actually needs. Per spec it does not fire at `observe()`
121
+ * time for an element that is not being rendered, so gaining a box
122
+ * *is* the first callback, and that is the reveal.
123
+ *
124
+ * It pays for itself twice: a window resize re-wraps the lines and
125
+ * changes what is at the bottom, and a following pane should be at
126
+ * the new bottom rather than a few lines above it.
127
+ *
128
+ * Only while following. A user who scrolled up to read an error is
129
+ * not dragged back by resizing their window.
130
+ */
131
+ useEffect(() => {
132
+ const pane = paneRef.current;
133
+ if (!pane || !isFollowing) {
134
+ return;
135
+ }
136
+ const observer = new ResizeObserver(() => {
137
+ pane.scrollTop = pane.scrollHeight;
138
+ });
139
+ observer.observe(pane);
140
+ return () => {
141
+ observer.disconnect();
142
+ };
143
+ }, [isFollowing]);
144
+ return (_jsxs("div", { className: toClassName("relative flex flex-col", className), children: [_jsx("div", { "aria-label": label, "aria-live": isAnnounced ? "polite" : "off", className: "max-h-64 overflow-y-auto rounded-md border border-border-subtle bg-surface-sunken p-2 font-mono text-content-secondary text-xs", onScroll: (scrollEvent) => {
145
+ const pane = scrollEvent.currentTarget;
146
+ // A few pixels of slack, because a fractional device pixel
147
+ // ratio means `scrollTop + clientHeight` never lands
148
+ // exactly on `scrollHeight` — an equality test here
149
+ // un-pins the pane on a 125% display and on nothing else,
150
+ // which is the kind of bug that gets reported as "it works
151
+ // on my machine".
152
+ const isPinnedToBottom = pane.scrollHeight -
153
+ pane.scrollTop -
154
+ pane.clientHeight <
155
+ 4;
156
+ setIsFollowing(isPinnedToBottom);
157
+ }, ref: paneRef, role: "log",
158
+ // biome-ignore lint/a11y/noNoninteractiveTabindex: a scroll container must be focusable or a keyboard user can see the pane and cannot scroll it — axe's `scrollable-region-focusable`, which fails this component without the line. Chrome started doing it automatically in 2024; Firefox and Safari have not. The rule is right about `<div>`s in general and wrong about scrollers.
159
+ tabIndex: 0, children: shownLines.length === 0 ? (_jsx("p", { className: "text-content-muted", children: "Waiting for output\u2026" })) : (shownLines.map((line) => (_jsx("p", { className: toClassName("whitespace-pre-wrap break-all", line.intent === undefined
160
+ ? null
161
+ : INTENT_CONTENT_CLASS[line.intent]), children: line.text }, line.key)))) }), isFollowing ? null : (_jsx(Button, { appearance: "soft", className: "absolute inset-x-0 bottom-2 mx-auto w-fit", intent: "neutral", onClick: () => {
162
+ setIsFollowing(true);
163
+ }, size: "sm", children: "Jump to latest" }))] }));
164
+ };
165
+ //# sourceMappingURL=LogViewer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LogViewer.js","sourceRoot":"","sources":["../../src/LogViewer/LogViewer.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,81 @@
1
+ import type { Placement } from "@floating-ui/react";
2
+ import type { ReactElement, ReactNode } from "react";
3
+ export type MenuItem = {
4
+ icon?: ReactNode;
5
+ isDisabled?: boolean;
6
+ key: string;
7
+ label: ReactNode;
8
+ onSelect: () => void;
9
+ };
10
+ export type MenuProps = {
11
+ className?: string;
12
+ isVisible: boolean;
13
+ items: MenuItem[];
14
+ /** Outside press, Escape, and choosing an item all land here. */
15
+ onDismiss: () => void;
16
+ placement?: Placement;
17
+ /** The control the menu hangs off. **Cloned, not wrapped.** */
18
+ trigger: ReactElement;
19
+ };
20
+ /**
21
+ * A menu is **not** a listbox, and mux-magic's `TypePicker` is the
22
+ * fleet's one attempt at telling them apart — it renders
23
+ * `role="menu"` over items that set a value, which is a listbox
24
+ * wearing the wrong role. The distinction is what the items *are*:
25
+ *
26
+ * - `menuitem` **does** something. Rename, Delete, Copy path.
27
+ * - `option` **is** something. It is a value you are choosing.
28
+ *
29
+ * A screen reader announces "menu, 4 items" for one and "listbox,
30
+ * selected, 2 of 4" for the other, and an agent driving the app
31
+ * needs the second to know what state it left behind. Getting it
32
+ * wrong is invisible in a browser and invisible to axe.
33
+ *
34
+ * ### There is no `label` prop, and that corrects a lesson
35
+ *
36
+ * `Spinner` (M3), `Popover` and `Modal` (M4) all take a **required**
37
+ * name, because a `role="status"` and a `role="dialog"` take none
38
+ * from their content. The obvious move here was a `label` prop doing
39
+ * the same job, and it was written that way first — then measured
40
+ * as doing nothing.
41
+ *
42
+ * `useRole(context, { role: "menu" })` puts **`aria-labelledby` on
43
+ * the panel pointing at the trigger**, and `aria-labelledby` beats
44
+ * `aria-label`. So the menu was already named — "Bay 3", from the
45
+ * button — and the prop was silently discarded. That is also the ARIA
46
+ * Authoring Practices menu-button pattern, where a menu is named by
47
+ * the control that opens it.
48
+ *
49
+ * The rule the other three teach is therefore narrower than it
50
+ * looked: **an overlay with no trigger relationship needs its own
51
+ * name.** A menu has one, so the requirement moves to the trigger —
52
+ * which `Button` and `IconButton` already enforce.
53
+ *
54
+ * ### Roving focus, and the composition `Tabs` already proved
55
+ *
56
+ * `RovingFocus` alone this time — there is no selection, because a
57
+ * menu item is not selected, it is invoked. That is the same
58
+ * two-kinds argument from the other side: `Tabs` needed both kinds
59
+ * because a tab is chosen *and* focused, and a menu needs one
60
+ * because a menu item is only ever focused.
61
+ *
62
+ * ### Focus moves in, once, and only on opening
63
+ *
64
+ * A menu is one of the few patterns where focus must jump on open —
65
+ * otherwise the keyboard user who pressed Enter on the trigger is
66
+ * still on the trigger with a menu they cannot reach. It happens in
67
+ * the panel's own effect rather than through
68
+ * `FloatingFocusManager`'s `initialFocus`, because the element to
69
+ * focus is whichever one `RovingFocus` says is active, and that is
70
+ * not known until the items have registered.
71
+ *
72
+ * ### No type-ahead, deliberately
73
+ *
74
+ * The APG lists type-ahead as optional for a menu and required for a
75
+ * listbox, and none of the fleet's menus are long enough to need it
76
+ * — the largest has six items. Adding it would mean a keystroke
77
+ * buffer and a timer in a component that currently has neither, for
78
+ * a menu you can cross in three arrow presses.
79
+ */
80
+ export declare const Menu: ({ className, isVisible, items, onDismiss, placement, trigger, }: MenuProps) => ReactNode;
81
+ //# sourceMappingURL=Menu.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Menu.d.ts","sourceRoot":"","sources":["../../src/Menu/Menu.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAYnD,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAMpD,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,SAAS,CAAA;IAChB,QAAQ,EAAE,MAAM,IAAI,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,OAAO,CAAA;IAClB,KAAK,EAAE,QAAQ,EAAE,CAAA;IACjB,iEAAiE;IACjE,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,+DAA+D;IAC/D,OAAO,EAAE,YAAY,CAAA;CACtB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AACH,eAAO,MAAM,IAAI,GAAI,iEAOlB,SAAS,KAAG,SA8Ld,CAAA"}