@civitai/blocks-react 0.45.1 → 0.47.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 +10 -3
- package/dist/ui/ReportButton.d.ts +92 -0
- package/dist/ui/ReportButton.d.ts.map +1 -0
- package/dist/ui/ReportButton.js +172 -0
- package/dist/ui/ReportButton.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/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/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 +1 -1
package/README.md
CHANGED
|
@@ -922,9 +922,16 @@ export function App() {
|
|
|
922
922
|
| `NumberInput` | controlled numeric (`value: number \| null`, `onChange`, `min`/`max`/`step`). Rejects non-numeric (never emits `NaN`), clamps to `[min,max]` on blur, empty → `null`. Same label/description/error wiring. |
|
|
923
923
|
| `Select` | controlled dropdown (`value: string`, `onChange`, `options: {value,label,disabled}[]` **or** `<option>` children, `placeholder`). Native `<select>`, `role="combobox"`. Same label/description/error wiring. |
|
|
924
924
|
| `Collapse` | controlled disclosure (`open` + `onOpenChange`, `title`, `disabled`) for the "advanced params reveal". `aria-expanded` + `aria-controls`; content region `role="region"`, `hidden` when closed. |
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
925
|
+
| `SegmentedControl` | controlled view/tab switcher (`value` + `onChange`, `data: {value,label,disabled}[]`, `fullWidth`, `size`). `role="tablist"` of `role="tab"` buttons; ArrowLeft/ArrowRight rove selection. |
|
|
926
|
+
| `ReportButton` | two-step control that files a shared-board row for platform moderator review via `useSharedStorage().report()`. `noun` + `onReport` (+ `reported` for server truth). 🔴 Its visible copy is deliberately **not** overridable — see the component's JSDoc. |
|
|
927
|
+
| `ResourceCard` | a picked generation resource (`BlockResourceInfo`) as a grid tile (`variant="card"`) or a compact line (`variant="row"`). `interactive` is an explicit discriminant that requires `onSelect` (+ `selected`/`disabled`); `thumbnailUrl` is optional because **`BlockResourceInfo` carries no image field**, and a missing *or failed* image falls back to a frozen "No preview" frame. `actions` is the trailing flow slot on both variants; `overlay` is the decorative corner badge over the thumbnail and is **`card`-only — a type error on a `row`**. Both render as siblings of the hit area, never inside it. The name fallback, type label, placeholder copy, selected mark and accessible-name order are frozen, not props. |
|
|
928
|
+
|
|
929
|
+
Every component carries a `data-civitai-ui="<name>"` hook. Most also forward
|
|
930
|
+
`className` + `style` and forward a `ref` to their DOM node. **Two do not:**
|
|
931
|
+
`ReportButton` renders a different element per handshake state and forwards none
|
|
932
|
+
of the three (only its `data-testid`, from which the other four hooks are derived
|
|
933
|
+
by suffix); `Modal` forwards `className` + `style` but takes no `ref` — it holds
|
|
934
|
+
its panel node internally, so `<Modal ref={…}>` is a type error. Need
|
|
928
935
|
to inject the CSS yourself (SSR, or a non-React shell)? Call
|
|
929
936
|
`injectBlocksStyles(doc?)` once, or read the raw `BLOCKS_UI_STYLES` string.
|
|
930
937
|
`useBlocksStyles()` is the hook the components call internally.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The post-write abuse seam for a shared board: a two-step control that files a
|
|
3
|
+
* row for PLATFORM moderator review via `useSharedStorage().report()`.
|
|
4
|
+
*
|
|
5
|
+
* 🔴 REPORTING IS ESCALATION, NOT REMOVAL, and this component exists so that
|
|
6
|
+
* fact is stated identically in every block. `report()` files the row and its
|
|
7
|
+
* own contract says filing does NOT hide it — a moderator decides. An app owner
|
|
8
|
+
* has no server-side hide to offer instead either: `update` and `withdraw` are
|
|
9
|
+
* author-scoped, so they reject for anyone but the row's author.
|
|
10
|
+
*
|
|
11
|
+
* 🔴 THE VISIBLE COPY IS NOT OVERRIDABLE — and that guarantee is exactly one
|
|
12
|
+
* thing, so do not read it as more. What is fixed is the WORDING THIS COMPONENT
|
|
13
|
+
* RENDERS: the confirm question, the failure line and the settled line are each
|
|
14
|
+
* pinned whole by tests, so none of them can come to imply a deletion. It does
|
|
15
|
+
* NOT constrain what {@link ReportButtonProps.onReport} actually does — a
|
|
16
|
+
* consumer can wire a real delete behind it and this control will still settle
|
|
17
|
+
* to "Reported for review" — and it cannot stop a host page restyling the
|
|
18
|
+
* settled text out of view. The honest claim is that the wording cannot drift by
|
|
19
|
+
* accident across blocks, which is what three divergent hand-rolled copies had
|
|
20
|
+
* already produced.
|
|
21
|
+
*
|
|
22
|
+
* The caller decides WHO sees it. Render it only for a viewer who is signed in
|
|
23
|
+
* and does not own the row: `report` rejects for an anonymous viewer, and an
|
|
24
|
+
* author has a real Remove. Offering it otherwise is offering an error.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* // 🔴 Key it by the ROW, not by index — the settled state belongs to the row,
|
|
28
|
+
* // and a list that re-orders under an index key shows a settled report
|
|
29
|
+
* // against a row nobody reported.
|
|
30
|
+
* {!isOwn && viewerId != null && (
|
|
31
|
+
* <ReportButton
|
|
32
|
+
* key={item.key}
|
|
33
|
+
* noun="generator"
|
|
34
|
+
* reported={myReports.has(item.key)}
|
|
35
|
+
* onReport={() => shared.report(item.key)}
|
|
36
|
+
* data-testid={`gen-${item.key}-report`}
|
|
37
|
+
* />
|
|
38
|
+
* )}
|
|
39
|
+
*/
|
|
40
|
+
export interface ReportButtonProps {
|
|
41
|
+
/**
|
|
42
|
+
* What the row is, lower-case and singular — "combination", "prompt",
|
|
43
|
+
* "generator", "request". Appears in the confirm question and in both
|
|
44
|
+
* accessible names.
|
|
45
|
+
*
|
|
46
|
+
* 🔴 It is spliced into an `aria-label`, so it is NOT a place for arbitrary
|
|
47
|
+
* text: a sentence here becomes the control's accessible name and can say
|
|
48
|
+
* whatever it likes to exactly the users the fixed wording exists to protect.
|
|
49
|
+
* Pass a bare noun.
|
|
50
|
+
*/
|
|
51
|
+
noun: string;
|
|
52
|
+
/**
|
|
53
|
+
* Files the report. Fires ONLY after the viewer confirms. Reject to surface
|
|
54
|
+
* the failure — a rejected report keeps the control armed rather than
|
|
55
|
+
* settling, because one that closed quietly would read as filed.
|
|
56
|
+
*/
|
|
57
|
+
onReport: () => Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* This viewer has ALREADY reported this row. Renders the settled state
|
|
60
|
+
* directly and skips the handshake.
|
|
61
|
+
*
|
|
62
|
+
* 🔴 THE SHARED STORE CANNOT TELL YOU THIS. `SharedListItem` carries
|
|
63
|
+
* `viewerVoted` and has no report equivalent, so unlike a vote there is no
|
|
64
|
+
* server field to read — the only source is your own app's per-viewer
|
|
65
|
+
* storage, recorded when `onReport` resolves. Supply it if you keep that
|
|
66
|
+
* record: without it the settled state is local-only, so any remount (a
|
|
67
|
+
* `list()` refresh, a tab switch, virtualized scroll) resets the control to
|
|
68
|
+
* "Report" and the same viewer can file the same row again. `report()` is NOT
|
|
69
|
+
* documented idempotent the way `vote` is, so that is a duplicate report
|
|
70
|
+
* rather than a no-op.
|
|
71
|
+
*
|
|
72
|
+
* Flipping this to `true` settles the control silently — no focus move — which
|
|
73
|
+
* is correct for the load-time case it is meant for; stealing focus on mount
|
|
74
|
+
* would be wrong.
|
|
75
|
+
*/
|
|
76
|
+
reported?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Test hook for the TRIGGER. The other four hooks are DERIVED from it by
|
|
79
|
+
* suffix, so two rows in one list stay distinguishable once armed or settled:
|
|
80
|
+
* `<id>-confirm`, `<id>-cancel`, `<id>-done`, `<id>-prompt`.
|
|
81
|
+
*
|
|
82
|
+
* 🔴 Grep for the SUFFIX, never for the composed value: a composed testid
|
|
83
|
+
* appears nowhere in source as a literal, so a search for `foo-confirm`
|
|
84
|
+
* returns zero whether the selector works or has just been deleted.
|
|
85
|
+
*
|
|
86
|
+
* Omitted, the ids are `report-button`, `report-confirm`, `report-cancel`,
|
|
87
|
+
* `report-done`, `report-confirm-prompt`.
|
|
88
|
+
*/
|
|
89
|
+
'data-testid'?: string;
|
|
90
|
+
}
|
|
91
|
+
export declare function ReportButton({ noun, onReport, reported, 'data-testid': testId, }: ReportButtonProps): React.JSX.Element;
|
|
92
|
+
//# sourceMappingURL=ReportButton.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReportButton.d.ts","sourceRoot":"","sources":["../../src/ui/ReportButton.tsx"],"names":[],"mappings":"AAcA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;;;;;OASG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;;;;;;OAWG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,wBAAgB,YAAY,CAAC,EAC3B,IAAI,EACJ,QAAQ,EACR,QAAgB,EAChB,aAAa,EAAE,MAAM,GACtB,EAAE,iBAAiB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CA8MvC"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Button } from './Button.js';
|
|
4
|
+
import { Group } from './Group.js';
|
|
5
|
+
import { useBlocksStyles } from './styles.js';
|
|
6
|
+
/** Shared by the two text renders so they cannot drift apart. */
|
|
7
|
+
const NOTE_STYLE = {
|
|
8
|
+
whiteSpace: 'nowrap',
|
|
9
|
+
fontSize: 12,
|
|
10
|
+
lineHeight: 1.45,
|
|
11
|
+
color: 'var(--civitai-color-text-dimmed)',
|
|
12
|
+
};
|
|
13
|
+
export function ReportButton({ noun, onReport, reported = false, 'data-testid': testId, }) {
|
|
14
|
+
const [confirming, setConfirming] = useState(false);
|
|
15
|
+
const [busy, setBusy] = useState(false);
|
|
16
|
+
const [done, setDone] = useState(false);
|
|
17
|
+
const [failed, setFailed] = useState(false);
|
|
18
|
+
const confirmRef = useRef(null);
|
|
19
|
+
const cancelRef = useRef(null);
|
|
20
|
+
const doneRef = useRef(null);
|
|
21
|
+
/**
|
|
22
|
+
* Monotonic id of the CURRENT attempt. Every settle compares against it and
|
|
23
|
+
* no-ops if it has moved on.
|
|
24
|
+
*
|
|
25
|
+
* 🔴 A single "abandoned" BOOLEAN is not enough, and the difference is not
|
|
26
|
+
* theoretical — it was measured. A boolean reset at the top of each attempt
|
|
27
|
+
* protects exactly one abandoned request and only until the next confirm, so
|
|
28
|
+
* cancel → re-arm → confirm let the FIRST request settle the control
|
|
29
|
+
* ("Reported for review" for a report the viewer withdrew from, while a second
|
|
30
|
+
* was still in flight) and, on the rejecting path, clear the shared `busy` so
|
|
31
|
+
* Confirm re-enabled mid-flight — three `onReport` calls for one row, against
|
|
32
|
+
* a `report()` that is not idempotent. An id per attempt cannot do that:
|
|
33
|
+
* anything that ends an attempt bumps it, and a superseded settle is inert.
|
|
34
|
+
*/
|
|
35
|
+
const attemptRef = useRef(0);
|
|
36
|
+
const settled = done || reported;
|
|
37
|
+
useBlocksStyles();
|
|
38
|
+
const ids = testId
|
|
39
|
+
? {
|
|
40
|
+
trigger: testId,
|
|
41
|
+
confirm: `${testId}-confirm`,
|
|
42
|
+
cancel: `${testId}-cancel`,
|
|
43
|
+
done: `${testId}-done`,
|
|
44
|
+
prompt: `${testId}-prompt`,
|
|
45
|
+
}
|
|
46
|
+
: {
|
|
47
|
+
trigger: 'report-button',
|
|
48
|
+
confirm: 'report-confirm',
|
|
49
|
+
cancel: 'report-cancel',
|
|
50
|
+
done: 'report-done',
|
|
51
|
+
prompt: 'report-confirm-prompt',
|
|
52
|
+
};
|
|
53
|
+
// 🔴 Move focus with the control, at BOTH transitions. Each step replaces the
|
|
54
|
+
// element the viewer just activated, so without this a keyboard user is
|
|
55
|
+
// dropped to <body> and must Tab from the top of the document to reach the
|
|
56
|
+
// second half of a two-step confirm.
|
|
57
|
+
//
|
|
58
|
+
// Focusing the settled note also does the announcing that `role="status"`
|
|
59
|
+
// alone cannot be relied on for here: that region is INSERTED already
|
|
60
|
+
// carrying its text, and a live region generally has to exist before its
|
|
61
|
+
// content changes to be announced. Moving focus to it is what makes the
|
|
62
|
+
// outcome reach a screen-reader user rather than hoping.
|
|
63
|
+
//
|
|
64
|
+
// 🔴 Keyed on `confirming` ALONE, deliberately. Keying it on `busy` too meant
|
|
65
|
+
// the effect re-fired when a request finished, so a viewer who pressed Confirm
|
|
66
|
+
// and then moved focus elsewhere had it YANKED BACK on rejection. `busy` is a
|
|
67
|
+
// request-lifecycle flag, not a "the control moved" transition.
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (confirming)
|
|
70
|
+
confirmRef.current?.focus();
|
|
71
|
+
}, [confirming]);
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (done)
|
|
74
|
+
doneRef.current?.focus();
|
|
75
|
+
}, [done]);
|
|
76
|
+
// 🔴 Confirm carries `loading`, which sets the native `disabled`, so the
|
|
77
|
+
// browser BLURS it the moment a request starts. Move focus to Cancel — which
|
|
78
|
+
// stays enabled precisely so the in-flight state has a live control — or the
|
|
79
|
+
// viewer is dropped to <body> with nothing tabbable, which is the failure this
|
|
80
|
+
// component's own focus handling exists to prevent.
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (busy)
|
|
83
|
+
cancelRef.current?.focus();
|
|
84
|
+
}, [busy]);
|
|
85
|
+
// 🔴 Server truth ENDS the handshake rather than hiding it. Without this the
|
|
86
|
+
// strip stays mounted underneath: a later `reported: false` resurrected a
|
|
87
|
+
// stale "Could not send" against a control the viewer had seen settle, and a
|
|
88
|
+
// rejection arriving after the flip landed behind an unmounted strip — the
|
|
89
|
+
// same dropped-error state the Cancel handling above exists to prevent.
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (reported) {
|
|
92
|
+
// 🔴 Bump the attempt too, or an in-flight request keeps writing into a
|
|
93
|
+
// control that has already settled: measured, a rejection arriving after
|
|
94
|
+
// this flip set `failed` behind the settled note, and withdrawing
|
|
95
|
+
// `reported` then surfaced "Could not send — try again?" for a report that
|
|
96
|
+
// was never submitted.
|
|
97
|
+
//
|
|
98
|
+
// 🔴 Neither `setBusy(false)` nor `setFailed(false)` is redundant here, and
|
|
99
|
+
// each went a full round with NO coverage on either tier before a case was
|
|
100
|
+
// written for it — see "a withdrawn `reported` leaves Confirm USABLE" and
|
|
101
|
+
// "does not leave a stale FAILURE behind either". Both now fail on the
|
|
102
|
+
// unit tier if their line is deleted.
|
|
103
|
+
//
|
|
104
|
+
// What they prevent, and note it surfaces on the NEXT ARM, not on the
|
|
105
|
+
// withdraw itself (this effect has already cleared `confirming`, so the
|
|
106
|
+
// viewer lands on the trigger): the attempt is superseded, so its
|
|
107
|
+
// `finally` never clears the shared `busy` and its rejection never clears
|
|
108
|
+
// `failed`. Re-arm and you get Confirm disabled with a spinner, or "Could
|
|
109
|
+
// not send" for a report never submitted in that attempt. Clearing both
|
|
110
|
+
// here is what ends the attempt completely rather than half-way.
|
|
111
|
+
attemptRef.current += 1;
|
|
112
|
+
setConfirming(false);
|
|
113
|
+
setFailed(false);
|
|
114
|
+
setBusy(false);
|
|
115
|
+
}
|
|
116
|
+
}, [reported]);
|
|
117
|
+
if (settled) {
|
|
118
|
+
return (_jsx("span", { ref: doneRef, tabIndex: -1, "data-testid": ids.done, role: "status", style: { ...NOTE_STYLE, outline: 'none' }, children: "Reported for review" }));
|
|
119
|
+
}
|
|
120
|
+
if (!confirming) {
|
|
121
|
+
return (_jsx(Button, { size: "sm", variant: "subtle", onClick: () => setConfirming(true), "data-testid": ids.trigger, "aria-label": `Report this ${noun} to moderators`, children: "Report" }));
|
|
122
|
+
}
|
|
123
|
+
const confirm = async () => {
|
|
124
|
+
const attempt = (attemptRef.current += 1);
|
|
125
|
+
/** This attempt is still the one on screen. */
|
|
126
|
+
const current = () => attemptRef.current === attempt;
|
|
127
|
+
setBusy(true);
|
|
128
|
+
setFailed(false);
|
|
129
|
+
try {
|
|
130
|
+
await onReport();
|
|
131
|
+
// 🔴 Superseded — the viewer cancelled, or the parent settled us. (A
|
|
132
|
+
// newer attempt cannot be started without one of those first, since
|
|
133
|
+
// Confirm is natively disabled while busy; the check is depth, not a
|
|
134
|
+
// reachable third case.) Settling here would report an action this viewer
|
|
135
|
+
// withdrew from.
|
|
136
|
+
if (!current())
|
|
137
|
+
return;
|
|
138
|
+
setDone(true);
|
|
139
|
+
setConfirming(false);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Same on the failure side: a rejection belonging to an abandoned or
|
|
143
|
+
// superseded attempt must not resurrect a strip, nor mark a live attempt
|
|
144
|
+
// failed.
|
|
145
|
+
if (!current())
|
|
146
|
+
return;
|
|
147
|
+
setFailed(true);
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
// 🔴 Load-bearing, not defensive. A superseded attempt clearing the shared
|
|
151
|
+
// `busy` is what re-enabled Confirm while a newer request was still
|
|
152
|
+
// running, which is how one row got filed three times.
|
|
153
|
+
if (current())
|
|
154
|
+
setBusy(false);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
return (_jsxs(Group, { gap: 6, align: "center", wrap: false, "data-testid": ids.prompt, children: [_jsx("span", { style: NOTE_STYLE, ...(failed ? { role: 'alert' } : {}), children: failed ? 'Could not send — try again?' : `Send this ${noun} to moderators for review?` }), _jsx(Button, { ref: confirmRef, size: "sm", loading: busy, onClick: confirm, "data-testid": ids.confirm, "aria-label": `Confirm reporting this ${noun} to moderators`, children: "Report" }), _jsx(Button, { ref: cancelRef, size: "sm", variant: "subtle",
|
|
158
|
+
// 🔴 DELIBERATELY NOT disabled in flight. It was, briefly, to stop a late
|
|
159
|
+
// resolve settling a cancelled report — but `onReport` here is
|
|
160
|
+
// `shared.report()` over a postMessage bridge with no timeout, so a
|
|
161
|
+
// reply that never arrives left BOTH buttons disabled and the control
|
|
162
|
+
// wedged with no way back short of a remount. Trading a rare race for a
|
|
163
|
+
// permanent dead end is the wrong side of that trade; the attempt token
|
|
164
|
+
// closes the race without taking the escape hatch away.
|
|
165
|
+
onClick: () => {
|
|
166
|
+
attemptRef.current += 1;
|
|
167
|
+
setBusy(false);
|
|
168
|
+
setFailed(false);
|
|
169
|
+
setConfirming(false);
|
|
170
|
+
}, "data-testid": ids.cancel, "aria-label": "Cancel the report", children: "Cancel" })] }));
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=ReportButton.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReportButton.js","sourceRoot":"","sources":["../../src/ui/ReportButton.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEpD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,iEAAiE;AACjE,MAAM,UAAU,GAAG;IACjB,UAAU,EAAE,QAAQ;IACpB,QAAQ,EAAE,EAAE;IACZ,UAAU,EAAE,IAAI;IAChB,KAAK,EAAE,kCAAkC;CACjC,CAAC;AA6FX,MAAM,UAAU,YAAY,CAAC,EAC3B,IAAI,EACJ,QAAQ,EACR,QAAQ,GAAG,KAAK,EAChB,aAAa,EAAE,MAAM,GACH;IAClB,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IACpD,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IACxC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IACxC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC5C,MAAM,UAAU,GAAG,MAAM,CAAoB,IAAI,CAAC,CAAC;IACnD,MAAM,SAAS,GAAG,MAAM,CAAoB,IAAI,CAAC,CAAC;IAClD,MAAM,OAAO,GAAG,MAAM,CAAkB,IAAI,CAAC,CAAC;IAC9C;;;;;;;;;;;;;OAaG;IACH,MAAM,UAAU,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IAC7B,MAAM,OAAO,GAAG,IAAI,IAAI,QAAQ,CAAC;IACjC,eAAe,EAAE,CAAC;IAElB,MAAM,GAAG,GAAG,MAAM;QAChB,CAAC,CAAC;YACE,OAAO,EAAE,MAAM;YACf,OAAO,EAAE,GAAG,MAAM,UAAU;YAC5B,MAAM,EAAE,GAAG,MAAM,SAAS;YAC1B,IAAI,EAAE,GAAG,MAAM,OAAO;YACtB,MAAM,EAAE,GAAG,MAAM,SAAS;SAC3B;QACH,CAAC,CAAC;YACE,OAAO,EAAE,eAAe;YACxB,OAAO,EAAE,gBAAgB;YACzB,MAAM,EAAE,eAAe;YACvB,IAAI,EAAE,aAAa;YACnB,MAAM,EAAE,uBAAuB;SAChC,CAAC;IAEN,8EAA8E;IAC9E,wEAAwE;IACxE,2EAA2E;IAC3E,qCAAqC;IACrC,EAAE;IACF,0EAA0E;IAC1E,sEAAsE;IACtE,yEAAyE;IACzE,wEAAwE;IACxE,yDAAyD;IACzD,EAAE;IACF,8EAA8E;IAC9E,+EAA+E;IAC/E,8EAA8E;IAC9E,gEAAgE;IAChE,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,UAAU;YAAE,UAAU,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC;IAC9C,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;IACjB,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,IAAI;YAAE,OAAO,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC;IACrC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IACX,yEAAyE;IACzE,6EAA6E;IAC7E,6EAA6E;IAC7E,+EAA+E;IAC/E,oDAAoD;IACpD,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,IAAI;YAAE,SAAS,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC;IACvC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IACX,6EAA6E;IAC7E,0EAA0E;IAC1E,6EAA6E;IAC7E,2EAA2E;IAC3E,wEAAwE;IACxE,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,QAAQ,EAAE,CAAC;YACb,wEAAwE;YACxE,yEAAyE;YACzE,kEAAkE;YAClE,2EAA2E;YAC3E,uBAAuB;YACvB,EAAE;YACF,4EAA4E;YAC5E,2EAA2E;YAC3E,0EAA0E;YAC1E,uEAAuE;YACvE,sCAAsC;YACtC,EAAE;YACF,sEAAsE;YACtE,wEAAwE;YACxE,kEAAkE;YAClE,0EAA0E;YAC1E,0EAA0E;YAC1E,wEAAwE;YACxE,iEAAiE;YACjE,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;YACxB,aAAa,CAAC,KAAK,CAAC,CAAC;YACrB,SAAS,CAAC,KAAK,CAAC,CAAC;YACjB,OAAO,CAAC,KAAK,CAAC,CAAC;QACjB,CAAC;IACH,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;IAEf,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO,CACL,eACE,GAAG,EAAE,OAAO,EACZ,QAAQ,EAAE,CAAC,CAAC,iBACC,GAAG,CAAC,IAAI,EACrB,IAAI,EAAC,QAAQ,EACb,KAAK,EAAE,EAAE,GAAG,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,oCAGpC,CACR,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,CACL,KAAC,MAAM,IACL,IAAI,EAAC,IAAI,EACT,OAAO,EAAC,QAAQ,EAChB,OAAO,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,iBACrB,GAAG,CAAC,OAAO,gBACZ,eAAe,IAAI,gBAAgB,uBAGxC,CACV,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,KAAK,IAAI,EAAE;QACzB,MAAM,OAAO,GAAG,CAAC,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC;QAC1C,+CAA+C;QAC/C,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,UAAU,CAAC,OAAO,KAAK,OAAO,CAAC;QACrD,OAAO,CAAC,IAAI,CAAC,CAAC;QACd,SAAS,CAAC,KAAK,CAAC,CAAC;QACjB,IAAI,CAAC;YACH,MAAM,QAAQ,EAAE,CAAC;YACjB,qEAAqE;YACrE,oEAAoE;YACpE,qEAAqE;YACrE,0EAA0E;YAC1E,iBAAiB;YACjB,IAAI,CAAC,OAAO,EAAE;gBAAE,OAAO;YACvB,OAAO,CAAC,IAAI,CAAC,CAAC;YACd,aAAa,CAAC,KAAK,CAAC,CAAC;QACvB,CAAC;QAAC,MAAM,CAAC;YACP,qEAAqE;YACrE,yEAAyE;YACzE,UAAU;YACV,IAAI,CAAC,OAAO,EAAE;gBAAE,OAAO;YACvB,SAAS,CAAC,IAAI,CAAC,CAAC;QAClB,CAAC;gBAAS,CAAC;YACT,2EAA2E;YAC3E,oEAAoE;YACpE,uDAAuD;YACvD,IAAI,OAAO,EAAE;gBAAE,OAAO,CAAC,KAAK,CAAC,CAAC;QAChC,CAAC;IACH,CAAC,CAAC;IAEF,OAAO,CACL,MAAC,KAAK,IAAC,GAAG,EAAE,CAAC,EAAE,KAAK,EAAC,QAAQ,EAAC,IAAI,EAAE,KAAK,iBAAe,GAAG,CAAC,MAAM,aAMhE,eAAM,KAAK,EAAE,UAAU,KAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAgB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,YACpE,MAAM,CAAC,CAAC,CAAC,6BAA6B,CAAC,CAAC,CAAC,aAAa,IAAI,4BAA4B,GAClF,EACP,KAAC,MAAM,IACL,GAAG,EAAE,UAAU,EACf,IAAI,EAAC,IAAI,EACT,OAAO,EAAE,IAAI,EACb,OAAO,EAAE,OAAO,iBACH,GAAG,CAAC,OAAO,gBACZ,0BAA0B,IAAI,gBAAgB,uBAGnD,EACT,KAAC,MAAM,IACL,GAAG,EAAE,SAAS,EACd,IAAI,EAAC,IAAI,EACT,OAAO,EAAC,QAAQ;gBAChB,0EAA0E;gBAC1E,+DAA+D;gBAC/D,oEAAoE;gBACpE,sEAAsE;gBACtE,wEAAwE;gBACxE,wEAAwE;gBACxE,wDAAwD;gBACxD,OAAO,EAAE,GAAG,EAAE;oBACZ,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;oBACxB,OAAO,CAAC,KAAK,CAAC,CAAC;oBACf,SAAS,CAAC,KAAK,CAAC,CAAC;oBACjB,aAAa,CAAC,KAAK,CAAC,CAAC;gBACvB,CAAC,iBACY,GAAG,CAAC,MAAM,gBACZ,mBAAmB,uBAGvB,IACH,CACT,CAAC;AACJ,CAAC"}
|
|
@@ -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"}
|