@civitai/blocks-react 0.46.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 +65 -3
- 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/ResourceCard.d.ts +272 -0
- package/dist/ui/ResourceCard.d.ts.map +1 -0
- package/dist/ui/ResourceCard.js +243 -0
- package/dist/ui/ResourceCard.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 +6 -0
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +3 -0
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/styles.js +202 -7
- package/dist/ui/styles.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import type { BlockResourceInfo } from '@civitai/app-sdk/blocks';
|
|
2
|
+
/**
|
|
3
|
+
* 🔴 FROZEN — the name a resource renders under, and the single most important
|
|
4
|
+
* thing this component takes away from its callers.
|
|
5
|
+
*
|
|
6
|
+
* `BlockResourceInfo.modelName` is typed `string` (REQUIRED), and that type is
|
|
7
|
+
* optimistic: `civitai-app-model-benchmarking` writes `modelName ?? '#'+versionId`
|
|
8
|
+
* in its own code, i.e. a first-party block has already seen it absent at
|
|
9
|
+
* runtime. A resource rendered as an empty string is indistinguishable from a
|
|
10
|
+
* broken card — the row is there, the name is not, and nothing tells the viewer
|
|
11
|
+
* which of the two it is.
|
|
12
|
+
*
|
|
13
|
+
* So the fallback is `#<versionId>`: still WRONG-looking, but wrong in a way
|
|
14
|
+
* that identifies the resource and can be pasted into a URL. It is not a prop
|
|
15
|
+
* because a per-app placeholder ("Untitled model", "Unnamed LoRA") is
|
|
16
|
+
* indistinguishable from a resource actually called that.
|
|
17
|
+
*
|
|
18
|
+
* Whitespace-only counts as absent — a name of `' '` renders as nothing at
|
|
19
|
+
* all, which is the failure this function exists to prevent.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resourceDisplayName(resource: BlockResourceInfo): string;
|
|
22
|
+
/** Which shape to render. See {@link ResourceCardProps.variant}. */
|
|
23
|
+
export type ResourceCardVariant = 'card' | 'row';
|
|
24
|
+
/** Props that do not depend on either discriminant. */
|
|
25
|
+
interface ResourceCardCommonProps {
|
|
26
|
+
/**
|
|
27
|
+
* The picked resource, exactly as the host handed it over
|
|
28
|
+
* (`RESOURCE_PICKER_RESULT.selected`, `useResourcePicker`, or the
|
|
29
|
+
* `generation-resources` rehydrate endpoint). Pass it through — do not
|
|
30
|
+
* pre-format it, and do not re-type it locally.
|
|
31
|
+
*/
|
|
32
|
+
resource: BlockResourceInfo;
|
|
33
|
+
/**
|
|
34
|
+
* Thumbnail image URL. OPTIONAL, and its absence is the NORMAL case.
|
|
35
|
+
*
|
|
36
|
+
* 🔴 `BlockResourceInfo` CARRIES NO IMAGE FIELD — measured against
|
|
37
|
+
* `packages/civitai-app-sdk/src/blocks/types.ts`, which declares
|
|
38
|
+
* `versionId, modelId, modelName, versionName, baseModel, modelType` plus the
|
|
39
|
+
* optional recommended-settings projection (`strength`, `minStrength`,
|
|
40
|
+
* `maxStrength`, `trainedWords`, `clipSkip`) and nothing image-shaped. The
|
|
41
|
+
* host's resource picker does not return a thumbnail, so this component
|
|
42
|
+
* CANNOT fetch or derive one, and a caller who has one got it from somewhere
|
|
43
|
+
* else (`civitai-app-gen-matrix` fetches its own catalog).
|
|
44
|
+
*
|
|
45
|
+
* Consequence, and the reason this is worth a paragraph: BOTH variants must
|
|
46
|
+
* look deliberate with no image, and `'card'` renders
|
|
47
|
+
* {@link NO_THUMBNAIL_LABEL} in a frame that keeps its aspect ratio rather
|
|
48
|
+
* than collapsing the tile. Two of the three known consumers have no
|
|
49
|
+
* thumbnail at all.
|
|
50
|
+
*
|
|
51
|
+
* A URL that FAILS to load falls back to the same placeholder, so a dead CDN
|
|
52
|
+
* link is never an empty grey square. The image is also `loading="lazy"` —
|
|
53
|
+
* load-bearing for the `card` variant, which is built for grids of dozens.
|
|
54
|
+
*/
|
|
55
|
+
thumbnailUrl?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Trailing slot — a weight slider, a Remove button, a "Change" link.
|
|
58
|
+
*
|
|
59
|
+
* 🔴 Rendered as a SIBLING of the interactive hit area, never inside it. A
|
|
60
|
+
* `<button>` nested in a `<button>` is invalid HTML: browsers reparent it, so
|
|
61
|
+
* the inner control is unreachable by keyboard and its click is eaten by the
|
|
62
|
+
* outer one. That is why this slot exists at all rather than callers wrapping
|
|
63
|
+
* their own controls around the card.
|
|
64
|
+
*
|
|
65
|
+
* This is the FLOW slot — its content sits after the card body. For a badge
|
|
66
|
+
* that must sit ON the thumbnail, use `overlay` (card variant only).
|
|
67
|
+
*/
|
|
68
|
+
actions?: React.ReactNode;
|
|
69
|
+
/** Forwarded to the root, per the pack convention for every `/ui` primitive. */
|
|
70
|
+
className?: string;
|
|
71
|
+
/** Forwarded to the root, per the pack convention for every `/ui` primitive. */
|
|
72
|
+
style?: React.CSSProperties;
|
|
73
|
+
/**
|
|
74
|
+
* Test hook for the ROOT. Every inner hook is DERIVED from it by suffix, so
|
|
75
|
+
* two cards in one grid stay distinguishable: `<id>-hit`, `<id>-thumb`,
|
|
76
|
+
* `<id>-placeholder`, `<id>-image`, `<id>-overlay`, `<id>-name`,
|
|
77
|
+
* `<id>-selected`, `<id>-meta`, `<id>-type`, `<id>-actions`.
|
|
78
|
+
*
|
|
79
|
+
* 🔴 Grep for the SUFFIX, never for the composed value — a composed testid
|
|
80
|
+
* appears nowhere in source as a literal, so a search for `foo-name` returns
|
|
81
|
+
* zero whether the selector works or has just been deleted.
|
|
82
|
+
*
|
|
83
|
+
* Omitted, the ids are `resource-card`, `resource-card-hit`, and so on.
|
|
84
|
+
*/
|
|
85
|
+
'data-testid'?: string;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* `variant="card"` — thumbnail-first tile for a picker or browse GRID, and the
|
|
89
|
+
* only shape with a thumbnail corner for {@link ResourceCardCardProps.overlay}
|
|
90
|
+
* to sit in.
|
|
91
|
+
*
|
|
92
|
+
* `variant` is REQUIRED on purpose. There is no defensible default: the two
|
|
93
|
+
* shapes exist because three first-party blocks split evenly-ish between them,
|
|
94
|
+
* and a component that silently picks one renders the wrong shape for half its
|
|
95
|
+
* callers with no diagnostic.
|
|
96
|
+
*/
|
|
97
|
+
interface ResourceCardCardProps extends ResourceCardCommonProps {
|
|
98
|
+
variant: 'card';
|
|
99
|
+
/**
|
|
100
|
+
* Decorative status pill drawn over the thumbnail corner — the "Added" /
|
|
101
|
+
* "In your queue" badge every picker ends up wanting.
|
|
102
|
+
*
|
|
103
|
+
* 🔴 STATUS, NOT CONTROLS — and the structure enforces that rather than
|
|
104
|
+
* asking you to remember it. It renders as a SIBLING of the hit area, never
|
|
105
|
+
* inside it, so a `<button>` here is not nested in the card's `<button>`; and
|
|
106
|
+
* it carries `pointer-events: none`, so it cannot swallow a click meant for
|
|
107
|
+
* the card. An earlier version rendered it inside the thumbnail frame — i.e.
|
|
108
|
+
* inside the hit `<button>` — which recreated exactly the hazard
|
|
109
|
+
* {@link ResourceCardCommonProps.actions} exists to prevent: measured, an
|
|
110
|
+
* `onClick` pill there fired the consumer's handler AND toggled selection, and
|
|
111
|
+
* the parser reparents the inner button so hydration disagrees with the server
|
|
112
|
+
* HTML. Put anything clickable in `actions`.
|
|
113
|
+
*
|
|
114
|
+
* 🔴 WHAT THIS SLOT IS FOR, stated accurately — an earlier version of this
|
|
115
|
+
* paragraph justified it with a comparison that its own CSS change had made
|
|
116
|
+
* false. It said a consumer's absolutely-positioned child in `actions` would
|
|
117
|
+
* "escape the card and be clipped by it (card bottom 51, child bottom 120)".
|
|
118
|
+
* That measurement was taken when the root had NO `position`; the root is now
|
|
119
|
+
* `position: relative`, so it IS the containing block and a hand-rolled corner
|
|
120
|
+
* badge lands where the consumer asked. The honest claim is narrower: this
|
|
121
|
+
* slot owns the corner OFFSETS (which are arithmetic over the hit area's
|
|
122
|
+
* padding and move with it), the `pointer-events` decision, and the
|
|
123
|
+
* sibling-of-the-button placement — three things every consumer would
|
|
124
|
+
* otherwise re-derive, and two of which are not obvious. It is also why the
|
|
125
|
+
* root's `position: relative` is now load-bearing rather than cosmetic.
|
|
126
|
+
*
|
|
127
|
+
* Because it is a sibling of the labelled `<button>`, its text IS reachable to
|
|
128
|
+
* assistive tech — content inside the button is not, since the explicit
|
|
129
|
+
* `aria-label` overrides it (that is why {@link SELECTED_MARK} is
|
|
130
|
+
* `aria-hidden`). 🔴 So do NOT put the selection state here as well: with
|
|
131
|
+
* `aria-pressed` already carrying it, an "Added" pill announces it a second
|
|
132
|
+
* time. Mark such a pill `aria-hidden`, or use this slot for something
|
|
133
|
+
* `aria-pressed` does not say.
|
|
134
|
+
*
|
|
135
|
+
* If you need a control here anyway, opt back in with your own
|
|
136
|
+
* `pointer-events: auto`, and accept that you are on your own for focus order.
|
|
137
|
+
*/
|
|
138
|
+
overlay?: React.ReactNode;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* `variant="row"` — compact line for a list of already-selected resources.
|
|
142
|
+
*
|
|
143
|
+
* 🔴 `overlay` is FORBIDDEN here rather than silently dropped. A row has no
|
|
144
|
+
* thumbnail corner to hang a pill on, and this component argues twice in its own
|
|
145
|
+
* words against exactly this shape of failure: `variant` is required because "a
|
|
146
|
+
* component that silently picks one renders the wrong shape for half its callers
|
|
147
|
+
* with no diagnostic", and `interactive` is a discriminated union because a
|
|
148
|
+
* handler on the wrong arm "silently never fires". An `overlay` on a row is
|
|
149
|
+
* content that silently never renders — the same class — so it is a type error,
|
|
150
|
+
* using the same `?: never` machinery the static arm already uses.
|
|
151
|
+
*/
|
|
152
|
+
interface ResourceCardRowProps extends ResourceCardCommonProps {
|
|
153
|
+
variant: 'row';
|
|
154
|
+
overlay?: never;
|
|
155
|
+
}
|
|
156
|
+
/** A card nobody can activate: no button, no tab stop, no toggle semantics. */
|
|
157
|
+
interface ResourceCardStaticArm {
|
|
158
|
+
interactive?: false;
|
|
159
|
+
onSelect?: never;
|
|
160
|
+
selected?: never;
|
|
161
|
+
disabled?: never;
|
|
162
|
+
}
|
|
163
|
+
/** A card that IS the control: one `<button>`, with toggle semantics. */
|
|
164
|
+
interface ResourceCardInteractiveArm {
|
|
165
|
+
interactive: true;
|
|
166
|
+
/** Fires on activation (click, Enter, Space). Required — see `interactive`. */
|
|
167
|
+
onSelect: () => void;
|
|
168
|
+
/**
|
|
169
|
+
* This resource is already picked. Sets `aria-pressed` for assistive tech AND
|
|
170
|
+
* renders the frozen {@link SELECTED_MARK} for everyone else, so the state is
|
|
171
|
+
* never carried by colour alone.
|
|
172
|
+
*/
|
|
173
|
+
selected?: boolean;
|
|
174
|
+
/**
|
|
175
|
+
* Cannot be activated. Sets the native `disabled`.
|
|
176
|
+
*
|
|
177
|
+
* 🔴 Do NOT wire this to the same expression as `selected` just because a
|
|
178
|
+
* picked card should not be re-picked. A disabled button leaves the tab order
|
|
179
|
+
* and can never show a focus ring, so a keyboard user tabbing a grid of 24
|
|
180
|
+
* silently skips every resource they have already chosen — and `aria-pressed`
|
|
181
|
+
* is exactly the affordance that makes re-pressing a selected card meaningful
|
|
182
|
+
* (it deselects). Reserve `disabled` for genuinely unavailable resources.
|
|
183
|
+
*/
|
|
184
|
+
disabled?: boolean;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* @see ResourceCard
|
|
188
|
+
*
|
|
189
|
+
* 🔴 A UNION OVER TWO DISCRIMINANTS, not a flat prop bag — the four arms below
|
|
190
|
+
* are `{card, row} × {static, interactive}`.
|
|
191
|
+
*
|
|
192
|
+
* `interactive` states whether this card is a control, rather than the component
|
|
193
|
+
* inferring it from whether `onSelect` happens to be defined. Inference gets
|
|
194
|
+
* both halves wrong in practice — an `onSelect` passed to something rendered as
|
|
195
|
+
* static is a handler that silently never fires, and an interactive card without
|
|
196
|
+
* one is a tab stop that does nothing.
|
|
197
|
+
*
|
|
198
|
+
* `variant` gates `overlay` for the same reason: a row has no thumbnail corner,
|
|
199
|
+
* so an `overlay` there is content that silently never renders. Both are type
|
|
200
|
+
* errors rather than diagnostics nobody sees.
|
|
201
|
+
*
|
|
202
|
+
* 🔴 CONSEQUENCE, because TypeScript's diagnostic for it is opaque: pass a
|
|
203
|
+
* LITERAL (`variant="card"`, `interactive` / `interactive={false}`) or branch on
|
|
204
|
+
* your condition. A `boolean` or a widened `string` VARIABLE narrows to no arm
|
|
205
|
+
* and fails with `TS2322: … not assignable to 'IntrinsicAttributes &
|
|
206
|
+
* ResourceCardProps'`, which names nothing useful. That is the correct behaviour
|
|
207
|
+
* — a `boolean` cannot carry a sound "then `onSelect` is required" — but it
|
|
208
|
+
* costs you a round if nobody says so. Spreading an `as const` prop bag
|
|
209
|
+
* compiles.
|
|
210
|
+
*/
|
|
211
|
+
export type ResourceCardProps = (ResourceCardCardProps & ResourceCardStaticArm) | (ResourceCardCardProps & ResourceCardInteractiveArm) | (ResourceCardRowProps & ResourceCardStaticArm) | (ResourceCardRowProps & ResourceCardInteractiveArm);
|
|
212
|
+
/**
|
|
213
|
+
* A picked Civitai generation resource — a checkpoint or a LoRA — rendered as a
|
|
214
|
+
* grid tile (`variant="card"`) or a compact list line (`variant="row"`).
|
|
215
|
+
*
|
|
216
|
+
* PRESENTATIONAL AND STATELESS by construction. It does not fetch, it calls no
|
|
217
|
+
* host hook, and it owns no selection state: callbacks in, markup out. (Its one
|
|
218
|
+
* piece of internal state is which thumbnail URL has failed to load, which is
|
|
219
|
+
* about the image element, not about your data.) Whoever owns the
|
|
220
|
+
* picked-resource list owns the state; this renders it.
|
|
221
|
+
*
|
|
222
|
+
* Derived from three first-party blocks that each built one:
|
|
223
|
+
* `civitai-app-gen-matrix` (a thumbnail tile in a browse grid, `aria-pressed`,
|
|
224
|
+
* disabled once added), `civitai-app-model-benchmarking` (compact rows with a
|
|
225
|
+
* weight slider and a Remove button), and `civitai-block-generate-from-model`
|
|
226
|
+
* (an inline "Generating with: Name (Version)" plus a Change link). The first
|
|
227
|
+
* is the `card` shape; the other two are `row`.
|
|
228
|
+
*
|
|
229
|
+
* 🔴 WHAT IS FROZEN, and why that is the point rather than the markup: the
|
|
230
|
+
* name fallback ({@link resourceDisplayName}), the type label
|
|
231
|
+
* ({@link TYPE_LABELS}), the missing-thumbnail copy
|
|
232
|
+
* ({@link NO_THUMBNAIL_LABEL}), the non-colour selected mark
|
|
233
|
+
* ({@link SELECTED_MARK}) and the accessible-name composition
|
|
234
|
+
* ({@link accessibleName}) are NOT props. Each is a statement about what a
|
|
235
|
+
* resource IS, and three apps disagreeing about it is three apps telling a
|
|
236
|
+
* viewer different things about the same model. What legitimately varies —
|
|
237
|
+
* variant, thumbnail, selected/disabled, the two content slots, className,
|
|
238
|
+
* style, the surrounding grid — is a prop.
|
|
239
|
+
*
|
|
240
|
+
* 🔴 NOT A LINK, on purpose. It has `modelId`/`versionId` and could build a
|
|
241
|
+
* civitai.com URL, but a block renders inside a sandboxed iframe where a
|
|
242
|
+
* top-level navigation is host-mediated (`useCivitaiNavigate`), so an `<a
|
|
243
|
+
* href>` here would either be inert or would punch the viewer out of the app
|
|
244
|
+
* mid-task. A card that navigates is a different component.
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* // Browse grid — the card IS the control. Note `selected` WITHOUT `disabled`:
|
|
248
|
+
* // a picked card stays focusable and re-pressing it deselects, which is what
|
|
249
|
+
* // `aria-pressed` promises. Disabling it instead drops every already-picked
|
|
250
|
+
* // resource out of the tab order.
|
|
251
|
+
* <ResourceCard
|
|
252
|
+
* variant="card"
|
|
253
|
+
* interactive
|
|
254
|
+
* resource={r}
|
|
255
|
+
* thumbnailUrl={catalog.get(r.versionId)?.thumbnailUrl}
|
|
256
|
+
* selected={picked.has(r.versionId)}
|
|
257
|
+
* onSelect={() => toggle(r)}
|
|
258
|
+
* // 🔴 aria-hidden, because `selected` already sets `aria-pressed`. The
|
|
259
|
+
* // overlay is a SIBLING of the button, so its text IS announced — an
|
|
260
|
+
* // un-hidden "Added" here states the selection a second time, which is the
|
|
261
|
+
* // duplication `SELECTED_MARK`'s own aria-hidden exists to prevent.
|
|
262
|
+
* overlay={picked.has(r.versionId) ? <span aria-hidden="true">Added</span> : null}
|
|
263
|
+
* data-testid={`browse-${r.versionId}`}
|
|
264
|
+
* />
|
|
265
|
+
*
|
|
266
|
+
* @example
|
|
267
|
+
* // Selected list — static, with the per-row controls in `actions`.
|
|
268
|
+
* <ResourceCard variant="row" resource={lora} actions={<RemoveButton … />} />
|
|
269
|
+
*/
|
|
270
|
+
export declare const ResourceCard: import("react").ForwardRefExoticComponent<ResourceCardProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
271
|
+
export {};
|
|
272
|
+
//# sourceMappingURL=ResourceCard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ResourceCard.d.ts","sourceRoot":"","sources":["../../src/ui/ResourceCard.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAsEjE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,MAAM,CAIvE;AAED,oEAAoE;AACpE,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,KAAK,CAAC;AAEjD,uDAAuD;AACvD,UAAU,uBAAuB;IAC/B;;;;;OAKG;IACH,QAAQ,EAAE,iBAAiB,CAAC;IAC5B;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B;;;;;;;;;;;OAWG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,UAAU,qBAAsB,SAAQ,uBAAuB;IAC7D,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAsCG;IACH,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED;;;;;;;;;;;GAWG;AACH,UAAU,oBAAqB,SAAQ,uBAAuB;IAC5D,OAAO,EAAE,KAAK,CAAC;IACf,OAAO,CAAC,EAAE,KAAK,CAAC;CACjB;AAED,+EAA+E;AAC/E,UAAU,qBAAqB;IAC7B,WAAW,CAAC,EAAE,KAAK,CAAC;IACpB,QAAQ,CAAC,EAAE,KAAK,CAAC;IACjB,QAAQ,CAAC,EAAE,KAAK,CAAC;IACjB,QAAQ,CAAC,EAAE,KAAK,CAAC;CAClB;AAED,yEAAyE;AACzE,UAAU,0BAA0B;IAClC,WAAW,EAAE,IAAI,CAAC;IAClB,+EAA+E;IAC/E,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,MAAM,iBAAiB,GACzB,CAAC,qBAAqB,GAAG,qBAAqB,CAAC,GAC/C,CAAC,qBAAqB,GAAG,0BAA0B,CAAC,GACpD,CAAC,oBAAoB,GAAG,qBAAqB,CAAC,GAC9C,CAAC,oBAAoB,GAAG,0BAA0B,CAAC,CAAC;AAqCxD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AACH,eAAO,MAAM,YAAY,8GAsKvB,CAAC"}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useState } from 'react';
|
|
3
|
+
import { Badge } from './Badge.js';
|
|
4
|
+
import { useBlocksStyles } from './styles.js';
|
|
5
|
+
/**
|
|
6
|
+
* 🔴 FROZEN — the LoRA/Checkpoint distinction, and it is deliberately NOT a
|
|
7
|
+
* prop. Three blocks rendered `modelType` three ways ('LORA' verbatim, a local
|
|
8
|
+
* map, and not at all), which means the same resource read as a different KIND
|
|
9
|
+
* of thing depending on which app you were in. A viewer who thinks a LoRA is a
|
|
10
|
+
* checkpoint picks a base model that cannot generate.
|
|
11
|
+
*
|
|
12
|
+
* Keys are lower-cased `modelType` values. LyCORIS/LoCon/DoRA are LoRA-family
|
|
13
|
+
* adapters the host may resolve for a LoRA pick, so they render as "LoRA"
|
|
14
|
+
* rather than as three near-synonyms nobody outside the training world can
|
|
15
|
+
* rank.
|
|
16
|
+
*/
|
|
17
|
+
const TYPE_LABELS = {
|
|
18
|
+
checkpoint: 'Checkpoint',
|
|
19
|
+
lora: 'LoRA',
|
|
20
|
+
locon: 'LoRA',
|
|
21
|
+
lycoris: 'LoRA',
|
|
22
|
+
dora: 'LoRA',
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* 🔴 FROZEN — shown in the `card` variant's thumbnail frame when there is no
|
|
26
|
+
* usable image. Not a prop, and not blank: see
|
|
27
|
+
* {@link ResourceCardProps.thumbnailUrl} for why this state is the COMMON one
|
|
28
|
+
* rather than an edge case. "No preview" says the picture is missing; an empty
|
|
29
|
+
* grey box reads as an image that failed to load, and "Loading…" would be an
|
|
30
|
+
* outright lie.
|
|
31
|
+
*
|
|
32
|
+
* 🔴 "No usable image" covers TWO cases, and an audit found the second one
|
|
33
|
+
* missing: a caller who supplied no `thumbnailUrl`, AND a caller who supplied
|
|
34
|
+
* one that FAILED TO LOAD (a CDN 404/403, an ad-blocked host, an offline
|
|
35
|
+
* viewer). Keying only on the URL being absent produced exactly the empty grey
|
|
36
|
+
* box this constant's own comment exists to prevent — measured at
|
|
37
|
+
* `naturalWidth: 0`, a 206x206 frame and `textContent: ""`, with `alt=""` +
|
|
38
|
+
* `aria-hidden` leaving assistive tech nothing either. The `onError` handler
|
|
39
|
+
* below is what closes it; gen-matrix, the one consumer that supplies
|
|
40
|
+
* thumbnails, sources them from a live catalog fetch and is the likeliest to
|
|
41
|
+
* hit it.
|
|
42
|
+
*/
|
|
43
|
+
const NO_THUMBNAIL_LABEL = 'No preview';
|
|
44
|
+
/**
|
|
45
|
+
* 🔴 FROZEN — the last-resort name when neither a name nor a usable id exists.
|
|
46
|
+
* See {@link resourceDisplayName}. This is an INVARIANT guard, not a regression
|
|
47
|
+
* fix: no host has been observed omitting `versionId`. It exists because the
|
|
48
|
+
* alternative is rendering the literal string "#undefined" at people.
|
|
49
|
+
*/
|
|
50
|
+
const UNKNOWN_NAME = 'Unknown resource';
|
|
51
|
+
/**
|
|
52
|
+
* 🔴 FROZEN — the non-colour half of the selected affordance, and it is not a
|
|
53
|
+
* prop for the same reason the type label is text rather than a colour swatch:
|
|
54
|
+
* colour alone is not an accessible distinction (WCAG 1.4.1).
|
|
55
|
+
*
|
|
56
|
+
* An audit caught this component asserting that principle for `modelType` two
|
|
57
|
+
* paragraphs above while conveying SELECTION by border hue alone. `aria-pressed`
|
|
58
|
+
* covered assistive tech and nothing covered a sighted viewer who cannot
|
|
59
|
+
* separate the border tokens. This glyph is that cover; the border-colour change
|
|
60
|
+
* stays as reinforcement, not as the only signal.
|
|
61
|
+
*
|
|
62
|
+
* `aria-hidden` on purpose: `selected` exists only on the interactive arm, which
|
|
63
|
+
* always carries `aria-pressed`, so announcing it again would say it twice.
|
|
64
|
+
*/
|
|
65
|
+
const SELECTED_MARK = '✓';
|
|
66
|
+
/**
|
|
67
|
+
* 🔴 FROZEN — the name a resource renders under, and the single most important
|
|
68
|
+
* thing this component takes away from its callers.
|
|
69
|
+
*
|
|
70
|
+
* `BlockResourceInfo.modelName` is typed `string` (REQUIRED), and that type is
|
|
71
|
+
* optimistic: `civitai-app-model-benchmarking` writes `modelName ?? '#'+versionId`
|
|
72
|
+
* in its own code, i.e. a first-party block has already seen it absent at
|
|
73
|
+
* runtime. A resource rendered as an empty string is indistinguishable from a
|
|
74
|
+
* broken card — the row is there, the name is not, and nothing tells the viewer
|
|
75
|
+
* which of the two it is.
|
|
76
|
+
*
|
|
77
|
+
* So the fallback is `#<versionId>`: still WRONG-looking, but wrong in a way
|
|
78
|
+
* that identifies the resource and can be pasted into a URL. It is not a prop
|
|
79
|
+
* because a per-app placeholder ("Untitled model", "Unnamed LoRA") is
|
|
80
|
+
* indistinguishable from a resource actually called that.
|
|
81
|
+
*
|
|
82
|
+
* Whitespace-only counts as absent — a name of `' '` renders as nothing at
|
|
83
|
+
* all, which is the failure this function exists to prevent.
|
|
84
|
+
*/
|
|
85
|
+
export function resourceDisplayName(resource) {
|
|
86
|
+
const name = typeof resource.modelName === 'string' ? resource.modelName.trim() : '';
|
|
87
|
+
if (name !== '')
|
|
88
|
+
return name;
|
|
89
|
+
return Number.isFinite(resource.versionId) ? `#${resource.versionId}` : UNKNOWN_NAME;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 🔴 FROZEN — the accessible name of an interactive card, composed in ONE fixed
|
|
93
|
+
* order everywhere.
|
|
94
|
+
*
|
|
95
|
+
* The visible tile is a name, a type pill and a muted meta row; read as raw
|
|
96
|
+
* content that is `"Juggernaut XLCheckpointv9SD 1.5"` — and `"✓Juggernaut XL…"`
|
|
97
|
+
* once selected. So the button carries an explicit label instead — and because
|
|
98
|
+
* it LEADS with the same string the tile shows, it satisfies WCAG 2.5.3 (Label
|
|
99
|
+
* in Name) rather than diverging from it.
|
|
100
|
+
*
|
|
101
|
+
* Absent segments are dropped, never rendered as an empty gap. The selected
|
|
102
|
+
* state is NOT spelled here: `aria-pressed` already carries it, and duplicating
|
|
103
|
+
* it in the name is how a control comes to announce "Added Added".
|
|
104
|
+
*/
|
|
105
|
+
function accessibleName(resource, typeLabel) {
|
|
106
|
+
const version = typeof resource.versionName === 'string' ? resource.versionName.trim() : '';
|
|
107
|
+
const base = typeof resource.baseModel === 'string' ? resource.baseModel.trim() : '';
|
|
108
|
+
return [resourceDisplayName(resource), version, typeLabel, base]
|
|
109
|
+
.filter((part) => part !== '')
|
|
110
|
+
.join(', ');
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* 🔴 FROZEN — see {@link TYPE_LABELS}. An UNKNOWN type renders VERBATIM rather
|
|
114
|
+
* than being coerced into one of the two known labels: a Controlnet or an
|
|
115
|
+
* embedding shown as "Checkpoint" is a confident lie, whereas the raw string is
|
|
116
|
+
* merely unpolished. Blank in, blank out — the pill is then omitted entirely,
|
|
117
|
+
* because an empty badge is chrome that means nothing.
|
|
118
|
+
*/
|
|
119
|
+
function typeLabelOf(resource) {
|
|
120
|
+
const raw = typeof resource.modelType === 'string' ? resource.modelType.trim() : '';
|
|
121
|
+
if (raw === '')
|
|
122
|
+
return '';
|
|
123
|
+
return TYPE_LABELS[raw.toLowerCase()] ?? raw;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* A picked Civitai generation resource — a checkpoint or a LoRA — rendered as a
|
|
127
|
+
* grid tile (`variant="card"`) or a compact list line (`variant="row"`).
|
|
128
|
+
*
|
|
129
|
+
* PRESENTATIONAL AND STATELESS by construction. It does not fetch, it calls no
|
|
130
|
+
* host hook, and it owns no selection state: callbacks in, markup out. (Its one
|
|
131
|
+
* piece of internal state is which thumbnail URL has failed to load, which is
|
|
132
|
+
* about the image element, not about your data.) Whoever owns the
|
|
133
|
+
* picked-resource list owns the state; this renders it.
|
|
134
|
+
*
|
|
135
|
+
* Derived from three first-party blocks that each built one:
|
|
136
|
+
* `civitai-app-gen-matrix` (a thumbnail tile in a browse grid, `aria-pressed`,
|
|
137
|
+
* disabled once added), `civitai-app-model-benchmarking` (compact rows with a
|
|
138
|
+
* weight slider and a Remove button), and `civitai-block-generate-from-model`
|
|
139
|
+
* (an inline "Generating with: Name (Version)" plus a Change link). The first
|
|
140
|
+
* is the `card` shape; the other two are `row`.
|
|
141
|
+
*
|
|
142
|
+
* 🔴 WHAT IS FROZEN, and why that is the point rather than the markup: the
|
|
143
|
+
* name fallback ({@link resourceDisplayName}), the type label
|
|
144
|
+
* ({@link TYPE_LABELS}), the missing-thumbnail copy
|
|
145
|
+
* ({@link NO_THUMBNAIL_LABEL}), the non-colour selected mark
|
|
146
|
+
* ({@link SELECTED_MARK}) and the accessible-name composition
|
|
147
|
+
* ({@link accessibleName}) are NOT props. Each is a statement about what a
|
|
148
|
+
* resource IS, and three apps disagreeing about it is three apps telling a
|
|
149
|
+
* viewer different things about the same model. What legitimately varies —
|
|
150
|
+
* variant, thumbnail, selected/disabled, the two content slots, className,
|
|
151
|
+
* style, the surrounding grid — is a prop.
|
|
152
|
+
*
|
|
153
|
+
* 🔴 NOT A LINK, on purpose. It has `modelId`/`versionId` and could build a
|
|
154
|
+
* civitai.com URL, but a block renders inside a sandboxed iframe where a
|
|
155
|
+
* top-level navigation is host-mediated (`useCivitaiNavigate`), so an `<a
|
|
156
|
+
* href>` here would either be inert or would punch the viewer out of the app
|
|
157
|
+
* mid-task. A card that navigates is a different component.
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* // Browse grid — the card IS the control. Note `selected` WITHOUT `disabled`:
|
|
161
|
+
* // a picked card stays focusable and re-pressing it deselects, which is what
|
|
162
|
+
* // `aria-pressed` promises. Disabling it instead drops every already-picked
|
|
163
|
+
* // resource out of the tab order.
|
|
164
|
+
* <ResourceCard
|
|
165
|
+
* variant="card"
|
|
166
|
+
* interactive
|
|
167
|
+
* resource={r}
|
|
168
|
+
* thumbnailUrl={catalog.get(r.versionId)?.thumbnailUrl}
|
|
169
|
+
* selected={picked.has(r.versionId)}
|
|
170
|
+
* onSelect={() => toggle(r)}
|
|
171
|
+
* // 🔴 aria-hidden, because `selected` already sets `aria-pressed`. The
|
|
172
|
+
* // overlay is a SIBLING of the button, so its text IS announced — an
|
|
173
|
+
* // un-hidden "Added" here states the selection a second time, which is the
|
|
174
|
+
* // duplication `SELECTED_MARK`'s own aria-hidden exists to prevent.
|
|
175
|
+
* overlay={picked.has(r.versionId) ? <span aria-hidden="true">Added</span> : null}
|
|
176
|
+
* data-testid={`browse-${r.versionId}`}
|
|
177
|
+
* />
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* // Selected list — static, with the per-row controls in `actions`.
|
|
181
|
+
* <ResourceCard variant="row" resource={lora} actions={<RemoveButton … />} />
|
|
182
|
+
*/
|
|
183
|
+
export const ResourceCard = forwardRef(function ResourceCard(props, ref) {
|
|
184
|
+
const { resource, variant, thumbnailUrl, actions, className, style, 'data-testid': testId, } = props;
|
|
185
|
+
// `overlay` lives only on the `card` arm, so it is read through the
|
|
186
|
+
// discriminant rather than destructured off the union.
|
|
187
|
+
const overlay = props.variant === 'card' ? props.overlay : undefined;
|
|
188
|
+
const interactive = props.interactive === true;
|
|
189
|
+
const selected = interactive ? props.selected === true : false;
|
|
190
|
+
const disabled = interactive ? props.disabled === true : false;
|
|
191
|
+
// 🔴 Keyed by URL rather than a bare boolean, so it RESETS when the caller
|
|
192
|
+
// supplies a different thumbnail. A `useState(false)` + `onError` pair would
|
|
193
|
+
// latch: one dead URL and every later image for this card renders as the
|
|
194
|
+
// placeholder, in a grid whose cards are recycled as the viewer pages.
|
|
195
|
+
const [failedUrl, setFailedUrl] = useState(null);
|
|
196
|
+
useBlocksStyles();
|
|
197
|
+
const id = testId ?? 'resource-card';
|
|
198
|
+
const ids = {
|
|
199
|
+
hit: `${id}-hit`,
|
|
200
|
+
thumb: `${id}-thumb`,
|
|
201
|
+
image: `${id}-image`,
|
|
202
|
+
placeholder: `${id}-placeholder`,
|
|
203
|
+
overlay: `${id}-overlay`,
|
|
204
|
+
name: `${id}-name`,
|
|
205
|
+
selected: `${id}-selected`,
|
|
206
|
+
meta: `${id}-meta`,
|
|
207
|
+
type: `${id}-type`,
|
|
208
|
+
actions: `${id}-actions`,
|
|
209
|
+
};
|
|
210
|
+
const name = resourceDisplayName(resource);
|
|
211
|
+
const typeLabel = typeLabelOf(resource);
|
|
212
|
+
const versionName = typeof resource.versionName === 'string' ? resource.versionName.trim() : '';
|
|
213
|
+
const baseModel = typeof resource.baseModel === 'string' ? resource.baseModel.trim() : '';
|
|
214
|
+
const showImage = thumbnailUrl != null && failedUrl !== thumbnailUrl;
|
|
215
|
+
const hasFrame = variant === 'card' || thumbnailUrl != null;
|
|
216
|
+
// 🔴 The frame is rendered in BOTH states, with the SAME wrapper element and
|
|
217
|
+
// the same CSS box. Only its CONTENT differs. Rendering nothing when there is
|
|
218
|
+
// no image collapses the tile — and since `BlockResourceInfo` has no image
|
|
219
|
+
// field, "no image" is the common case, not the edge one.
|
|
220
|
+
const thumb = hasFrame ? (_jsx("span", { "data-civitai-ui-resource-thumb": "", "data-testid": ids.thumb, children: showImage ? (
|
|
221
|
+
// `alt=""` + aria-hidden: DECORATIVE. The name is carried by the
|
|
222
|
+
// visible text and, when interactive, by the button's own label — an
|
|
223
|
+
// alt here would make a screen reader read the resource twice.
|
|
224
|
+
_jsx("img", { src: thumbnailUrl, alt: "", "aria-hidden": "true",
|
|
225
|
+
// 🔴 Load-bearing, not hygiene: the `card` variant exists for grids
|
|
226
|
+
// of dozens of tiles, and eager-loading a full page of thumbnails is
|
|
227
|
+
// the class of regression this package already keeps
|
|
228
|
+
// `pickerOverlay.perf.browser.test.ts` for.
|
|
229
|
+
loading: "lazy", "data-testid": ids.image,
|
|
230
|
+
// 🔴 A dead URL must reach the SAME placeholder as a missing one. See
|
|
231
|
+
// NO_THUMBNAIL_LABEL: without this a CDN 404 renders an empty grey
|
|
232
|
+
// square with no copy and nothing for assistive tech.
|
|
233
|
+
onError: () => setFailedUrl(thumbnailUrl) })) : (_jsx("span", { "data-civitai-ui-resource-placeholder": "", "data-testid": ids.placeholder, children: NO_THUMBNAIL_LABEL })) })) : null;
|
|
234
|
+
const body = (_jsxs(_Fragment, { children: [thumb, _jsxs("span", { "data-civitai-ui-resource-text": "", children: [_jsxs("span", { "data-civitai-ui-resource-nameline": "", children: [selected ? (_jsx("span", { "data-civitai-ui-resource-selected": "", "data-testid": ids.selected, "aria-hidden": "true", children: SELECTED_MARK })) : null, _jsx("span", { "data-civitai-ui-resource-name": "", "data-testid": ids.name, children: name })] }), _jsxs("span", { "data-civitai-ui-resource-meta": "", "data-testid": ids.meta, children: [typeLabel !== '' ? (_jsx(Badge, { size: "sm", variant: "outline", "data-testid": ids.type, children: typeLabel })) : null, versionName !== '' ? _jsx("span", { children: versionName }) : null, baseModel !== '' ? _jsx("span", { children: baseModel }) : null] })] })] }));
|
|
235
|
+
return (_jsxs("div", { ref: ref, className: className, style: style, "data-civitai-ui": "resource-card", "data-variant": variant, "data-interactive": interactive ? 'true' : undefined, "data-selected": selected ? 'true' : undefined, "data-disabled": disabled ? 'true' : undefined, "data-testid": id, children: [interactive ? (_jsx("button", { type: "button", "data-civitai-ui-resource-hit": "", "data-testid": ids.hit, "aria-pressed": selected, disabled: disabled, "aria-label": accessibleName(resource, typeLabel), onClick: () => props.onSelect(), children: body })) : (
|
|
236
|
+
// 🔴 A plain <div>, with NO role, NO tabIndex and NO handler. A static
|
|
237
|
+
// card must not be a tab stop: the `row` variant exists for lists of
|
|
238
|
+
// resources the viewer has ALREADY chosen, and making every one of them
|
|
239
|
+
// focusable puts N dead stops between the keyboard user and the control
|
|
240
|
+
// they actually want, which lives in `actions`.
|
|
241
|
+
_jsx("div", { "data-civitai-ui-resource-hit": "", "data-testid": ids.hit, children: body })), overlay != null && variant === 'card' ? (_jsx("span", { "data-civitai-ui-resource-overlay": "", "data-testid": ids.overlay, children: overlay })) : null, actions != null ? (_jsx("div", { "data-civitai-ui-resource-actions": "", "data-testid": ids.actions, children: actions })) : null] }));
|
|
242
|
+
});
|
|
243
|
+
//# sourceMappingURL=ResourceCard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ResourceCard.js","sourceRoot":"","sources":["../../src/ui/ResourceCard.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"}
|