@civitai/blocks-react 0.47.0 → 0.48.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 (43) hide show
  1. package/README.md +55 -0
  2. package/dist/hooks/useCollectionFollow.d.ts +141 -0
  3. package/dist/hooks/useCollectionFollow.d.ts.map +1 -0
  4. package/dist/hooks/useCollectionFollow.js +202 -0
  5. package/dist/hooks/useCollectionFollow.js.map +1 -0
  6. package/dist/index.d.ts +4 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +5 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/internal/iframeTransport.d.ts.map +1 -1
  11. package/dist/internal/iframeTransport.js +5 -2
  12. package/dist/internal/iframeTransport.js.map +1 -1
  13. package/dist/internal/liveHost.d.ts.map +1 -1
  14. package/dist/internal/liveHost.js +33 -0
  15. package/dist/internal/liveHost.js.map +1 -1
  16. package/dist/internal/mockHost.d.ts +17 -2
  17. package/dist/internal/mockHost.d.ts.map +1 -1
  18. package/dist/internal/mockHost.js +49 -0
  19. package/dist/internal/mockHost.js.map +1 -1
  20. package/dist/internal/requestTimeouts.d.ts.map +1 -1
  21. package/dist/internal/requestTimeouts.js +8 -0
  22. package/dist/internal/requestTimeouts.js.map +1 -1
  23. package/dist/internal/transport.d.ts +19 -0
  24. package/dist/internal/transport.d.ts.map +1 -1
  25. package/dist/internal/transport.js +24 -0
  26. package/dist/internal/transport.js.map +1 -1
  27. package/dist/internal/validate.d.ts +20 -0
  28. package/dist/internal/validate.d.ts.map +1 -1
  29. package/dist/internal/validate.js +43 -0
  30. package/dist/internal/validate.js.map +1 -1
  31. package/dist/ui/FollowButton.d.ts +85 -0
  32. package/dist/ui/FollowButton.d.ts.map +1 -0
  33. package/dist/ui/FollowButton.js +162 -0
  34. package/dist/ui/FollowButton.js.map +1 -0
  35. package/dist/ui/TipButton.d.ts +115 -0
  36. package/dist/ui/TipButton.d.ts.map +1 -0
  37. package/dist/ui/TipButton.js +308 -0
  38. package/dist/ui/TipButton.js.map +1 -0
  39. package/dist/ui/index.d.ts +4 -0
  40. package/dist/ui/index.d.ts.map +1 -1
  41. package/dist/ui/index.js +2 -0
  42. package/dist/ui/index.js.map +1 -1
  43. package/package.json +2 -2
@@ -0,0 +1,162 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useRef, useState } from 'react';
3
+ import { CollectionFollowError, useCollectionFollow } from '../hooks/useCollectionFollow.js';
4
+ import { useRequestSignIn } from '../hooks/useRequestSignIn.js';
5
+ import { Button } from './Button.js';
6
+ import { useBlocksStyles } from './styles.js';
7
+ const NOTE_STYLE = {
8
+ fontSize: 12,
9
+ lineHeight: 1.45,
10
+ color: 'var(--civitai-color-text-dimmed)',
11
+ };
12
+ /**
13
+ * Follow / unfollow a collection for the viewer, through the host bridge.
14
+ *
15
+ * 🔴 EVERY PRESS OPENS A HOST-CHROME CONSENT CONFIRM naming the collection the
16
+ * HOST resolved from `collectionId`. That click is the only consent this path
17
+ * has ever had (the HTTP predecessor's `collections:write:self` scope was
18
+ * consent-exempt server-side and prompted nobody), so this control TIGHTENS the
19
+ * flow rather than loosening it. There is no way to suppress the confirm and no
20
+ * option should be added for one.
21
+ *
22
+ * 🔴 WHY THIS IS SHARED RATHER THAN HAND-ROLLED. Three outcomes of that bridge
23
+ * are easy to get wrong in a way that looks fine:
24
+ * - `declined` is NOT a failure. The viewer dismissed the confirm and NOTHING
25
+ * was written, so the control reverts and says nothing. A hand-rolled button
26
+ * that renders every rejection shows "Could not follow" to someone who chose
27
+ * not to.
28
+ * - `sign-in-required` is not a failure either — it is a missing session, so it
29
+ * routes into `REQUEST_SIGN_IN` instead of an error line.
30
+ * - the optimistic flip must be reverted on EVERY other rejection, and then
31
+ * replaced by the host's ECHO on success rather than by the guess.
32
+ *
33
+ * The `followed` prop stays the source of truth. While a write settles the
34
+ * control shows its optimistic value; on success it holds the host's echo until
35
+ * `followed` agrees, so it never blinks back to a stale server value while the
36
+ * parent catches up.
37
+ *
38
+ * @example
39
+ * <FollowButton
40
+ * collectionId={collection.id}
41
+ * collectionName={collection.name}
42
+ * followed={collection.followed}
43
+ * onChange={(f) => setCollection((c) => ({ ...c, followed: f }))}
44
+ * data-testid="collection-follow"
45
+ * />
46
+ */
47
+ export function FollowButton({ collectionId, followed, onChange, collectionName, disabled = false, size = 'md', variant = 'filled', 'data-testid': testId, }) {
48
+ useBlocksStyles();
49
+ const { setFollow, pending } = useCollectionFollow();
50
+ const { requestSignIn } = useRequestSignIn();
51
+ /** The value this control is asserting over `followed`, or `null`. */
52
+ const [optimistic, setOptimistic] = useState(null);
53
+ const [failed, setFailed] = useState(false);
54
+ /**
55
+ * The `collectionId` the in-flight write is FOR.
56
+ *
57
+ * 🔴 THIS IS THE CORRELATION GUARD, AND IT REPLACES AN ATTEMPT COUNTER THAT
58
+ * GUARDED NOTHING. A counter incremented only inside `toggle()` cannot see the
59
+ * parent change anything, so the case its comment claimed to close — a settle
60
+ * arriving after the parent moved the row — went straight through it; deleting
61
+ * all three of its lines left this file's suite fully green, which is how the
62
+ * false claim survived review.
63
+ *
64
+ * The reachable bug it was pretending to cover: a single mounted instance whose
65
+ * `collectionId` prop CHANGES mid-flight (a rail showing "the currently
66
+ * selected collection", or an unkeyed recycled list row). The host's reply for
67
+ * the OLD collection then lands, and without this the control adopts it as the
68
+ * NEW one's state and reports it through `onChange` — so the app records a
69
+ * follow the viewer never made, on an account-write control.
70
+ *
71
+ * Correlating on the id is possible because the host ECHOES it:
72
+ * `BlockCollectionFollowResult.collectionId` exists for exactly this, and
73
+ * `isValidCollectionFollowResult` already pins it to a positive integer so a
74
+ * malformed echo cannot reach this comparison.
75
+ */
76
+ const inFlightForRef = useRef(null);
77
+ const shown = optimistic ?? followed;
78
+ // 🔴 THE OPTIMISTIC VALUE BELONGS TO A COLLECTION, NOT TO THIS COMPONENT.
79
+ // When the prop moves, whatever this control was asserting is about the id it
80
+ // has just stopped showing — keeping it paints the OLD collection's state onto
81
+ // the NEW one, which is the same wrong-row bug as an uncorrelated settle and
82
+ // needs no network round trip to happen. Drop it, drop any failure note, and
83
+ // release the in-flight marker so a reply for the old id can never re-adopt.
84
+ useEffect(() => {
85
+ setOptimistic(null);
86
+ setFailed(false);
87
+ inFlightForRef.current = null;
88
+ }, [collectionId]);
89
+ // The parent has caught up — stop asserting. Kept as an effect rather than
90
+ // clearing on success, because clearing there is only correct if the parent
91
+ // adopts `onChange` synchronously, and `onChange` is optional.
92
+ useEffect(() => {
93
+ if (optimistic !== null && optimistic === followed)
94
+ setOptimistic(null);
95
+ }, [optimistic, followed]);
96
+ const ids = testId
97
+ ? { trigger: testId, note: `${testId}-note` }
98
+ : { trigger: 'follow-button', note: 'follow-button-note' };
99
+ const toggle = useCallback(async () => {
100
+ const target = collectionId;
101
+ inFlightForRef.current = target;
102
+ /**
103
+ * This settle still belongs to the collection on screen. Both halves are
104
+ * load-bearing: the ECHO comparison catches a reply for a collection this
105
+ * control has moved off, and the REF comparison catches the case where the
106
+ * prop moved away and back again while a write was in flight.
107
+ */
108
+ const stillOurs = (echoedId) => echoedId === target && inFlightForRef.current === target;
109
+ const next = !shown;
110
+ setOptimistic(next);
111
+ setFailed(false);
112
+ try {
113
+ const result = await setFollow({ collectionId: target, follow: next });
114
+ // 🔴 Correlate on the HOST'S echoed id, not on a counter. A reply for a
115
+ // collection this control no longer shows must change nothing and must
116
+ // NOT be reported through `onChange` — adopting it records a follow the
117
+ // viewer never made against whatever row is on screen now.
118
+ if (!stillOurs(result.collectionId))
119
+ return;
120
+ // 🔴 Adopt the ECHO, not `next`. They agree today; reading the host's
121
+ // answer is what keeps this correct if one ever settles differently, and
122
+ // it costs nothing.
123
+ setOptimistic(result.followed);
124
+ onChange?.(result.followed);
125
+ }
126
+ catch (err) {
127
+ // A rejection carries no echoed id, so correlate on the ref alone: a
128
+ // failure belonging to a collection this control has moved off must not
129
+ // revert or announce anything about the one now on screen.
130
+ if (inFlightForRef.current !== target)
131
+ return;
132
+ // Revert first, unconditionally: every path below this line is one where
133
+ // no write occurred.
134
+ setOptimistic(null);
135
+ if (err instanceof CollectionFollowError) {
136
+ // No session. The viewer's next step is signing in, not retrying — and
137
+ // a "could not follow" line here would be actively misleading.
138
+ if (err.signInRequired) {
139
+ requestSignIn();
140
+ return;
141
+ }
142
+ // The viewer dismissed the host's confirm. Say NOTHING: they answered,
143
+ // and the answer was no.
144
+ if (err.declined)
145
+ return;
146
+ }
147
+ setFailed(true);
148
+ }
149
+ }, [collectionId, onChange, requestSignIn, setFollow, shown]);
150
+ const name = collectionName ? `"${collectionName}"` : 'this collection';
151
+ return (_jsxs(_Fragment, { children: [_jsx(Button, { size: size,
152
+ // The following state is always `light` so the two states differ
153
+ // visually and not only in wording — the label alone is a poor signal at
154
+ // a glance, and this control is often rendered small in a card corner.
155
+ variant: shown ? 'light' : variant, loading: pending, disabled: disabled, onClick: () => void toggle(), "data-testid": ids.trigger, "aria-pressed": shown, "aria-label": shown ? `Unfollow ${name}` : `Follow ${name}`, children: shown ? 'Following' : 'Follow' }), failed && (
156
+ // 🔴 `role="alert"`. `aria-pressed` reverts on failure, but a reverted
157
+ // toggle state is indistinguishable from never having pressed it — a
158
+ // screen-reader user would be told nothing at all. Success needs no
159
+ // equivalent: `aria-pressed` changing IS the announcement.
160
+ _jsx("span", { role: "alert", style: NOTE_STYLE, "data-testid": ids.note, children: "Could not update \u2014 try again?" }))] }));
161
+ }
162
+ //# sourceMappingURL=FollowButton.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FollowButton.js","sourceRoot":"","sources":["../../src/ui/FollowButton.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,115 @@
1
+ import type { TipParams } from '../hooks/useTip.js';
2
+ import type { ButtonSize, ButtonVariant } from './Button.js';
3
+ export interface TipButtonProps {
4
+ /** The recipient. The SENDER is always the token subject — server-self-bound. */
5
+ toUserId: number;
6
+ /** Buzz to send. Must be a positive integer. */
7
+ amount: number;
8
+ /** Optional context recorded on the transaction. */
9
+ entityType?: TipParams['entityType'];
10
+ entityId?: number;
11
+ /**
12
+ * What the viewer is tipping FOR, lower-case and singular — "creator",
13
+ * "curator", "post". Appears in the confirm question and both accessible
14
+ * names.
15
+ *
16
+ * 🔴 It is spliced into an `aria-label`, so pass a bare noun, not a sentence.
17
+ */
18
+ noun: string;
19
+ /**
20
+ * This viewer has ALREADY tipped this target in this session. Renders the
21
+ * settled state and skips the handshake.
22
+ *
23
+ * 🔴 THERE IS NO SERVER FIELD FOR THIS. `useTip` returns the transaction echo
24
+ * and nothing reads back "has this viewer tipped X?" — so without a value
25
+ * here the settled state is local-only and any remount re-arms the control.
26
+ * That is not a no-op the way a duplicate vote would be: a second press is a
27
+ * SECOND TRANSFER. Record it in your own per-viewer storage when the tip
28
+ * resolves and feed it back.
29
+ */
30
+ tipped?: boolean;
31
+ /**
32
+ * The viewer's remaining daily tip allowance, in Buzz. When supplied and
33
+ * smaller than `amount`, the control refuses locally with a note instead of
34
+ * sending a request the server would reject.
35
+ *
36
+ * 🔴 DELIBERATELY A PROP, NOT AN INTERNAL `useTipAllowance()`. This control is
37
+ * rendered per card / per rail, so fetching inside it would fan one screen out
38
+ * into N identical HTTP reads. Hold ONE `useTipAllowance()` in the view and
39
+ * pass `allowance?.remaining` down; call its `refetch()` from `onTipped`.
40
+ *
41
+ * Omitted → no local ceiling check, and the server's own limit is the only
42
+ * gate. That is correct, just later and less legible.
43
+ */
44
+ remaining?: number;
45
+ /**
46
+ * Why tipping is unavailable, e.g. "You can't tip your own collection."
47
+ * Present ⇒ the control is disabled and this is shown as its `title` and
48
+ * appended to its accessible name.
49
+ *
50
+ * 🔴 USE THIS FOR THE SELF-TIP CASE. The server answers a self-tip with a 403,
51
+ * so without it the viewer presses, confirms, and is told a transfer failed —
52
+ * for something that was never going to be allowed.
53
+ */
54
+ disabledReason?: string;
55
+ /** Disable for any other reason (view still loading, no target resolved yet). */
56
+ disabled?: boolean;
57
+ /**
58
+ * Called after a SUCCESSFUL transfer, with the amount sent. Refetch your
59
+ * allowance and record the `tipped` flag here.
60
+ */
61
+ onTipped?: (amount: number) => void;
62
+ /** Button size preset. Defaults to `'md'`. */
63
+ size?: ButtonSize;
64
+ /** Variant for the trigger. Defaults to `'light'`. */
65
+ variant?: ButtonVariant;
66
+ /**
67
+ * Test hook for the TRIGGER. The other four are DERIVED by suffix:
68
+ * `<id>-confirm`, `<id>-cancel`, `<id>-done`, `<id>-prompt`. Omitted, the ids
69
+ * are `tip-button`, `tip-confirm`, `tip-cancel`, `tip-done`,
70
+ * `tip-confirm-prompt`.
71
+ *
72
+ * 🔴 Grep for the SUFFIX, never the composed value.
73
+ */
74
+ 'data-testid'?: string;
75
+ }
76
+ /**
77
+ * Send a Buzz tip, behind an in-block two-step confirm.
78
+ *
79
+ * 🔴 THE CONFIRM IS THIS COMPONENT'S, NOT HOST CHROME — state that honestly
80
+ * rather than implying platform mediation. `useTip` posts to the block-token-
81
+ * gated tip endpoint directly (scope `social:tip:self`), so unlike the
82
+ * collection-follow bridge NOTHING outside the iframe asks the viewer anything.
83
+ * A one-press money spend is therefore reachable by construction, and the
84
+ * two-step handshake here is the only thing between a stray tap and a transfer.
85
+ * Do not add a prop to skip it.
86
+ *
87
+ * 🔴 IT MINTS ONE IDEMPOTENCY KEY PER LOGICAL TIP AND REUSES IT ON RETRY. That
88
+ * is the property a hand-rolled button most reliably misses: `useTip` mints a
89
+ * FRESH key per call when you do not pass one, so retrying after a timeout
90
+ * whose response was merely LOST sends a SECOND transfer. Here the key is
91
+ * minted when the control arms and rotates only after a tip actually settles,
92
+ * so "try again" on a failed press is collapsed server-side to one transfer.
93
+ *
94
+ * The caller decides who sees it: `tip` rejects for an anonymous viewer and a
95
+ * self-tip 403s. Pass `disabledReason` for the self case rather than offering an
96
+ * error.
97
+ *
98
+ * @example
99
+ * // ONE allowance read for the whole view, passed down.
100
+ * const { allowance, refetch } = useTipAllowance();
101
+ * <TipButton
102
+ * noun="curator"
103
+ * toUserId={collection.curator.id}
104
+ * amount={50}
105
+ * entityType="Collection"
106
+ * entityId={collection.id}
107
+ * remaining={allowance?.remaining}
108
+ * tipped={tippedCurators.has(collection.id)}
109
+ * disabledReason={isSelf ? "You can't tip your own collection." : undefined}
110
+ * onTipped={() => { refetch(); markTipped(collection.id); }}
111
+ * data-testid="tip-curator"
112
+ * />
113
+ */
114
+ export declare function TipButton({ toUserId, amount, entityType, entityId, noun, tipped, remaining, disabledReason, disabled, onTipped, size, variant, 'data-testid': testId, }: TipButtonProps): React.JSX.Element;
115
+ //# sourceMappingURL=TipButton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TipButton.d.ts","sourceRoot":"","sources":["../../src/ui/TipButton.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEpD,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAU7D,MAAM,WAAW,cAAc;IAC7B,iFAAiF;IACjF,QAAQ,EAAE,MAAM,CAAC;IACjB,gDAAgD;IAChD,MAAM,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,UAAU,CAAC,EAAE,SAAS,CAAC,YAAY,CAAC,CAAC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;OAMG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;;;;;;;;OAYG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,8CAA8C;IAC9C,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,sDAAsD;IACtD,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAgB,SAAS,CAAC,EACxB,QAAQ,EACR,MAAM,EACN,UAAU,EACV,QAAQ,EACR,IAAI,EACJ,MAAc,EACd,SAAS,EACT,cAAc,EACd,QAAgB,EAChB,QAAQ,EACR,IAAW,EACX,OAAiB,EACjB,aAAa,EAAE,MAAM,GACtB,EAAE,cAAc,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAmTpC"}
@@ -0,0 +1,308 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useId, useRef, useState } from 'react';
3
+ import { useTip } from '../hooks/useTip.js';
4
+ import { Button } from './Button.js';
5
+ import { Group } from './Group.js';
6
+ import { useBlocksStyles } from './styles.js';
7
+ const NOTE_STYLE = {
8
+ fontSize: 12,
9
+ lineHeight: 1.45,
10
+ color: 'var(--civitai-color-text-dimmed)',
11
+ };
12
+ /**
13
+ * Send a Buzz tip, behind an in-block two-step confirm.
14
+ *
15
+ * 🔴 THE CONFIRM IS THIS COMPONENT'S, NOT HOST CHROME — state that honestly
16
+ * rather than implying platform mediation. `useTip` posts to the block-token-
17
+ * gated tip endpoint directly (scope `social:tip:self`), so unlike the
18
+ * collection-follow bridge NOTHING outside the iframe asks the viewer anything.
19
+ * A one-press money spend is therefore reachable by construction, and the
20
+ * two-step handshake here is the only thing between a stray tap and a transfer.
21
+ * Do not add a prop to skip it.
22
+ *
23
+ * 🔴 IT MINTS ONE IDEMPOTENCY KEY PER LOGICAL TIP AND REUSES IT ON RETRY. That
24
+ * is the property a hand-rolled button most reliably misses: `useTip` mints a
25
+ * FRESH key per call when you do not pass one, so retrying after a timeout
26
+ * whose response was merely LOST sends a SECOND transfer. Here the key is
27
+ * minted when the control arms and rotates only after a tip actually settles,
28
+ * so "try again" on a failed press is collapsed server-side to one transfer.
29
+ *
30
+ * The caller decides who sees it: `tip` rejects for an anonymous viewer and a
31
+ * self-tip 403s. Pass `disabledReason` for the self case rather than offering an
32
+ * error.
33
+ *
34
+ * @example
35
+ * // ONE allowance read for the whole view, passed down.
36
+ * const { allowance, refetch } = useTipAllowance();
37
+ * <TipButton
38
+ * noun="curator"
39
+ * toUserId={collection.curator.id}
40
+ * amount={50}
41
+ * entityType="Collection"
42
+ * entityId={collection.id}
43
+ * remaining={allowance?.remaining}
44
+ * tipped={tippedCurators.has(collection.id)}
45
+ * disabledReason={isSelf ? "You can't tip your own collection." : undefined}
46
+ * onTipped={() => { refetch(); markTipped(collection.id); }}
47
+ * data-testid="tip-curator"
48
+ * />
49
+ */
50
+ export function TipButton({ toUserId, amount, entityType, entityId, noun, tipped = false, remaining, disabledReason, disabled = false, onTipped, size = 'md', variant = 'light', 'data-testid': testId, }) {
51
+ useBlocksStyles();
52
+ const { tip } = useTip();
53
+ const [confirming, setConfirming] = useState(false);
54
+ const [busy, setBusy] = useState(false);
55
+ const [done, setDone] = useState(false);
56
+ const [failure, setFailure] = useState(null);
57
+ const confirmRef = useRef(null);
58
+ const cancelRef = useRef(null);
59
+ const doneRef = useRef(null);
60
+ /** Monotonic id of the current attempt — see `ReportButton` for the measured why. */
61
+ const attemptRef = useRef(0);
62
+ /**
63
+ * The idempotency keys this mount has already reported a landed transfer for.
64
+ *
65
+ * 🔴 KEYED, NOT A BOOLEAN — and the boolean it replaces was wrong in the
66
+ * OPPOSITE direction from the bug it fixed. "At most once per mount" collapses
67
+ * two transfers that are genuinely distinct: Cancel does not abort POST #1, so
68
+ * if the parent then moves `amount` or the entity a NEW key is minted, the
69
+ * server does NOT collapse them, and 150 Buzz moves while the app is told 50.
70
+ * `onTipped` is where a caller refetches the allowance and records its
71
+ * `tipped` flag, so the second transfer left no record — re-creating on
72
+ * remount the very harm the previous round cited. The correct scope is once
73
+ * per KEY: same key ⇒ the server saw one transfer ⇒ report once; different
74
+ * key ⇒ two transfers ⇒ report both.
75
+ *
76
+ * 🔴 A REF, NOT STATE. The success path reads it in the same tick it would
77
+ * write it, so two promises resolving in one turn both see stale state and
78
+ * both report; a ref is written synchronously, so the second sees the first.
79
+ */
80
+ const reportedKeysRef = useRef(new Set());
81
+ /**
82
+ * The idempotency key for THIS logical tip. Stable across retries, which is
83
+ * the point; `useId()` seeds it so two TipButtons mounted in one tree never
84
+ * collide, and the target + amount are folded in so that changing either (an
85
+ * amount preset switcher) correctly starts a NEW logical tip rather than
86
+ * colliding with the abandoned one.
87
+ *
88
+ * 🔴 EVERY FIELD OF THE TIP'S IDENTITY IS IN THE KEY, and the earlier version
89
+ * omitted two. `entityType`/`entityId` are sent in the POST body and recorded
90
+ * on the transaction, so they are part of WHICH tip this is — leaving them out
91
+ * meant that changing the entity after a failed attempt reused the same key,
92
+ * and if the first attempt had actually landed (the lost-response case this
93
+ * mechanism exists for) the second, deliberate tip to a DIFFERENT object was
94
+ * collapsed into it and the block was told it succeeded. Under-charge rather
95
+ * than double-charge, but wrong either way.
96
+ *
97
+ * 🔴 NOT ROTATED AFTER A SUCCESS, and what licenses that is narrower than it
98
+ * first appears. `done` is never cleared, so a control that has settled
99
+ * THROUGH ITS OWN SUCCESS PATH cannot send again from this mount. `settled` is
100
+ * `done || tipped`, and the `tipped` half is a PROP — a parent can withdraw it
101
+ * — so terminality is a property of `done`, not of `settled`.
102
+ *
103
+ * ⚠️ An earlier revision continued "…which is why the success path reports
104
+ * UNCONDITIONALLY". It no longer does — it reports once per KEY — and the
105
+ * sentence is corrected rather than deleted because it was load-bearing. The
106
+ * invariant that actually licenses keeping the key is unchanged: `done` is set
107
+ * on the first landed transfer for a given key, and a repeat under the SAME
108
+ * key is that same transfer. Change either and rotate.
109
+ */
110
+ const keySeed = useId();
111
+ const idempotencyKey = `${keySeed}:${toUserId}:${amount}:${entityType ?? '-'}:${entityId ?? '-'}`;
112
+ const settled = done || tipped;
113
+ // Both guards are about a NUMBER reaching a money path, and both were once
114
+ // missing: `amount={0}` rendered "Tip 0" and posted it, and an unusable
115
+ // `remaining` silently REMOVED the ceiling rather than blocking.
116
+ //
117
+ // `amount` must be a real, positive number. `Infinity` is not a tippable
118
+ // quantity, so `isFinite` is load-bearing on its own rather than a longer
119
+ // spelling of `> 0` — and it does not coerce, so a non-number is rejected too.
120
+ const amountValid = Number.isFinite(amount) && amount > 0;
121
+ // 🔴 THREE cases for `remaining`, not two, and a previous revision lost one
122
+ // while fixing another. `NaN` is an UNUSABLE reading — every comparison
123
+ // against it is false — so it must BLOCK. `Infinity` is a MEANINGFUL reading,
124
+ // an unlimited allowance, so blocking it refuses a viewer who is allowed
125
+ // everything; sweeping the two together under `!Number.isFinite` got that
126
+ // wrong. And a NON-NUMBER must block for the same reason as `NaN`: 🔴
127
+ // `Number.isNaN` does NOT coerce, so the `!isFinite` → `isNaN` fix silently
128
+ // dropped the type check and let `remaining: 'abc'` through — removing the
129
+ // ceiling, the wrong direction, and precisely what the comment it replaced
130
+ // claimed could not happen.
131
+ const overAllowance = remaining !== undefined &&
132
+ (typeof remaining !== 'number' || Number.isNaN(remaining) || amount > remaining);
133
+ const blocked = disabled || disabledReason !== undefined || !amountValid;
134
+ const ids = testId
135
+ ? {
136
+ trigger: testId,
137
+ confirm: `${testId}-confirm`,
138
+ cancel: `${testId}-cancel`,
139
+ done: `${testId}-done`,
140
+ prompt: `${testId}-prompt`,
141
+ }
142
+ : {
143
+ trigger: 'tip-button',
144
+ confirm: 'tip-confirm',
145
+ cancel: 'tip-cancel',
146
+ done: 'tip-done',
147
+ prompt: 'tip-confirm-prompt',
148
+ };
149
+ // Move focus with the control at both transitions — each step REPLACES the
150
+ // element the viewer just activated, so without this a keyboard user is
151
+ // dropped to <body>. Same three effects, and the same reasoning, as
152
+ // `ReportButton`; keyed on the transition, never on the request lifecycle.
153
+ useEffect(() => {
154
+ if (confirming)
155
+ confirmRef.current?.focus();
156
+ }, [confirming]);
157
+ useEffect(() => {
158
+ if (done)
159
+ doneRef.current?.focus();
160
+ }, [done]);
161
+ useEffect(() => {
162
+ if (busy)
163
+ cancelRef.current?.focus();
164
+ }, [busy]);
165
+ // Server/app truth ENDS the handshake rather than leaving it mounted beneath
166
+ // the settled note, where a late rejection could resurrect a stale failure
167
+ // against a tip the viewer has already seen settle.
168
+ useEffect(() => {
169
+ if (tipped) {
170
+ attemptRef.current += 1;
171
+ setConfirming(false);
172
+ setFailure(null);
173
+ setBusy(false);
174
+ }
175
+ }, [tipped]);
176
+ const confirm = useCallback(async () => {
177
+ // 🔴 RE-CHECK AT THE SPEND, NOT ONLY AT THE ARM. `blocked` gates the trigger,
178
+ // but the prompt stays mounted across a re-render — so a parent moving
179
+ // `amount` to 0 (or NaN) AFTER the viewer armed the control left an enabled
180
+ // Send that posted it. An amount switcher mid-handshake is a flow this
181
+ // component's own JSDoc contemplates, so this is reachable, not theoretical.
182
+ if (!amountValid || overAllowance || blocked) {
183
+ // 🔴 NAME THE ACTUAL REASON. A fixed "That amount cannot be sent." is
184
+ // FALSE when the block is a `disabledReason` (the amount is fine, and a
185
+ // real reason string is in hand) — and this file's own comment about the
186
+ // stale-closure bug calls that message out as "false about the amount"
187
+ // while shipping it on a path it pins. Order matches the gate.
188
+ // 🔴 `||`, NOT `??` — a mutation found this, and it is the same distinction
189
+ // as the error channel's. `blocked` tests `disabledReason !== undefined`,
190
+ // so an EMPTY-STRING reason (a consumer writing
191
+ // `disabledReason={bad ? reason : ''}`) blocks — and `??` would then set an
192
+ // EMPTY failure message, rendering a blank refusal, which is the worst
193
+ // outcome on a money control. `||` falls through to a real sentence.
194
+ setFailure(disabledReason ||
195
+ (!amountValid
196
+ ? 'That amount cannot be sent.'
197
+ : overAllowance
198
+ ? 'Over your remaining daily tip allowance.'
199
+ : 'Tipping is unavailable right now.'));
200
+ return;
201
+ }
202
+ const attempt = (attemptRef.current += 1);
203
+ const current = () => attemptRef.current === attempt;
204
+ setBusy(true);
205
+ setFailure(null);
206
+ try {
207
+ await tip({ toUserId, amount, ...(entityType ? { entityType } : {}), ...(entityId !== undefined ? { entityId } : {}) }, { idempotencyKey });
208
+ // 🔴 A LANDED TRANSFER IS REPORTED UNCONDITIONALLY — no `current()` check
209
+ // on this path, deliberately, and the earlier version's check here was the
210
+ // defect rather than the safety.
211
+ //
212
+ // Buzz has MOVED by the time this line runs. Nothing the viewer or the
213
+ // parent did in the meantime can un-send it: Cancel resets this control's
214
+ // UI but does not abort the POST, and a parent flipping `tipped` mid-flight
215
+ // bumps the attempt for its own reasons. Suppressing the settle in either
216
+ // case left the app never learning the transfer succeeded — so `onTipped`
217
+ // never fired, the allowance was never refetched, and no `tipped` record
218
+ // was written, which re-armed the control over money that was already gone.
219
+ //
220
+ // Showing "Tipped" for a transfer that happened is correct even if the
221
+ // viewer pressed Cancel a moment earlier; showing "Tip 50" again is what
222
+ // invites the second one. Only the FAILURE path below respects supersession
223
+ // — there, nothing moved, so an abandoned attempt has nothing to report.
224
+ //
225
+ // 🔴 BUT AT MOST ONCE PER KEY, and "unconditionally" without this was a
226
+ // REGRESSION the previous revision introduced. Cancel-then-retry sends a SECOND POST
227
+ // carrying the SAME idempotency key, so the server collapses the pair into
228
+ // ONE transfer while both promises resolve — and an unguarded report then
229
+ // called `onTipped(amount)` twice for money that moved once. `onTipped` is
230
+ // handed the amount precisely so a caller can decrement an allowance with
231
+ // it, so double-firing double-counts. The answer is "once per KEY" — not
232
+ // the old "never after supersession", not a bare "always", and not the
233
+ // "once per mount" an intermediate revision used, which collapsed two
234
+ // GENUINELY DISTINCT transfers (see `reportedKeysRef`).
235
+ if (!reportedKeysRef.current.has(idempotencyKey)) {
236
+ reportedKeysRef.current.add(idempotencyKey);
237
+ setDone(true);
238
+ setConfirming(false);
239
+ setFailure(null);
240
+ onTipped?.(amount);
241
+ }
242
+ }
243
+ catch (err) {
244
+ if (!current())
245
+ return;
246
+ // Show the SERVER's message. Unlike a report, a failed tip has real and
247
+ // varied causes the viewer can act on (insufficient balance, over the
248
+ // daily cap, self-tip) and a fixed "could not send" throws all of them
249
+ // away.
250
+ setFailure(err instanceof Error && err.message ? err.message : 'Could not send the tip.');
251
+ }
252
+ finally {
253
+ if (current())
254
+ setBusy(false);
255
+ }
256
+ // 🔴 `amountValid`, `overAllowance` and `blocked` MUST be here. The gate at
257
+ // the top of this callback reads all three, and the previous revision listed
258
+ // none of them — so they were frozen at whatever render last recreated the
259
+ // callback, and the gate decided a SPEND on stale values. It failed in both
260
+ // directions and there is no eslint in this repo to catch it:
261
+ //
262
+ // • STALE-RESTRICTIVE, and permanent: a parent that tops up `remaining`,
263
+ // or clears `disabled` / `disabledReason` (the "view still loading"
264
+ // usage this component's own JSDoc names), left Send refusing a
265
+ // perfectly good tip with a message that is false about the amount —
266
+ // for the life of the mount, since nothing else moved a dep.
267
+ // • STALE-PERMISSIVE: the mirror case the gate's own comment claims to
268
+ // cover — `remaining` dropping, or `disabledReason` appearing, AFTER
269
+ // arming — sailed straight through.
270
+ //
271
+ // 🔴 IT WAS ALSO CONSUMER-DEPENDENT, which is why no test caught it: an
272
+ // INLINE `onTipped={() => {}}` (the shape the @example uses) recreates the
273
+ // callback every render and hides the whole thing, while a consumer doing
274
+ // the idiomatic `useCallback` gets the wedge. Listing the derived booleans
275
+ // rather than the raw props keeps this honest — add an input to the gate and
276
+ // the dep is already named.
277
+ }, [
278
+ amount,
279
+ amountValid,
280
+ blocked,
281
+ entityId,
282
+ entityType,
283
+ idempotencyKey,
284
+ onTipped,
285
+ overAllowance,
286
+ tip,
287
+ toUserId,
288
+ ]);
289
+ if (settled) {
290
+ return (_jsx("span", { ref: doneRef, tabIndex: -1, "data-testid": ids.done, role: "status", style: { ...NOTE_STYLE, outline: 'none' }, children: `Tipped ${noun}` }));
291
+ }
292
+ if (!confirming) {
293
+ const reason = disabledReason ?? (overAllowance ? 'Over your remaining daily tip allowance.' : undefined);
294
+ return (_jsx(Button, { size: size, variant: variant, disabled: blocked || overAllowance, onClick: () => setConfirming(true), "data-testid": ids.trigger, ...(reason ? { title: reason } : {}), "aria-label": reason ? `Tip the ${noun} — unavailable: ${reason}` : `Tip the ${noun} ${amount} Buzz`, children: `Tip ${amount}` }));
295
+ }
296
+ return (_jsxs(Group, { gap: 6, align: "center", wrap: false, "data-testid": ids.prompt, children: [_jsx("span", { style: NOTE_STYLE, ...(failure ? { role: 'alert' } : {}), children: failure ?? `Send ${amount} Buzz to the ${noun}?` }), _jsx(Button, { ref: confirmRef, size: "sm", loading: busy, onClick: () => void confirm(), "data-testid": ids.confirm, "aria-label": `Confirm sending ${amount} Buzz to the ${noun}`, children: "Send" }), _jsx(Button, { ref: cancelRef, size: "sm", variant: "subtle",
297
+ // 🔴 NOT disabled in flight, for the reason `ReportButton` records: a
298
+ // reply that never arrives would otherwise leave both controls dead with
299
+ // no way back short of a remount. The attempt token closes the race
300
+ // without taking the escape hatch away.
301
+ onClick: () => {
302
+ attemptRef.current += 1;
303
+ setBusy(false);
304
+ setFailure(null);
305
+ setConfirming(false);
306
+ }, "data-testid": ids.cancel, "aria-label": "Cancel the tip", children: "Cancel" })] }));
307
+ }
308
+ //# sourceMappingURL=TipButton.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TipButton.js","sourceRoot":"","sources":["../../src/ui/TipButton.tsx"],"names":[],"mappings":"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"}
@@ -40,6 +40,10 @@ export { Badge } from './Badge.js';
40
40
  export type { BadgeProps, BadgeVariant, BadgeSize } from './Badge.js';
41
41
  export { ReportButton } from './ReportButton.js';
42
42
  export type { ReportButtonProps } from './ReportButton.js';
43
+ export { FollowButton } from './FollowButton.js';
44
+ export type { FollowButtonProps } from './FollowButton.js';
45
+ export { TipButton } from './TipButton.js';
46
+ export type { TipButtonProps } from './TipButton.js';
43
47
  export { ResourceCard, resourceDisplayName } from './ResourceCard.js';
44
48
  export type { ResourceCardProps, ResourceCardVariant } from './ResourceCard.js';
45
49
  export { Modal } from './Modal.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACpF,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAK3D,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAC/D,YAAY,EAAE,cAAc,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAG9E,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,gBAAgB,GACjB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE1E,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAErD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAExD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEzD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE3D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEtE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACtE,YAAY,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAEhF,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAExD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEzD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE7D,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,YAAY,EACV,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"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"}
package/dist/ui/index.js CHANGED
@@ -34,6 +34,8 @@ export { Alert } from './Alert.js';
34
34
  export { Loader } from './Loader.js';
35
35
  export { Badge } from './Badge.js';
36
36
  export { ReportButton } from './ReportButton.js';
37
+ export { FollowButton } from './FollowButton.js';
38
+ export { TipButton } from './TipButton.js';
37
39
  export { ResourceCard, resourceDisplayName } from './ResourceCard.js';
38
40
  export { Modal } from './Modal.js';
39
41
  export { Slider } from './Slider.js';