@civitai/blocks-react 0.46.0 → 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 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
- Each component forwards `className` + `style`, forwards a `ref` to its DOM
927
- node (where it wraps one), and carries a `data-civitai-ui="<name>"` hook. Need
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,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":";AAAA,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAI7C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,GAAqC;IACpD,UAAU,EAAE,YAAY;IACxB,IAAI,EAAE,MAAM;IACZ,KAAK,EAAE,MAAM;IACb,OAAO,EAAE,MAAM;IACf,IAAI,EAAE,MAAM;CACb,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,kBAAkB,GAAG,YAAY,CAAC;AAExC;;;;;GAKG;AACH,MAAM,YAAY,GAAG,kBAAkB,CAAC;AAExC;;;;;;;;;;;;;GAaG;AACH,MAAM,aAAa,GAAG,GAAG,CAAC;AAE1B;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,mBAAmB,CAAC,QAA2B;IAC7D,MAAM,IAAI,GAAG,OAAO,QAAQ,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACrF,IAAI,IAAI,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAC7B,OAAO,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC;AACvF,CAAC;AA2MD;;;;;;;;;;;;;GAaG;AACH,SAAS,cAAc,CAAC,QAA2B,EAAE,SAAiB;IACpE,MAAM,OAAO,GAAG,OAAO,QAAQ,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC5F,MAAM,IAAI,GAAG,OAAO,QAAQ,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACrF,OAAO,CAAC,mBAAmB,CAAC,QAAQ,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC;SAC7D,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC;SAC7B,IAAI,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC;AAED;;;;;;GAMG;AACH,SAAS,WAAW,CAAC,QAA2B;IAC9C,MAAM,GAAG,GAAG,OAAO,QAAQ,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACpF,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO,EAAE,CAAC;IAC1B,OAAO,WAAW,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,IAAI,GAAG,CAAC;AAC/C,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,UAAU,CAAoC,SAAS,YAAY,CAC7F,KAAK,EACL,GAAG;IAEH,MAAM,EACJ,QAAQ,EACR,OAAO,EACP,YAAY,EACZ,OAAO,EACP,SAAS,EACT,KAAK,EACL,aAAa,EAAE,MAAM,GACtB,GAAG,KAAK,CAAC;IACV,oEAAoE;IACpE,uDAAuD;IACvD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;IACrE,MAAM,WAAW,GAAG,KAAK,CAAC,WAAW,KAAK,IAAI,CAAC;IAC/C,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;IAC/D,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;IAC/D,2EAA2E;IAC3E,6EAA6E;IAC7E,yEAAyE;IACzE,uEAAuE;IACvE,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAC;IAChE,eAAe,EAAE,CAAC;IAElB,MAAM,EAAE,GAAG,MAAM,IAAI,eAAe,CAAC;IACrC,MAAM,GAAG,GAAG;QACV,GAAG,EAAE,GAAG,EAAE,MAAM;QAChB,KAAK,EAAE,GAAG,EAAE,QAAQ;QACpB,KAAK,EAAE,GAAG,EAAE,QAAQ;QACpB,WAAW,EAAE,GAAG,EAAE,cAAc;QAChC,OAAO,EAAE,GAAG,EAAE,UAAU;QACxB,IAAI,EAAE,GAAG,EAAE,OAAO;QAClB,QAAQ,EAAE,GAAG,EAAE,WAAW;QAC1B,IAAI,EAAE,GAAG,EAAE,OAAO;QAClB,IAAI,EAAE,GAAG,EAAE,OAAO;QAClB,OAAO,EAAE,GAAG,EAAE,UAAU;KACzB,CAAC;IAEF,MAAM,IAAI,GAAG,mBAAmB,CAAC,QAAQ,CAAC,CAAC;IAC3C,MAAM,SAAS,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;IACxC,MAAM,WAAW,GACf,OAAO,QAAQ,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9E,MAAM,SAAS,GAAG,OAAO,QAAQ,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1F,MAAM,SAAS,GAAG,YAAY,IAAI,IAAI,IAAI,SAAS,KAAK,YAAY,CAAC;IACrE,MAAM,QAAQ,GAAG,OAAO,KAAK,MAAM,IAAI,YAAY,IAAI,IAAI,CAAC;IAE5D,6EAA6E;IAC7E,8EAA8E;IAC9E,2EAA2E;IAC3E,0DAA0D;IAC1D,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,CACvB,iDAAqC,EAAE,iBAAc,GAAG,CAAC,KAAK,YAC3D,SAAS,CAAC,CAAC,CAAC;QACX,iEAAiE;QACjE,qEAAqE;QACrE,+DAA+D;QAC/D,cACE,GAAG,EAAE,YAAY,EACjB,GAAG,EAAC,EAAE,iBACM,MAAM;YAClB,oEAAoE;YACpE,qEAAqE;YACrE,qDAAqD;YACrD,4CAA4C;YAC5C,OAAO,EAAC,MAAM,iBACD,GAAG,CAAC,KAAK;YACtB,sEAAsE;YACtE,mEAAmE;YACnE,sDAAsD;YACtD,OAAO,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,YAAY,CAAC,GACzC,CACH,CAAC,CAAC,CAAC,CACF,uDAA2C,EAAE,iBAAc,GAAG,CAAC,WAAW,YACvE,kBAAkB,GACd,CACR,GACI,CACR,CAAC,CAAC,CAAC,IAAI,CAAC;IAET,MAAM,IAAI,GAAG,CACX,8BACG,KAAK,EACN,iDAAoC,EAAE,aACpC,qDAAwC,EAAE,aACvC,QAAQ,CAAC,CAAC,CAAC,CACV,oDACoC,EAAE,iBACvB,GAAG,CAAC,QAAQ,iBACb,MAAM,YAEjB,aAAa,GACT,CACR,CAAC,CAAC,CAAC,IAAI,EACR,gDAAoC,EAAE,iBAAc,GAAG,CAAC,IAAI,YACzD,IAAI,GACA,IACF,EACP,iDAAoC,EAAE,iBAAc,GAAG,CAAC,IAAI,aACzD,SAAS,KAAK,EAAE,CAAC,CAAC,CAAC,CAClB,KAAC,KAAK,IAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,SAAS,iBAAc,GAAG,CAAC,IAAI,YACrD,SAAS,GACJ,CACT,CAAC,CAAC,CAAC,IAAI,EACP,WAAW,KAAK,EAAE,CAAC,CAAC,CAAC,yBAAO,WAAW,GAAQ,CAAC,CAAC,CAAC,IAAI,EACtD,SAAS,KAAK,EAAE,CAAC,CAAC,CAAC,yBAAO,SAAS,GAAQ,CAAC,CAAC,CAAC,IAAI,IAC9C,IACF,IACN,CACJ,CAAC;IAEF,OAAO,CACL,eACE,GAAG,EAAE,GAAG,EAER,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,KAAK,qBACI,eAAe,kBACjB,OAAO,sBACH,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,mBACnC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,mBAC7B,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,iBAC/B,EAAE,aAEd,WAAW,CAAC,CAAC,CAAC,CACb,iBACE,IAAI,EAAC,QAAQ,kCACgB,EAAE,iBAClB,GAAG,CAAC,GAAG,kBACN,QAAQ,EACtB,QAAQ,EAAE,QAAQ,gBACN,cAAc,CAAC,QAAQ,EAAE,SAAS,CAAC,EAC/C,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,QAAQ,EAAE,YAE9B,IAAI,GACE,CACV,CAAC,CAAC,CAAC;YACF,uEAAuE;YACvE,qEAAqE;YACrE,wEAAwE;YACxE,wEAAwE;YACxE,gDAAgD;YAChD,8CAAkC,EAAE,iBAAc,GAAG,CAAC,GAAG,YACtD,IAAI,GACD,CACP,EAQA,OAAO,IAAI,IAAI,IAAI,OAAO,KAAK,MAAM,CAAC,CAAC,CAAC,CACvC,mDAAuC,EAAE,iBAAc,GAAG,CAAC,OAAO,YAC/D,OAAO,GACH,CACR,CAAC,CAAC,CAAC,IAAI,EACP,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,CACjB,kDAAsC,EAAE,iBAAc,GAAG,CAAC,OAAO,YAC9D,OAAO,GACJ,CACP,CAAC,CAAC,CAAC,IAAI,IACJ,CACP,CAAC;AACJ,CAAC,CAAC,CAAC"}
@@ -40,6 +40,8 @@ export { Badge } from './Badge.js';
40
40
  export type { BadgeProps, BadgeVariant, BadgeSize } from './Badge.js';
41
41
  export { ReportButton } from './ReportButton.js';
42
42
  export type { ReportButtonProps } from './ReportButton.js';
43
+ export { ResourceCard, resourceDisplayName } from './ResourceCard.js';
44
+ export type { ResourceCardProps, ResourceCardVariant } from './ResourceCard.js';
43
45
  export { Modal } from './Modal.js';
44
46
  export type { ModalProps, ModalSize } from './Modal.js';
45
47
  export { Slider } from './Slider.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACpF,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAK3D,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAC/D,YAAY,EAAE,cAAc,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAG9E,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,gBAAgB,GACjB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE1E,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAErD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAExD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEzD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE3D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEtE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAExD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEzD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE7D,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,YAAY,EACV,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACpF,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAK3D,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAC/D,YAAY,EAAE,cAAc,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAG9E,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,gBAAgB,GACjB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE1E,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAErD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAExD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEzD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE3D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEtE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACtE,YAAY,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAEhF,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAExD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEzD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE7D,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,YAAY,EACV,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,uBAAuB,CAAC"}
package/dist/ui/index.js CHANGED
@@ -34,6 +34,7 @@ export { Alert } from './Alert.js';
34
34
  export { Loader } from './Loader.js';
35
35
  export { Badge } from './Badge.js';
36
36
  export { ReportButton } from './ReportButton.js';
37
+ export { ResourceCard, resourceDisplayName } from './ResourceCard.js';
37
38
  export { Modal } from './Modal.js';
38
39
  export { Slider } from './Slider.js';
39
40
  export { NumberInput } from './NumberInput.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,oEAAoE;AACpE,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGpF,4EAA4E;AAC5E,6EAA6E;AAC7E,mEAAmE;AACnE,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAG/D,gCAAgC;AAChC,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,gBAAgB,GACjB,MAAM,aAAa,CAAC;AAErB,uBAAuB;AACvB,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAG3C,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAGzC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGnC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGnC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGnC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGnC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGjD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGnC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAG/C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAGzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,oEAAoE;AACpE,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGpF,4EAA4E;AAC5E,6EAA6E;AAC7E,mEAAmE;AACnE,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAG/D,gCAAgC;AAChC,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,gBAAgB,GACjB,MAAM,aAAa,CAAC;AAErB,uBAAuB;AACvB,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAG3C,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAGzC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGnC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGnC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGnC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGnC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGjD,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAGtE,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGnC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAG/C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAGzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC"}
@@ -5,7 +5,7 @@
5
5
  * CSS. (At RUNTIME, `injectBlocksStyles()` injects these as three separately
6
6
  * marked `<style>` elements instead — see below.)
7
7
  */
8
- export declare const BLOCKS_UI_STYLES = "/* AUTOGENERATED by @civitai/theme (scripts/build-tokens.ts). DO NOT EDIT.\n Regenerate: pnpm --filter @civitai/theme build. Guarded by generation-parity test. */\n@property --civitai-color-text {\n syntax: '<color>';\n inherits: true;\n initial-value: #222;\n}\n@property --civitai-color-text-dimmed {\n syntax: '<color>';\n inherits: true;\n initial-value: #868e96;\n}\n@property --civitai-color-body {\n syntax: '<color>';\n inherits: true;\n initial-value: #fefefe;\n}\n@property --civitai-color-surface {\n syntax: '<color>';\n inherits: true;\n initial-value: #fefefe;\n}\n@property --civitai-color-surface-2 {\n syntax: '<color>';\n inherits: true;\n initial-value: #fefefe;\n}\n@property --civitai-color-border {\n syntax: '<color>';\n inherits: true;\n initial-value: #ced4da;\n}\n@property --civitai-color-primary {\n syntax: '<color>';\n inherits: true;\n initial-value: #228BE6;\n}\n@property --civitai-color-primary-hover {\n syntax: '<color>';\n inherits: true;\n initial-value: #1C7ED6;\n}\n@property --civitai-color-primary-fg {\n syntax: '<color>';\n inherits: true;\n initial-value: #fefefe;\n}\n@property --civitai-color-primary-light {\n syntax: '<color>';\n inherits: true;\n initial-value: rgba(34, 139, 230, 0.1);\n}\n@property --civitai-color-error {\n syntax: '<color>';\n inherits: true;\n initial-value: #fa5252;\n}\n@property --civitai-color-success {\n syntax: '<color>';\n inherits: true;\n initial-value: #299C7A;\n}\n@property --civitai-color-warning {\n syntax: '<color>';\n inherits: true;\n initial-value: #fd7e14;\n}\n@property --civitai-color-info {\n syntax: '<color>';\n inherits: true;\n initial-value: #228BE6;\n}\n@property --civitai-color-gray-0 {\n syntax: '<color>';\n inherits: true;\n initial-value: #f8f9fa;\n}\n@property --civitai-color-gray-1 {\n syntax: '<color>';\n inherits: true;\n initial-value: #f1f3f5;\n}\n@property --civitai-color-gray-2 {\n syntax: '<color>';\n inherits: true;\n initial-value: #e9ecef;\n}\n@property --civitai-color-gray-3 {\n syntax: '<color>';\n inherits: true;\n initial-value: #dee2e6;\n}\n@property --civitai-color-gray-4 {\n syntax: '<color>';\n inherits: true;\n initial-value: #ced4da;\n}\n@property --civitai-color-gray-5 {\n syntax: '<color>';\n inherits: true;\n initial-value: #adb5bd;\n}\n@property --civitai-color-gray-6 {\n syntax: '<color>';\n inherits: true;\n initial-value: #868e96;\n}\n@property --civitai-color-gray-7 {\n syntax: '<color>';\n inherits: true;\n initial-value: #495057;\n}\n@property --civitai-color-gray-8 {\n syntax: '<color>';\n inherits: true;\n initial-value: #343a40;\n}\n@property --civitai-color-gray-9 {\n syntax: '<color>';\n inherits: true;\n initial-value: #212529;\n}\n\n:root {\n --civitai-font: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;\n --civitai-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;\n --civitai-radius: 0.25rem;\n --civitai-color-text: #222;\n --civitai-color-text-dimmed: #868e96;\n --civitai-color-body: #fefefe;\n --civitai-color-surface: #fefefe;\n --civitai-color-surface-2: #fefefe;\n --civitai-color-border: #ced4da;\n --civitai-color-primary: #228BE6;\n --civitai-color-primary-hover: #1C7ED6;\n --civitai-color-primary-fg: #fefefe;\n --civitai-color-primary-light: rgba(34, 139, 230, 0.1);\n --civitai-color-error: #fa5252;\n --civitai-color-success: #299C7A;\n --civitai-color-warning: #fd7e14;\n --civitai-color-info: #228BE6;\n --civitai-color-gray-0: #f8f9fa;\n --civitai-color-gray-1: #f1f3f5;\n --civitai-color-gray-2: #e9ecef;\n --civitai-color-gray-3: #dee2e6;\n --civitai-color-gray-4: #ced4da;\n --civitai-color-gray-5: #adb5bd;\n --civitai-color-gray-6: #868e96;\n --civitai-color-gray-7: #495057;\n --civitai-color-gray-8: #343a40;\n --civitai-color-gray-9: #212529;\n --civitai-bp-xs: 480px;\n --civitai-bp-sm: 768px;\n --civitai-bp-md: 1024px;\n --civitai-bp-lg: 1184px;\n --civitai-bp-xl: 1440px;\n}\n\n[data-theme='light'] {\n --civitai-font: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;\n --civitai-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;\n --civitai-radius: 0.25rem;\n --civitai-color-text: #222;\n --civitai-color-text-dimmed: #868e96;\n --civitai-color-body: #fefefe;\n --civitai-color-surface: #fefefe;\n --civitai-color-surface-2: #fefefe;\n --civitai-color-border: #ced4da;\n --civitai-color-primary: #228BE6;\n --civitai-color-primary-hover: #1C7ED6;\n --civitai-color-primary-fg: #fefefe;\n --civitai-color-primary-light: rgba(34, 139, 230, 0.1);\n --civitai-color-error: #fa5252;\n --civitai-color-success: #299C7A;\n --civitai-color-warning: #fd7e14;\n --civitai-color-info: #228BE6;\n --civitai-color-gray-0: #f8f9fa;\n --civitai-color-gray-1: #f1f3f5;\n --civitai-color-gray-2: #e9ecef;\n --civitai-color-gray-3: #dee2e6;\n --civitai-color-gray-4: #ced4da;\n --civitai-color-gray-5: #adb5bd;\n --civitai-color-gray-6: #868e96;\n --civitai-color-gray-7: #495057;\n --civitai-color-gray-8: #343a40;\n --civitai-color-gray-9: #212529;\n --civitai-bp-xs: 480px;\n --civitai-bp-sm: 768px;\n --civitai-bp-md: 1024px;\n --civitai-bp-lg: 1184px;\n --civitai-bp-xl: 1440px;\n}\n\n[data-theme='dark'] {\n --civitai-color-text: #C1C2C5;\n --civitai-color-text-dimmed: #8c8fa3;\n --civitai-color-body: #1A1B1E;\n --civitai-color-surface: #1A1B1E;\n --civitai-color-surface-2: #25262B;\n --civitai-color-border: #373A40;\n --civitai-color-primary: #1971C2;\n --civitai-color-primary-hover: #1864AB;\n --civitai-color-primary-fg: #fefefe;\n --civitai-color-primary-light: rgba(34, 139, 230, 0.15);\n --civitai-color-error: #e03131;\n --civitai-color-success: #326D5C;\n --civitai-color-warning: #e8590c;\n --civitai-color-info: #1971C2;\n}\n\n/*\n * @civitai/components \u2014 attribute-driven, framework-agnostic component CSS.\n *\n * Contract: style is selected by `data-civitai-ui=\"<name>\"` + `data-variant` +\n * `data-size` (+ a few component-specific `data-*`). Theme via an ancestor\n * `[data-theme='light'|'dark']`. Tokens come from @civitai/theme's `--civitai-*`\n * custom properties (link `@civitai/theme/styles.css` alongside this file, or\n * call `injectStyles()` which injects both).\n *\n * ALL rules live in `@layer civitai.components` so a consumer's own unlayered\n * CSS always wins the cascade WITHOUT specificity fights. State colors are\n * derived with `color-mix()` from base tokens (no shade enumeration), and\n * structure uses native CSS nesting (no preprocessor).\n *\n * See MARKUP.md for the full per-component markup + ARIA contract.\n */\n@layer civitai.components {\n [data-civitai-ui] {\n box-sizing: border-box;\n font-family: var(--civitai-font);\n\n & *,\n & *::before,\n & *::after {\n box-sizing: border-box;\n }\n }\n\n /* ----- Button ----- */\n [data-civitai-ui='button'] {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n border: 1px solid transparent;\n border-radius: var(--civitai-radius);\n font-family: var(--civitai-font);\n font-weight: 600;\n line-height: 1;\n cursor: pointer;\n user-select: none;\n text-decoration: none;\n transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;\n\n /*\n * Documented DEFAULTS (MARKUP.md): variant=filled, size=md. Applied on the\n * BASE rule (unconditionally) so BARE markup \u2014 a `<button data-civitai-ui=\n * \"button\">` with no `data-variant`/`data-size` \u2014 renders the documented\n * default (filled + md), not an unstyled/zero-size button. The explicit\n * `[data-variant]` / `[data-size]` rules below OVERRIDE (all four variants\n * set their own bg/border/hover, so the base filled default only ever wins\n * for a bare button). Every non-filled variant also sets its own\n * `border-color`, so the base primary border never leaks to them.\n */\n background: var(--civitai-color-primary);\n color: var(--civitai-color-primary-fg);\n border-color: var(--civitai-color-primary);\n height: 36px;\n padding: 0 18px;\n font-size: 14px;\n\n &:hover:not(:disabled) {\n background: var(--civitai-color-primary-hover);\n border-color: var(--civitai-color-primary-hover);\n }\n\n &[data-size='sm'] { height: 30px; padding: 0 14px; font-size: 13px; }\n &[data-size='md'] { height: 36px; padding: 0 18px; font-size: 14px; }\n &[data-size='lg'] { height: 44px; padding: 0 22px; font-size: 16px; }\n &[data-full-width='true'] { width: 100%; }\n\n &[data-variant='filled'] {\n background: var(--civitai-color-primary);\n color: var(--civitai-color-primary-fg);\n border-color: var(--civitai-color-primary);\n\n &:hover:not(:disabled) {\n background: var(--civitai-color-primary-hover);\n border-color: var(--civitai-color-primary-hover);\n }\n }\n &[data-variant='light'] {\n background: color-mix(in srgb, var(--civitai-color-primary) 12%, transparent);\n color: var(--civitai-color-primary);\n border-color: transparent;\n\n &:hover:not(:disabled) {\n background: color-mix(in srgb, var(--civitai-color-primary) 22%, transparent);\n }\n }\n &[data-variant='outline'] {\n background: transparent;\n color: var(--civitai-color-primary);\n border-color: var(--civitai-color-primary);\n\n &:hover:not(:disabled) {\n background: color-mix(in srgb, var(--civitai-color-primary) 10%, transparent);\n }\n }\n &[data-variant='subtle'] {\n background: transparent;\n color: var(--civitai-color-primary);\n border-color: transparent;\n\n &:hover:not(:disabled) {\n background: color-mix(in srgb, var(--civitai-color-primary) 10%, transparent);\n }\n }\n &:disabled,\n &[aria-busy='true'] {\n opacity: 0.6;\n cursor: not-allowed;\n }\n & [data-civitai-ui-section] {\n display: inline-flex;\n align-items: center;\n }\n }\n\n /* ----- TextInput / Textarea / NumberInput / Select ----- */\n [data-civitai-ui='text-input'],\n [data-civitai-ui='textarea'],\n [data-civitai-ui='number-input'],\n [data-civitai-ui='select'] {\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n [data-civitai-ui-label] {\n font-size: 14px;\n font-weight: 600;\n color: var(--civitai-color-text);\n }\n [data-civitai-ui-required] {\n color: var(--civitai-color-error);\n margin-left: 2px;\n }\n [data-civitai-ui-description] {\n font-size: 12px;\n color: var(--civitai-color-text-dimmed);\n }\n [data-civitai-ui-error] {\n font-size: 12px;\n color: var(--civitai-color-error);\n }\n [data-civitai-ui-control] {\n width: 100%;\n font-family: var(--civitai-font);\n font-size: 14px;\n color: var(--civitai-color-text);\n background: var(--civitai-color-surface);\n border: 1px solid var(--civitai-color-border);\n border-radius: var(--civitai-radius);\n padding: 8px 12px;\n transition: border-color 120ms ease;\n\n &:focus {\n outline: none;\n border-color: var(--civitai-color-primary);\n }\n &[aria-invalid='true'] {\n border-color: var(--civitai-color-error);\n }\n &:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n }\n textarea[data-civitai-ui-control] {\n resize: vertical;\n line-height: 1.5;\n }\n /*\n * Select (issue #181 F6): a native <select> reusing the shared\n * `-control` field chrome (border/background/radius/focus/invalid, above).\n * Keep the native disclosure caret (most robust + accessible); just make the\n * control feel interactive and reserve room on the right so the caret never\n * overlaps a long option label.\n */\n select[data-civitai-ui-control] {\n cursor: pointer;\n padding-right: 28px;\n line-height: 1.4;\n }\n select[data-civitai-ui-control]:disabled {\n cursor: not-allowed;\n }\n\n /* ----- Checkbox / Radio (issue #181 F6) ----- */\n /*\n * Themed native <input type=\"checkbox\">/<input type=\"radio\">: `accent-color`\n * carries the primary tint (native control, so keyboard/indeterminate/tab all\n * work for free), plus custom sizing, a token focus ring, and disabled state.\n * The box + its inline label sit in a `-choice` row; description + error live\n * below, reusing the shared field markers. The inputs deliberately do NOT\n * carry `data-civitai-ui-control` (that is the full-width field-input chrome).\n */\n [data-civitai-ui='checkbox'],\n [data-civitai-ui='radio'] {\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n [data-civitai-ui-choice] {\n display: flex;\n align-items: center;\n gap: 8px;\n }\n [data-civitai-ui='checkbox'] [data-civitai-ui-label],\n [data-civitai-ui='radio'] [data-civitai-ui-label] {\n font-weight: 500;\n cursor: pointer;\n user-select: none;\n }\n [data-civitai-ui='checkbox'] input[type='checkbox'],\n [data-civitai-ui='radio'] input[type='radio'] {\n accent-color: var(--civitai-color-primary);\n width: 16px;\n height: 16px;\n margin: 0;\n flex: none;\n cursor: pointer;\n }\n [data-civitai-ui='checkbox'] input[type='checkbox']:focus-visible,\n [data-civitai-ui='radio'] input[type='radio']:focus-visible {\n outline: 2px solid var(--civitai-color-primary);\n outline-offset: 2px;\n }\n [data-civitai-ui='checkbox'] input[type='checkbox']:disabled,\n [data-civitai-ui='radio'] input[type='radio']:disabled {\n cursor: not-allowed;\n opacity: 0.6;\n }\n [data-civitai-ui='checkbox'] input[type='checkbox']:disabled ~ [data-civitai-ui-label],\n [data-civitai-ui='radio'] input[type='radio']:disabled ~ [data-civitai-ui-label] {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n /* ----- RadioGroup (issue #181 F6) \u2014 role=radiogroup layout ----- */\n [data-civitai-ui='radio-group'] {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n [data-civitai-ui-radio-options] {\n display: flex;\n flex-direction: column;\n gap: 8px;\n\n &[data-orientation='horizontal'] {\n flex-direction: row;\n flex-wrap: wrap;\n gap: 16px;\n }\n }\n /*\n * RadioGroup group-level error (0.2.0): mirrors the field `-error` treatment.\n * The message reuses the shared `[data-civitai-ui-error]` styling (12px, error\n * token); the invalid state (`data-invalid` on the group, set alongside\n * `aria-invalid`) tints the child radios' `accent-color` to the error token so\n * the invalid cue reads on the group, not just the message text \u2014 the group\n * analogue of a field control's error-colored border.\n */\n [data-civitai-ui='radio-group'] [data-civitai-ui-error] {\n font-size: 12px;\n color: var(--civitai-color-error);\n }\n [data-civitai-ui='radio-group'][data-invalid] input[type='radio'] {\n accent-color: var(--civitai-color-error);\n }\n\n /* ----- Card ----- */\n /*\n * F5 (issue #181): in LIGHT mode --civitai-color-surface == --civitai-color-body\n * (both #fefefe), so a borderless card is invisible against the page. Give\n * every card a subtle DEFAULT hairline (a low-alpha mix of the border token)\n * so a card WITHOUT `data-with-border` is still visible. `data-with-border`\n * stays the stronger, explicit, fully-opaque border. Dark mode already\n * differentiates surface from body, so the default hairline is removed there\n * (see the [data-theme='dark'] rule below) \u2014 no over-adding. The default\n * (no [data-theme]) light palette keeps the hairline, matching light mode.\n */\n [data-civitai-ui='card'] {\n background: var(--civitai-color-surface);\n border-radius: var(--civitai-radius);\n color: var(--civitai-color-text);\n border: 1px solid color-mix(in srgb, var(--civitai-color-border) 55%, transparent);\n\n &[data-with-border='true'] {\n border: 1px solid var(--civitai-color-border);\n }\n &[data-padding='sm'] { padding: 10px; }\n &[data-padding='md'] { padding: 16px; }\n &[data-padding='lg'] { padding: 24px; }\n }\n /*\n * Dark already differentiates surface (#1A1B1E) from body \u2014 drop the default\n * hairline on a borderless card so dark is visually unchanged. An explicit\n * `data-with-border` still renders its full border.\n */\n [data-theme='dark'] [data-civitai-ui='card']:not([data-with-border='true']) {\n border: 0;\n }\n\n /* ----- Stack / Group ----- */\n [data-civitai-ui='stack'] {\n display: flex;\n flex-direction: column;\n gap: 12px;\n\n &[data-gap='sm'] { gap: 8px; }\n &[data-gap='md'] { gap: 16px; }\n &[data-gap='lg'] { gap: 24px; }\n }\n [data-civitai-ui='group'] {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 8px;\n\n /*\n * RESPONSIVE BASE. A group is a ROW, and a row of three or more controls\n * has no escape at phone widths \u2014 a block's slot is ~360px there, so the\n * row simply ran off the edge.\n *\n * \uD83D\uDD34 There are THREE `group` surfaces, and for TWO of them this is a NEW\n * DEFAULT \u2014 do not read it as the CSS merely catching up:\n * - `@civitai/blocks-react`'s `<Group>` defaults `wrap = true` and writes\n * `flex-wrap` as an INLINE style, so its consumers already wrapped.\n * For that one, and only that one, this is the CSS agreeing with a\n * default that was already shipping.\n * - `@civitai/components-react`'s `<Group>` writes NO inline style and has\n * NO `wrap` prop, so it resolves against this rule \u2014 a React consumer\n * that did NOT wrap before.\n * - bare `data-civitai-ui=\"group\"` markup, the framework-agnostic contract\n * this package exists to serve \u2014 likewise.\n * Nothing could see the disagreement, because each surface was only ever\n * tested against itself. TWO tests now pin it, one per React surface \u2014 there\n * is no single \"the parity test\":\n * - `civitai-blocks-react/test/Group.test.tsx` reads the wrap default off\n * ITS `<Group>` and compares it to this rule, so flipping either alone\n * fails;\n * - `civitai-components-react/test/html-vs-react-parity.browser.test.tsx`\n * (\"styling anchors \u2014 Group\") asserts the COMPUTED value on both the\n * React and the bare-markup render, which is what covers the other two\n * surfaces. A parity check alone cannot: deleting this rule moves both\n * of its arms identically, so only the absolute anchor sees it.\n *\n * \uD83D\uDD34 The three-surface distinction is load-bearing for the RELEASE, not just\n * the prose: \"existing callers notice\" is what `RELEASING.md` reserves\n * `major` for, and it is true for two of the three surfaces. Shipped as a\n * MINOR by maintainer decision \u2014 see the changeset for the reasoning and for\n * the upgrade note that decision makes load-bearing.\n *\n * Opt out with `data-nowrap=\"true\"`. An inline `flex-wrap` also still\n * outranks this layer, so existing markup that already sets it is untouched.\n */\n flex-wrap: wrap;\n\n /*\n * A flex item defaults to `min-width: auto`, which refuses to shrink below\n * its CONTENT width \u2014 so one long label (a model name, a prompt) pushes the\n * row past the slot even WITH wrap. `min-width: 0` lets it shrink.\n *\n * \uD83D\uDD34 SCOPE, measured: this only matters for a child whose computed\n * `overflow` is `visible`. Per CSS Flexbox \u00A74.5 an item with any other\n * `overflow` ALREADY has an automatic minimum size of 0 \u2014 so a child that\n * sets `overflow: hidden`, as any `text-overflow: ellipsis` child must, is\n * unaffected by this line. An earlier version of this comment claimed\n * ellipsis \"can never engage\" without it, which was exactly backwards; the\n * browser guard built on that belief used an ellipsis fixture and therefore\n * passed with this rule DELETED. Both are fixed \u2014 see\n * `civitai-blocks-react/test/responsive-group.browser.test.tsx`.\n *\n * Specificity is (0,1,0), not zero: `&` carries the parent selector's, and\n * `:where(*)` is specificity-identical to `*`, so that wrapper bought\n * nothing and has been dropped. What actually lets a CONSUMER's own rule\n * win is `@layer civitai.components`, which every rule in this file sits\n * inside \u2014 unlayered author CSS beats it regardless of specificity. Note\n * the limit: another rule INSIDE this layer does not get that protection,\n * so a future non-zero `min-width` on a nested component here must be more\n * specific or come later.\n */\n & > * { min-width: 0; }\n\n &[data-nowrap='true'] { flex-wrap: nowrap; }\n\n &[data-gap='sm'] { gap: 6px; }\n &[data-gap='md'] { gap: 16px; }\n &[data-gap='lg'] { gap: 24px; }\n }\n\n /* ----- Alert ----- */\n [data-civitai-ui='alert'] {\n display: flex;\n gap: 10px;\n align-items: flex-start;\n padding: 12px 14px;\n border-radius: var(--civitai-radius);\n border: 1px solid transparent;\n font-size: 14px;\n color: var(--civitai-color-text);\n\n /*\n * Documented DEFAULT (MARKUP.md): color=info (default intent). Applied on\n * the base rule so a BARE alert \u2014 `<div data-civitai-ui=\"alert\">` with no\n * `data-color` \u2014 renders the info intent (tinted bg + border), not just the\n * neutral chrome. The explicit `[data-color]` rules below OVERRIDE (success\n * / warning / error each set both bg + border-color), so the info default\n * only ever wins when `data-color` is omitted.\n */\n background: color-mix(in srgb, var(--civitai-color-info) 12%, transparent);\n border-color: color-mix(in srgb, var(--civitai-color-info) 35%, transparent);\n\n &[data-color='info'] {\n background: color-mix(in srgb, var(--civitai-color-info) 12%, transparent);\n border-color: color-mix(in srgb, var(--civitai-color-info) 35%, transparent);\n }\n &[data-color='success'] {\n background: color-mix(in srgb, var(--civitai-color-success) 12%, transparent);\n border-color: color-mix(in srgb, var(--civitai-color-success) 35%, transparent);\n }\n &[data-color='warning'] {\n background: color-mix(in srgb, var(--civitai-color-warning) 14%, transparent);\n border-color: color-mix(in srgb, var(--civitai-color-warning) 35%, transparent);\n }\n &[data-color='error'] {\n background: color-mix(in srgb, var(--civitai-color-error) 12%, transparent);\n border-color: color-mix(in srgb, var(--civitai-color-error) 35%, transparent);\n }\n & [data-civitai-ui-alert-body] {\n flex: 1;\n min-width: 0;\n }\n & [data-civitai-ui-alert-title] {\n font-weight: 600;\n margin-bottom: 2px;\n }\n & [data-civitai-ui-alert-close] {\n background: transparent;\n border: none;\n cursor: pointer;\n color: inherit;\n font-size: 16px;\n line-height: 1;\n padding: 0;\n opacity: 0.7;\n\n &:hover { opacity: 1; }\n }\n }\n\n /* ----- Loader ----- */\n [data-civitai-ui='loader'] {\n display: inline-block;\n border-radius: 50%;\n border-style: solid;\n border-color: color-mix(in srgb, currentColor 25%, transparent);\n border-top-color: currentColor;\n color: var(--civitai-color-primary);\n animation: civitai-ui-spin 0.7s linear infinite;\n\n /*\n * Documented DEFAULT (MARKUP.md): size=md. Applied on the base rule so a\n * BARE loader \u2014 `<span data-civitai-ui=\"loader\">` with no `data-size` \u2014 has\n * real dimensions (otherwise 0\u00D70 and invisible). sm/lg OVERRIDE below.\n */\n width: 22px;\n height: 22px;\n border-width: 3px;\n\n &[data-size='sm'] { width: 16px; height: 16px; border-width: 2px; }\n &[data-size='md'] { width: 22px; height: 22px; border-width: 3px; }\n &[data-size='lg'] { width: 32px; height: 32px; border-width: 4px; }\n }\n [data-civitai-ui='button'] [data-civitai-ui='loader'] {\n color: currentColor;\n }\n @keyframes civitai-ui-spin {\n to { transform: rotate(360deg); }\n }\n\n /* ----- Badge ----- */\n [data-civitai-ui='badge'] {\n display: inline-flex;\n align-items: center;\n border: 1px solid transparent;\n border-radius: 999px;\n font-weight: 600;\n line-height: 1;\n white-space: nowrap;\n text-transform: uppercase;\n letter-spacing: 0.02em;\n\n /*\n * Documented DEFAULTS (MARKUP.md): variant=filled, size=md. Applied on the\n * base rule so a BARE badge \u2014 `<span data-civitai-ui=\"badge\">` with no\n * `data-variant`/`data-size` (MARKUP's own minimal example) \u2014 renders\n * filled + md (padding + primary fill), not an unstyled, zero-padding pill.\n * The explicit `[data-variant]`/`[data-size]` rules below OVERRIDE. Because\n * the base now carries a primary `border-color`, the `light` variant (which\n * previously relied on the base transparent border) explicitly resets it to\n * transparent so light badges are visually unchanged.\n */\n height: 22px;\n padding: 0 10px;\n font-size: 11px;\n background: var(--civitai-color-primary);\n color: var(--civitai-color-primary-fg);\n border-color: var(--civitai-color-primary);\n\n &[data-size='sm'] { height: 18px; padding: 0 8px; font-size: 10px; }\n &[data-size='md'] { height: 22px; padding: 0 10px; font-size: 11px; }\n &[data-size='lg'] { height: 26px; padding: 0 12px; font-size: 13px; }\n &[data-variant='filled'] {\n background: var(--civitai-color-primary);\n color: var(--civitai-color-primary-fg);\n border-color: var(--civitai-color-primary);\n }\n &[data-variant='light'] {\n background: color-mix(in srgb, var(--civitai-color-primary) 14%, transparent);\n color: var(--civitai-color-primary);\n border-color: transparent;\n }\n &[data-variant='outline'] {\n background: transparent;\n color: var(--civitai-color-primary);\n border-color: var(--civitai-color-primary);\n }\n\n /*\n * Intent color via `data-color`, mirroring Alert's `data-color` contract\n * (info / success / warning / error). Absent `data-color` => the default\n * primary above (non-breaking). Each intent recolors the `filled`, `light`\n * and `outline` variants with the same `color-mix()` token approach Alert\n * uses; `filled` keeps the white `--civitai-color-primary-fg` text. These\n * `[data-color][data-variant]` rules out-specify the plain-variant rules\n * above, so order-independence holds.\n */\n &[data-color='info'] {\n &[data-variant='filled'] {\n background: var(--civitai-color-info);\n border-color: var(--civitai-color-info);\n }\n &[data-variant='light'] {\n background: color-mix(in srgb, var(--civitai-color-info) 14%, transparent);\n color: var(--civitai-color-info);\n }\n &[data-variant='outline'] {\n color: var(--civitai-color-info);\n border-color: var(--civitai-color-info);\n }\n }\n &[data-color='success'] {\n &[data-variant='filled'] {\n background: var(--civitai-color-success);\n border-color: var(--civitai-color-success);\n }\n &[data-variant='light'] {\n background: color-mix(in srgb, var(--civitai-color-success) 14%, transparent);\n color: var(--civitai-color-success);\n }\n &[data-variant='outline'] {\n color: var(--civitai-color-success);\n border-color: var(--civitai-color-success);\n }\n }\n &[data-color='warning'] {\n &[data-variant='filled'] {\n background: var(--civitai-color-warning);\n border-color: var(--civitai-color-warning);\n }\n &[data-variant='light'] {\n background: color-mix(in srgb, var(--civitai-color-warning) 14%, transparent);\n color: var(--civitai-color-warning);\n }\n &[data-variant='outline'] {\n color: var(--civitai-color-warning);\n border-color: var(--civitai-color-warning);\n }\n }\n &[data-color='error'] {\n &[data-variant='filled'] {\n background: var(--civitai-color-error);\n border-color: var(--civitai-color-error);\n }\n &[data-variant='light'] {\n background: color-mix(in srgb, var(--civitai-color-error) 14%, transparent);\n color: var(--civitai-color-error);\n }\n &[data-variant='outline'] {\n color: var(--civitai-color-error);\n border-color: var(--civitai-color-error);\n }\n }\n }\n\n /* ----- Slider ----- */\n /*\n * A themed native <input type=\"range\">: `accent-color` carries the primary\n * tint (native control, so keyboard arrow-key nav + ARIA come for free), plus\n * a full-width track, a token focus ring, and disabled/invalid states. Like\n * checkbox/radio, the range input deliberately does NOT carry\n * `data-civitai-ui-control` (that is the bordered field-input chrome). The\n * label/description/error reuse the shared field markers, laid out in a\n * column; an optional value read-out sits inline with the label in a header.\n */\n [data-civitai-ui='slider'] {\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n [data-civitai-ui-slider-header] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n }\n [data-civitai-ui-slider-value] {\n font-size: 13px;\n font-weight: 600;\n color: var(--civitai-color-text-dimmed);\n font-variant-numeric: tabular-nums;\n }\n [data-civitai-ui='slider'] input[type='range'] {\n -webkit-appearance: none;\n appearance: none;\n width: 100%;\n height: 6px;\n margin: 6px 0;\n padding: 0;\n border-radius: 999px;\n accent-color: var(--civitai-color-primary);\n background: var(--civitai-color-gray-2);\n cursor: pointer;\n }\n [data-theme='dark'] [data-civitai-ui='slider'] input[type='range'] {\n background: var(--civitai-color-surface-2);\n }\n [data-civitai-ui='slider'] input[type='range']:focus-visible {\n outline: 2px solid var(--civitai-color-primary);\n outline-offset: 4px;\n }\n [data-civitai-ui='slider'] input[type='range']:disabled {\n cursor: not-allowed;\n opacity: 0.6;\n accent-color: var(--civitai-color-gray-5);\n }\n [data-civitai-ui='slider'][data-invalid] input[type='range'] {\n accent-color: var(--civitai-color-error);\n }\n\n /* ----- SegmentedControl / Tabs ----- */\n /*\n * A `role=tablist` of segment buttons (`role=tab`). Presentational chrome\n * only \u2014 the roving-tabindex + arrow-key nav + selection follow-focus live in\n * the React binding (or must be author-provided for hand HTML). The selected\n * segment (`aria-selected='true'`) lifts to the surface color with a soft\n * shadow; unselected segments read dimmed. Paired tab panels use\n * `data-civitai-ui-tabpanel` (role=tabpanel).\n */\n [data-civitai-ui='segmented-control'] {\n display: inline-flex;\n gap: 2px;\n padding: 4px;\n background: var(--civitai-color-gray-1);\n border-radius: var(--civitai-radius);\n max-width: 100%;\n overflow-x: auto;\n }\n [data-theme='dark'] [data-civitai-ui='segmented-control'] {\n background: var(--civitai-color-surface-2);\n }\n [data-civitai-ui-segment] {\n -webkit-appearance: none;\n appearance: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: 0;\n background: transparent;\n color: var(--civitai-color-text-dimmed);\n font-family: var(--civitai-font);\n font-weight: 600;\n line-height: 1;\n white-space: nowrap;\n cursor: pointer;\n border-radius: calc(var(--civitai-radius) - 1px);\n transition: background-color 120ms ease, color 120ms ease;\n\n /* Documented DEFAULT (MARKUP.md): size=md. On the base rule so a bare\n segment renders at md height, not zero-height. sm/lg override below. */\n height: 30px;\n padding: 0 14px;\n font-size: 13px;\n\n &[data-size='sm'] { height: 26px; padding: 0 10px; font-size: 12px; }\n &[data-size='md'] { height: 30px; padding: 0 14px; font-size: 13px; }\n &[data-size='lg'] { height: 38px; padding: 0 18px; font-size: 15px; }\n\n &:hover:not(:disabled):not([aria-selected='true']):not([aria-checked='true']) {\n color: var(--civitai-color-text);\n }\n /* Selected state \u2014 `aria-selected` in tabs mode (role=tab) OR `aria-checked`\n in toggle mode (role=radio). Same visual treatment for both. */\n &[aria-selected='true'],\n &[aria-checked='true'] {\n background: var(--civitai-color-surface);\n color: var(--civitai-color-text);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);\n }\n &:focus-visible {\n outline: 2px solid var(--civitai-color-primary);\n outline-offset: 2px;\n }\n &:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n }\n [data-civitai-ui-tabpanel] {\n color: var(--civitai-color-text);\n\n &:focus-visible {\n outline: 2px solid var(--civitai-color-primary);\n outline-offset: 2px;\n }\n &[hidden] { display: none; }\n }\n\n /* ----- Toast ----- */\n /*\n * `toast-region` is the fixed-position `aria-live` host (bottom-right stack).\n * The behavior \u2014 enqueue, auto-dismiss timers, portal \u2014 lives in the React\n * binding's ToastProvider/useToast; the presentational `toast` is a standalone\n * component so hand HTML and React share one visual contract. Each toast is\n * opaque (it sits over content) with an intent-colored left accent, mirroring\n * Alert's `data-color` set.\n */\n [data-civitai-ui='toast-region'] {\n position: fixed;\n bottom: 16px;\n right: 16px;\n z-index: 9999;\n display: flex;\n flex-direction: column;\n gap: 8px;\n width: min(92vw, 380px);\n pointer-events: none;\n }\n [data-civitai-ui='toast'] {\n pointer-events: auto;\n display: flex;\n gap: 10px;\n align-items: flex-start;\n padding: 12px 14px;\n border-radius: var(--civitai-radius);\n background: var(--civitai-color-surface);\n color: var(--civitai-color-text);\n border: 1px solid var(--civitai-color-border);\n border-left: 4px solid var(--civitai-color-border);\n box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);\n font-size: 14px;\n\n &[data-color='info'] { border-left-color: var(--civitai-color-info); }\n &[data-color='success'] { border-left-color: var(--civitai-color-success); }\n &[data-color='warning'] { border-left-color: var(--civitai-color-warning); }\n &[data-color='error'] { border-left-color: var(--civitai-color-error); }\n\n & [data-civitai-ui-toast-body] {\n flex: 1;\n min-width: 0;\n }\n & [data-civitai-ui-toast-title] {\n font-weight: 600;\n margin-bottom: 2px;\n }\n & [data-civitai-ui-toast-close] {\n background: transparent;\n border: none;\n cursor: pointer;\n color: inherit;\n font-size: 16px;\n line-height: 1;\n padding: 0;\n opacity: 0.7;\n\n &:hover { opacity: 1; }\n }\n }\n\n /* ----- Tooltip ----- */\n /*\n * A hover/focus tooltip: a positioned `role=tooltip` bubble revealed when the\n * wrapper is hovered or contains focus (pure-CSS reveal), or explicitly via\n * `data-open='true'` (the React binding also wires `aria-describedby` +\n * Escape-to-dismiss). The bubble is a dark chip (gray-9) with light text in\n * both themes.\n */\n [data-civitai-ui='tooltip'] {\n position: relative;\n display: inline-flex;\n }\n [data-civitai-ui-tooltip-bubble] {\n position: absolute;\n bottom: calc(100% + 6px);\n left: 50%;\n transform: translateX(-50%);\n z-index: 200;\n max-width: 260px;\n width: max-content;\n padding: 4px 8px;\n border-radius: var(--civitai-radius);\n background: var(--civitai-color-gray-9);\n color: var(--civitai-color-primary-fg);\n font-size: 12px;\n font-weight: 500;\n line-height: 1.4;\n text-align: center;\n pointer-events: none;\n opacity: 0;\n visibility: hidden;\n transition: opacity 120ms ease;\n }\n /*\n * Reveal on hover/focus or an explicit `data-open='true'` \u2014 BUT\n * `data-dismissed='true'` overrides (gates every reveal selector), so the\n * React binding's Escape-to-dismiss can force-HIDE the bubble even while the\n * pointer still hovers or focus is still within. The dismissed flag is cleared\n * on the next hover/focus so the tooltip can re-open normally.\n */\n [data-civitai-ui='tooltip']:hover [data-civitai-ui-tooltip-bubble]:not([data-dismissed='true']),\n [data-civitai-ui='tooltip']:focus-within [data-civitai-ui-tooltip-bubble]:not([data-dismissed='true']),\n [data-civitai-ui-tooltip-bubble][data-open='true']:not([data-dismissed='true']) {\n opacity: 1;\n visibility: visible;\n }\n\n /* ----- Image ----- */\n /*\n * A media container with a token placeholder background (visible while the\n * image loads), object-fit control, and a broken-image fallback. `data-status`\n * (loading|loaded|error) \u2014 set by the React binding's onLoad/onError, or by\n * the author for hand HTML \u2014 fades the <img> in on load and swaps to the\n * fallback overlay on error. Bare markup (no `data-status`) shows the image.\n */\n [data-civitai-ui='image'] {\n position: relative;\n display: block;\n overflow: hidden;\n background: var(--civitai-color-gray-2);\n border-radius: var(--civitai-radius);\n }\n [data-theme='dark'] [data-civitai-ui='image'] {\n background: var(--civitai-color-surface-2);\n }\n [data-civitai-ui-image-img] {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: cover;\n opacity: 1;\n transition: opacity 200ms ease;\n\n &[data-fit='contain'] { object-fit: contain; }\n &[data-fit='cover'] { object-fit: cover; }\n }\n [data-civitai-ui='image'][data-status='loading'] [data-civitai-ui-image-img],\n [data-civitai-ui='image'][data-status='error'] [data-civitai-ui-image-img] {\n opacity: 0;\n }\n [data-civitai-ui-image-fallback] {\n position: absolute;\n inset: 0;\n display: none;\n align-items: center;\n justify-content: center;\n padding: 8px;\n color: var(--civitai-color-text-dimmed);\n font-size: 13px;\n text-align: center;\n }\n [data-civitai-ui='image'][data-status='error'] [data-civitai-ui-image-fallback] {\n display: flex;\n }\n}\n\n\n/* ----- Select / Slider field wrappers -----\n The shared -control/-label/-required/-description/-error primitives come from\n @civitai/components (layered). Only the select/slider WRAPPERS live here \u2014 the\n design-system package scopes its field-wrapper rule to the presentational\n inputs (text-input/textarea/number-input). */\n[data-civitai-ui='select'],\n[data-civitai-ui='slider'] {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n/* ----- Modal ----- */\n[data-civitai-ui='modal-overlay'] {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.55);\n display: flex;\n align-items: flex-start;\n justify-content: center;\n padding: 32px 16px;\n overflow-y: auto;\n z-index: 1000;\n}\n[data-civitai-ui='modal'] {\n background: var(--civitai-color-surface);\n color: var(--civitai-color-text);\n border: 1px solid var(--civitai-color-border);\n border-radius: var(--civitai-radius);\n box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);\n width: 100%;\n max-width: 440px;\n outline: none;\n}\n[data-civitai-ui='modal'][data-size='sm'] { max-width: 340px; }\n[data-civitai-ui='modal'][data-size='md'] { max-width: 440px; }\n[data-civitai-ui='modal'][data-size='lg'] { max-width: 620px; }\n[data-civitai-ui='modal'] [data-civitai-ui-modal-header] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 16px 18px;\n border-bottom: 1px solid var(--civitai-color-border);\n}\n[data-civitai-ui='modal'] [data-civitai-ui-modal-title] {\n font-size: 16px;\n font-weight: 700;\n margin: 0;\n}\n[data-civitai-ui='modal'] [data-civitai-ui-modal-close] {\n background: transparent;\n border: none;\n cursor: pointer;\n color: var(--civitai-color-text-dimmed);\n font-size: 20px;\n line-height: 1;\n padding: 0;\n}\n[data-civitai-ui='modal'] [data-civitai-ui-modal-close]:hover {\n color: var(--civitai-color-text);\n}\n[data-civitai-ui='modal'] [data-civitai-ui-modal-body] {\n padding: 18px;\n}\n\n/* ----- Slider ----- */\n[data-civitai-ui='slider'] [data-civitai-ui-label] {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n gap: 8px;\n}\n[data-civitai-ui-slider-value] {\n font-weight: 500;\n color: var(--civitai-color-text-dimmed);\n font-variant-numeric: tabular-nums;\n}\n[data-civitai-ui-range] {\n width: 100%;\n margin: 0;\n accent-color: var(--civitai-color-primary);\n cursor: pointer;\n}\n[data-civitai-ui-range]:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n[data-civitai-ui-range]:focus-visible {\n outline: 2px solid var(--civitai-color-primary);\n outline-offset: 2px;\n}\n\n/* ----- Collapse ----- */\n[data-civitai-ui='collapse'] [data-civitai-ui-collapse-trigger] {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 6px 0;\n background: transparent;\n border: none;\n font-family: var(--civitai-font);\n font-size: 14px;\n font-weight: 600;\n color: var(--civitai-color-text);\n text-align: left;\n cursor: pointer;\n}\n[data-civitai-ui='collapse'] [data-civitai-ui-collapse-trigger]:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n[data-civitai-ui='collapse'] [data-civitai-ui-collapse-chevron] {\n display: inline-block;\n width: 1em;\n color: var(--civitai-color-text-dimmed);\n}\n[data-civitai-ui='collapse'] [data-civitai-ui-collapse-region] {\n padding-top: 4px;\n}\n\n/* ----- SegmentedControl ----- */\n[data-civitai-ui='segmented-control'] {\n display: inline-flex;\n flex-direction: row;\n gap: 2px;\n padding: 3px;\n background: var(--civitai-color-surface-2);\n border: 1px solid var(--civitai-color-border);\n border-radius: var(--civitai-radius);\n vertical-align: middle;\n}\n[data-civitai-ui='segmented-control'][data-full-width='true'] {\n display: flex;\n width: 100%;\n}\n[data-civitai-ui='segmented-control'] [data-civitai-ui-segment] {\n flex: 0 0 auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: none;\n border-radius: calc(var(--civitai-radius) - 3px);\n background: transparent;\n color: var(--civitai-color-text-dimmed);\n font-family: var(--civitai-font);\n font-weight: 600;\n line-height: 1;\n white-space: nowrap;\n cursor: pointer;\n user-select: none;\n transition: background-color 120ms ease, color 120ms ease, box-shadow 120ms ease;\n}\n[data-civitai-ui='segmented-control'][data-full-width='true'] [data-civitai-ui-segment] {\n flex: 1 1 0;\n}\n[data-civitai-ui='segmented-control'][data-size='sm'] [data-civitai-ui-segment] { height: 24px; padding: 0 12px; font-size: 13px; }\n[data-civitai-ui='segmented-control'][data-size='md'] [data-civitai-ui-segment] { height: 30px; padding: 0 16px; font-size: 14px; }\n[data-civitai-ui='segmented-control'][data-size='lg'] [data-civitai-ui-segment] { height: 38px; padding: 0 20px; font-size: 16px; }\n[data-civitai-ui='segmented-control'] [data-civitai-ui-segment]:hover:not(:disabled):not([data-active]) {\n color: var(--civitai-color-text);\n background: color-mix(in srgb, var(--civitai-color-text) 6%, transparent);\n}\n[data-civitai-ui='segmented-control'] [data-civitai-ui-segment][data-active] {\n background: var(--civitai-color-surface);\n color: var(--civitai-color-primary);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);\n}\n[data-civitai-ui='segmented-control'] [data-civitai-ui-segment]:focus-visible {\n outline: 2px solid var(--civitai-color-primary);\n outline-offset: 1px;\n}\n[data-civitai-ui='segmented-control'] [data-civitai-ui-segment]:disabled {\n opacity: 0.55;\n cursor: not-allowed;\n}\n[data-civitai-ui='segmented-control'][data-disabled='true'] {\n opacity: 0.7;\n}\n";
8
+ export declare const BLOCKS_UI_STYLES = "/* AUTOGENERATED by @civitai/theme (scripts/build-tokens.ts). DO NOT EDIT.\n Regenerate: pnpm --filter @civitai/theme build. Guarded by generation-parity test. */\n@property --civitai-color-text {\n syntax: '<color>';\n inherits: true;\n initial-value: #222;\n}\n@property --civitai-color-text-dimmed {\n syntax: '<color>';\n inherits: true;\n initial-value: #868e96;\n}\n@property --civitai-color-body {\n syntax: '<color>';\n inherits: true;\n initial-value: #fefefe;\n}\n@property --civitai-color-surface {\n syntax: '<color>';\n inherits: true;\n initial-value: #fefefe;\n}\n@property --civitai-color-surface-2 {\n syntax: '<color>';\n inherits: true;\n initial-value: #fefefe;\n}\n@property --civitai-color-border {\n syntax: '<color>';\n inherits: true;\n initial-value: #ced4da;\n}\n@property --civitai-color-primary {\n syntax: '<color>';\n inherits: true;\n initial-value: #228BE6;\n}\n@property --civitai-color-primary-hover {\n syntax: '<color>';\n inherits: true;\n initial-value: #1C7ED6;\n}\n@property --civitai-color-primary-fg {\n syntax: '<color>';\n inherits: true;\n initial-value: #fefefe;\n}\n@property --civitai-color-primary-light {\n syntax: '<color>';\n inherits: true;\n initial-value: rgba(34, 139, 230, 0.1);\n}\n@property --civitai-color-error {\n syntax: '<color>';\n inherits: true;\n initial-value: #fa5252;\n}\n@property --civitai-color-success {\n syntax: '<color>';\n inherits: true;\n initial-value: #299C7A;\n}\n@property --civitai-color-warning {\n syntax: '<color>';\n inherits: true;\n initial-value: #fd7e14;\n}\n@property --civitai-color-info {\n syntax: '<color>';\n inherits: true;\n initial-value: #228BE6;\n}\n@property --civitai-color-gray-0 {\n syntax: '<color>';\n inherits: true;\n initial-value: #f8f9fa;\n}\n@property --civitai-color-gray-1 {\n syntax: '<color>';\n inherits: true;\n initial-value: #f1f3f5;\n}\n@property --civitai-color-gray-2 {\n syntax: '<color>';\n inherits: true;\n initial-value: #e9ecef;\n}\n@property --civitai-color-gray-3 {\n syntax: '<color>';\n inherits: true;\n initial-value: #dee2e6;\n}\n@property --civitai-color-gray-4 {\n syntax: '<color>';\n inherits: true;\n initial-value: #ced4da;\n}\n@property --civitai-color-gray-5 {\n syntax: '<color>';\n inherits: true;\n initial-value: #adb5bd;\n}\n@property --civitai-color-gray-6 {\n syntax: '<color>';\n inherits: true;\n initial-value: #868e96;\n}\n@property --civitai-color-gray-7 {\n syntax: '<color>';\n inherits: true;\n initial-value: #495057;\n}\n@property --civitai-color-gray-8 {\n syntax: '<color>';\n inherits: true;\n initial-value: #343a40;\n}\n@property --civitai-color-gray-9 {\n syntax: '<color>';\n inherits: true;\n initial-value: #212529;\n}\n\n:root {\n --civitai-font: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;\n --civitai-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;\n --civitai-radius: 0.25rem;\n --civitai-color-text: #222;\n --civitai-color-text-dimmed: #868e96;\n --civitai-color-body: #fefefe;\n --civitai-color-surface: #fefefe;\n --civitai-color-surface-2: #fefefe;\n --civitai-color-border: #ced4da;\n --civitai-color-primary: #228BE6;\n --civitai-color-primary-hover: #1C7ED6;\n --civitai-color-primary-fg: #fefefe;\n --civitai-color-primary-light: rgba(34, 139, 230, 0.1);\n --civitai-color-error: #fa5252;\n --civitai-color-success: #299C7A;\n --civitai-color-warning: #fd7e14;\n --civitai-color-info: #228BE6;\n --civitai-color-gray-0: #f8f9fa;\n --civitai-color-gray-1: #f1f3f5;\n --civitai-color-gray-2: #e9ecef;\n --civitai-color-gray-3: #dee2e6;\n --civitai-color-gray-4: #ced4da;\n --civitai-color-gray-5: #adb5bd;\n --civitai-color-gray-6: #868e96;\n --civitai-color-gray-7: #495057;\n --civitai-color-gray-8: #343a40;\n --civitai-color-gray-9: #212529;\n --civitai-bp-xs: 480px;\n --civitai-bp-sm: 768px;\n --civitai-bp-md: 1024px;\n --civitai-bp-lg: 1184px;\n --civitai-bp-xl: 1440px;\n}\n\n[data-theme='light'] {\n --civitai-font: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;\n --civitai-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;\n --civitai-radius: 0.25rem;\n --civitai-color-text: #222;\n --civitai-color-text-dimmed: #868e96;\n --civitai-color-body: #fefefe;\n --civitai-color-surface: #fefefe;\n --civitai-color-surface-2: #fefefe;\n --civitai-color-border: #ced4da;\n --civitai-color-primary: #228BE6;\n --civitai-color-primary-hover: #1C7ED6;\n --civitai-color-primary-fg: #fefefe;\n --civitai-color-primary-light: rgba(34, 139, 230, 0.1);\n --civitai-color-error: #fa5252;\n --civitai-color-success: #299C7A;\n --civitai-color-warning: #fd7e14;\n --civitai-color-info: #228BE6;\n --civitai-color-gray-0: #f8f9fa;\n --civitai-color-gray-1: #f1f3f5;\n --civitai-color-gray-2: #e9ecef;\n --civitai-color-gray-3: #dee2e6;\n --civitai-color-gray-4: #ced4da;\n --civitai-color-gray-5: #adb5bd;\n --civitai-color-gray-6: #868e96;\n --civitai-color-gray-7: #495057;\n --civitai-color-gray-8: #343a40;\n --civitai-color-gray-9: #212529;\n --civitai-bp-xs: 480px;\n --civitai-bp-sm: 768px;\n --civitai-bp-md: 1024px;\n --civitai-bp-lg: 1184px;\n --civitai-bp-xl: 1440px;\n}\n\n[data-theme='dark'] {\n --civitai-color-text: #C1C2C5;\n --civitai-color-text-dimmed: #8c8fa3;\n --civitai-color-body: #1A1B1E;\n --civitai-color-surface: #1A1B1E;\n --civitai-color-surface-2: #25262B;\n --civitai-color-border: #373A40;\n --civitai-color-primary: #1971C2;\n --civitai-color-primary-hover: #1864AB;\n --civitai-color-primary-fg: #fefefe;\n --civitai-color-primary-light: rgba(34, 139, 230, 0.15);\n --civitai-color-error: #e03131;\n --civitai-color-success: #326D5C;\n --civitai-color-warning: #e8590c;\n --civitai-color-info: #1971C2;\n}\n\n/*\n * @civitai/components \u2014 attribute-driven, framework-agnostic component CSS.\n *\n * Contract: style is selected by `data-civitai-ui=\"<name>\"` + `data-variant` +\n * `data-size` (+ a few component-specific `data-*`). Theme via an ancestor\n * `[data-theme='light'|'dark']`. Tokens come from @civitai/theme's `--civitai-*`\n * custom properties (link `@civitai/theme/styles.css` alongside this file, or\n * call `injectStyles()` which injects both).\n *\n * ALL rules live in `@layer civitai.components` so a consumer's own unlayered\n * CSS always wins the cascade WITHOUT specificity fights. State colors are\n * derived with `color-mix()` from base tokens (no shade enumeration), and\n * structure uses native CSS nesting (no preprocessor).\n *\n * See MARKUP.md for the full per-component markup + ARIA contract.\n */\n@layer civitai.components {\n [data-civitai-ui] {\n box-sizing: border-box;\n font-family: var(--civitai-font);\n\n & *,\n & *::before,\n & *::after {\n box-sizing: border-box;\n }\n }\n\n /* ----- Button ----- */\n [data-civitai-ui='button'] {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n border: 1px solid transparent;\n border-radius: var(--civitai-radius);\n font-family: var(--civitai-font);\n font-weight: 600;\n line-height: 1;\n cursor: pointer;\n user-select: none;\n text-decoration: none;\n transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;\n\n /*\n * Documented DEFAULTS (MARKUP.md): variant=filled, size=md. Applied on the\n * BASE rule (unconditionally) so BARE markup \u2014 a `<button data-civitai-ui=\n * \"button\">` with no `data-variant`/`data-size` \u2014 renders the documented\n * default (filled + md), not an unstyled/zero-size button. The explicit\n * `[data-variant]` / `[data-size]` rules below OVERRIDE (all four variants\n * set their own bg/border/hover, so the base filled default only ever wins\n * for a bare button). Every non-filled variant also sets its own\n * `border-color`, so the base primary border never leaks to them.\n */\n background: var(--civitai-color-primary);\n color: var(--civitai-color-primary-fg);\n border-color: var(--civitai-color-primary);\n height: 36px;\n padding: 0 18px;\n font-size: 14px;\n\n &:hover:not(:disabled) {\n background: var(--civitai-color-primary-hover);\n border-color: var(--civitai-color-primary-hover);\n }\n\n &[data-size='sm'] { height: 30px; padding: 0 14px; font-size: 13px; }\n &[data-size='md'] { height: 36px; padding: 0 18px; font-size: 14px; }\n &[data-size='lg'] { height: 44px; padding: 0 22px; font-size: 16px; }\n &[data-full-width='true'] { width: 100%; }\n\n &[data-variant='filled'] {\n background: var(--civitai-color-primary);\n color: var(--civitai-color-primary-fg);\n border-color: var(--civitai-color-primary);\n\n &:hover:not(:disabled) {\n background: var(--civitai-color-primary-hover);\n border-color: var(--civitai-color-primary-hover);\n }\n }\n &[data-variant='light'] {\n background: color-mix(in srgb, var(--civitai-color-primary) 12%, transparent);\n color: var(--civitai-color-primary);\n border-color: transparent;\n\n &:hover:not(:disabled) {\n background: color-mix(in srgb, var(--civitai-color-primary) 22%, transparent);\n }\n }\n &[data-variant='outline'] {\n background: transparent;\n color: var(--civitai-color-primary);\n border-color: var(--civitai-color-primary);\n\n &:hover:not(:disabled) {\n background: color-mix(in srgb, var(--civitai-color-primary) 10%, transparent);\n }\n }\n &[data-variant='subtle'] {\n background: transparent;\n color: var(--civitai-color-primary);\n border-color: transparent;\n\n &:hover:not(:disabled) {\n background: color-mix(in srgb, var(--civitai-color-primary) 10%, transparent);\n }\n }\n &:disabled,\n &[aria-busy='true'] {\n opacity: 0.6;\n cursor: not-allowed;\n }\n & [data-civitai-ui-section] {\n display: inline-flex;\n align-items: center;\n }\n }\n\n /* ----- TextInput / Textarea / NumberInput / Select ----- */\n [data-civitai-ui='text-input'],\n [data-civitai-ui='textarea'],\n [data-civitai-ui='number-input'],\n [data-civitai-ui='select'] {\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n [data-civitai-ui-label] {\n font-size: 14px;\n font-weight: 600;\n color: var(--civitai-color-text);\n }\n [data-civitai-ui-required] {\n color: var(--civitai-color-error);\n margin-left: 2px;\n }\n [data-civitai-ui-description] {\n font-size: 12px;\n color: var(--civitai-color-text-dimmed);\n }\n [data-civitai-ui-error] {\n font-size: 12px;\n color: var(--civitai-color-error);\n }\n [data-civitai-ui-control] {\n width: 100%;\n font-family: var(--civitai-font);\n font-size: 14px;\n color: var(--civitai-color-text);\n background: var(--civitai-color-surface);\n border: 1px solid var(--civitai-color-border);\n border-radius: var(--civitai-radius);\n padding: 8px 12px;\n transition: border-color 120ms ease;\n\n &:focus {\n outline: none;\n border-color: var(--civitai-color-primary);\n }\n &[aria-invalid='true'] {\n border-color: var(--civitai-color-error);\n }\n &:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n }\n textarea[data-civitai-ui-control] {\n resize: vertical;\n line-height: 1.5;\n }\n /*\n * Select (issue #181 F6): a native <select> reusing the shared\n * `-control` field chrome (border/background/radius/focus/invalid, above).\n * Keep the native disclosure caret (most robust + accessible); just make the\n * control feel interactive and reserve room on the right so the caret never\n * overlaps a long option label.\n */\n select[data-civitai-ui-control] {\n cursor: pointer;\n padding-right: 28px;\n line-height: 1.4;\n }\n select[data-civitai-ui-control]:disabled {\n cursor: not-allowed;\n }\n\n /* ----- Checkbox / Radio (issue #181 F6) ----- */\n /*\n * Themed native <input type=\"checkbox\">/<input type=\"radio\">: `accent-color`\n * carries the primary tint (native control, so keyboard/indeterminate/tab all\n * work for free), plus custom sizing, a token focus ring, and disabled state.\n * The box + its inline label sit in a `-choice` row; description + error live\n * below, reusing the shared field markers. The inputs deliberately do NOT\n * carry `data-civitai-ui-control` (that is the full-width field-input chrome).\n */\n [data-civitai-ui='checkbox'],\n [data-civitai-ui='radio'] {\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n [data-civitai-ui-choice] {\n display: flex;\n align-items: center;\n gap: 8px;\n }\n [data-civitai-ui='checkbox'] [data-civitai-ui-label],\n [data-civitai-ui='radio'] [data-civitai-ui-label] {\n font-weight: 500;\n cursor: pointer;\n user-select: none;\n }\n [data-civitai-ui='checkbox'] input[type='checkbox'],\n [data-civitai-ui='radio'] input[type='radio'] {\n accent-color: var(--civitai-color-primary);\n width: 16px;\n height: 16px;\n margin: 0;\n flex: none;\n cursor: pointer;\n }\n [data-civitai-ui='checkbox'] input[type='checkbox']:focus-visible,\n [data-civitai-ui='radio'] input[type='radio']:focus-visible {\n outline: 2px solid var(--civitai-color-primary);\n outline-offset: 2px;\n }\n [data-civitai-ui='checkbox'] input[type='checkbox']:disabled,\n [data-civitai-ui='radio'] input[type='radio']:disabled {\n cursor: not-allowed;\n opacity: 0.6;\n }\n [data-civitai-ui='checkbox'] input[type='checkbox']:disabled ~ [data-civitai-ui-label],\n [data-civitai-ui='radio'] input[type='radio']:disabled ~ [data-civitai-ui-label] {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n /* ----- RadioGroup (issue #181 F6) \u2014 role=radiogroup layout ----- */\n [data-civitai-ui='radio-group'] {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n [data-civitai-ui-radio-options] {\n display: flex;\n flex-direction: column;\n gap: 8px;\n\n &[data-orientation='horizontal'] {\n flex-direction: row;\n flex-wrap: wrap;\n gap: 16px;\n }\n }\n /*\n * RadioGroup group-level error (0.2.0): mirrors the field `-error` treatment.\n * The message reuses the shared `[data-civitai-ui-error]` styling (12px, error\n * token); the invalid state (`data-invalid` on the group, set alongside\n * `aria-invalid`) tints the child radios' `accent-color` to the error token so\n * the invalid cue reads on the group, not just the message text \u2014 the group\n * analogue of a field control's error-colored border.\n */\n [data-civitai-ui='radio-group'] [data-civitai-ui-error] {\n font-size: 12px;\n color: var(--civitai-color-error);\n }\n [data-civitai-ui='radio-group'][data-invalid] input[type='radio'] {\n accent-color: var(--civitai-color-error);\n }\n\n /* ----- Card ----- */\n /*\n * F5 (issue #181): in LIGHT mode --civitai-color-surface == --civitai-color-body\n * (both #fefefe), so a borderless card is invisible against the page. Give\n * every card a subtle DEFAULT hairline (a low-alpha mix of the border token)\n * so a card WITHOUT `data-with-border` is still visible. `data-with-border`\n * stays the stronger, explicit, fully-opaque border. Dark mode already\n * differentiates surface from body, so the default hairline is removed there\n * (see the [data-theme='dark'] rule below) \u2014 no over-adding. The default\n * (no [data-theme]) light palette keeps the hairline, matching light mode.\n */\n [data-civitai-ui='card'] {\n background: var(--civitai-color-surface);\n border-radius: var(--civitai-radius);\n color: var(--civitai-color-text);\n border: 1px solid color-mix(in srgb, var(--civitai-color-border) 55%, transparent);\n\n &[data-with-border='true'] {\n border: 1px solid var(--civitai-color-border);\n }\n &[data-padding='sm'] { padding: 10px; }\n &[data-padding='md'] { padding: 16px; }\n &[data-padding='lg'] { padding: 24px; }\n }\n /*\n * Dark already differentiates surface (#1A1B1E) from body \u2014 drop the default\n * hairline on a borderless card so dark is visually unchanged. An explicit\n * `data-with-border` still renders its full border.\n */\n [data-theme='dark'] [data-civitai-ui='card']:not([data-with-border='true']) {\n border: 0;\n }\n\n /* ----- Stack / Group ----- */\n [data-civitai-ui='stack'] {\n display: flex;\n flex-direction: column;\n gap: 12px;\n\n &[data-gap='sm'] { gap: 8px; }\n &[data-gap='md'] { gap: 16px; }\n &[data-gap='lg'] { gap: 24px; }\n }\n [data-civitai-ui='group'] {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 8px;\n\n /*\n * RESPONSIVE BASE. A group is a ROW, and a row of three or more controls\n * has no escape at phone widths \u2014 a block's slot is ~360px there, so the\n * row simply ran off the edge.\n *\n * \uD83D\uDD34 There are THREE `group` surfaces, and for TWO of them this is a NEW\n * DEFAULT \u2014 do not read it as the CSS merely catching up:\n * - `@civitai/blocks-react`'s `<Group>` defaults `wrap = true` and writes\n * `flex-wrap` as an INLINE style, so its consumers already wrapped.\n * For that one, and only that one, this is the CSS agreeing with a\n * default that was already shipping.\n * - `@civitai/components-react`'s `<Group>` writes NO inline style and has\n * NO `wrap` prop, so it resolves against this rule \u2014 a React consumer\n * that did NOT wrap before.\n * - bare `data-civitai-ui=\"group\"` markup, the framework-agnostic contract\n * this package exists to serve \u2014 likewise.\n * Nothing could see the disagreement, because each surface was only ever\n * tested against itself. TWO tests now pin it, one per React surface \u2014 there\n * is no single \"the parity test\":\n * - `civitai-blocks-react/test/Group.test.tsx` reads the wrap default off\n * ITS `<Group>` and compares it to this rule, so flipping either alone\n * fails;\n * - `civitai-components-react/test/html-vs-react-parity.browser.test.tsx`\n * (\"styling anchors \u2014 Group\") asserts the COMPUTED value on both the\n * React and the bare-markup render, which is what covers the other two\n * surfaces. A parity check alone cannot: deleting this rule moves both\n * of its arms identically, so only the absolute anchor sees it.\n *\n * \uD83D\uDD34 The three-surface distinction is load-bearing for the RELEASE, not just\n * the prose: \"existing callers notice\" is what `RELEASING.md` reserves\n * `major` for, and it is true for two of the three surfaces. Shipped as a\n * MINOR by maintainer decision \u2014 see the changeset for the reasoning and for\n * the upgrade note that decision makes load-bearing.\n *\n * Opt out with `data-nowrap=\"true\"`. An inline `flex-wrap` also still\n * outranks this layer, so existing markup that already sets it is untouched.\n */\n flex-wrap: wrap;\n\n /*\n * A flex item defaults to `min-width: auto`, which refuses to shrink below\n * its CONTENT width \u2014 so one long label (a model name, a prompt) pushes the\n * row past the slot even WITH wrap. `min-width: 0` lets it shrink.\n *\n * \uD83D\uDD34 SCOPE, measured: this only matters for a child whose computed\n * `overflow` is `visible`. Per CSS Flexbox \u00A74.5 an item with any other\n * `overflow` ALREADY has an automatic minimum size of 0 \u2014 so a child that\n * sets `overflow: hidden`, as any `text-overflow: ellipsis` child must, is\n * unaffected by this line. An earlier version of this comment claimed\n * ellipsis \"can never engage\" without it, which was exactly backwards; the\n * browser guard built on that belief used an ellipsis fixture and therefore\n * passed with this rule DELETED. Both are fixed \u2014 see\n * `civitai-blocks-react/test/responsive-group.browser.test.tsx`.\n *\n * Specificity is (0,1,0), not zero: `&` carries the parent selector's, and\n * `:where(*)` is specificity-identical to `*`, so that wrapper bought\n * nothing and has been dropped. What actually lets a CONSUMER's own rule\n * win is `@layer civitai.components`, which every rule in this file sits\n * inside \u2014 unlayered author CSS beats it regardless of specificity. Note\n * the limit: another rule INSIDE this layer does not get that protection,\n * so a future non-zero `min-width` on a nested component here must be more\n * specific or come later.\n */\n & > * { min-width: 0; }\n\n &[data-nowrap='true'] { flex-wrap: nowrap; }\n\n &[data-gap='sm'] { gap: 6px; }\n &[data-gap='md'] { gap: 16px; }\n &[data-gap='lg'] { gap: 24px; }\n }\n\n /* ----- Alert ----- */\n [data-civitai-ui='alert'] {\n display: flex;\n gap: 10px;\n align-items: flex-start;\n padding: 12px 14px;\n border-radius: var(--civitai-radius);\n border: 1px solid transparent;\n font-size: 14px;\n color: var(--civitai-color-text);\n\n /*\n * Documented DEFAULT (MARKUP.md): color=info (default intent). Applied on\n * the base rule so a BARE alert \u2014 `<div data-civitai-ui=\"alert\">` with no\n * `data-color` \u2014 renders the info intent (tinted bg + border), not just the\n * neutral chrome. The explicit `[data-color]` rules below OVERRIDE (success\n * / warning / error each set both bg + border-color), so the info default\n * only ever wins when `data-color` is omitted.\n */\n background: color-mix(in srgb, var(--civitai-color-info) 12%, transparent);\n border-color: color-mix(in srgb, var(--civitai-color-info) 35%, transparent);\n\n &[data-color='info'] {\n background: color-mix(in srgb, var(--civitai-color-info) 12%, transparent);\n border-color: color-mix(in srgb, var(--civitai-color-info) 35%, transparent);\n }\n &[data-color='success'] {\n background: color-mix(in srgb, var(--civitai-color-success) 12%, transparent);\n border-color: color-mix(in srgb, var(--civitai-color-success) 35%, transparent);\n }\n &[data-color='warning'] {\n background: color-mix(in srgb, var(--civitai-color-warning) 14%, transparent);\n border-color: color-mix(in srgb, var(--civitai-color-warning) 35%, transparent);\n }\n &[data-color='error'] {\n background: color-mix(in srgb, var(--civitai-color-error) 12%, transparent);\n border-color: color-mix(in srgb, var(--civitai-color-error) 35%, transparent);\n }\n & [data-civitai-ui-alert-body] {\n flex: 1;\n min-width: 0;\n }\n & [data-civitai-ui-alert-title] {\n font-weight: 600;\n margin-bottom: 2px;\n }\n & [data-civitai-ui-alert-close] {\n background: transparent;\n border: none;\n cursor: pointer;\n color: inherit;\n font-size: 16px;\n line-height: 1;\n padding: 0;\n opacity: 0.7;\n\n &:hover { opacity: 1; }\n }\n }\n\n /* ----- Loader ----- */\n [data-civitai-ui='loader'] {\n display: inline-block;\n border-radius: 50%;\n border-style: solid;\n border-color: color-mix(in srgb, currentColor 25%, transparent);\n border-top-color: currentColor;\n color: var(--civitai-color-primary);\n animation: civitai-ui-spin 0.7s linear infinite;\n\n /*\n * Documented DEFAULT (MARKUP.md): size=md. Applied on the base rule so a\n * BARE loader \u2014 `<span data-civitai-ui=\"loader\">` with no `data-size` \u2014 has\n * real dimensions (otherwise 0\u00D70 and invisible). sm/lg OVERRIDE below.\n */\n width: 22px;\n height: 22px;\n border-width: 3px;\n\n &[data-size='sm'] { width: 16px; height: 16px; border-width: 2px; }\n &[data-size='md'] { width: 22px; height: 22px; border-width: 3px; }\n &[data-size='lg'] { width: 32px; height: 32px; border-width: 4px; }\n }\n [data-civitai-ui='button'] [data-civitai-ui='loader'] {\n color: currentColor;\n }\n @keyframes civitai-ui-spin {\n to { transform: rotate(360deg); }\n }\n\n /* ----- Badge ----- */\n [data-civitai-ui='badge'] {\n display: inline-flex;\n align-items: center;\n border: 1px solid transparent;\n border-radius: 999px;\n font-weight: 600;\n line-height: 1;\n white-space: nowrap;\n text-transform: uppercase;\n letter-spacing: 0.02em;\n\n /*\n * Documented DEFAULTS (MARKUP.md): variant=filled, size=md. Applied on the\n * base rule so a BARE badge \u2014 `<span data-civitai-ui=\"badge\">` with no\n * `data-variant`/`data-size` (MARKUP's own minimal example) \u2014 renders\n * filled + md (padding + primary fill), not an unstyled, zero-padding pill.\n * The explicit `[data-variant]`/`[data-size]` rules below OVERRIDE. Because\n * the base now carries a primary `border-color`, the `light` variant (which\n * previously relied on the base transparent border) explicitly resets it to\n * transparent so light badges are visually unchanged.\n */\n height: 22px;\n padding: 0 10px;\n font-size: 11px;\n background: var(--civitai-color-primary);\n color: var(--civitai-color-primary-fg);\n border-color: var(--civitai-color-primary);\n\n &[data-size='sm'] { height: 18px; padding: 0 8px; font-size: 10px; }\n &[data-size='md'] { height: 22px; padding: 0 10px; font-size: 11px; }\n &[data-size='lg'] { height: 26px; padding: 0 12px; font-size: 13px; }\n &[data-variant='filled'] {\n background: var(--civitai-color-primary);\n color: var(--civitai-color-primary-fg);\n border-color: var(--civitai-color-primary);\n }\n &[data-variant='light'] {\n background: color-mix(in srgb, var(--civitai-color-primary) 14%, transparent);\n color: var(--civitai-color-primary);\n border-color: transparent;\n }\n &[data-variant='outline'] {\n background: transparent;\n color: var(--civitai-color-primary);\n border-color: var(--civitai-color-primary);\n }\n\n /*\n * Intent color via `data-color`, mirroring Alert's `data-color` contract\n * (info / success / warning / error). Absent `data-color` => the default\n * primary above (non-breaking). Each intent recolors the `filled`, `light`\n * and `outline` variants with the same `color-mix()` token approach Alert\n * uses; `filled` keeps the white `--civitai-color-primary-fg` text. These\n * `[data-color][data-variant]` rules out-specify the plain-variant rules\n * above, so order-independence holds.\n */\n &[data-color='info'] {\n &[data-variant='filled'] {\n background: var(--civitai-color-info);\n border-color: var(--civitai-color-info);\n }\n &[data-variant='light'] {\n background: color-mix(in srgb, var(--civitai-color-info) 14%, transparent);\n color: var(--civitai-color-info);\n }\n &[data-variant='outline'] {\n color: var(--civitai-color-info);\n border-color: var(--civitai-color-info);\n }\n }\n &[data-color='success'] {\n &[data-variant='filled'] {\n background: var(--civitai-color-success);\n border-color: var(--civitai-color-success);\n }\n &[data-variant='light'] {\n background: color-mix(in srgb, var(--civitai-color-success) 14%, transparent);\n color: var(--civitai-color-success);\n }\n &[data-variant='outline'] {\n color: var(--civitai-color-success);\n border-color: var(--civitai-color-success);\n }\n }\n &[data-color='warning'] {\n &[data-variant='filled'] {\n background: var(--civitai-color-warning);\n border-color: var(--civitai-color-warning);\n }\n &[data-variant='light'] {\n background: color-mix(in srgb, var(--civitai-color-warning) 14%, transparent);\n color: var(--civitai-color-warning);\n }\n &[data-variant='outline'] {\n color: var(--civitai-color-warning);\n border-color: var(--civitai-color-warning);\n }\n }\n &[data-color='error'] {\n &[data-variant='filled'] {\n background: var(--civitai-color-error);\n border-color: var(--civitai-color-error);\n }\n &[data-variant='light'] {\n background: color-mix(in srgb, var(--civitai-color-error) 14%, transparent);\n color: var(--civitai-color-error);\n }\n &[data-variant='outline'] {\n color: var(--civitai-color-error);\n border-color: var(--civitai-color-error);\n }\n }\n }\n\n /* ----- Slider ----- */\n /*\n * A themed native <input type=\"range\">: `accent-color` carries the primary\n * tint (native control, so keyboard arrow-key nav + ARIA come for free), plus\n * a full-width track, a token focus ring, and disabled/invalid states. Like\n * checkbox/radio, the range input deliberately does NOT carry\n * `data-civitai-ui-control` (that is the bordered field-input chrome). The\n * label/description/error reuse the shared field markers, laid out in a\n * column; an optional value read-out sits inline with the label in a header.\n */\n [data-civitai-ui='slider'] {\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n [data-civitai-ui-slider-header] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n }\n [data-civitai-ui-slider-value] {\n font-size: 13px;\n font-weight: 600;\n color: var(--civitai-color-text-dimmed);\n font-variant-numeric: tabular-nums;\n }\n [data-civitai-ui='slider'] input[type='range'] {\n -webkit-appearance: none;\n appearance: none;\n width: 100%;\n height: 6px;\n margin: 6px 0;\n padding: 0;\n border-radius: 999px;\n accent-color: var(--civitai-color-primary);\n background: var(--civitai-color-gray-2);\n cursor: pointer;\n }\n [data-theme='dark'] [data-civitai-ui='slider'] input[type='range'] {\n background: var(--civitai-color-surface-2);\n }\n [data-civitai-ui='slider'] input[type='range']:focus-visible {\n outline: 2px solid var(--civitai-color-primary);\n outline-offset: 4px;\n }\n [data-civitai-ui='slider'] input[type='range']:disabled {\n cursor: not-allowed;\n opacity: 0.6;\n accent-color: var(--civitai-color-gray-5);\n }\n [data-civitai-ui='slider'][data-invalid] input[type='range'] {\n accent-color: var(--civitai-color-error);\n }\n\n /* ----- SegmentedControl / Tabs ----- */\n /*\n * A `role=tablist` of segment buttons (`role=tab`). Presentational chrome\n * only \u2014 the roving-tabindex + arrow-key nav + selection follow-focus live in\n * the React binding (or must be author-provided for hand HTML). The selected\n * segment (`aria-selected='true'`) lifts to the surface color with a soft\n * shadow; unselected segments read dimmed. Paired tab panels use\n * `data-civitai-ui-tabpanel` (role=tabpanel).\n */\n [data-civitai-ui='segmented-control'] {\n display: inline-flex;\n gap: 2px;\n padding: 4px;\n background: var(--civitai-color-gray-1);\n border-radius: var(--civitai-radius);\n max-width: 100%;\n overflow-x: auto;\n }\n [data-theme='dark'] [data-civitai-ui='segmented-control'] {\n background: var(--civitai-color-surface-2);\n }\n [data-civitai-ui-segment] {\n -webkit-appearance: none;\n appearance: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: 0;\n background: transparent;\n color: var(--civitai-color-text-dimmed);\n font-family: var(--civitai-font);\n font-weight: 600;\n line-height: 1;\n white-space: nowrap;\n cursor: pointer;\n border-radius: calc(var(--civitai-radius) - 1px);\n transition: background-color 120ms ease, color 120ms ease;\n\n /* Documented DEFAULT (MARKUP.md): size=md. On the base rule so a bare\n segment renders at md height, not zero-height. sm/lg override below. */\n height: 30px;\n padding: 0 14px;\n font-size: 13px;\n\n &[data-size='sm'] { height: 26px; padding: 0 10px; font-size: 12px; }\n &[data-size='md'] { height: 30px; padding: 0 14px; font-size: 13px; }\n &[data-size='lg'] { height: 38px; padding: 0 18px; font-size: 15px; }\n\n &:hover:not(:disabled):not([aria-selected='true']):not([aria-checked='true']) {\n color: var(--civitai-color-text);\n }\n /* Selected state \u2014 `aria-selected` in tabs mode (role=tab) OR `aria-checked`\n in toggle mode (role=radio). Same visual treatment for both. */\n &[aria-selected='true'],\n &[aria-checked='true'] {\n background: var(--civitai-color-surface);\n color: var(--civitai-color-text);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);\n }\n &:focus-visible {\n outline: 2px solid var(--civitai-color-primary);\n outline-offset: 2px;\n }\n &:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n }\n [data-civitai-ui-tabpanel] {\n color: var(--civitai-color-text);\n\n &:focus-visible {\n outline: 2px solid var(--civitai-color-primary);\n outline-offset: 2px;\n }\n &[hidden] { display: none; }\n }\n\n /* ----- Toast ----- */\n /*\n * `toast-region` is the fixed-position `aria-live` host (bottom-right stack).\n * The behavior \u2014 enqueue, auto-dismiss timers, portal \u2014 lives in the React\n * binding's ToastProvider/useToast; the presentational `toast` is a standalone\n * component so hand HTML and React share one visual contract. Each toast is\n * opaque (it sits over content) with an intent-colored left accent, mirroring\n * Alert's `data-color` set.\n */\n [data-civitai-ui='toast-region'] {\n position: fixed;\n bottom: 16px;\n right: 16px;\n z-index: 9999;\n display: flex;\n flex-direction: column;\n gap: 8px;\n width: min(92vw, 380px);\n pointer-events: none;\n }\n [data-civitai-ui='toast'] {\n pointer-events: auto;\n display: flex;\n gap: 10px;\n align-items: flex-start;\n padding: 12px 14px;\n border-radius: var(--civitai-radius);\n background: var(--civitai-color-surface);\n color: var(--civitai-color-text);\n border: 1px solid var(--civitai-color-border);\n border-left: 4px solid var(--civitai-color-border);\n box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);\n font-size: 14px;\n\n &[data-color='info'] { border-left-color: var(--civitai-color-info); }\n &[data-color='success'] { border-left-color: var(--civitai-color-success); }\n &[data-color='warning'] { border-left-color: var(--civitai-color-warning); }\n &[data-color='error'] { border-left-color: var(--civitai-color-error); }\n\n & [data-civitai-ui-toast-body] {\n flex: 1;\n min-width: 0;\n }\n & [data-civitai-ui-toast-title] {\n font-weight: 600;\n margin-bottom: 2px;\n }\n & [data-civitai-ui-toast-close] {\n background: transparent;\n border: none;\n cursor: pointer;\n color: inherit;\n font-size: 16px;\n line-height: 1;\n padding: 0;\n opacity: 0.7;\n\n &:hover { opacity: 1; }\n }\n }\n\n /* ----- Tooltip ----- */\n /*\n * A hover/focus tooltip: a positioned `role=tooltip` bubble revealed when the\n * wrapper is hovered or contains focus (pure-CSS reveal), or explicitly via\n * `data-open='true'` (the React binding also wires `aria-describedby` +\n * Escape-to-dismiss). The bubble is a dark chip (gray-9) with light text in\n * both themes.\n */\n [data-civitai-ui='tooltip'] {\n position: relative;\n display: inline-flex;\n }\n [data-civitai-ui-tooltip-bubble] {\n position: absolute;\n bottom: calc(100% + 6px);\n left: 50%;\n transform: translateX(-50%);\n z-index: 200;\n max-width: 260px;\n width: max-content;\n padding: 4px 8px;\n border-radius: var(--civitai-radius);\n background: var(--civitai-color-gray-9);\n color: var(--civitai-color-primary-fg);\n font-size: 12px;\n font-weight: 500;\n line-height: 1.4;\n text-align: center;\n pointer-events: none;\n opacity: 0;\n visibility: hidden;\n transition: opacity 120ms ease;\n }\n /*\n * Reveal on hover/focus or an explicit `data-open='true'` \u2014 BUT\n * `data-dismissed='true'` overrides (gates every reveal selector), so the\n * React binding's Escape-to-dismiss can force-HIDE the bubble even while the\n * pointer still hovers or focus is still within. The dismissed flag is cleared\n * on the next hover/focus so the tooltip can re-open normally.\n */\n [data-civitai-ui='tooltip']:hover [data-civitai-ui-tooltip-bubble]:not([data-dismissed='true']),\n [data-civitai-ui='tooltip']:focus-within [data-civitai-ui-tooltip-bubble]:not([data-dismissed='true']),\n [data-civitai-ui-tooltip-bubble][data-open='true']:not([data-dismissed='true']) {\n opacity: 1;\n visibility: visible;\n }\n\n /* ----- Image ----- */\n /*\n * A media container with a token placeholder background (visible while the\n * image loads), object-fit control, and a broken-image fallback. `data-status`\n * (loading|loaded|error) \u2014 set by the React binding's onLoad/onError, or by\n * the author for hand HTML \u2014 fades the <img> in on load and swaps to the\n * fallback overlay on error. Bare markup (no `data-status`) shows the image.\n */\n [data-civitai-ui='image'] {\n position: relative;\n display: block;\n overflow: hidden;\n background: var(--civitai-color-gray-2);\n border-radius: var(--civitai-radius);\n }\n [data-theme='dark'] [data-civitai-ui='image'] {\n background: var(--civitai-color-surface-2);\n }\n [data-civitai-ui-image-img] {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: cover;\n opacity: 1;\n transition: opacity 200ms ease;\n\n &[data-fit='contain'] { object-fit: contain; }\n &[data-fit='cover'] { object-fit: cover; }\n }\n [data-civitai-ui='image'][data-status='loading'] [data-civitai-ui-image-img],\n [data-civitai-ui='image'][data-status='error'] [data-civitai-ui-image-img] {\n opacity: 0;\n }\n [data-civitai-ui-image-fallback] {\n position: absolute;\n inset: 0;\n display: none;\n align-items: center;\n justify-content: center;\n padding: 8px;\n color: var(--civitai-color-text-dimmed);\n font-size: 13px;\n text-align: center;\n }\n [data-civitai-ui='image'][data-status='error'] [data-civitai-ui-image-fallback] {\n display: flex;\n }\n}\n\n\n/* ----- Select / Slider field wrappers -----\n The shared -control/-label/-required/-description/-error primitives come from\n @civitai/components (layered). Only the select/slider WRAPPERS live here \u2014 the\n design-system package scopes its field-wrapper rule to the presentational\n inputs (text-input/textarea/number-input). */\n[data-civitai-ui='select'],\n[data-civitai-ui='slider'] {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n/* ----- Modal ----- */\n[data-civitai-ui='modal-overlay'] {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.55);\n display: flex;\n align-items: flex-start;\n justify-content: center;\n padding: 32px 16px;\n overflow-y: auto;\n z-index: 1000;\n}\n[data-civitai-ui='modal'] {\n background: var(--civitai-color-surface);\n color: var(--civitai-color-text);\n border: 1px solid var(--civitai-color-border);\n border-radius: var(--civitai-radius);\n box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);\n width: 100%;\n max-width: 440px;\n outline: none;\n}\n[data-civitai-ui='modal'][data-size='sm'] { max-width: 340px; }\n[data-civitai-ui='modal'][data-size='md'] { max-width: 440px; }\n[data-civitai-ui='modal'][data-size='lg'] { max-width: 620px; }\n[data-civitai-ui='modal'] [data-civitai-ui-modal-header] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 16px 18px;\n border-bottom: 1px solid var(--civitai-color-border);\n}\n[data-civitai-ui='modal'] [data-civitai-ui-modal-title] {\n font-size: 16px;\n font-weight: 700;\n margin: 0;\n}\n[data-civitai-ui='modal'] [data-civitai-ui-modal-close] {\n background: transparent;\n border: none;\n cursor: pointer;\n color: var(--civitai-color-text-dimmed);\n font-size: 20px;\n line-height: 1;\n padding: 0;\n}\n[data-civitai-ui='modal'] [data-civitai-ui-modal-close]:hover {\n color: var(--civitai-color-text);\n}\n[data-civitai-ui='modal'] [data-civitai-ui-modal-body] {\n padding: 18px;\n}\n\n/* ----- Slider ----- */\n[data-civitai-ui='slider'] [data-civitai-ui-label] {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n gap: 8px;\n}\n[data-civitai-ui-slider-value] {\n font-weight: 500;\n color: var(--civitai-color-text-dimmed);\n font-variant-numeric: tabular-nums;\n}\n[data-civitai-ui-range] {\n width: 100%;\n margin: 0;\n accent-color: var(--civitai-color-primary);\n cursor: pointer;\n}\n[data-civitai-ui-range]:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n[data-civitai-ui-range]:focus-visible {\n outline: 2px solid var(--civitai-color-primary);\n outline-offset: 2px;\n}\n\n/* ----- Collapse ----- */\n[data-civitai-ui='collapse'] [data-civitai-ui-collapse-trigger] {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 6px 0;\n background: transparent;\n border: none;\n font-family: var(--civitai-font);\n font-size: 14px;\n font-weight: 600;\n color: var(--civitai-color-text);\n text-align: left;\n cursor: pointer;\n}\n[data-civitai-ui='collapse'] [data-civitai-ui-collapse-trigger]:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n[data-civitai-ui='collapse'] [data-civitai-ui-collapse-chevron] {\n display: inline-block;\n width: 1em;\n color: var(--civitai-color-text-dimmed);\n}\n[data-civitai-ui='collapse'] [data-civitai-ui-collapse-region] {\n padding-top: 4px;\n}\n\n/* ----- ResourceCard -----\n Two variants off ONE box. data-variant='card' stacks (grid tile),\n data-variant='row' runs inline (compact list line). Every selector is\n double-qualified with [data-civitai-ui='resource-card'] on purpose:\n data-variant is also emitted by Button and Badge, so a bare\n [data-variant='card'] would reach across the whole pack.\n NOTE: this block lives inside a JS TEMPLATE LITERAL. No backticks, and no\n dollar-brace, or the string ends here and the file stops parsing as\n TypeScript (backticks in this comment did exactly that once). */\n[data-civitai-ui='resource-card'] {\n display: flex;\n box-sizing: border-box;\n /* The positioned ancestor for the overlay slot. It lives on the ROOT, not on\n the thumbnail frame, so the overlay can be a SIBLING of the hit <button>\n rather than a descendant of it \u2014 see the overlay prop's doc for why that\n distinction is the whole point of the slot. */\n position: relative;\n font-family: var(--civitai-font);\n color: var(--civitai-color-text);\n background: var(--civitai-color-surface);\n border: 1px solid var(--civitai-color-border);\n border-radius: var(--civitai-radius);\n overflow: hidden;\n}\n[data-civitai-ui='resource-card'][data-variant='card'] {\n flex-direction: column;\n align-items: stretch;\n}\n[data-civitai-ui='resource-card'][data-variant='row'] {\n flex-direction: row;\n align-items: center;\n gap: 8px;\n padding: 6px 8px;\n}\n[data-civitai-ui='resource-card'][data-selected='true'] {\n border-color: var(--civitai-color-primary);\n}\n[data-civitai-ui='resource-card'][data-disabled='true'] {\n opacity: 0.6;\n}\n/* The hit area. A <button> when interactive, a <div> when not \u2014 both are reset\n to the same box so the two variants lay out identically either way. */\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-hit] {\n display: flex;\n /* \uD83D\uDD34 A flex ITEM defaults to min-width:auto, which refuses to shrink below its\n content \u2014 so without this the name's nowrap+ellipsis never engages and a\n long model name blows the card out of its grid cell instead of truncating. */\n min-width: 0;\n box-sizing: border-box;\n margin: 0;\n border: none;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n}\n[data-civitai-ui='resource-card'][data-variant='card'] [data-civitai-ui-resource-hit] {\n flex-direction: column;\n align-items: stretch;\n gap: 6px;\n padding: 6px;\n}\n[data-civitai-ui='resource-card'][data-variant='row'] [data-civitai-ui-resource-hit] {\n flex: 1 1 auto;\n flex-direction: row;\n align-items: center;\n gap: 8px;\n padding: 0;\n}\n[data-civitai-ui='resource-card'] button[data-civitai-ui-resource-hit] {\n cursor: pointer;\n}\n[data-civitai-ui='resource-card'] button[data-civitai-ui-resource-hit]:disabled {\n cursor: not-allowed;\n}\n[data-civitai-ui='resource-card'] button[data-civitai-ui-resource-hit]:focus-visible {\n outline: 2px solid var(--civitai-color-primary);\n outline-offset: -2px;\n}\n/* The thumbnail frame. Rendered whether or not there is an image \u2014 see the\n component's thumbnailUrl doc: BlockResourceInfo has no image field, so \"no\n image\" is the COMMON case and the frame must keep its box regardless. */\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-thumb] {\n display: flex;\n align-items: center;\n justify-content: center;\n flex: none;\n overflow: hidden;\n background: var(--civitai-color-surface-2);\n border-radius: calc(var(--civitai-radius) - 1px);\n color: var(--civitai-color-text-dimmed);\n}\n[data-civitai-ui='resource-card'][data-variant='card'] [data-civitai-ui-resource-thumb] {\n /* \uD83D\uDD34 aspect-ratio, not a fixed height: the tile is grid-sized by the caller,\n and this is the line that stops a thumbnail-less card collapsing to a\n text-height sliver. */\n width: 100%;\n aspect-ratio: 1 / 1;\n font-size: 11px;\n}\n[data-civitai-ui='resource-card'][data-variant='row'] [data-civitai-ui-resource-thumb] {\n width: 36px;\n height: 36px;\n font-size: 9px;\n}\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-thumb] img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n}\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-placeholder] {\n padding: 0 4px;\n text-align: center;\n line-height: 1.2;\n}\n/* Positioned from the ROOT, over the thumbnail corner. 10px = the hit area's\n 6px padding plus a 4px inset inside the frame; the three move together, and\n the browser tier asserts the result lands inside the frame's own rect rather\n than trusting the arithmetic.\n pointer-events: none is load-bearing, not polish: this slot is STATUS, and a\n pill that can swallow a click meant for the card is the same class of bug as\n nesting a control inside the hit <button>. A consumer who really wants an\n interactive badge opts back in with pointer-events: auto on their own node. */\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-overlay] {\n position: absolute;\n top: 10px;\n right: 10px;\n z-index: 1;\n pointer-events: none;\n display: flex;\n align-items: center;\n gap: 4px;\n max-width: calc(100% - 20px);\n font-size: 11px;\n line-height: 1;\n}\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-nameline] {\n display: flex;\n align-items: center;\n gap: 4px;\n min-width: 0;\n}\n/* The non-colour half of the selected affordance (WCAG 1.4.1). The border-hue\n change on the root is reinforcement; this glyph is what carries it. */\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-selected] {\n flex: none;\n font-size: 12px;\n font-weight: 700;\n line-height: 1;\n color: var(--civitai-color-primary);\n}\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-text] {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n flex: 1 1 auto;\n}\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-name] {\n display: block;\n font-size: 13px;\n font-weight: 600;\n line-height: 1.3;\n flex: 1 1 auto;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-meta] {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 4px 6px;\n min-width: 0;\n font-size: 11px;\n color: var(--civitai-color-text-dimmed);\n}\n[data-civitai-ui='resource-card'] [data-civitai-ui-resource-actions] {\n display: flex;\n align-items: center;\n flex: none;\n gap: 6px;\n}\n[data-civitai-ui='resource-card'][data-variant='card'] [data-civitai-ui-resource-actions] {\n padding: 0 6px 6px;\n}\n\n/* ----- SegmentedControl ----- */\n[data-civitai-ui='segmented-control'] {\n display: inline-flex;\n flex-direction: row;\n gap: 2px;\n padding: 3px;\n background: var(--civitai-color-surface-2);\n border: 1px solid var(--civitai-color-border);\n border-radius: var(--civitai-radius);\n vertical-align: middle;\n}\n[data-civitai-ui='segmented-control'][data-full-width='true'] {\n display: flex;\n width: 100%;\n}\n[data-civitai-ui='segmented-control'] [data-civitai-ui-segment] {\n flex: 0 0 auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: none;\n border-radius: calc(var(--civitai-radius) - 3px);\n background: transparent;\n color: var(--civitai-color-text-dimmed);\n font-family: var(--civitai-font);\n font-weight: 600;\n line-height: 1;\n white-space: nowrap;\n cursor: pointer;\n user-select: none;\n transition: background-color 120ms ease, color 120ms ease, box-shadow 120ms ease;\n}\n[data-civitai-ui='segmented-control'][data-full-width='true'] [data-civitai-ui-segment] {\n flex: 1 1 0;\n}\n[data-civitai-ui='segmented-control'][data-size='sm'] [data-civitai-ui-segment] { height: 24px; padding: 0 12px; font-size: 13px; }\n[data-civitai-ui='segmented-control'][data-size='md'] [data-civitai-ui-segment] { height: 30px; padding: 0 16px; font-size: 14px; }\n[data-civitai-ui='segmented-control'][data-size='lg'] [data-civitai-ui-segment] { height: 38px; padding: 0 20px; font-size: 16px; }\n[data-civitai-ui='segmented-control'] [data-civitai-ui-segment]:hover:not(:disabled):not([data-active]) {\n color: var(--civitai-color-text);\n background: color-mix(in srgb, var(--civitai-color-text) 6%, transparent);\n}\n[data-civitai-ui='segmented-control'] [data-civitai-ui-segment][data-active] {\n background: var(--civitai-color-surface);\n color: var(--civitai-color-primary);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);\n}\n[data-civitai-ui='segmented-control'] [data-civitai-ui-segment]:focus-visible {\n outline: 2px solid var(--civitai-color-primary);\n outline-offset: 1px;\n}\n[data-civitai-ui='segmented-control'] [data-civitai-ui-segment]:disabled {\n opacity: 0.55;\n cursor: not-allowed;\n}\n[data-civitai-ui='segmented-control'][data-disabled='true'] {\n opacity: 0.7;\n}\n";
9
9
  /**
10
10
  * Inject the pack's stylesheets into a document's `<head>`, idempotently.
11
11
  *
@@ -1 +1 @@
1
- {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/ui/styles.ts"],"names":[],"mappings":"AAqPA;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,4r3CAA0D,CAAC;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,CAAC,EAAE,QAAQ,GAAG,IAAI,CAmBvD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAItC"}
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/ui/styles.ts"],"names":[],"mappings":"AAwbA;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,8wkDAA0D,CAAC;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,CAAC,EAAE,QAAQ,GAAG,IAAI,CAmBvD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAItC"}
package/dist/ui/styles.js CHANGED
@@ -20,10 +20,11 @@ import { tokensCss } from '@civitai/theme';
20
20
  * presentational components (Button, TextInput, Textarea, NumberInput,
21
21
  * Card, Stack, Group, Alert, Loader, Badge), wrapped in
22
22
  * `@layer civitai.components`.
23
- * Only the 5 INTERACTIVE components that live entirely in this package
24
- * (Modal, Select, Slider, Collapse, SegmentedControl) keep their CSS here,
25
- * repointed onto the `--civitai-*` tokens. This is a VISIBLE repaint — the
26
- * design-system tokens differ from the pack's retired `--ci-*` palette
23
+ * Only the components that live entirely in this package keep their CSS here,
24
+ * repointed onto the `--civitai-*` tokens: the original interactive-5
25
+ * (Modal, Select, Slider, Collapse, SegmentedControl) plus ResourceCard,
26
+ * which has no `@civitai/components` counterpart. This is a VISIBLE repaint —
27
+ * the design-system tokens differ from the pack's retired `--ci-*` palette
27
28
  * (radius 8px→4px, teal success, dark primary `#1971C2`, etc.).
28
29
  *
29
30
  * `injectBlocksStyles()` injects THREE `<style>` elements, each with its own
@@ -42,9 +43,14 @@ import { tokensCss } from '@civitai/theme';
42
43
  /** Marker attribute on the injected interactive-5 `<style>` so injection is idempotent. */
43
44
  const STYLE_MARKER = 'data-civitai-blocks-ui';
44
45
  /**
45
- * Interactive-5 component CSS — the styles that live ONLY in this package
46
- * (Modal / Select / Slider / Collapse / SegmentedControl). Repointed onto the
47
- * `--civitai-*` tokens supplied by `@civitai/theme`.
46
+ * Package-local component CSS — the styles that live ONLY in this package
47
+ * (Modal / Select / Slider / Collapse / SegmentedControl, plus ResourceCard).
48
+ * Repointed onto the `--civitai-*` tokens supplied by `@civitai/theme`.
49
+ *
50
+ * (The name `INTERACTIVE_STYLES` and the `interactive-5` label elsewhere are
51
+ * historical: ResourceCard is only sometimes interactive, and joined the block
52
+ * because `@civitai/components` has no counterpart for it, not because it is a
53
+ * sixth control.)
48
54
  *
49
55
  * These rules are intentionally UNLAYERED (unlike @civitai/components' rules,
50
56
  * which live in `@layer civitai.components`). Their selectors are DISJOINT from
@@ -179,6 +185,195 @@ const INTERACTIVE_STYLES = `
179
185
  padding-top: 4px;
180
186
  }
181
187
 
188
+ /* ----- ResourceCard -----
189
+ Two variants off ONE box. data-variant='card' stacks (grid tile),
190
+ data-variant='row' runs inline (compact list line). Every selector is
191
+ double-qualified with [data-civitai-ui='resource-card'] on purpose:
192
+ data-variant is also emitted by Button and Badge, so a bare
193
+ [data-variant='card'] would reach across the whole pack.
194
+ NOTE: this block lives inside a JS TEMPLATE LITERAL. No backticks, and no
195
+ dollar-brace, or the string ends here and the file stops parsing as
196
+ TypeScript (backticks in this comment did exactly that once). */
197
+ [data-civitai-ui='resource-card'] {
198
+ display: flex;
199
+ box-sizing: border-box;
200
+ /* The positioned ancestor for the overlay slot. It lives on the ROOT, not on
201
+ the thumbnail frame, so the overlay can be a SIBLING of the hit <button>
202
+ rather than a descendant of it — see the overlay prop's doc for why that
203
+ distinction is the whole point of the slot. */
204
+ position: relative;
205
+ font-family: var(--civitai-font);
206
+ color: var(--civitai-color-text);
207
+ background: var(--civitai-color-surface);
208
+ border: 1px solid var(--civitai-color-border);
209
+ border-radius: var(--civitai-radius);
210
+ overflow: hidden;
211
+ }
212
+ [data-civitai-ui='resource-card'][data-variant='card'] {
213
+ flex-direction: column;
214
+ align-items: stretch;
215
+ }
216
+ [data-civitai-ui='resource-card'][data-variant='row'] {
217
+ flex-direction: row;
218
+ align-items: center;
219
+ gap: 8px;
220
+ padding: 6px 8px;
221
+ }
222
+ [data-civitai-ui='resource-card'][data-selected='true'] {
223
+ border-color: var(--civitai-color-primary);
224
+ }
225
+ [data-civitai-ui='resource-card'][data-disabled='true'] {
226
+ opacity: 0.6;
227
+ }
228
+ /* The hit area. A <button> when interactive, a <div> when not — both are reset
229
+ to the same box so the two variants lay out identically either way. */
230
+ [data-civitai-ui='resource-card'] [data-civitai-ui-resource-hit] {
231
+ display: flex;
232
+ /* 🔴 A flex ITEM defaults to min-width:auto, which refuses to shrink below its
233
+ content — so without this the name's nowrap+ellipsis never engages and a
234
+ long model name blows the card out of its grid cell instead of truncating. */
235
+ min-width: 0;
236
+ box-sizing: border-box;
237
+ margin: 0;
238
+ border: none;
239
+ background: transparent;
240
+ color: inherit;
241
+ font: inherit;
242
+ text-align: left;
243
+ }
244
+ [data-civitai-ui='resource-card'][data-variant='card'] [data-civitai-ui-resource-hit] {
245
+ flex-direction: column;
246
+ align-items: stretch;
247
+ gap: 6px;
248
+ padding: 6px;
249
+ }
250
+ [data-civitai-ui='resource-card'][data-variant='row'] [data-civitai-ui-resource-hit] {
251
+ flex: 1 1 auto;
252
+ flex-direction: row;
253
+ align-items: center;
254
+ gap: 8px;
255
+ padding: 0;
256
+ }
257
+ [data-civitai-ui='resource-card'] button[data-civitai-ui-resource-hit] {
258
+ cursor: pointer;
259
+ }
260
+ [data-civitai-ui='resource-card'] button[data-civitai-ui-resource-hit]:disabled {
261
+ cursor: not-allowed;
262
+ }
263
+ [data-civitai-ui='resource-card'] button[data-civitai-ui-resource-hit]:focus-visible {
264
+ outline: 2px solid var(--civitai-color-primary);
265
+ outline-offset: -2px;
266
+ }
267
+ /* The thumbnail frame. Rendered whether or not there is an image — see the
268
+ component's thumbnailUrl doc: BlockResourceInfo has no image field, so "no
269
+ image" is the COMMON case and the frame must keep its box regardless. */
270
+ [data-civitai-ui='resource-card'] [data-civitai-ui-resource-thumb] {
271
+ display: flex;
272
+ align-items: center;
273
+ justify-content: center;
274
+ flex: none;
275
+ overflow: hidden;
276
+ background: var(--civitai-color-surface-2);
277
+ border-radius: calc(var(--civitai-radius) - 1px);
278
+ color: var(--civitai-color-text-dimmed);
279
+ }
280
+ [data-civitai-ui='resource-card'][data-variant='card'] [data-civitai-ui-resource-thumb] {
281
+ /* 🔴 aspect-ratio, not a fixed height: the tile is grid-sized by the caller,
282
+ and this is the line that stops a thumbnail-less card collapsing to a
283
+ text-height sliver. */
284
+ width: 100%;
285
+ aspect-ratio: 1 / 1;
286
+ font-size: 11px;
287
+ }
288
+ [data-civitai-ui='resource-card'][data-variant='row'] [data-civitai-ui-resource-thumb] {
289
+ width: 36px;
290
+ height: 36px;
291
+ font-size: 9px;
292
+ }
293
+ [data-civitai-ui='resource-card'] [data-civitai-ui-resource-thumb] img {
294
+ width: 100%;
295
+ height: 100%;
296
+ object-fit: cover;
297
+ display: block;
298
+ }
299
+ [data-civitai-ui='resource-card'] [data-civitai-ui-resource-placeholder] {
300
+ padding: 0 4px;
301
+ text-align: center;
302
+ line-height: 1.2;
303
+ }
304
+ /* Positioned from the ROOT, over the thumbnail corner. 10px = the hit area's
305
+ 6px padding plus a 4px inset inside the frame; the three move together, and
306
+ the browser tier asserts the result lands inside the frame's own rect rather
307
+ than trusting the arithmetic.
308
+ pointer-events: none is load-bearing, not polish: this slot is STATUS, and a
309
+ pill that can swallow a click meant for the card is the same class of bug as
310
+ nesting a control inside the hit <button>. A consumer who really wants an
311
+ interactive badge opts back in with pointer-events: auto on their own node. */
312
+ [data-civitai-ui='resource-card'] [data-civitai-ui-resource-overlay] {
313
+ position: absolute;
314
+ top: 10px;
315
+ right: 10px;
316
+ z-index: 1;
317
+ pointer-events: none;
318
+ display: flex;
319
+ align-items: center;
320
+ gap: 4px;
321
+ max-width: calc(100% - 20px);
322
+ font-size: 11px;
323
+ line-height: 1;
324
+ }
325
+ [data-civitai-ui='resource-card'] [data-civitai-ui-resource-nameline] {
326
+ display: flex;
327
+ align-items: center;
328
+ gap: 4px;
329
+ min-width: 0;
330
+ }
331
+ /* The non-colour half of the selected affordance (WCAG 1.4.1). The border-hue
332
+ change on the root is reinforcement; this glyph is what carries it. */
333
+ [data-civitai-ui='resource-card'] [data-civitai-ui-resource-selected] {
334
+ flex: none;
335
+ font-size: 12px;
336
+ font-weight: 700;
337
+ line-height: 1;
338
+ color: var(--civitai-color-primary);
339
+ }
340
+ [data-civitai-ui='resource-card'] [data-civitai-ui-resource-text] {
341
+ display: flex;
342
+ flex-direction: column;
343
+ gap: 2px;
344
+ min-width: 0;
345
+ flex: 1 1 auto;
346
+ }
347
+ [data-civitai-ui='resource-card'] [data-civitai-ui-resource-name] {
348
+ display: block;
349
+ font-size: 13px;
350
+ font-weight: 600;
351
+ line-height: 1.3;
352
+ flex: 1 1 auto;
353
+ min-width: 0;
354
+ overflow: hidden;
355
+ text-overflow: ellipsis;
356
+ white-space: nowrap;
357
+ }
358
+ [data-civitai-ui='resource-card'] [data-civitai-ui-resource-meta] {
359
+ display: flex;
360
+ flex-wrap: wrap;
361
+ align-items: center;
362
+ gap: 4px 6px;
363
+ min-width: 0;
364
+ font-size: 11px;
365
+ color: var(--civitai-color-text-dimmed);
366
+ }
367
+ [data-civitai-ui='resource-card'] [data-civitai-ui-resource-actions] {
368
+ display: flex;
369
+ align-items: center;
370
+ flex: none;
371
+ gap: 6px;
372
+ }
373
+ [data-civitai-ui='resource-card'][data-variant='card'] [data-civitai-ui-resource-actions] {
374
+ padding: 0 6px 6px;
375
+ }
376
+
182
377
  /* ----- SegmentedControl ----- */
183
378
  [data-civitai-ui='segmented-control'] {
184
379
  display: inline-flex;
@@ -1 +1 @@
1
- {"version":3,"file":"styles.js","sourceRoot":"","sources":["../../src/ui/styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAElC,OAAO,EAAE,aAAa,EAAE,YAAY,IAAI,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC5F,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,2FAA2F;AAC3F,MAAM,YAAY,GAAG,wBAAwB,CAAC;AAE9C;;;;;;;;;;;;;;GAcG;AACH,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqL1B,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,SAAS,KAAK,aAAa,KAAK,kBAAkB,EAAE,CAAC;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAc;IAC/C,MAAM,MAAM,GACV,GAAG,IAAI,CAAC,OAAO,QAAQ,KAAK,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAClE,IAAI,CAAC,MAAM;QAAE,OAAO;IACpB,gFAAgF;IAChF,4EAA4E;IAC5E,sBAAsB,CAAC,MAAM,CAAC,CAAC;IAC/B,iEAAiE;IACjE,IAAI,MAAM,CAAC,aAAa,CAAC,SAAS,YAAY,GAAG,CAAC;QAAE,OAAO;IAC3D,MAAM,KAAK,GAAG,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;IAC5C,KAAK,CAAC,YAAY,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IACzC,KAAK,CAAC,WAAW,GAAG,kBAAkB,CAAC;IACvC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACnE,IAAI,IAAI,EAAE,CAAC;QACT,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IAC1B,CAAC;SAAM,CAAC;QACN,oEAAoE;QACpE,MAAM,CAAC,eAAe,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IAC5C,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe;IAC7B,SAAS,CAAC,GAAG,EAAE;QACb,kBAAkB,EAAE,CAAC;IACvB,CAAC,EAAE,EAAE,CAAC,CAAC;AACT,CAAC"}
1
+ {"version":3,"file":"styles.js","sourceRoot":"","sources":["../../src/ui/styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAElC,OAAO,EAAE,aAAa,EAAE,YAAY,IAAI,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC5F,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,2FAA2F;AAC3F,MAAM,YAAY,GAAG,wBAAwB,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkX1B,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,SAAS,KAAK,aAAa,KAAK,kBAAkB,EAAE,CAAC;AAExF;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAc;IAC/C,MAAM,MAAM,GACV,GAAG,IAAI,CAAC,OAAO,QAAQ,KAAK,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAClE,IAAI,CAAC,MAAM;QAAE,OAAO;IACpB,gFAAgF;IAChF,4EAA4E;IAC5E,sBAAsB,CAAC,MAAM,CAAC,CAAC;IAC/B,iEAAiE;IACjE,IAAI,MAAM,CAAC,aAAa,CAAC,SAAS,YAAY,GAAG,CAAC;QAAE,OAAO;IAC3D,MAAM,KAAK,GAAG,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;IAC5C,KAAK,CAAC,YAAY,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IACzC,KAAK,CAAC,WAAW,GAAG,kBAAkB,CAAC;IACvC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACnE,IAAI,IAAI,EAAE,CAAC;QACT,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IAC1B,CAAC;SAAM,CAAC;QACN,oEAAoE;QACpE,MAAM,CAAC,eAAe,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IAC5C,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe;IAC7B,SAAS,CAAC,GAAG,EAAE;QACb,kBAAkB,EAAE,CAAC;IACvB,CAAC,EAAE,EAAE,CAAC,CAAC;AACT,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civitai/blocks-react",
3
- "version": "0.46.0",
3
+ "version": "0.47.0",
4
4
  "description": "React hooks and iframe transport for Civitai Apps. Pairs with @civitai/app-sdk/blocks.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -31,8 +31,8 @@
31
31
  "node": ">=20"
32
32
  },
33
33
  "dependencies": {
34
- "@civitai/theme": "0.3.1",
35
- "@civitai/components": "0.4.1"
34
+ "@civitai/components": "0.4.1",
35
+ "@civitai/theme": "0.3.1"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@civitai/app-sdk": ">=0.29.0 <1.0.0",