@civitai/blocks-react 0.47.0 → 0.49.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 (51) hide show
  1. package/README.md +89 -6
  2. package/dist/hooks/SfwGate.d.ts +16 -9
  3. package/dist/hooks/SfwGate.d.ts.map +1 -1
  4. package/dist/hooks/SfwGate.js +14 -8
  5. package/dist/hooks/SfwGate.js.map +1 -1
  6. package/dist/hooks/useCollectionFollow.d.ts +141 -0
  7. package/dist/hooks/useCollectionFollow.d.ts.map +1 -0
  8. package/dist/hooks/useCollectionFollow.js +202 -0
  9. package/dist/hooks/useCollectionFollow.js.map +1 -0
  10. package/dist/hooks/useDomainMaturity.d.ts +57 -17
  11. package/dist/hooks/useDomainMaturity.d.ts.map +1 -1
  12. package/dist/hooks/useDomainMaturity.js +37 -11
  13. package/dist/hooks/useDomainMaturity.js.map +1 -1
  14. package/dist/index.d.ts +4 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +5 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/internal/iframeTransport.d.ts.map +1 -1
  19. package/dist/internal/iframeTransport.js +5 -2
  20. package/dist/internal/iframeTransport.js.map +1 -1
  21. package/dist/internal/liveHost.d.ts.map +1 -1
  22. package/dist/internal/liveHost.js +33 -0
  23. package/dist/internal/liveHost.js.map +1 -1
  24. package/dist/internal/mockHost.d.ts +39 -2
  25. package/dist/internal/mockHost.d.ts.map +1 -1
  26. package/dist/internal/mockHost.js +61 -0
  27. package/dist/internal/mockHost.js.map +1 -1
  28. package/dist/internal/requestTimeouts.d.ts.map +1 -1
  29. package/dist/internal/requestTimeouts.js +8 -0
  30. package/dist/internal/requestTimeouts.js.map +1 -1
  31. package/dist/internal/transport.d.ts +28 -1
  32. package/dist/internal/transport.d.ts.map +1 -1
  33. package/dist/internal/transport.js +25 -0
  34. package/dist/internal/transport.js.map +1 -1
  35. package/dist/internal/validate.d.ts +20 -0
  36. package/dist/internal/validate.d.ts.map +1 -1
  37. package/dist/internal/validate.js +60 -0
  38. package/dist/internal/validate.js.map +1 -1
  39. package/dist/ui/FollowButton.d.ts +85 -0
  40. package/dist/ui/FollowButton.d.ts.map +1 -0
  41. package/dist/ui/FollowButton.js +162 -0
  42. package/dist/ui/FollowButton.js.map +1 -0
  43. package/dist/ui/TipButton.d.ts +115 -0
  44. package/dist/ui/TipButton.d.ts.map +1 -0
  45. package/dist/ui/TipButton.js +308 -0
  46. package/dist/ui/TipButton.js.map +1 -0
  47. package/dist/ui/index.d.ts +4 -0
  48. package/dist/ui/index.d.ts.map +1 -1
  49. package/dist/ui/index.js +2 -0
  50. package/dist/ui/index.js.map +1 -1
  51. package/package.json +2 -2
@@ -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';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,oEAAoE;AACpE,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGpF,4EAA4E;AAC5E,6EAA6E;AAC7E,mEAAmE;AACnE,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAG/D,gCAAgC;AAChC,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,gBAAgB,GACjB,MAAM,aAAa,CAAC;AAErB,uBAAuB;AACvB,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAG3C,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAGzC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGnC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGnC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGnC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGnC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGjD,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAGtE,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGnC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAG/C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAGzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,oEAAoE;AACpE,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGpF,4EAA4E;AAC5E,6EAA6E;AAC7E,mEAAmE;AACnE,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAG/D,gCAAgC;AAChC,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,gBAAgB,GACjB,MAAM,aAAa,CAAC;AAErB,uBAAuB;AACvB,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAG3C,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAGzC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGnC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGnC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGnC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGnC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGjD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGjD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAG3C,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAGtE,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGnC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAG/C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAGzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civitai/blocks-react",
3
- "version": "0.47.0",
3
+ "version": "0.49.0",
4
4
  "description": "React hooks and iframe transport for Civitai Apps. Pairs with @civitai/app-sdk/blocks.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -50,7 +50,7 @@
50
50
  "react-dom": "^19.0.0",
51
51
  "typescript": "^5.9.2",
52
52
  "vitest": "^4.1.11",
53
- "@civitai/app-sdk": "^0.37.0"
53
+ "@civitai/app-sdk": "^0.39.0"
54
54
  },
55
55
  "publishConfig": {
56
56
  "access": "public"