@civitai/blocks-react 0.73.2 → 0.74.1

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 CHANGED
@@ -3013,11 +3013,13 @@ from `@civitai/app-sdk/blocks` is the same thing without the hook.
3013
3013
  ## The `/ui` subexport
3014
3014
 
3015
3015
  Opinionated components, imported separately so a transport-only block stays lean.
3016
- Two surfaces live here:
3016
+ Three surfaces live here:
3017
3017
 
3018
3018
  1. The **W6 component pack** — a small, Civitai-looking, self-styled component
3019
3019
  set you drop straight into a block.
3020
- 2. The headless, manifest-driven **`SettingsForm`** (host-themed native controls).
3020
+ 2. **`WorkflowPicker`** — lets the viewer pick a posted workflow, theirs or
3021
+ anyone's, from the Civitai catalog.
3022
+ 3. The headless, manifest-driven **`SettingsForm`** (host-themed native controls).
3021
3023
 
3022
3024
  ### W6 component pack
3023
3025
 
@@ -3109,6 +3111,92 @@ to inject the CSS yourself (SSR, or a non-React shell)? Call
3109
3111
  > inside the sandboxed block; a full focus-trap is a v1 follow-up (kept
3110
3112
  > dependency-free here on purpose).
3111
3113
 
3114
+ ### `WorkflowPicker`
3115
+
3116
+ `<civitai-resource-picker>` from `@civitai/components`, set up for workflows: it
3117
+ lists `Workflows` and the retired `ComfyWorkflows` type (so older posts can be
3118
+ picked), lets the viewer switch between **their own** published workflows and
3119
+ **anyone's**, and picks one **version** (the newest by default). It calls
3120
+ `GET /api/v1/blocks/models` at `useHostOrigin()` with `useBlockToken().raw` —
3121
+ no host bridge, no extra scope.
3122
+
3123
+ ```tsx
3124
+ import { useState } from 'react';
3125
+ import { WorkflowPicker, type PickedResource } from '@civitai/blocks-react/ui';
3126
+
3127
+ export function ChooseWorkflow() {
3128
+ const [open, setOpen] = useState(false);
3129
+ const [picked, setPicked] = useState<PickedResource | null>(null);
3130
+ return (
3131
+ <>
3132
+ <button type="button" onClick={() => setOpen(true)}>Use a workflow</button>
3133
+ <WorkflowPicker
3134
+ open={open}
3135
+ owner="mine"
3136
+ onPick={(workflow) => {
3137
+ setPicked(workflow);
3138
+ setOpen(false);
3139
+ }}
3140
+ onDismiss={() => setOpen(false)}
3141
+ />
3142
+ {picked && <p>{picked.name} — {picked.versionName}</p>}
3143
+ </>
3144
+ );
3145
+ }
3146
+ ```
3147
+
3148
+ A pick is a `PickedResource` — `{ modelId, modelVersionId, name, versionName,
3149
+ creatorUsername, baseModel, earlyAccessDeadline?, access, owner }`; `/ui`
3150
+ re-exports it and the picker's other data types (`ResourceItem`,
3151
+ `ResourceSource`, …). Close the picker yourself in
3152
+ **both** callbacks: `open` is controlled. The picker closes itself on Escape or
3153
+ a backdrop click, and re-opens on the next render if `open` still says `true`.
3154
+
3155
+ - 🔴 **`owner` is a UX hint, never proof of ownership.** It says which list the
3156
+ viewer was looking at, and your app's own code can set it to anything. A
3157
+ server that must know "this is the viewer's own workflow" checks that itself.
3158
+ - 🔴 **A "mine" list is only ever the server's.** The picker shows an error, not
3159
+ a list, when a `mine` search comes back without `metadata.owner: 'mine'` — a
3160
+ server older than the parameter ignores it and returns everybody's. It never
3161
+ falls back to the public `/api/v1/models`.
3162
+ - **Early-access and paid versions are listed, with a badge.** `access` on a
3163
+ pick is `'early-access'` (a timed paid-access gate, with its end date in
3164
+ `earlyAccessDeadline` when the server knows it), `'paid'` (a permanent gate)
3165
+ or `'open'`. The viewer may not be able to download a gated version later.
3166
+ - **Search is debounced** (300 ms), and a newer search cancels the one in
3167
+ flight. A busy catalog (`429` / `503`, rate limit 150 requests per 10 s per
3168
+ block) is retried after its `Retry-After`, twice, then offered as "Try again".
3169
+ - **An anonymous viewer** gets a sign-in message on the "mine" list as soon as
3170
+ the catalog answers `401`, with no `REQUEST_TOKEN` to the host first — the
3171
+ picker knows the viewer is anonymous from `useBlockContext()`. "All" works. An expired block token is not mistaken for
3172
+ that: for a signed-in viewer, or on "All", a `401` makes the picker mint a
3173
+ fresh token through `useBlockToken().refresh()` and retry once.
3174
+ - **Before `BLOCK_INIT`** the picker says it is not connected yet, and lists as
3175
+ soon as the host origin lands.
3176
+ - **Under the mock host** there is no catalog to call. Pass canned results from
3177
+ `/testing`:
3178
+
3179
+ ```tsx
3180
+ import { WorkflowPicker } from '@civitai/blocks-react/ui';
3181
+ import { fakeResourceSource } from '@civitai/blocks-react/testing';
3182
+
3183
+ const source = fakeResourceSource({
3184
+ mine: [
3185
+ {
3186
+ modelId: 1,
3187
+ name: 'My upscale chain',
3188
+ type: 'Workflows',
3189
+ creatorUsername: 'me',
3190
+ versions: [{ id: 11, name: 'v2', baseModel: 'SDXL 1.0' }],
3191
+ },
3192
+ ],
3193
+ });
3194
+
3195
+ export const Picker = () => <WorkflowPicker open source={source} onPick={console.log} />;
3196
+ ```
3197
+
3198
+ Under `dev:live` it works for real: the dev token can read `/blocks/models`.
3199
+
3112
3200
  ### `SettingsForm`
3113
3201
 
3114
3202
  The headless, manifest-driven settings form (its contract is intentionally
@@ -3202,6 +3290,7 @@ what went stale in [#334](https://github.com/civitai/civitai-app-starters/issues
3202
3290
  | `createMockHost` | A framework-agnostic fake of the embedding host — answers every `*_RESULT` message, with knobs for generation cost/latency/failure, Buzz balance, app + shared storage, consent, maturity. Returns a `MockHost`; call `.install()` and keep the returned teardown. **No network, no Buzz.** |
3203
3291
  | `readMockHostUrlOptions` | Reads the harness URL toggles (`?viewer` `?consent` `?fail` `?theme` `?pick` `?balance` `?latency` `?seed` …) into a `Partial<MockHostOptions>`. `Harness` applies it for you; call it directly only in a hand-rolled harness. |
3204
3292
  | `Harness` | The React wrapper: installs a `createMockHost` on mount, tears it down on unmount, and renders an optional on-screen outbound-message log. Takes every `MockHostOptions` field plus `applyUrlToggles` and `showLog`. |
3293
+ | `fakeResourceSource` | Canned results for `<WorkflowPicker>` (from `/ui`) or `<civitai-resource-picker>`: `fakeResourceSource({ mine, any })` filters by owner, type and query, echoes the owner it applied, and records each search on `.calls`. The mock host has no catalog, so pass it as the picker's `source`. **No network.** |
3205
3294
  | `installPopoverShim` | Stands in for the HTML popover API, which neither `jsdom` nor `happy-dom` implements at any version. Needed only if your own code calls `showPopover`/`hidePopover`/`togglePopover` or queries `:popover-open` — `@civitai/components`' own elements do not. Returns a `PopoverShimHandle`; inert (`installed: false`) in a real browser. **Read [Testing overlay elements](#testing-overlay-elements) first: it does not make trigger clicks work.** |
3206
3295
 
3207
3296
  <!-- TESTING-SURFACE:VALUES:END -->
@@ -3216,6 +3305,8 @@ type.
3216
3305
  ```text
3217
3306
  CannedPick
3218
3307
  CostSpec
3308
+ FakeResourceCanned
3309
+ FakeResourceSource
3219
3310
  HarnessProps
3220
3311
  ImageSpec
3221
3312
  MockBuzzBalance
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `fakeResourceSource` — canned results for `<WorkflowPicker>` /
3
+ * `<civitai-resource-picker>` where there is no catalog to call: the mock host,
4
+ * a unit test, a story. No network.
5
+ *
6
+ * It answers the way the real endpoint does where the picker can tell:
7
+ * it filters by `owner`, `types` and `query`, and echoes the `owner` it applied.
8
+ */
9
+ import type { ResourceItem, ResourceSearchParams, ResourceSource } from '@civitai/components/civitai-resource-picker';
10
+ /** What a {@link fakeResourceSource} lists. */
11
+ export interface FakeResourceCanned {
12
+ /** The viewer's own resources: what `owner=mine` lists. */
13
+ mine?: readonly ResourceItem[];
14
+ /**
15
+ * Everybody's: what `owner=any` lists. Defaults to `mine` plus nothing else,
16
+ * so a fake with only `mine` still shows them under "All".
17
+ */
18
+ any?: readonly ResourceItem[];
19
+ }
20
+ /** A {@link ResourceSource} that also records what it was asked. */
21
+ export interface FakeResourceSource extends ResourceSource {
22
+ /** Every search, oldest first. */
23
+ readonly calls: readonly ResourceSearchParams[];
24
+ }
25
+ /** Canned picker results. See {@link FakeResourceCanned}. */
26
+ export declare function fakeResourceSource(canned?: FakeResourceCanned): FakeResourceSource;
27
+ //# sourceMappingURL=fakeResourceSource.d.ts.map
@@ -0,0 +1,21 @@
1
+ /** Canned picker results. See {@link FakeResourceCanned}. */
2
+ export function fakeResourceSource(canned = {}) {
3
+ const calls = [];
4
+ const lists = {
5
+ mine: canned.mine ?? [],
6
+ any: canned.any ?? canned.mine ?? [],
7
+ };
8
+ return {
9
+ calls,
10
+ async search(params, signal) {
11
+ calls.push(params);
12
+ if (signal.aborted)
13
+ throw signal.reason ?? new DOMException('Aborted', 'AbortError');
14
+ const query = params.query?.trim().toLowerCase() ?? '';
15
+ const items = lists[params.owner].filter((item) => (params.types.length === 0 || params.types.includes(item.type)) &&
16
+ (query === '' || item.name.toLowerCase().includes(query)));
17
+ return { items: [...items], nextCursor: null, owner: params.owner };
18
+ },
19
+ };
20
+ }
21
+ //# sourceMappingURL=fakeResourceSource.js.map
package/dist/testing.d.ts CHANGED
@@ -22,6 +22,7 @@ import { type MockHostOptions } from './internal/mockHost.js';
22
22
  export { __resetTransport as resetTransport };
23
23
  export { installPopoverShim, type PopoverShimHandle, type PopoverShimOptions, } from './internal/popoverShim.js';
24
24
  export { createMockHost, readMockHostUrlOptions, type MockHost, type MockHostOptions, type MockHostFailMode, type MockHostScenarioPatch, type MockGenerationScenario, type MockBuzzScenario, type MockBuzzBalance, type MockBuzzHandle, type MockStorageScenario, type MockSharedScenario, type MockSharedSeed, type MockCannedImageScan, type MockWorkflowStepInfo, type MockWorkflowQuoteFailure, type CostSpec, type ImageSpec, type CannedPick, } from './internal/mockHost.js';
25
+ export { fakeResourceSource, type FakeResourceCanned, type FakeResourceSource, } from './internal/fakeResourceSource.js';
25
26
  /**
26
27
  * Props for the dev {@link Harness}.
27
28
  */
package/dist/testing.js CHANGED
@@ -23,6 +23,7 @@ import { createMockHost, readMockHostUrlOptions, } from './internal/mockHost.js'
23
23
  export { __resetTransport as resetTransport };
24
24
  export { installPopoverShim, } from './internal/popoverShim.js';
25
25
  export { createMockHost, readMockHostUrlOptions, } from './internal/mockHost.js';
26
+ export { fakeResourceSource, } from './internal/fakeResourceSource.js';
26
27
  /**
27
28
  * What the harness chrome's `consent=` readout says, from the TWO INDEPENDENT
28
29
  * booleans that describe consent in the mock host.
@@ -0,0 +1,37 @@
1
+ import { type PickedResource, type ResourceOwner, type ResourceSource } from '@civitai/components/civitai-resource-picker';
2
+ /** The two model types a posted workflow can have; `ComfyWorkflows` is the retired one. */
3
+ export declare const WORKFLOW_PICKER_TYPES = "Workflows ComfyWorkflows";
4
+ export interface WorkflowPickerProps {
5
+ /**
6
+ * Shows the picker. Controlled: set it back to `false` in both callbacks. The
7
+ * picker closes itself on Escape or a backdrop click, and re-opens on the
8
+ * next render if this still says `true`.
9
+ */
10
+ open: boolean;
11
+ /**
12
+ * The viewer picked a version. 🔴 `picked.owner` is a UX hint, never proof the
13
+ * workflow is the viewer's own — check that server-side.
14
+ */
15
+ onPick: (picked: PickedResource) => void;
16
+ /** The viewer closed the picker without picking. */
17
+ onDismiss?: () => void;
18
+ /** Which list shows first. Defaults to `'any'`. */
19
+ owner?: ResourceOwner;
20
+ /** Defaults to `'Choose a workflow'`. */
21
+ heading?: string;
22
+ /**
23
+ * Where results come from. Defaults to the App Blocks catalog
24
+ * (`/api/v1/blocks/models`) through this block's host origin and token. Pass
25
+ * `fakeResourceSource(...)` from `@civitai/blocks-react/testing` under the
26
+ * mock host, which has no catalog to call.
27
+ */
28
+ source?: ResourceSource;
29
+ }
30
+ /**
31
+ * A workflow picker for a block: `<civitai-resource-picker>` listing
32
+ * `Workflows` and `ComfyWorkflows`, wired to {@link useHostOrigin} and
33
+ * {@link useBlockToken} so the viewer can pick one of their own posted
34
+ * workflows, or anyone's.
35
+ */
36
+ export declare function WorkflowPicker({ open, onPick, onDismiss, owner, heading, source, }: WorkflowPickerProps): React.JSX.Element;
37
+ //# sourceMappingURL=WorkflowPicker.d.ts.map
@@ -0,0 +1,92 @@
1
+ import { createElement, useEffect, useLayoutEffect, useMemo, useRef } from 'react';
2
+ import { blockTokenSource, defineCivitaiResourcePicker, ResourceSourceError, } from '@civitai/components/civitai-resource-picker';
3
+ import { isSignedIn } from '@civitai/app-sdk/blocks';
4
+ import { useBlockContext } from '../hooks/useBlockContext.js';
5
+ import { useBlockToken } from '../hooks/useBlockToken.js';
6
+ import { useHostOrigin } from '../hooks/useHostOrigin.js';
7
+ /** The two model types a posted workflow can have; `ComfyWorkflows` is the retired one. */
8
+ export const WORKFLOW_PICKER_TYPES = 'Workflows ComfyWorkflows';
9
+ /**
10
+ * Answers every search while the host origin is unknown: an error the picker
11
+ * shows with a retry, never a silent wait. The picker searches again on its own
12
+ * once the real source replaces this one.
13
+ */
14
+ const NOT_CONNECTED = {
15
+ search: async () => {
16
+ throw new ResourceSourceError('failed', 'Not connected to Civitai yet.');
17
+ },
18
+ };
19
+ /**
20
+ * A workflow picker for a block: `<civitai-resource-picker>` listing
21
+ * `Workflows` and `ComfyWorkflows`, wired to {@link useHostOrigin} and
22
+ * {@link useBlockToken} so the viewer can pick one of their own posted
23
+ * workflows, or anyone's.
24
+ */
25
+ export function WorkflowPicker({ open, onPick, onDismiss, owner = 'any', heading = 'Choose a workflow', source, }) {
26
+ defineCivitaiResourcePicker();
27
+ const host = useHostOrigin();
28
+ const { raw, refresh } = useBlockToken();
29
+ const { ready, viewer } = useBlockContext();
30
+ // Read at each request, so a refreshed token is the one sent.
31
+ const token = useRef({ raw, refresh });
32
+ token.current = { raw, refresh };
33
+ // Defensive, and unreachable today: the live source exists only once
34
+ // `useHostOrigin()` is set, which happens at BLOCK_INIT — the same moment
35
+ // `ready` flips — so no search reads the `undefined` arm and no test covers
36
+ // it. It is here so that a pre-init `viewer: null` (also null) can never be
37
+ // read as anonymous if that ordering changes.
38
+ const signedIn = useRef(undefined);
39
+ signedIn.current = ready ? isSignedIn(viewer) : undefined;
40
+ const live = useMemo(() => host
41
+ ? blockTokenSource({
42
+ origin: host,
43
+ // After a 401: mint a new token (an expired one is routine in a
44
+ // tab that slept). A failed mint returns the old token, which ends
45
+ // the search as `unauthorized`.
46
+ getToken: (options) => options?.fresh
47
+ ? token.current.refresh().then((fresh) => fresh.raw, () => token.current.raw)
48
+ : token.current.raw,
49
+ // An anonymous viewer's `mine` 401 is "sign in", not "expired":
50
+ // shown at once, without a REQUEST_TOKEN round trip to the host
51
+ // (which would only hand back the same anonymous token).
52
+ signedIn: () => signedIn.current,
53
+ })
54
+ : NOT_CONNECTED, [host]);
55
+ const ref = useRef(null);
56
+ const handlers = useRef({ onPick, onDismiss });
57
+ handlers.current = { onPick, onDismiss };
58
+ // Properties, not attributes: React 18 would stringify the source.
59
+ useLayoutEffect(() => {
60
+ if (ref.current)
61
+ ref.current.source = source ?? live;
62
+ }, [source, live]);
63
+ // No dependency list on purpose: the element closes itself on Escape or a
64
+ // backdrop click, so it can disagree with `open` without the prop changing.
65
+ // Every render puts it back in line, which is what lets a consumer that
66
+ // never handled `onDismiss` open it again.
67
+ useLayoutEffect(() => {
68
+ if (ref.current && ref.current.open !== open)
69
+ ref.current.open = open;
70
+ });
71
+ useEffect(() => {
72
+ const el = ref.current;
73
+ if (!el)
74
+ return;
75
+ const pick = (event) => handlers.current.onPick(event.detail);
76
+ const dismiss = () => handlers.current.onDismiss?.();
77
+ el.addEventListener('pick', pick);
78
+ el.addEventListener('dismiss', dismiss);
79
+ return () => {
80
+ el.removeEventListener('pick', pick);
81
+ el.removeEventListener('dismiss', dismiss);
82
+ };
83
+ }, []);
84
+ return createElement('civitai-resource-picker', {
85
+ ref,
86
+ types: WORKFLOW_PICKER_TYPES,
87
+ noun: 'workflow',
88
+ owner,
89
+ heading,
90
+ });
91
+ }
92
+ //# sourceMappingURL=WorkflowPicker.js.map
@@ -58,4 +58,7 @@ export { Collapse } from './Collapse.js';
58
58
  export type { CollapseProps } from './Collapse.js';
59
59
  export { SegmentedControl } from './SegmentedControl.js';
60
60
  export type { SegmentedControlProps, SegmentedControlItem, SegmentedControlSize, } from './SegmentedControl.js';
61
+ export { WorkflowPicker, WORKFLOW_PICKER_TYPES } from './WorkflowPicker.js';
62
+ export type { WorkflowPickerProps } from './WorkflowPicker.js';
63
+ export type { PickedResource, ResourceAccess, ResourceItem, ResourceOwner, ResourcePage, ResourceSearchParams, ResourceSource, ResourceVersion, } from '@civitai/components/civitai-resource-picker';
61
64
  //# sourceMappingURL=index.d.ts.map
package/dist/ui/index.js CHANGED
@@ -43,4 +43,5 @@ export { NumberInput } from './NumberInput.js';
43
43
  export { Select } from './Select.js';
44
44
  export { Collapse } from './Collapse.js';
45
45
  export { SegmentedControl } from './SegmentedControl.js';
46
+ export { WorkflowPicker, WORKFLOW_PICKER_TYPES } from './WorkflowPicker.js';
46
47
  //# sourceMappingURL=index.js.map
@@ -1,3 +1,3 @@
1
1
  /** This package's version, reported to the host as `sdkVersion` in `BLOCK_READY`. */
2
- export declare const VERSION = "0.73.2";
2
+ export declare const VERSION = "0.74.1";
3
3
  //# sourceMappingURL=version.generated.d.ts.map
@@ -1,4 +1,4 @@
1
1
  /* AUTOGENERATED from package.json by scripts/gen-version.mjs. DO NOT EDIT. */
2
2
  /** This package's version, reported to the host as `sdkVersion` in `BLOCK_READY`. */
3
- export const VERSION = "0.73.2";
3
+ export const VERSION = "0.74.1";
4
4
  //# sourceMappingURL=version.generated.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civitai/blocks-react",
3
- "version": "0.73.2",
3
+ "version": "0.74.1",
4
4
  "description": "React hooks and iframe transport for Civitai Apps. Pairs with @civitai/app-sdk/blocks.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,7 +37,7 @@
37
37
  "node": ">=20"
38
38
  },
39
39
  "dependencies": {
40
- "@civitai/components": "^0.10.0",
40
+ "@civitai/components": "^0.12.0",
41
41
  "@civitai/theme": "^0.5.2"
42
42
  },
43
43
  "comment-peerDependencies": "This floor is DERIVED, not chosen — the lowest published app-sdk that exports every symbol this package imports. Do not edit it without reading ./PEER_FLOOR.md (in-repo, unpublished) and tests/guards/blocks-react-peer-floor.test.mjs.",