@charcuterie/ui 3.10.0 → 3.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/Card/Card.d.ts +12 -1
- package/dist/Card/Card.d.ts.map +1 -1
- package/dist/Card/Card.js +14 -2
- package/dist/Card/Card.js.map +1 -1
- package/dist/Card/cardAccentEdge.d.ts +101 -0
- package/dist/Card/cardAccentEdge.d.ts.map +1 -0
- package/dist/Card/cardAccentEdge.js +92 -0
- package/dist/Card/cardAccentEdge.js.map +1 -0
- package/dist/CopyButton/CopyButton.d.ts +67 -0
- package/dist/CopyButton/CopyButton.d.ts.map +1 -0
- package/dist/CopyButton/CopyButton.js +76 -0
- package/dist/CopyButton/CopyButton.js.map +1 -0
- package/dist/CopyButton/copyText.d.ts +23 -0
- package/dist/CopyButton/copyText.d.ts.map +1 -0
- package/dist/CopyButton/copyText.js +65 -0
- package/dist/CopyButton/copyText.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/Card/Card.mdx +21 -0
- package/src/Card/Card.stories.tsx +91 -0
- package/src/Card/Card.test.tsx +92 -1
- package/src/Card/Card.tsx +34 -0
- package/src/Card/cardAccentEdge.test.ts +105 -0
- package/src/Card/cardAccentEdge.ts +135 -0
- package/src/CopyButton/CopyButton.mdx +84 -0
- package/src/CopyButton/CopyButton.stories.tsx +111 -0
- package/src/CopyButton/CopyButton.test.tsx +227 -0
- package/src/CopyButton/CopyButton.tsx +155 -0
- package/src/CopyButton/copyText.ts +68 -0
- package/src/index.ts +7 -0
- package/src/sourceRules.test.ts +7 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAOH,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA2B,CAAA;AAErD,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAiC,CAAA;AAE9D,OAAO,EACL,aAAa,EACb,wBAAwB,EACxB,0BAA0B,EAC1B,iCAAiC,EACjC,gBAAgB,EAChB,uBAAuB,GACxB,MAAM,iCAAiC,CAAA;AAKxC,OAAO,EACL,6BAA6B,EAC7B,gBAAgB,EAChB,kBAAkB,EAClB,yBAAyB,GAC1B,MAAM,sCAAsC,CAAA;AAK7C,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAEzC,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAGzC,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA+B,CAAA;AAK3D,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AASzC,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,mBAAmB,EACnB,cAAc,GACf,MAAM,sBAAsB,CAAA;AAE7B,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAqB,CAAA;AAE5C,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA6B,CAAA;AAMxD,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAiB,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;AAErD,OAAO,EAAE,YAAY,EAAE,MAAM,gCAAiC,CAAA;AAE9D,OAAO,EACL,aAAa,EACb,wBAAwB,EACxB,0BAA0B,EAC1B,iCAAiC,EACjC,gBAAgB,EAChB,uBAAuB,GACxB,MAAM,iCAAiC,CAAA;AAKxC,OAAO,EACL,6BAA6B,EAC7B,gBAAgB,EAChB,kBAAkB,EAClB,yBAAyB,GAC1B,MAAM,sCAAsC,CAAA;AAK7C,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAEzC,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAGzC,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA+B,CAAA;AAK3D,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AASzC,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,mBAAmB,EACnB,cAAc,GACf,MAAM,sBAAsB,CAAA;AAE7B,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAqB,CAAA;AAE5C,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA6B,CAAA;AAMxD,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAiB,CAAA;AAGtC,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAyB,CAAA;AAElD,OAAO,EAAE,mBAAmB,EAAE,MAAM,8CAA+C,CAAA;AAKnF,OAAO,EAAE,iBAAiB,EAAE,MAAM,0CAA2C,CAAA;AAE7E,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAyB,CAAA;AAKlD,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA6B,CAAA;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AACnD,OAAO,EACL,4BAA4B,EAC5B,yBAAyB,EACzB,uBAAuB,EACvB,4BAA4B,GAC7B,MAAM,wBAAwB,CAAA;AAE/B,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,EACtB,kBAAkB,GACnB,MAAM,oBAAoB,CAAA;AAO3B,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA2B,CAAA;AAErD,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA6B,CAAA;AAExD,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAA;AAMlE,OAAO,EACL,2BAA2B,EAC3B,cAAc,GACf,MAAM,gCAAgC,CAAA;AAMvC,OAAO,EACL,OAAO,EACP,SAAS,EACT,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,YAAY,EACZ,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,aAAa,EACb,YAAY,EACZ,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACb,yBAAyB,EACzB,UAAU,EACV,eAAe,EACf,YAAY,EACZ,SAAS,GACV,MAAM,2BAA2B,CAAA;AAKlC,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAqB,CAAA;AAK5C,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,MAAM,EAAE,MAAM,oBAAqB,CAAA;AAE5C,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA6B,CAAA;AAExD,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,uBAAuB,EACvB,oBAAoB,EACpB,kBAAkB,EAClB,uBAAuB,GACxB,MAAM,mBAAmB,CAAA;AAE1B,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAyB,CAAA;AAKlD,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAuB,CAAA;AAK/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,8CAA+C,CAAA;AAKnF,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA2B,CAAA;AAErD,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAiB,CAAA;AAKtC,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAqC,CAAA;AACpE,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAA;AAOtE,OAAO,EACL,WAAW,EACX,eAAe,GAChB,MAAM,mCAAmC,CAAA;AAK1C,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA2B,CAAA;AAErD,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAK7D,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAiB,CAAA;AAEtC,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAKzC,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAqB,CAAA;AAE5C,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,YAAY,EAAE,MAAM,gCAAiC,CAAA;AAK9D,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA6B,CAAA;AAMxD,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAiB,CAAA;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA6B,CAAA;AACxD,OAAO,EACL,kBAAkB,EAClB,aAAa,GACd,MAAM,oCAAqC,CAAA;AAK5C,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AAK5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,wCAAyC,CAAA;AAY1E,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAqB,CAAA;AAM5C,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAKzC,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAyB,CAAA;AAKlD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAqB,CAAA;AAK5C,OAAO,EAAE,mBAAmB,EAAE,MAAM,8CAA+C,CAAA;AAEnF,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAuB,CAAA;AAO/C,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAuB,CAAA;AAM/C,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAqB,CAAA;AAE5C,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAqB,CAAA;AAE5C,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;AAKtC,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAyB,CAAA;AAElD,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAmB,CAAA;AAKzC,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAyB,CAAA;AAErD,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAA;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAA;AAOpE,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAuB,CAAA;AAE/C,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAuB,CAAA;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAA;AAElE,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAuC,CAAA;AAEvE,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAqC,CAAA"}
|
package/package.json
CHANGED
package/src/Card/Card.mdx
CHANGED
|
@@ -51,3 +51,24 @@ The header stacks below `--cq-sm` and sits on one row above it, and the containe
|
|
|
51
51
|
cannot tell apart, and it is where the fleet's poster grids look wrong today.
|
|
52
52
|
|
|
53
53
|
<Canvas of={CardStories.Responsive} />
|
|
54
|
+
|
|
55
|
+
## The accent edge follows the corner
|
|
56
|
+
|
|
57
|
+
`accentEdge` draws a coloured bar down the leading edge. `{ categorical: 3 }` is a colour a
|
|
58
|
+
user picked, from the ten-wide contrast-audited family; `{ color }` is one the app computed,
|
|
59
|
+
for the case where ten indices are not enough — Folio hashes a repo's name so a repo added
|
|
60
|
+
tomorrow already has a hue and nobody maintains a palette.
|
|
61
|
+
|
|
62
|
+
> **It is a pseudo-element, not a border, and that is the whole point.** Three apps grew this
|
|
63
|
+
> shape independently and all three drew a straight border beside a rounded box, so the bar
|
|
64
|
+
> ran past the curve and stopped square while the card was round. A border is painted on the
|
|
65
|
+
> border box; it cannot follow a corner. The overlay here takes `border-radius: inherit`, so
|
|
66
|
+
> the bar reads the radius off whatever card it is on and nobody states one twice.
|
|
67
|
+
|
|
68
|
+
Two shapes this deliberately is not. **Not `box-shadow: inset` on the card itself** — `Card`
|
|
69
|
+
carries `shadow-low` from the elevation scale, and a `box-shadow` at the call site outranks
|
|
70
|
+
that utility and takes the elevation with it. **Not a 3px-wide pseudo-element with a rounded
|
|
71
|
+
leading edge** — a `0.5rem` radius on a 3px box is clamped to fit, so it caps the bar instead
|
|
72
|
+
of following the curve.
|
|
73
|
+
|
|
74
|
+
<Canvas of={CardStories.AccentEdge} />
|
|
@@ -13,12 +13,18 @@ import { IconButton } from "../IconButton/IconButton.tsx"
|
|
|
13
13
|
import { SettingsIcon } from "../icons.storyHelpers.tsx"
|
|
14
14
|
import { ProgressBar } from "../ProgressBar/ProgressBar.tsx"
|
|
15
15
|
import { Card, ELEVATION_CLASS } from "./Card.tsx"
|
|
16
|
+
import { ACCENT_EDGE_CATEGORICAL_CLASS } from "./cardAccentEdge.ts"
|
|
16
17
|
|
|
17
18
|
const meta = {
|
|
18
19
|
title: "Components/Layout/Card",
|
|
19
20
|
component: Card,
|
|
20
21
|
parameters: { layout: "padded" },
|
|
21
22
|
argTypes: {
|
|
23
|
+
// An object union, so an object control is the right one — but
|
|
24
|
+
// it is stated rather than inferred, because docgen's fallback
|
|
25
|
+
// for a type it cannot read is also an object control and the
|
|
26
|
+
// two are indistinguishable in the panel.
|
|
27
|
+
accentEdge: { control: "object" },
|
|
22
28
|
elevation: toStoryChoice(
|
|
23
29
|
Object.keys(
|
|
24
30
|
ELEVATION_CLASS,
|
|
@@ -205,3 +211,88 @@ export const Interactive: Story = {
|
|
|
205
211
|
</div>
|
|
206
212
|
),
|
|
207
213
|
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The leading edge, and the two answers the fleet has for what
|
|
217
|
+
* colour it is.
|
|
218
|
+
*
|
|
219
|
+
* A **categorical index** is a colour a user picked — Docket's
|
|
220
|
+
* project colours come from this ten-wide family, and every pair in
|
|
221
|
+
* it is contrast-audited. An explicit **colour** is one the app
|
|
222
|
+
* computed: Folio hashes a repo's name so a repo added tomorrow
|
|
223
|
+
* already has a hue and nobody maintains a palette, which is 360
|
|
224
|
+
* answers rather than ten.
|
|
225
|
+
*
|
|
226
|
+
* Both bars follow the card's own corner, because the treatment
|
|
227
|
+
* inherits its radius instead of naming one. Every app that grew
|
|
228
|
+
* this shape by hand drew it as a straight border beside a rounded
|
|
229
|
+
* box, and a border cannot follow a curve.
|
|
230
|
+
*/
|
|
231
|
+
export const AccentEdge: Story = {
|
|
232
|
+
args: { children: "Content" },
|
|
233
|
+
render: () => (
|
|
234
|
+
<StorySection title="A categorical index, and an app's own colour. Both take the card's radius.">
|
|
235
|
+
<StoryGrid columns={3}>
|
|
236
|
+
{(
|
|
237
|
+
Object.keys(
|
|
238
|
+
ACCENT_EDGE_CATEGORICAL_CLASS,
|
|
239
|
+
) as unknown as (keyof typeof ACCENT_EDGE_CATEGORICAL_CLASS)[]
|
|
240
|
+
).map((index) => (
|
|
241
|
+
<StoryCell
|
|
242
|
+
align="stretch"
|
|
243
|
+
key={index}
|
|
244
|
+
label={`categorical ${index}`}
|
|
245
|
+
>
|
|
246
|
+
<Card
|
|
247
|
+
accentEdge={{
|
|
248
|
+
categorical: Number(index) as 1,
|
|
249
|
+
}}
|
|
250
|
+
heading={`Project ${index}`}
|
|
251
|
+
>
|
|
252
|
+
<p className="text-content-secondary text-sm">
|
|
253
|
+
A colour the user picked.
|
|
254
|
+
</p>
|
|
255
|
+
</Card>
|
|
256
|
+
</StoryCell>
|
|
257
|
+
))}
|
|
258
|
+
|
|
259
|
+
<StoryCell align="stretch" label="a hashed hue">
|
|
260
|
+
<Card
|
|
261
|
+
accentEdge={{ color: "hsl(287 55% 52%)" }}
|
|
262
|
+
heading="home-assistant"
|
|
263
|
+
>
|
|
264
|
+
<p className="text-content-secondary text-sm">
|
|
265
|
+
A hue hashed from the name, not an index.
|
|
266
|
+
</p>
|
|
267
|
+
</Card>
|
|
268
|
+
</StoryCell>
|
|
269
|
+
|
|
270
|
+
<StoryCell
|
|
271
|
+
align="stretch"
|
|
272
|
+
label="a rounder card, same rule"
|
|
273
|
+
>
|
|
274
|
+
<Card
|
|
275
|
+
accentEdge={{ color: "hsl(24 55% 52%)" }}
|
|
276
|
+
className="rounded-3xl"
|
|
277
|
+
heading="A wider radius"
|
|
278
|
+
>
|
|
279
|
+
<p className="text-content-secondary text-sm">
|
|
280
|
+
The bar reads the radius off the card rather
|
|
281
|
+
than naming one, so an override still lines
|
|
282
|
+
up.
|
|
283
|
+
</p>
|
|
284
|
+
</Card>
|
|
285
|
+
</StoryCell>
|
|
286
|
+
|
|
287
|
+
<StoryCell align="stretch" label="a link inside it">
|
|
288
|
+
<Card
|
|
289
|
+
accentEdge={{ categorical: 4 }}
|
|
290
|
+
heading="Still clickable"
|
|
291
|
+
>
|
|
292
|
+
<Button size="sm">Open</Button>
|
|
293
|
+
</Card>
|
|
294
|
+
</StoryCell>
|
|
295
|
+
</StoryGrid>
|
|
296
|
+
</StorySection>
|
|
297
|
+
),
|
|
298
|
+
}
|
package/src/Card/Card.test.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import { mountStory } from "../mountStory.testHelpers.ts"
|
|
|
6
6
|
import { expectAgentDrivable } from "../testing/index.ts"
|
|
7
7
|
import * as stories from "./Card.stories.tsx"
|
|
8
8
|
|
|
9
|
-
const { AllStates, Default, Interactive } =
|
|
9
|
+
const { AccentEdge, AllStates, Default, Interactive } =
|
|
10
10
|
composeStories(stories)
|
|
11
11
|
|
|
12
12
|
test("a heading turns the card into a named landmark", async () => {
|
|
@@ -54,3 +54,94 @@ test("region scoping disambiguates two identical buttons", async () => {
|
|
|
54
54
|
}),
|
|
55
55
|
).toHaveAttribute("aria-valuenow", "40")
|
|
56
56
|
})
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The bar overlays the entire card, links and buttons included, so
|
|
60
|
+
* `pointer-events: none` on it is the difference between a card you
|
|
61
|
+
* can use and one that swallows every click. Driven, not asserted
|
|
62
|
+
* off a class name — the class is what the fix looks like, and this
|
|
63
|
+
* is what it is for.
|
|
64
|
+
*/
|
|
65
|
+
test("the accent edge does not swallow a click", async () => {
|
|
66
|
+
const { canvas } = await mountStory(AccentEdge)
|
|
67
|
+
|
|
68
|
+
const open = expectAgentDrivable(canvas, {
|
|
69
|
+
name: "Open",
|
|
70
|
+
role: "button",
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
let isClicked = false
|
|
74
|
+
|
|
75
|
+
open.addEventListener("click", () => {
|
|
76
|
+
isClicked = true
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
open.click()
|
|
80
|
+
|
|
81
|
+
await expect(isClicked).toBe(true)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const readRadius = (card: Element, isBar: boolean) =>
|
|
85
|
+
globalThis.getComputedStyle(
|
|
86
|
+
card,
|
|
87
|
+
isBar ? "::before" : undefined,
|
|
88
|
+
).borderTopLeftRadius
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The radius is INHERITED rather than restated, which is what lets
|
|
92
|
+
* one treatment sit on `rounded-lg`, on `rounded-3xl`, and on
|
|
93
|
+
* whatever an app overrides it to. Computed styles, because a class
|
|
94
|
+
* name is the thing that can be right while the paint is wrong.
|
|
95
|
+
*/
|
|
96
|
+
test("the bar takes the card's own radius, whatever it is", async () => {
|
|
97
|
+
const { canvas } = await mountStory(AccentEdge)
|
|
98
|
+
|
|
99
|
+
const wider = expectAgentDrivable(canvas, {
|
|
100
|
+
name: "A wider radius",
|
|
101
|
+
role: "region",
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const stock = expectAgentDrivable(canvas, {
|
|
105
|
+
name: "Project 1",
|
|
106
|
+
role: "region",
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
await expect(readRadius(wider, true)).toBe(
|
|
110
|
+
readRadius(wider, false),
|
|
111
|
+
)
|
|
112
|
+
await expect(readRadius(stock, true)).toBe(
|
|
113
|
+
readRadius(stock, false),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
// And the two cards genuinely differ, or the assertions above
|
|
117
|
+
// would pass on a component that hardcoded one radius.
|
|
118
|
+
await expect(readRadius(wider, false)).not.toBe(
|
|
119
|
+
readRadius(stock, false),
|
|
120
|
+
)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* `Card` carries `shadow-low`. The accent edge is a second shadow,
|
|
125
|
+
* and the first version of this fix wrote it onto the card itself —
|
|
126
|
+
* where an unlayered rule outranks the utility and the elevation
|
|
127
|
+
* silently goes away.
|
|
128
|
+
*/
|
|
129
|
+
test("the accent edge leaves the card's elevation alone", async () => {
|
|
130
|
+
const { canvas } = await mountStory(AccentEdge)
|
|
131
|
+
|
|
132
|
+
const card = expectAgentDrivable(canvas, {
|
|
133
|
+
name: "Project 1",
|
|
134
|
+
role: "region",
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const cardShadow =
|
|
138
|
+
globalThis.getComputedStyle(card).boxShadow
|
|
139
|
+
const barShadow = globalThis.getComputedStyle(
|
|
140
|
+
card,
|
|
141
|
+
"::before",
|
|
142
|
+
).boxShadow
|
|
143
|
+
|
|
144
|
+
await expect(cardShadow).not.toBe("none")
|
|
145
|
+
await expect(cardShadow).not.toContain("inset")
|
|
146
|
+
await expect(barShadow).toContain("inset")
|
|
147
|
+
})
|
package/src/Card/Card.tsx
CHANGED
|
@@ -9,6 +9,11 @@ import type {
|
|
|
9
9
|
} from "react"
|
|
10
10
|
|
|
11
11
|
import { toClassName } from "../toClassName.ts"
|
|
12
|
+
import type { CardAccentEdge } from "./cardAccentEdge.ts"
|
|
13
|
+
import {
|
|
14
|
+
getAccentEdgeClassName,
|
|
15
|
+
getAccentEdgeStyle,
|
|
16
|
+
} from "./cardAccentEdge.ts"
|
|
12
17
|
|
|
13
18
|
export type CardSurface = Extract<
|
|
14
19
|
SurfaceRole,
|
|
@@ -18,6 +23,16 @@ export type CardSurface = Extract<
|
|
|
18
23
|
export type CardPadding = "lg" | "md" | "none" | "sm"
|
|
19
24
|
|
|
20
25
|
export type CardProps = ComponentPropsWithRef<"section"> & {
|
|
26
|
+
/**
|
|
27
|
+
* A coloured bar down the leading edge, following the card's own
|
|
28
|
+
* corners.
|
|
29
|
+
*
|
|
30
|
+
* `{ categorical: 3 }` for a colour a user picked; `{ color }`
|
|
31
|
+
* for one an app computed — a hue hashed from a repo's name, say,
|
|
32
|
+
* where ten indices are not enough. See `cardAccentEdge.ts` for
|
|
33
|
+
* why it is a pseudo-element and not a border.
|
|
34
|
+
*/
|
|
35
|
+
accentEdge?: CardAccentEdge
|
|
21
36
|
/** Buttons or a badge, laid out beside the heading. */
|
|
22
37
|
actions?: ReactNode
|
|
23
38
|
elevation?: ElevationStep
|
|
@@ -79,6 +94,7 @@ const PADDING_CLASS: Record<CardPadding, string> = {
|
|
|
79
94
|
* a layout fact rather than a density one.
|
|
80
95
|
*/
|
|
81
96
|
export const Card = ({
|
|
97
|
+
accentEdge,
|
|
82
98
|
actions,
|
|
83
99
|
children,
|
|
84
100
|
className,
|
|
@@ -87,11 +103,25 @@ export const Card = ({
|
|
|
87
103
|
heading,
|
|
88
104
|
headingLevel = 2,
|
|
89
105
|
padding = "md",
|
|
106
|
+
style,
|
|
90
107
|
surface = "raised",
|
|
91
108
|
...sectionProps
|
|
92
109
|
}: CardProps): ReactNode => {
|
|
93
110
|
const headingId = useUniqueId()
|
|
94
111
|
|
|
112
|
+
// The caller's own `style` wins. An accent colour is one custom
|
|
113
|
+
// property, and silently dropping whatever else the call site set
|
|
114
|
+
// would be a worse trade than letting it override this one.
|
|
115
|
+
const accentEdgeStyle =
|
|
116
|
+
accentEdge == null
|
|
117
|
+
? undefined
|
|
118
|
+
: getAccentEdgeStyle(accentEdge)
|
|
119
|
+
|
|
120
|
+
const mergedStyle =
|
|
121
|
+
accentEdgeStyle == null
|
|
122
|
+
? style
|
|
123
|
+
: { ...accentEdgeStyle, ...style }
|
|
124
|
+
|
|
95
125
|
const Heading = `h${headingLevel}` as const
|
|
96
126
|
|
|
97
127
|
return (
|
|
@@ -108,8 +138,12 @@ export const Card = ({
|
|
|
108
138
|
SURFACE_CLASS[surface],
|
|
109
139
|
ELEVATION_CLASS[elevation],
|
|
110
140
|
PADDING_CLASS[padding],
|
|
141
|
+
accentEdge == null
|
|
142
|
+
? undefined
|
|
143
|
+
: getAccentEdgeClassName(accentEdge),
|
|
111
144
|
className,
|
|
112
145
|
)}
|
|
146
|
+
style={mergedStyle}
|
|
113
147
|
>
|
|
114
148
|
{heading || actions ? (
|
|
115
149
|
<header className="flex flex-col gap-2 cq-sm:flex-row cq-sm:items-start cq-sm:justify-between">
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ACCENT_EDGE_BASE_CLASS,
|
|
5
|
+
ACCENT_EDGE_CATEGORICAL_CLASS,
|
|
6
|
+
ACCENT_EDGE_COLOR_CLASS,
|
|
7
|
+
ACCENT_EDGE_COLOR_PROPERTY,
|
|
8
|
+
getAccentEdgeClassName,
|
|
9
|
+
getAccentEdgeStyle,
|
|
10
|
+
} from "./cardAccentEdge.ts"
|
|
11
|
+
|
|
12
|
+
describe("getAccentEdgeClassName", () => {
|
|
13
|
+
test("reads a categorical index off the ten-wide family", () => {
|
|
14
|
+
const className = getAccentEdgeClassName({
|
|
15
|
+
categorical: 3,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
expect(className).toContain(ACCENT_EDGE_BASE_CLASS)
|
|
19
|
+
expect(className).toContain(
|
|
20
|
+
ACCENT_EDGE_CATEGORICAL_CLASS[3],
|
|
21
|
+
)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test("threads an app's own colour through one literal", () => {
|
|
25
|
+
const className = getAccentEdgeClassName({
|
|
26
|
+
color: "hsl(210 55% 52%)",
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
expect(className).toContain(ACCENT_EDGE_BASE_CLASS)
|
|
30
|
+
expect(className).toContain(ACCENT_EDGE_COLOR_CLASS)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The bar overlays the whole card, links and buttons included, so
|
|
35
|
+
* this is the difference between a card you can click and one you
|
|
36
|
+
* cannot.
|
|
37
|
+
*/
|
|
38
|
+
test("never eats a pointer", () => {
|
|
39
|
+
expect(
|
|
40
|
+
getAccentEdgeClassName({ categorical: 1 }),
|
|
41
|
+
).toContain("before:pointer-events-none")
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The card already has a radius; the bar must not restate it.
|
|
46
|
+
* `inherit` is what makes one component work on `rounded-lg`,
|
|
47
|
+
* `rounded-xl` and whatever an app overrides it to.
|
|
48
|
+
*/
|
|
49
|
+
test("takes the card's own radius rather than naming one", () => {
|
|
50
|
+
expect(
|
|
51
|
+
getAccentEdgeClassName({ categorical: 1 }),
|
|
52
|
+
).toContain("before:rounded-[inherit]")
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* `box-shadow` has no logical form. Without the `rtl:` twin the
|
|
57
|
+
* bar sits on the left of an Arabic page, which is the trailing
|
|
58
|
+
* edge there.
|
|
59
|
+
*/
|
|
60
|
+
test("carries the bar to the other side under rtl", () => {
|
|
61
|
+
for (const className of [
|
|
62
|
+
...Object.values(ACCENT_EDGE_CATEGORICAL_CLASS),
|
|
63
|
+
ACCENT_EDGE_COLOR_CLASS,
|
|
64
|
+
]) {
|
|
65
|
+
expect(className).toContain("rtl:before:shadow-")
|
|
66
|
+
expect(className).toContain("inset_-3px")
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Tailwind scans source text for COMPLETE class strings, so an
|
|
72
|
+
* interpolated index generates nothing, paints nothing and
|
|
73
|
+
* reports nothing. This is the cheap half of that guarantee;
|
|
74
|
+
* `tailwindCandidates.test.ts` is the half that compiles them.
|
|
75
|
+
*/
|
|
76
|
+
test("names every categorical index in full", () => {
|
|
77
|
+
for (const [index, className] of Object.entries(
|
|
78
|
+
ACCENT_EDGE_CATEGORICAL_CLASS,
|
|
79
|
+
)) {
|
|
80
|
+
expect(className).toContain(
|
|
81
|
+
`var(--color-categorical-${index}-solid)`,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe("getAccentEdgeStyle", () => {
|
|
88
|
+
test("writes one custom property for an app's own colour", () => {
|
|
89
|
+
expect(
|
|
90
|
+
getAccentEdgeStyle({ color: "hsl(210 55% 52%)" }),
|
|
91
|
+
).toStrictEqual({
|
|
92
|
+
[ACCENT_EDGE_COLOR_PROPERTY]: "hsl(210 55% 52%)",
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* `undefined`, not `{}`, so a categorical card renders no `style`
|
|
98
|
+
* attribute at all.
|
|
99
|
+
*/
|
|
100
|
+
test("writes nothing when the colour came from an index", () => {
|
|
101
|
+
expect(
|
|
102
|
+
getAccentEdgeStyle({ categorical: 7 }),
|
|
103
|
+
).toBeUndefined()
|
|
104
|
+
})
|
|
105
|
+
})
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A coloured bar down a card's leading edge — and the reason it is
|
|
3
|
+
* a pseudo-element rather than a border.
|
|
4
|
+
*
|
|
5
|
+
* Three apps had grown this shape independently and all three drew
|
|
6
|
+
* it as a straight border beside a rounded box, so the bar ran past
|
|
7
|
+
* the curve and stopped square while the card it belonged to was
|
|
8
|
+
* round. Folio put `border-inline-start` on a WRAPPER around the
|
|
9
|
+
* card; mail-sifter puts `borderInlineStartWidth: 4` on a
|
|
10
|
+
* `rounded-xl` anchor; spoolbuddy writes `rounded-lg border-l-3`.
|
|
11
|
+
* A border is painted on the border box, so none of them can follow
|
|
12
|
+
* a corner — the notch is not a bug in any one app, it is what that
|
|
13
|
+
* technique does.
|
|
14
|
+
*
|
|
15
|
+
* A pseudo-element overlays the card and takes
|
|
16
|
+
* `border-radius: inherit`, so the bar follows whatever radius the
|
|
17
|
+
* card already has and nobody states a radius twice.
|
|
18
|
+
*
|
|
19
|
+
* ⚠️ **The shadow goes on the pseudo-element, never on the card.**
|
|
20
|
+
* `Card` carries `shadow-low` from the elevation scale, and an app
|
|
21
|
+
* stylesheet is unlayered while Tailwind's utilities sit in
|
|
22
|
+
* `@layer utilities` — so a `box-shadow` written at the call site
|
|
23
|
+
* outranks the utility and takes the card's elevation away with it.
|
|
24
|
+
* That is not hypothetical; it is the version of this fix that was
|
|
25
|
+
* written first.
|
|
26
|
+
*
|
|
27
|
+
* ⚠️ **Not a 3px-wide pseudo-element with a rounded leading edge.**
|
|
28
|
+
* A `border-radius` of `0.5rem` on a 3px-wide box is clamped by the
|
|
29
|
+
* browser to fit, so it caps the bar instead of following the
|
|
30
|
+
* card's curve.
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ **`box-shadow` has no logical form**, so the offset is
|
|
33
|
+
* physical and the `rtl:` variant is what carries the bar to the
|
|
34
|
+
* other side. A single `inset 3px` would put it on the left of an
|
|
35
|
+
* Arabic page, which is the trailing edge there.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import type { CategoricalIndex } from "@charcuterie/tokens"
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Which colour the bar takes.
|
|
42
|
+
*
|
|
43
|
+
* Two arms, because the fleet has two answers and neither is wrong.
|
|
44
|
+
* Docket picks a **categorical index** — a colour a user chose for
|
|
45
|
+
* a project, from the ten-wide contrast-audited family. Folio and
|
|
46
|
+
* mail-sifter derive a **hue from a name hash**, so a repo added
|
|
47
|
+
* tomorrow already has a colour and nobody maintains a palette;
|
|
48
|
+
* that is 360 answers, not ten, and it cannot be an index.
|
|
49
|
+
*
|
|
50
|
+
* `intent` is deliberately absent. An intent is a claim the design
|
|
51
|
+
* system makes — `danger` says what happens if you press the thing
|
|
52
|
+
* — and no card in this fleet wants its edge to make one. When one
|
|
53
|
+
* does, that is a third arm and a decision record, not a guess
|
|
54
|
+
* made here.
|
|
55
|
+
*/
|
|
56
|
+
export type CardAccentEdge =
|
|
57
|
+
| { categorical: CategoricalIndex; color?: never }
|
|
58
|
+
| { categorical?: never; color: string }
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The custom property an explicit colour is threaded through.
|
|
62
|
+
*
|
|
63
|
+
* A property rather than an inline `box-shadow`, because the shadow
|
|
64
|
+
* string has to stay a COMPLETE literal in the source for Tailwind
|
|
65
|
+
* to generate it at all. One literal reading one variable covers
|
|
66
|
+
* every colour an app can compute; interpolating the colour into
|
|
67
|
+
* the class name would generate nothing, paint nothing, and report
|
|
68
|
+
* nothing.
|
|
69
|
+
*/
|
|
70
|
+
export const ACCENT_EDGE_COLOR_PROPERTY =
|
|
71
|
+
"--charcuterie-accent-edge"
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The overlay itself. Colour arrives separately.
|
|
75
|
+
*
|
|
76
|
+
* `pointer-events: none` is load-bearing rather than tidy: this box
|
|
77
|
+
* covers the whole card, including every link and button in it.
|
|
78
|
+
*/
|
|
79
|
+
export const ACCENT_EDGE_BASE_CLASS =
|
|
80
|
+
"relative before:pointer-events-none before:absolute before:inset-0 before:rounded-[inherit] before:content-['']"
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Ten indices, written out in full, for the reason
|
|
84
|
+
* `categoricalStyles.ts` opens with: Tailwind scans source text for
|
|
85
|
+
* COMPLETE class strings, so a template literal generates nothing
|
|
86
|
+
* at all and the bar renders invisible with no error and no failing
|
|
87
|
+
* test. `tailwindCandidates.test.ts` compiles every literal in this
|
|
88
|
+
* package through the real Tailwind, which is the something that
|
|
89
|
+
* checks.
|
|
90
|
+
*/
|
|
91
|
+
export const ACCENT_EDGE_CATEGORICAL_CLASS: Record<
|
|
92
|
+
CategoricalIndex,
|
|
93
|
+
string
|
|
94
|
+
> = {
|
|
95
|
+
1: "before:shadow-[inset_3px_0_0_var(--color-categorical-1-solid)] rtl:before:shadow-[inset_-3px_0_0_var(--color-categorical-1-solid)]",
|
|
96
|
+
2: "before:shadow-[inset_3px_0_0_var(--color-categorical-2-solid)] rtl:before:shadow-[inset_-3px_0_0_var(--color-categorical-2-solid)]",
|
|
97
|
+
3: "before:shadow-[inset_3px_0_0_var(--color-categorical-3-solid)] rtl:before:shadow-[inset_-3px_0_0_var(--color-categorical-3-solid)]",
|
|
98
|
+
4: "before:shadow-[inset_3px_0_0_var(--color-categorical-4-solid)] rtl:before:shadow-[inset_-3px_0_0_var(--color-categorical-4-solid)]",
|
|
99
|
+
5: "before:shadow-[inset_3px_0_0_var(--color-categorical-5-solid)] rtl:before:shadow-[inset_-3px_0_0_var(--color-categorical-5-solid)]",
|
|
100
|
+
6: "before:shadow-[inset_3px_0_0_var(--color-categorical-6-solid)] rtl:before:shadow-[inset_-3px_0_0_var(--color-categorical-6-solid)]",
|
|
101
|
+
7: "before:shadow-[inset_3px_0_0_var(--color-categorical-7-solid)] rtl:before:shadow-[inset_-3px_0_0_var(--color-categorical-7-solid)]",
|
|
102
|
+
8: "before:shadow-[inset_3px_0_0_var(--color-categorical-8-solid)] rtl:before:shadow-[inset_-3px_0_0_var(--color-categorical-8-solid)]",
|
|
103
|
+
9: "before:shadow-[inset_3px_0_0_var(--color-categorical-9-solid)] rtl:before:shadow-[inset_-3px_0_0_var(--color-categorical-9-solid)]",
|
|
104
|
+
10: "before:shadow-[inset_3px_0_0_var(--color-categorical-10-solid)] rtl:before:shadow-[inset_-3px_0_0_var(--color-categorical-10-solid)]",
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The same bar, reading the caller's own colour. */
|
|
108
|
+
export const ACCENT_EDGE_COLOR_CLASS =
|
|
109
|
+
"before:shadow-[inset_3px_0_0_var(--charcuterie-accent-edge)] rtl:before:shadow-[inset_-3px_0_0_var(--charcuterie-accent-edge)]"
|
|
110
|
+
|
|
111
|
+
export const getAccentEdgeClassName = (
|
|
112
|
+
accentEdge: CardAccentEdge,
|
|
113
|
+
): string =>
|
|
114
|
+
`${ACCENT_EDGE_BASE_CLASS} ${
|
|
115
|
+
accentEdge.categorical == null
|
|
116
|
+
? ACCENT_EDGE_COLOR_CLASS
|
|
117
|
+
: ACCENT_EDGE_CATEGORICAL_CLASS[
|
|
118
|
+
accentEdge.categorical
|
|
119
|
+
]
|
|
120
|
+
}`
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The inline style an explicit colour needs, and nothing when the
|
|
124
|
+
* colour came from an index.
|
|
125
|
+
*
|
|
126
|
+
* Returns `undefined` rather than `{}` so a card with a categorical
|
|
127
|
+
* edge and no `style` of its own renders no `style` attribute at
|
|
128
|
+
* all.
|
|
129
|
+
*/
|
|
130
|
+
export const getAccentEdgeStyle = (
|
|
131
|
+
accentEdge: CardAccentEdge,
|
|
132
|
+
): Record<string, string> | undefined =>
|
|
133
|
+
accentEdge.color == null
|
|
134
|
+
? undefined
|
|
135
|
+
: { [ACCENT_EDGE_COLOR_PROPERTY]: accentEdge.color }
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Canvas, Controls, Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
import * as CopyButtonStories from "./CopyButton.stories";
|
|
4
|
+
|
|
5
|
+
<Meta of={CopyButtonStories} />
|
|
6
|
+
|
|
7
|
+
# CopyButton
|
|
8
|
+
|
|
9
|
+
**A button that puts a string on the clipboard and says so.** Press it, the label turns to
|
|
10
|
+
`Copied`, and the caller is told whether that is true.
|
|
11
|
+
|
|
12
|
+
<Canvas of={CopyButtonStories.Default} />
|
|
13
|
+
|
|
14
|
+
## Props
|
|
15
|
+
|
|
16
|
+
<Controls />
|
|
17
|
+
|
|
18
|
+
## Where it came from
|
|
19
|
+
|
|
20
|
+
Four repos spell this shape by hand, and no two agree:
|
|
21
|
+
|
|
22
|
+
| Repo | How it copies | What it does when the clipboard says no |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| mux-magic (`YamlModal`) | flips its own label on a `setTimeout` | nothing — the label still says `Copied!` |
|
|
25
|
+
| spoolbuddy (`Settings`) | 15 lines inline, with the `execCommand` fallback | a toast |
|
|
26
|
+
| bambuddy (`ArchivesPage`) | a menu item, twice in one file | a toast |
|
|
27
|
+
| mail-sifter | a verification-code banner | **marks the mail done anyway** |
|
|
28
|
+
|
|
29
|
+
The last row is why the failure path is a first-class state here rather than a `catch` that
|
|
30
|
+
shrugs. That consumer files the mail away on a copy, so a copy that did not happen has to be
|
|
31
|
+
distinguishable from one that did.
|
|
32
|
+
|
|
33
|
+
## The confirmation is the button
|
|
34
|
+
|
|
35
|
+
The press and the proof are the same object, so the eye does not travel. A toast is the
|
|
36
|
+
right shape for something that happened **somewhere else** — a background job, another pane
|
|
37
|
+
— and the wrong shape for the thing directly under the pointer. A consumer that wants a
|
|
38
|
+
toast as well still gets one from `onCopy`.
|
|
39
|
+
|
|
40
|
+
<Canvas of={CopyButtonStories.Confirmed} />
|
|
41
|
+
|
|
42
|
+
## The label is a word, and so is the failure
|
|
43
|
+
|
|
44
|
+
`Copy` → `Copied`, with the intent moving to `success` underneath it. The colour is not the
|
|
45
|
+
message: a colour change alone is invisible to a screen reader, unreliable for a
|
|
46
|
+
colour-blind reader, and this library [ships no icons](?path=/docs/guides-building-an-app--docs),
|
|
47
|
+
so a tick glyph is not available to carry it either.
|
|
48
|
+
|
|
49
|
+
The refusal says what to do next rather than only that something went wrong.
|
|
50
|
+
|
|
51
|
+
<Canvas of={CopyButtonStories.Refused} />
|
|
52
|
+
|
|
53
|
+
## Three routes to the clipboard
|
|
54
|
+
|
|
55
|
+
`copyText.ts` tries them in order and never throws:
|
|
56
|
+
|
|
57
|
+
1. **`navigator.clipboard.writeText`** — the real API, and it exists only in a *secure
|
|
58
|
+
context*. A `devshare` URL and `localhost` are secure; a plain `http://` dev server on
|
|
59
|
+
the LAN is not.
|
|
60
|
+
2. **A hidden `<textarea>` and `document.execCommand("copy")`** — deprecated, kept because
|
|
61
|
+
it is the only thing that works in that insecure context, and because spoolbuddy already
|
|
62
|
+
carries this exact block inline.
|
|
63
|
+
3. **Report the failure**, so the caller can hold on to whatever it was about to discard.
|
|
64
|
+
|
|
65
|
+
`copy` is injectable, which is how the `Refused` story above exists and how a host with its
|
|
66
|
+
own clipboard bridge — Electron, a kiosk shell — supplies it.
|
|
67
|
+
|
|
68
|
+
## Own labels
|
|
69
|
+
|
|
70
|
+
The labels are content. A surface that copies one specific thing should say which thing.
|
|
71
|
+
|
|
72
|
+
<Canvas of={CopyButtonStories.OwnLabels} />
|
|
73
|
+
|
|
74
|
+
<Canvas of={CopyButtonStories.AllAppearances} />
|
|
75
|
+
|
|
76
|
+
## What it does not do
|
|
77
|
+
|
|
78
|
+
**It does not own the value.** No `children`-as-value, no reading text out of a sibling
|
|
79
|
+
node: the string is a prop, because the thing on screen is regularly not the thing worth
|
|
80
|
+
copying (a masked key, a truncated URL, a code split across two spans).
|
|
81
|
+
|
|
82
|
+
**It does not clear the timer for you and hope.** The confirmation timer is cancelled on
|
|
83
|
+
unmount and on a re-press — two of the four hand-rolled copies set state on an unmounted
|
|
84
|
+
component if you press and navigate away.
|