@charcuterie/ui 3.11.0 → 3.13.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 (36) hide show
  1. package/dist/Avatar/Avatar.d.ts +163 -0
  2. package/dist/Avatar/Avatar.d.ts.map +1 -0
  3. package/dist/Avatar/Avatar.js +139 -0
  4. package/dist/Avatar/Avatar.js.map +1 -0
  5. package/dist/Board/BoardLaneList.d.ts +29 -0
  6. package/dist/Board/BoardLaneList.d.ts.map +1 -1
  7. package/dist/Board/BoardLaneList.js +2 -1
  8. package/dist/Board/BoardLaneList.js.map +1 -1
  9. package/dist/CopyButton/CopyButton.d.ts +67 -0
  10. package/dist/CopyButton/CopyButton.d.ts.map +1 -0
  11. package/dist/CopyButton/CopyButton.js +76 -0
  12. package/dist/CopyButton/CopyButton.js.map +1 -0
  13. package/dist/CopyButton/copyText.d.ts +23 -0
  14. package/dist/CopyButton/copyText.d.ts.map +1 -0
  15. package/dist/CopyButton/copyText.js +65 -0
  16. package/dist/CopyButton/copyText.js.map +1 -0
  17. package/dist/index.d.ts +5 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +3 -0
  20. package/dist/index.js.map +1 -1
  21. package/package.json +1 -1
  22. package/src/Avatar/Avatar.mdx +92 -0
  23. package/src/Avatar/Avatar.stories.tsx +254 -0
  24. package/src/Avatar/Avatar.test.tsx +143 -0
  25. package/src/Avatar/Avatar.tsx +270 -0
  26. package/src/Board/Board.mdx +26 -0
  27. package/src/Board/Board.stories.tsx +47 -21
  28. package/src/Board/Board.test.tsx +62 -0
  29. package/src/Board/BoardLaneList.tsx +52 -1
  30. package/src/CopyButton/CopyButton.mdx +84 -0
  31. package/src/CopyButton/CopyButton.stories.tsx +111 -0
  32. package/src/CopyButton/CopyButton.test.tsx +227 -0
  33. package/src/CopyButton/CopyButton.tsx +155 -0
  34. package/src/CopyButton/copyText.ts +68 -0
  35. package/src/index.ts +11 -0
  36. package/src/sourceRules.test.ts +15 -1
@@ -0,0 +1,163 @@
1
+ import type { CategoricalIndex } from "@charcuterie/tokens";
2
+ import type { ComponentPropsWithRef, ReactNode } from "react";
3
+ import type { IntentAppearance } from "../intentStyles.ts";
4
+ export type AvatarSize = keyof typeof AVATAR_SIZE_CLASS;
5
+ export type AvatarProps = Omit<ComponentPropsWithRef<"span">, "children"> & {
6
+ /**
7
+ * The same three the rest of the categorical family wears, minus
8
+ * `ghost` — a chip with no fill and no border is a floating
9
+ * letter, and the whole job here is to be a **shape** the eye
10
+ * finds in a list.
11
+ */
12
+ appearance?: Exclude<IntentAppearance, "ghost">;
13
+ /**
14
+ * The colour, when somebody has chosen one. Omitted, it is derived
15
+ * — see `categoricalKey` — so an avatar always has a colour and
16
+ * never has to be given one.
17
+ */
18
+ categorical?: CategoricalIndex;
19
+ /**
20
+ * What the derived colour is hashed from when `categorical` is
21
+ * absent. Defaults to `name`.
22
+ *
23
+ * Pass the **user's id** wherever one exists. `getCategoricalIndex`
24
+ * does no normalization on purpose, so hashing the name means a
25
+ * rename — or a trailing space — silently repaints a person the
26
+ * reader had already learned to recognise. An id does not move.
27
+ */
28
+ categoricalKey?: string;
29
+ /**
30
+ * A glyph instead of the initials — a robot for an agent account,
31
+ * a lock for a service account. The library ships **no icons**, so
32
+ * this is the app's own
33
+ * ([decision](../../../docs/decisions/2026-07-29-ship-no-icons-and-no-symbol-glyphs.md)).
34
+ * It is decoration: the name still travels in `aria-label`, and a
35
+ * glyph the font cannot render must not be the only thing
36
+ * carrying who this is.
37
+ */
38
+ icon?: ReactNode;
39
+ /**
40
+ * A photo. Optional, and **it is never the only thing** — a
41
+ * request that 404s, a signed URL that expired, or an app that
42
+ * simply has no photos falls back to the initials with no empty
43
+ * box in between.
44
+ */
45
+ imageUrl?: string;
46
+ /**
47
+ * The letters, when the derivation is wrong. Derived from `name`
48
+ * otherwise: the first character of each of the first two
49
+ * whitespace-separated words, upper-cased.
50
+ *
51
+ * Worth passing for a name the split gets wrong — a mononym that
52
+ * wants two letters, a name whose family part comes first, a
53
+ * handle with punctuation in it.
54
+ */
55
+ initials?: string;
56
+ /**
57
+ * Who this is. Read by assistive technology and shown on hover;
58
+ * **never printed as visible text**, which is the entire point of
59
+ * the component.
60
+ *
61
+ * Nullable, and that is a decision rather than a loose type — see
62
+ * the note on the component about what an unassigned row draws.
63
+ */
64
+ name: string | null | undefined;
65
+ size?: AvatarSize;
66
+ };
67
+ /**
68
+ * One rule: **the circle is 1.9 x its own type size**, and `size`
69
+ * picks which step of the type scale that is.
70
+ *
71
+ * 1.9 rather than a tighter number because a circle only offers its
72
+ * full width on the centre line. Two capitals measure about 1.25em
73
+ * in this face, and a chord taken at the cap height of a 1.75em
74
+ * circle is barely wider than that — measured, not guessed.
75
+ *
76
+ * It is em-based rather than a fixed `size-5` / `size-7` / `size-9`,
77
+ * and that is the whole of the design. Every length in this token
78
+ * set moves with the density axis — `--font-size-xs` is 0.88rem
79
+ * compact, 0.94rem at desktop and 1.11rem on the kiosk — so a chip
80
+ * pinned to 20px holds two letters at one density and clips them at
81
+ * the next. `controlStyles.ts` says the same thing about badges and
82
+ * dots: non-control sizing is type-relative on purpose, because a
83
+ * chip that stays 20px while the kiosk scales the prose around it
84
+ * reads as a rendering fault.
85
+ *
86
+ * The hand-rolled avatar this replaces is the worked example. It was
87
+ * `size-5` with `text-[0.625rem]` — a 10px font, which is **not a
88
+ * step of this type scale at any density**, because 20px is not
89
+ * enough room for two characters at the smallest size the tokens
90
+ * actually offer. A component that has to invent a font size to fit
91
+ * its own box has the wrong box.
92
+ */
93
+ declare const AVATAR_SIZE_CLASS: {
94
+ readonly sm: "size-[1.9em] text-xs";
95
+ readonly md: "size-[1.9em] text-md";
96
+ readonly lg: "size-[1.9em] text-xl";
97
+ };
98
+ /**
99
+ * A person, as a coloured chip — a glyph or an initial, with the
100
+ * name attached where only assistive technology and a hover can
101
+ * reach it.
102
+ *
103
+ * It exists to delete a printed name. Docket's board put the word
104
+ * *"Owner"* on most of its cards, which is a column of identical
105
+ * text doing the work of a colour; the fleet's boards all want the
106
+ * same chip, and `Board.stories.tsx` had already hand-rolled one
107
+ * inside a story file.
108
+ *
109
+ * ### Unassigned draws **nothing**, and the component owns that
110
+ *
111
+ * `name` accepts `null` and `undefined`, and either renders `null`.
112
+ * A grey *"None"* placeholder is a decision the design system makes
113
+ * against the reader: it is a chip that says a fact nobody needed,
114
+ * repeated down every unassigned row, at the same weight as the
115
+ * chips that mean something.
116
+ *
117
+ * The alternative is a ternary at every call site, and the call
118
+ * sites are the whole problem — a board, a table, a card footer, a
119
+ * detail pane, each written on a different day. The one that forgets
120
+ * is the one that ships the grey circle. Absence is expressed once,
121
+ * here, which is the same call `Board` makes with `onMove`: no
122
+ * handler means no affordance, rather than a disabled one.
123
+ *
124
+ * A consumer that genuinely wants to *say* "unassigned" still can —
125
+ * that is text, or an `EmptyState`, and neither of them is an
126
+ * avatar.
127
+ *
128
+ * ### The name is never visible text
129
+ *
130
+ * `role="img"` with an `aria-label`, exactly as `Swatch` does, so
131
+ * `getByRole("img", { name })` is the one handle whether the chip is
132
+ * showing a photo, a glyph or two letters. `title` carries the same
133
+ * string for a pointer, which is the only channel a person browsing
134
+ * with their eyes has — the letters `KG` are not a name.
135
+ *
136
+ * That is also why there is no `isNameVisible`: an avatar whose name
137
+ * is printed beside it is a name with a decoration in front of it,
138
+ * and the layout that wants one should print the name and skip the
139
+ * chip.
140
+ *
141
+ * ### The colour is derived, never invented
142
+ *
143
+ * `categorical` is the ten-wide non-semantic scale — the same one
144
+ * `Badge` takes — and **not** an `intent`. An intent is a claim the
145
+ * design system makes; a person is not `danger`. With no
146
+ * `categorical` the index comes from `getCategoricalIndex`, which is
147
+ * a pure function of the key, so the same person is the same colour
148
+ * on every machine, every reload, and in a server render. Ten
149
+ * colours means collisions past ten people, and that is a property
150
+ * of the palette rather than a fault in the hash: the colour helps
151
+ * you *scan*, and the accessible name is what identifies.
152
+ *
153
+ * ### There is no `AvatarGroup`
154
+ *
155
+ * Deliberately. A stack of overlapping chips with a `+3` needs
156
+ * answers to which three survive, in what order, and what the
157
+ * overflow announces — and no consumer has asked the question yet. A
158
+ * row of `Avatar`s in a flex box is what a multi-assignee list looks
159
+ * like until one does.
160
+ */
161
+ export declare const Avatar: ({ appearance, categorical, categoricalKey, className, icon, imageUrl, initials, name, size, title, ...spanProps }: AvatarProps) => ReactNode;
162
+ export {};
163
+ //# sourceMappingURL=Avatar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Avatar.d.ts","sourceRoot":"","sources":["../../src/Avatar/Avatar.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAE3D,OAAO,KAAK,EACV,qBAAqB,EACrB,SAAS,EACV,MAAM,OAAO,CAAA;AAId,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAG1D,MAAM,MAAM,UAAU,GAAG,MAAM,OAAO,iBAAiB,CAAA;AAEvD,MAAM,MAAM,WAAW,GAAG,IAAI,CAC5B,qBAAqB,CAAC,MAAM,CAAC,EAC7B,UAAU,CACX,GAAG;IACF;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAA;IAC/C;;;;OAIG;IACH,WAAW,CAAC,EAAE,gBAAgB,CAAA;IAC9B;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;;OAOG;IACH,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IAC/B,IAAI,CAAC,EAAE,UAAU,CAAA;CAClB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,QAAA,MAAM,iBAAiB;;;;CAIb,CAAA;AAkBV;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AACH,eAAO,MAAM,MAAM,GAAI,mHAYpB,WAAW,KAAG,SAkEhB,CAAA"}
@@ -0,0 +1,139 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { getCategoricalIndex } from "@charcuterie/tokens";
3
+ import { useState } from "react";
4
+ import { CATEGORICAL_APPEARANCE_CLASS } from "../categoricalStyles.js";
5
+ import { toClassName } from "../toClassName.js";
6
+ /**
7
+ * One rule: **the circle is 1.9 x its own type size**, and `size`
8
+ * picks which step of the type scale that is.
9
+ *
10
+ * 1.9 rather than a tighter number because a circle only offers its
11
+ * full width on the centre line. Two capitals measure about 1.25em
12
+ * in this face, and a chord taken at the cap height of a 1.75em
13
+ * circle is barely wider than that — measured, not guessed.
14
+ *
15
+ * It is em-based rather than a fixed `size-5` / `size-7` / `size-9`,
16
+ * and that is the whole of the design. Every length in this token
17
+ * set moves with the density axis — `--font-size-xs` is 0.88rem
18
+ * compact, 0.94rem at desktop and 1.11rem on the kiosk — so a chip
19
+ * pinned to 20px holds two letters at one density and clips them at
20
+ * the next. `controlStyles.ts` says the same thing about badges and
21
+ * dots: non-control sizing is type-relative on purpose, because a
22
+ * chip that stays 20px while the kiosk scales the prose around it
23
+ * reads as a rendering fault.
24
+ *
25
+ * The hand-rolled avatar this replaces is the worked example. It was
26
+ * `size-5` with `text-[0.625rem]` — a 10px font, which is **not a
27
+ * step of this type scale at any density**, because 20px is not
28
+ * enough room for two characters at the smallest size the tokens
29
+ * actually offer. A component that has to invent a font size to fit
30
+ * its own box has the wrong box.
31
+ */
32
+ const AVATAR_SIZE_CLASS = {
33
+ sm: "size-[1.9em] text-xs",
34
+ md: "size-[1.9em] text-md",
35
+ lg: "size-[1.9em] text-xl",
36
+ };
37
+ /**
38
+ * The first character of each of the first two words.
39
+ *
40
+ * `Array.from` rather than `charAt`, so a name that starts outside
41
+ * the BMP contributes a whole character instead of half a surrogate
42
+ * pair and a replacement glyph.
43
+ */
44
+ const toInitials = (name) => name
45
+ .trim()
46
+ .split(/\s+/)
47
+ .slice(0, 2)
48
+ .map((word) => Array.from(word)[0] ?? "")
49
+ .join("")
50
+ .toLocaleUpperCase();
51
+ /**
52
+ * A person, as a coloured chip — a glyph or an initial, with the
53
+ * name attached where only assistive technology and a hover can
54
+ * reach it.
55
+ *
56
+ * It exists to delete a printed name. Docket's board put the word
57
+ * *"Owner"* on most of its cards, which is a column of identical
58
+ * text doing the work of a colour; the fleet's boards all want the
59
+ * same chip, and `Board.stories.tsx` had already hand-rolled one
60
+ * inside a story file.
61
+ *
62
+ * ### Unassigned draws **nothing**, and the component owns that
63
+ *
64
+ * `name` accepts `null` and `undefined`, and either renders `null`.
65
+ * A grey *"None"* placeholder is a decision the design system makes
66
+ * against the reader: it is a chip that says a fact nobody needed,
67
+ * repeated down every unassigned row, at the same weight as the
68
+ * chips that mean something.
69
+ *
70
+ * The alternative is a ternary at every call site, and the call
71
+ * sites are the whole problem — a board, a table, a card footer, a
72
+ * detail pane, each written on a different day. The one that forgets
73
+ * is the one that ships the grey circle. Absence is expressed once,
74
+ * here, which is the same call `Board` makes with `onMove`: no
75
+ * handler means no affordance, rather than a disabled one.
76
+ *
77
+ * A consumer that genuinely wants to *say* "unassigned" still can —
78
+ * that is text, or an `EmptyState`, and neither of them is an
79
+ * avatar.
80
+ *
81
+ * ### The name is never visible text
82
+ *
83
+ * `role="img"` with an `aria-label`, exactly as `Swatch` does, so
84
+ * `getByRole("img", { name })` is the one handle whether the chip is
85
+ * showing a photo, a glyph or two letters. `title` carries the same
86
+ * string for a pointer, which is the only channel a person browsing
87
+ * with their eyes has — the letters `KG` are not a name.
88
+ *
89
+ * That is also why there is no `isNameVisible`: an avatar whose name
90
+ * is printed beside it is a name with a decoration in front of it,
91
+ * and the layout that wants one should print the name and skip the
92
+ * chip.
93
+ *
94
+ * ### The colour is derived, never invented
95
+ *
96
+ * `categorical` is the ten-wide non-semantic scale — the same one
97
+ * `Badge` takes — and **not** an `intent`. An intent is a claim the
98
+ * design system makes; a person is not `danger`. With no
99
+ * `categorical` the index comes from `getCategoricalIndex`, which is
100
+ * a pure function of the key, so the same person is the same colour
101
+ * on every machine, every reload, and in a server render. Ten
102
+ * colours means collisions past ten people, and that is a property
103
+ * of the palette rather than a fault in the hash: the colour helps
104
+ * you *scan*, and the accessible name is what identifies.
105
+ *
106
+ * ### There is no `AvatarGroup`
107
+ *
108
+ * Deliberately. A stack of overlapping chips with a `+3` needs
109
+ * answers to which three survive, in what order, and what the
110
+ * overflow announces — and no consumer has asked the question yet. A
111
+ * row of `Avatar`s in a flex box is what a multi-assignee list looks
112
+ * like until one does.
113
+ */
114
+ export const Avatar = ({ appearance = "soft", categorical, categoricalKey, className, icon, imageUrl, initials, name, size = "md", title, ...spanProps }) => {
115
+ /**
116
+ * The URL that failed, rather than a `hasFailed` boolean.
117
+ *
118
+ * A boolean has to be *reset* when `imageUrl` changes, which is an
119
+ * effect syncing a prop into state — the shape this library
120
+ * refuses everywhere else
121
+ * ([decision](../../../docs/decisions/2026-07-29-logic-hooks-are-uncontrolled.md)).
122
+ * Storing the URL makes the comparison below derive the answer, so
123
+ * a second photo gets its own attempt and a re-render never
124
+ * resurrects a dead one.
125
+ */
126
+ const [failedImageUrl, setFailedImageUrl] = useState(undefined);
127
+ const hasImageError = imageUrl !== undefined && failedImageUrl === imageUrl;
128
+ const trimmedName = name?.trim();
129
+ if (!trimmedName) {
130
+ return null;
131
+ }
132
+ const resolvedInitials = initials ?? toInitials(trimmedName);
133
+ const resolvedCategorical = categorical ??
134
+ getCategoricalIndex(categoricalKey ?? trimmedName);
135
+ return (_jsx("span", { ...spanProps, "aria-label": trimmedName, className: toClassName("inline-grid shrink-0 place-items-center overflow-hidden rounded-full border font-medium leading-none", AVATAR_SIZE_CLASS[size], CATEGORICAL_APPEARANCE_CLASS[resolvedCategorical][appearance], className), role: "img", title: title ?? trimmedName, children: imageUrl !== undefined && !hasImageError ? (_jsx("img", { alt: "", className: "size-full object-cover", onError: () => {
136
+ setFailedImageUrl(imageUrl);
137
+ }, src: imageUrl })) : (_jsx("span", { "aria-hidden": "true", children: icon ?? resolvedInitials })) }));
138
+ };
139
+ //# sourceMappingURL=Avatar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Avatar.js","sourceRoot":"","sources":["../../src/Avatar/Avatar.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAKzD,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEhC,OAAO,EAAE,4BAA4B,EAAE,MAAM,yBAAyB,CAAA;AAEtE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAsE/C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,iBAAiB,GAAG;IACxB,EAAE,EAAE,sBAAsB;IAC1B,EAAE,EAAE,sBAAsB;IAC1B,EAAE,EAAE,sBAAsB;CAClB,CAAA;AAEV;;;;;;GAMG;AACH,MAAM,UAAU,GAAG,CAAC,IAAY,EAAU,EAAE,CAC1C,IAAI;KACD,IAAI,EAAE;KACN,KAAK,CAAC,KAAK,CAAC;KACZ,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;KACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;KACxC,IAAI,CAAC,EAAE,CAAC;KACR,iBAAiB,EAAE,CAAA;AAExB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,EACrB,UAAU,GAAG,MAAM,EACnB,WAAW,EACX,cAAc,EACd,SAAS,EACT,IAAI,EACJ,QAAQ,EACR,QAAQ,EACR,IAAI,EACJ,IAAI,GAAG,IAAI,EACX,KAAK,EACL,GAAG,SAAS,EACA,EAAa,EAAE;IAC3B;;;;;;;;;;OAUG;IACH,MAAM,CAAC,cAAc,EAAE,iBAAiB,CAAC,GAAG,QAAQ,CAElD,SAAS,CAAC,CAAA;IAEZ,MAAM,aAAa,GACjB,QAAQ,KAAK,SAAS,IAAI,cAAc,KAAK,QAAQ,CAAA;IAEvD,MAAM,WAAW,GAAG,IAAI,EAAE,IAAI,EAAE,CAAA;IAEhC,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,gBAAgB,GACpB,QAAQ,IAAI,UAAU,CAAC,WAAW,CAAC,CAAA;IAErC,MAAM,mBAAmB,GACvB,WAAW;QACX,mBAAmB,CAAC,cAAc,IAAI,WAAW,CAAC,CAAA;IAEpD,OAAO,CACL,kBACM,SAAS,gBACD,WAAW,EACvB,SAAS,EAAE,WAAW,CACpB,sGAAsG,EACtG,iBAAiB,CAAC,IAAI,CAAC,EACvB,4BAA4B,CAAC,mBAAmB,CAAC,CAC/C,UAAU,CACX,EACD,SAAS,CACV,EACD,IAAI,EAAC,KAAK,EACV,KAAK,EAAE,KAAK,IAAI,WAAW,YAE1B,QAAQ,KAAK,SAAS,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAI1C,cACE,GAAG,EAAC,EAAE,EACN,SAAS,EAAC,wBAAwB,EAClC,OAAO,EAAE,GAAG,EAAE;gBACZ,iBAAiB,CAAC,QAAQ,CAAC,CAAA;YAC7B,CAAC,EACD,GAAG,EAAE,QAAQ,GACb,CACH,CAAC,CAAC,CAAC,CACF,8BAAkB,MAAM,YACrB,IAAI,IAAI,gBAAgB,GACpB,CACR,GACI,CACR,CAAA;AACH,CAAC,CAAA"}
@@ -12,6 +12,23 @@ export type BoardLane = {
12
12
  * requirement and an empty lane is the one a person sees most.
13
13
  */
14
14
  emptyState?: ReactNode;
15
+ /**
16
+ * Where the lane's own heading navigates — a route showing this
17
+ * lane and nothing else.
18
+ *
19
+ * **A link inside the heading, never a heading replaced by a
20
+ * link.** The `<h3>` keeps its level, its id and its place in the
21
+ * document outline, so the lane stays a named `group` and the
22
+ * screen-reader outline is unchanged; the anchor sits inside it.
23
+ * Routed through `RouterLinkProvider` like every other `href` in
24
+ * this library, so an app with a client-side router navigates
25
+ * without a full page load and one without it still gets a real
26
+ * `<a href>`.
27
+ *
28
+ * Absent means the heading is plain text, which is what every
29
+ * existing consumer gets.
30
+ */
31
+ href?: string;
15
32
  /**
16
33
  * The **true** size of the lane, when `items` is a truncated page
17
34
  * of it. This is what the count badge shows, and the difference
@@ -23,6 +40,18 @@ export type BoardLane = {
23
40
  itemCount?: number;
24
41
  items: readonly BoardItem[];
25
42
  key: string;
43
+ /**
44
+ * The lane's name, and it stays a **string** rather than widening
45
+ * to a `ReactNode`.
46
+ *
47
+ * It is not only a heading: the same value names the lane in the
48
+ * move menu on every card, in the Narrow View's segmented control,
49
+ * in the "+ n more in Todo" line, and in the sentence the live
50
+ * region reads out after a move. A `ReactNode` is not something
51
+ * any of those four can put in a string, so widening this would
52
+ * trade one linkable heading for four places that can no longer
53
+ * say which lane they mean. `href` buys the link without that.
54
+ */
26
55
  label: string;
27
56
  /**
28
57
  * What "+ n more in this lane" does. With no handler the line is
@@ -1 +1 @@
1
- {"version":3,"file":"BoardLaneList.d.ts","sourceRoot":"","sources":["../../src/Board/BoardLaneList.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAMtC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAGhD,MAAM,MAAM,SAAS,GAAG;IACtB;;;OAGG;IACH,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,SAAS,CAAA;IACtB;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,SAAS,SAAS,EAAE,CAAA;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B;;;;;;OAMG;IACH,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC;;;;;;OAMG;IACH,YAAY,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAC3B,mBAAmB,EAAE,OAAO,CAAA;IAC5B,IAAI,EAAE,SAAS,CAAA;IACf,QAAQ,CAAC,EAAE,SAAS,CAAA;IACpB,WAAW,EAAE,SAAS;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACtD,YAAY,CAAC,EAAE,CACb,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,KACd,IAAI,CAAA;IACT,WAAW,CAAC,EAAE,CACZ,YAAY,EAAE;QACZ,OAAO,EAAE,MAAM,CAAA;QACf,OAAO,EAAE,MAAM,CAAA;QACf,aAAa,EAAE,WAAW,GAAG,OAAO,CAAA;QACpC,SAAS,EAAE,MAAM,CAAA;KAClB,EACD,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,KAC3C,IAAI,CAAA;IACT,mBAAmB,EAAE,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAA;CAC3D,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,aAAa,GAAI,0IAU3B,kBAAkB,KAAG,SAyJvB,CAAA"}
1
+ {"version":3,"file":"BoardLaneList.d.ts","sourceRoot":"","sources":["../../src/Board/BoardLaneList.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAOtC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAGhD,MAAM,MAAM,SAAS,GAAG;IACtB;;;OAGG;IACH,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,SAAS,CAAA;IACtB;;;;;;;;;;;;;;;OAeG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,SAAS,SAAS,EAAE,CAAA;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX;;;;;;;;;;;OAWG;IACH,KAAK,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B;;;;;;OAMG;IACH,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC;;;;;;OAMG;IACH,YAAY,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAC3B,mBAAmB,EAAE,OAAO,CAAA;IAC5B,IAAI,EAAE,SAAS,CAAA;IACf,QAAQ,CAAC,EAAE,SAAS,CAAA;IACpB,WAAW,EAAE,SAAS;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACtD,YAAY,CAAC,EAAE,CACb,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,KACd,IAAI,CAAA;IACT,WAAW,CAAC,EAAE,CACZ,YAAY,EAAE;QACZ,OAAO,EAAE,MAAM,CAAA;QACf,OAAO,EAAE,MAAM,CAAA;QACf,aAAa,EAAE,WAAW,GAAG,OAAO,CAAA;QACpC,SAAS,EAAE,MAAM,CAAA;KAClB,EACD,MAAM,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,KAC3C,IAAI,CAAA;IACT,mBAAmB,EAAE,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAA;CAC3D,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,aAAa,GAAI,0IAU3B,kBAAkB,KAAG,SA8KvB,CAAA"}
@@ -3,6 +3,7 @@ import { useUniqueId } from "@charcuterie/logic";
3
3
  import { Badge } from "../Badge/Badge.js";
4
4
  import { Button } from "../Button/Button.js";
5
5
  import { EmptyState } from "../EmptyState/EmptyState.js";
6
+ import { TextLink } from "../TextLink/TextLink.js";
6
7
  import { toClassName } from "../toClassName.js";
7
8
  import { BoardCard } from "./BoardCard.js";
8
9
  /**
@@ -48,7 +49,7 @@ export const BoardLaneList = ({ dropIndicatorOffset, headingLevel, isVisibleWhen
48
49
  // queries the **board**, not the list below — an element
49
50
  // carrying `container-type` still resolves its own container
50
51
  // queries against its nearest ancestor container.
51
- isVisibleWhenNarrow ? "flex" : "hidden cq-lg:flex"), children: _jsx("div", { className: "@container", children: _jsxs("div", { className: "flex flex-col gap-2 rounded-lg border border-border-subtle bg-surface-raised px-3 py-2 cq-lg:border-0 cq-lg:bg-transparent cq-lg:p-0", children: [_jsxs("header", { className: "flex items-center gap-2", children: [_jsx(Heading, { className: "font-semibold text-content-primary text-sm", id: headingId, children: lane.label }), _jsx(Badge, { appearance: "soft", intent: "neutral", size: "sm", children: String(itemCount) }), _jsx("span", { className: "flex-1" }), lane.actions] }), _jsxs("ul", { className: "relative flex flex-col", ref: registerListElement, children: [lane.items.length === 0 ? (_jsx("li", { children: lane.emptyState ?? (_jsx(EmptyState, { description: "Nothing is waiting here.", heading: "Nothing here", headingLevel:
52
+ isVisibleWhenNarrow ? "flex" : "hidden cq-lg:flex"), children: _jsx("div", { className: "@container", children: _jsxs("div", { className: "flex flex-col gap-2 rounded-lg border border-border-subtle bg-surface-raised px-3 py-2 cq-lg:border-0 cq-lg:bg-transparent cq-lg:p-0", children: [_jsxs("header", { className: "flex items-center gap-2", children: [_jsx(Heading, { className: "font-semibold text-content-primary text-sm", id: headingId, children: lane.href ? (_jsx(TextLink, { appearance: "standalone", className: "font-semibold text-inherit", href: lane.href, children: lane.label })) : (lane.label) }), _jsx(Badge, { appearance: "soft", intent: "neutral", size: "sm", children: String(itemCount) }), _jsx("span", { className: "flex-1" }), lane.actions] }), _jsxs("ul", { className: "relative flex flex-col", ref: registerListElement, children: [lane.items.length === 0 ? (_jsx("li", { children: lane.emptyState ?? (_jsx(EmptyState, { description: "Nothing is waiting here.", heading: "Nothing here", headingLevel:
52
53
  // One below the lane's own, so the outline
53
54
  // stays whole wherever the board is mounted.
54
55
  (headingLevel + 1), size: "sm" })) })) : (lane.items.map((item, index) => (_jsx(BoardCard, { item: item, laneLabel: lane.label, moveIcon: moveIcon, moveTargets: moveTargets, onMoveToLane: onMoveToLane
@@ -1 +1 @@
1
- {"version":3,"file":"BoardLaneList.js","sourceRoot":"","sources":["../../src/Board/BoardLaneList.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"BoardLaneList.js","sourceRoot":"","sources":["../../src/Board/BoardLaneList.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ import type { ReactNode } from "react";
2
+ import type { ButtonProps } from "../Button/Button.tsx";
3
+ /** Where the button is in the one action it performs. */
4
+ export type CopyStatus = "copied" | "failed" | "idle";
5
+ export type CopyButtonProps = Omit<ButtonProps, "children" | "isLoading" | "loadingLabel" | "onCopy"> & {
6
+ /**
7
+ * How long the confirmation stays up, in milliseconds.
8
+ *
9
+ * Long enough to be read after the eye has moved back to the form
10
+ * being filled in, short enough that a second copy of a *different*
11
+ * value is not mistaken for the first one's tick.
12
+ */
13
+ confirmDuration?: number;
14
+ /** Resting label. A word, not a glyph — this library ships no icons. */
15
+ children?: ReactNode;
16
+ /** Shown while the copy is confirmed. */
17
+ copiedLabel?: ReactNode;
18
+ /**
19
+ * The clipboard write, injected. Swapped in tests and in a host
20
+ * that owns its own clipboard bridge (Electron, a kiosk shell).
21
+ */
22
+ copy?: (value: string) => Promise<boolean> | boolean;
23
+ /** Shown when the clipboard refused. */
24
+ failedLabel?: ReactNode;
25
+ /**
26
+ * Told what actually happened, every press.
27
+ *
28
+ * `isCopied` is the point of the callback rather than a detail: a
29
+ * consumer that files the mail away on a copy must not file it away
30
+ * on a copy the browser refused.
31
+ */
32
+ onCopy?: (isCopied: boolean, value: string) => void;
33
+ /** The string that goes on the clipboard. */
34
+ value: string;
35
+ };
36
+ /**
37
+ * A button that puts a string on the clipboard and says so.
38
+ *
39
+ * From a consumer, like `Alert` and `BadgeButton` before it: four
40
+ * repos spell this shape by hand — mux-magic's YAML dialog flips its
41
+ * own label on a `setTimeout`, spoolbuddy carries a 15-line
42
+ * `copyToClipboard` with the `execCommand` fallback inline, bambuddy
43
+ * copies a download link from two menus, and mail-sifter's
44
+ * verification-code banner is the fourth. None of the four agree on
45
+ * what happens when the clipboard says no, and three of them cannot
46
+ * tell.
47
+ *
48
+ * **The confirmation is the button, not a toast.** The press and the
49
+ * proof are the same object, so the eye does not travel; a toast is
50
+ * right for something that happened *elsewhere*. A consumer that
51
+ * wants both still gets `onCopy`.
52
+ *
53
+ * **The label is a word.** `Copy` → `Copied`, and the intent goes
54
+ * with it, because a colour change alone is not a message and a tick
55
+ * glyph is an icon this library does not ship.
56
+ *
57
+ * **It announces.** A screen reader is not reliably told that a
58
+ * focused button's own name changed, so the outcome is also written
59
+ * into a `role="status"` region — and the failure says what to do
60
+ * next rather than only that it went wrong.
61
+ *
62
+ * **The timer is cancelled on unmount and on a re-press**, which is
63
+ * the bug in two of the four hand-rolled copies: press, navigate
64
+ * away, and a `setState` lands on nothing.
65
+ */
66
+ export declare const CopyButton: ({ appearance, children, confirmDuration, copiedLabel, copy, failedLabel, intent, onClick, onCopy, value, ...buttonProps }: CopyButtonProps) => ReactNode;
67
+ //# sourceMappingURL=CopyButton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CopyButton.d.ts","sourceRoot":"","sources":["../../src/CopyButton/CopyButton.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAKvD,yDAAyD;AACzD,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAA;AAErD,MAAM,MAAM,eAAe,GAAG,IAAI,CAChC,WAAW,EACX,UAAU,GAAG,WAAW,GAAG,cAAc,GAAG,QAAQ,CACrD,GAAG;IACF;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,SAAS,CAAA;IACpB,yCAAyC;IACzC,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB;;;OAGG;IACH,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAA;IACpD,wCAAwC;IACxC,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACnD,6CAA6C;IAC7C,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,UAAU,GAAI,2HAYxB,eAAe,KAAG,SAmEpB,CAAA"}
@@ -0,0 +1,76 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { Button } from "../Button/Button.js";
4
+ import { VisuallyHidden } from "../VisuallyHidden/VisuallyHidden.js";
5
+ import { copyText as defaultCopyText } from "./copyText.js";
6
+ /**
7
+ * A button that puts a string on the clipboard and says so.
8
+ *
9
+ * From a consumer, like `Alert` and `BadgeButton` before it: four
10
+ * repos spell this shape by hand — mux-magic's YAML dialog flips its
11
+ * own label on a `setTimeout`, spoolbuddy carries a 15-line
12
+ * `copyToClipboard` with the `execCommand` fallback inline, bambuddy
13
+ * copies a download link from two menus, and mail-sifter's
14
+ * verification-code banner is the fourth. None of the four agree on
15
+ * what happens when the clipboard says no, and three of them cannot
16
+ * tell.
17
+ *
18
+ * **The confirmation is the button, not a toast.** The press and the
19
+ * proof are the same object, so the eye does not travel; a toast is
20
+ * right for something that happened *elsewhere*. A consumer that
21
+ * wants both still gets `onCopy`.
22
+ *
23
+ * **The label is a word.** `Copy` → `Copied`, and the intent goes
24
+ * with it, because a colour change alone is not a message and a tick
25
+ * glyph is an icon this library does not ship.
26
+ *
27
+ * **It announces.** A screen reader is not reliably told that a
28
+ * focused button's own name changed, so the outcome is also written
29
+ * into a `role="status"` region — and the failure says what to do
30
+ * next rather than only that it went wrong.
31
+ *
32
+ * **The timer is cancelled on unmount and on a re-press**, which is
33
+ * the bug in two of the four hand-rolled copies: press, navigate
34
+ * away, and a `setState` lands on nothing.
35
+ */
36
+ export const CopyButton = ({ appearance = "solid", children = "Copy", confirmDuration = 2000, copiedLabel = "Copied", copy = defaultCopyText, failedLabel = "Copy failed", intent = "accent", onClick, onCopy, value, ...buttonProps }) => {
37
+ const [status, setStatus] = useState("idle");
38
+ const timerRef = useRef(undefined);
39
+ // One cleanup covers both the unmount and the re-press: every path
40
+ // that starts a timer clears the previous one first.
41
+ useEffect(() => () => {
42
+ clearTimeout(timerRef.current);
43
+ }, []);
44
+ const settle = (next) => {
45
+ setStatus(next);
46
+ clearTimeout(timerRef.current);
47
+ timerRef.current = setTimeout(() => {
48
+ setStatus("idle");
49
+ }, confirmDuration);
50
+ };
51
+ const handleClick = (event) => {
52
+ onClick?.(event);
53
+ if (event.defaultPrevented)
54
+ return;
55
+ void (async () => {
56
+ const isCopied = await copy(value);
57
+ settle(isCopied ? "copied" : "failed");
58
+ onCopy?.(isCopied, value);
59
+ })();
60
+ };
61
+ const label = status === "copied"
62
+ ? copiedLabel
63
+ : status === "failed"
64
+ ? failedLabel
65
+ : children;
66
+ return (_jsxs(_Fragment, { children: [_jsx(Button, { ...buttonProps, appearance: appearance, intent: status === "copied"
67
+ ? "success"
68
+ : status === "failed"
69
+ ? "danger"
70
+ : intent, onClick: handleClick, children: label }), _jsx(VisuallyHidden, { role: "status", children: status === "copied"
71
+ ? "Copied to the clipboard."
72
+ : status === "failed"
73
+ ? "The clipboard refused. Select the value and press Control C."
74
+ : "" })] }));
75
+ };
76
+ //# sourceMappingURL=CopyButton.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CopyButton.js","sourceRoot":"","sources":["../../src/CopyButton/CopyButton.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEnD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAsB,CAAA;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAsC,CAAA;AACrE,OAAO,EAAE,QAAQ,IAAI,eAAe,EAAE,MAAM,eAAe,CAAA;AAwC3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,EACzB,UAAU,GAAG,OAAO,EACpB,QAAQ,GAAG,MAAM,EACjB,eAAe,GAAG,IAAI,EACtB,WAAW,GAAG,QAAQ,EACtB,IAAI,GAAG,eAAe,EACtB,WAAW,GAAG,aAAa,EAC3B,MAAM,GAAG,QAAQ,EACjB,OAAO,EACP,MAAM,EACN,KAAK,EACL,GAAG,WAAW,EACE,EAAa,EAAE;IAC/B,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAa,MAAM,CAAC,CAAA;IACxD,MAAM,QAAQ,GACZ,MAAM,CAAgC,SAAS,CAAC,CAAA;IAElD,mEAAmE;IACnE,qDAAqD;IACrD,SAAS,CACP,GAAG,EAAE,CAAC,GAAG,EAAE;QACT,YAAY,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;IAChC,CAAC,EACD,EAAE,CACH,CAAA;IAED,MAAM,MAAM,GAAG,CAAC,IAAgB,EAAE,EAAE;QAClC,SAAS,CAAC,IAAI,CAAC,CAAA;QACf,YAAY,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;QAC9B,QAAQ,CAAC,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE;YACjC,SAAS,CAAC,MAAM,CAAC,CAAA;QACnB,CAAC,EAAE,eAAe,CAAC,CAAA;IACrB,CAAC,CAAA;IAED,MAAM,WAAW,GAAwC,CACvD,KAAK,EACL,EAAE;QACF,OAAO,EAAE,CAAC,KAAK,CAAC,CAAA;QAChB,IAAI,KAAK,CAAC,gBAAgB;YAAE,OAAM;QAElC,KAAK,CAAC,KAAK,IAAI,EAAE;YACf,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,CAAA;YAClC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAA;YACtC,MAAM,EAAE,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAA;QAC3B,CAAC,CAAC,EAAE,CAAA;IACN,CAAC,CAAA;IAED,MAAM,KAAK,GACT,MAAM,KAAK,QAAQ;QACjB,CAAC,CAAC,WAAW;QACb,CAAC,CAAC,MAAM,KAAK,QAAQ;YACnB,CAAC,CAAC,WAAW;YACb,CAAC,CAAC,QAAQ,CAAA;IAEhB,OAAO,CACL,8BACE,KAAC,MAAM,OACD,WAAW,EACf,UAAU,EAAE,UAAU,EACtB,MAAM,EACJ,MAAM,KAAK,QAAQ;oBACjB,CAAC,CAAC,SAAS;oBACX,CAAC,CAAC,MAAM,KAAK,QAAQ;wBACnB,CAAC,CAAC,QAAQ;wBACV,CAAC,CAAC,MAAM,EAEd,OAAO,EAAE,WAAW,YAEnB,KAAK,GACC,EACT,KAAC,cAAc,IAAC,IAAI,EAAC,QAAQ,YAC1B,MAAM,KAAK,QAAQ;oBAClB,CAAC,CAAC,0BAA0B;oBAC5B,CAAC,CAAC,MAAM,KAAK,QAAQ;wBACnB,CAAC,CAAC,8DAA8D;wBAChE,CAAC,CAAC,EAAE,GACO,IAChB,CACJ,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Put a string on the clipboard, and say whether it landed.
3
+ *
4
+ * Three routes, tried in order, because the fleet runs in all three
5
+ * conditions:
6
+ *
7
+ * 1. **`navigator.clipboard.writeText`.** The real API. It exists
8
+ * only in a *secure context*, which a `devshare` URL and
9
+ * `localhost` both are — and a plain `http://storeman.octen:8080`
10
+ * dev server is not.
11
+ * 2. **A hidden `<textarea>` plus `document.execCommand("copy")`.**
12
+ * Deprecated, and kept anyway: it is the only thing that works in
13
+ * an insecure context, spoolbuddy already hand-rolls exactly this
14
+ * block, and the alternative is a Copy button that silently does
15
+ * nothing on the household's own LAN.
16
+ * 3. **Give up and report it.** A caller that marks mail done on a
17
+ * copy must not mark it done on a copy that never happened.
18
+ *
19
+ * Never throws. The boolean is the whole result, so a call site is an
20
+ * `if` rather than a `try`.
21
+ */
22
+ export declare const copyText: (value: string) => Promise<boolean>;
23
+ //# sourceMappingURL=copyText.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"copyText.d.ts","sourceRoot":"","sources":["../../src/CopyButton/copyText.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,QAAQ,GACnB,OAAO,MAAM,KACZ,OAAO,CAAC,OAAO,CAWjB,CAAA"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Put a string on the clipboard, and say whether it landed.
3
+ *
4
+ * Three routes, tried in order, because the fleet runs in all three
5
+ * conditions:
6
+ *
7
+ * 1. **`navigator.clipboard.writeText`.** The real API. It exists
8
+ * only in a *secure context*, which a `devshare` URL and
9
+ * `localhost` both are — and a plain `http://storeman.octen:8080`
10
+ * dev server is not.
11
+ * 2. **A hidden `<textarea>` plus `document.execCommand("copy")`.**
12
+ * Deprecated, and kept anyway: it is the only thing that works in
13
+ * an insecure context, spoolbuddy already hand-rolls exactly this
14
+ * block, and the alternative is a Copy button that silently does
15
+ * nothing on the household's own LAN.
16
+ * 3. **Give up and report it.** A caller that marks mail done on a
17
+ * copy must not mark it done on a copy that never happened.
18
+ *
19
+ * Never throws. The boolean is the whole result, so a call site is an
20
+ * `if` rather than a `try`.
21
+ */
22
+ export const copyText = async (value) => {
23
+ try {
24
+ // `writeText` rejects on a denied permission and on a document
25
+ // that is not focused — both real, both recoverable below.
26
+ await globalThis.navigator?.clipboard?.writeText(value);
27
+ return true;
28
+ }
29
+ catch {
30
+ // Fall through to the legacy route.
31
+ }
32
+ return copyBySelection(value);
33
+ };
34
+ /**
35
+ * The pre-Clipboard-API route.
36
+ *
37
+ * The textarea is positioned off-screen rather than hidden: a
38
+ * `display: none` element has no selection to copy, and
39
+ * `visibility: hidden` is no better. `readOnly` stops the mobile
40
+ * keyboard appearing for the frame it is in the document.
41
+ */
42
+ const copyBySelection = (value) => {
43
+ const owner = globalThis.document;
44
+ if (!owner?.body)
45
+ return false;
46
+ const field = owner.createElement("textarea");
47
+ field.value = value;
48
+ field.readOnly = true;
49
+ field.setAttribute("aria-hidden", "true");
50
+ field.style.position = "fixed";
51
+ field.style.insetInlineStart = "-9999px";
52
+ field.style.top = "0";
53
+ owner.body.append(field);
54
+ try {
55
+ field.select();
56
+ return owner.execCommand("copy");
57
+ }
58
+ catch {
59
+ return false;
60
+ }
61
+ finally {
62
+ field.remove();
63
+ }
64
+ };
65
+ //# sourceMappingURL=copyText.js.map