@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.
- package/README.md +55 -0
- package/dist/hooks/useCollectionFollow.d.ts +141 -0
- package/dist/hooks/useCollectionFollow.d.ts.map +1 -0
- package/dist/hooks/useCollectionFollow.js +202 -0
- package/dist/hooks/useCollectionFollow.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/iframeTransport.d.ts.map +1 -1
- package/dist/internal/iframeTransport.js +5 -2
- package/dist/internal/iframeTransport.js.map +1 -1
- package/dist/internal/liveHost.d.ts.map +1 -1
- package/dist/internal/liveHost.js +33 -0
- package/dist/internal/liveHost.js.map +1 -1
- package/dist/internal/mockHost.d.ts +17 -2
- package/dist/internal/mockHost.d.ts.map +1 -1
- package/dist/internal/mockHost.js +49 -0
- package/dist/internal/mockHost.js.map +1 -1
- package/dist/internal/requestTimeouts.d.ts.map +1 -1
- package/dist/internal/requestTimeouts.js +8 -0
- package/dist/internal/requestTimeouts.js.map +1 -1
- package/dist/internal/transport.d.ts +19 -0
- package/dist/internal/transport.d.ts.map +1 -1
- package/dist/internal/transport.js +24 -0
- package/dist/internal/transport.js.map +1 -1
- package/dist/internal/validate.d.ts +20 -0
- package/dist/internal/validate.d.ts.map +1 -1
- package/dist/internal/validate.js +43 -0
- package/dist/internal/validate.js.map +1 -1
- package/dist/ui/FollowButton.d.ts +85 -0
- package/dist/ui/FollowButton.d.ts.map +1 -0
- package/dist/ui/FollowButton.js +162 -0
- package/dist/ui/FollowButton.js.map +1 -0
- package/dist/ui/TipButton.d.ts +115 -0
- package/dist/ui/TipButton.d.ts.map +1 -0
- package/dist/ui/TipButton.js +308 -0
- package/dist/ui/TipButton.js.map +1 -0
- package/dist/ui/index.d.ts +4 -0
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +2 -0
- package/dist/ui/index.js.map +1 -1
- 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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"}
|
package/dist/ui/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/ui/index.d.ts.map
CHANGED
|
@@ -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';
|