@civitai/blocks-react 0.65.0 → 0.65.2
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 +36 -22
- package/dist/hooks/useBuzzPurchase.d.ts +3 -1
- package/dist/hooks/useBuzzPurchase.js +3 -1
- package/dist/hooks/useBuzzWorkflow.d.ts +83 -25
- package/dist/hooks/useBuzzWorkflow.js +27 -21
- package/dist/hooks/useGoodPurchase.d.ts +7 -7
- package/dist/ui/styles.d.ts +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -325,12 +325,10 @@ if (priced) {
|
|
|
325
325
|
try {
|
|
326
326
|
const snap = await submit(body); // status 'submitting' → 'polling'
|
|
327
327
|
if (snap.status === 'failed') {
|
|
328
|
-
// 🔴 A RESOLVED `failed` IS A
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
// unavailable" deny and a missing price quote are NOT. Selling Buzz for
|
|
333
|
-
// one of those takes money and fixes nothing, so branch before you offer.
|
|
328
|
+
// 🔴 A RESOLVED `failed` IS A SERVER OUTCOME, NEVER A TOP-UP CUE: a spend
|
|
329
|
+
// cap or limit buying Buzz does not raise, or a run that may already have
|
|
330
|
+
// spent. The complete list is on `useBuzzWorkflow`'s `submit` docs.
|
|
331
|
+
// Running OUT of Buzz rejects instead — see the catch below.
|
|
334
332
|
showError(submitOutcomeMessage(snap)); // YOUR app owns this copy
|
|
335
333
|
} else {
|
|
336
334
|
await poll(snap.workflowId); // you loop this on a backoff until terminal
|
|
@@ -375,11 +373,14 @@ if (priced) {
|
|
|
375
373
|
`status: 'failed'`, an `error` string, **and a numeric `cost.total`** — the
|
|
376
374
|
price the server refused to charge. That is a workflow *outcome*, not an error.
|
|
377
375
|
Check `snap.status`, not just `try/catch`.
|
|
378
|
-
🔴 **
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
376
|
+
🔴 **None of them is a top-up cue.** Each is a spend cap or limit that buying
|
|
377
|
+
Buzz does not raise — the per-call `buzzBudget` and the viewer's daily cap
|
|
378
|
+
included — or a reply that may already have spent. The complete list, and
|
|
379
|
+
which of them may say "nothing was charged", is in `useBuzzWorkflow`'s
|
|
380
|
+
`submit` docs. A viewer who is genuinely **out of Buzz** makes `submit`
|
|
381
|
+
**reject** (`WorkflowSubmitError` code `'exception'`, shared with other
|
|
382
|
+
thrown submits): decide a top-up from `useBuzzBalance()` — blue plus the
|
|
383
|
+
block's domain pool, never all three — against the quoted cost.
|
|
383
384
|
- **`submit` REJECTS when the reply carries no usable workflow outcome**
|
|
384
385
|
(`@civitai/blocks-react@0.44.0+`). Every failure-shaped reply reports
|
|
385
386
|
`status: 'failed'`, so `status` cannot tell them apart — **`cost` presence
|
|
@@ -468,7 +469,9 @@ if (priced) {
|
|
|
468
469
|
|
|
469
470
|
### `useBuzzPurchase()`
|
|
470
471
|
|
|
471
|
-
Open the Buzz purchase modal
|
|
472
|
+
Open the Buzz purchase modal. It raises the viewer's WALLET, never a spend cap:
|
|
473
|
+
offer it when the viewer's spendable Buzz is below a quoted cost, never on a
|
|
474
|
+
resolved `failed` submit (see `useBuzzWorkflow`'s `submit` docs).
|
|
472
475
|
|
|
473
476
|
```tsx
|
|
474
477
|
const { openPurchaseModal } = useBuzzPurchase();
|
|
@@ -1140,10 +1143,25 @@ The viewer searches in host chrome; the block only ever sees the one resource it
|
|
|
1140
1143
|
picked. DISCOVERY ONLY — the returned `versionId` is re-validated + re-priced
|
|
1141
1144
|
server-side at estimate/submit.
|
|
1142
1145
|
|
|
1143
|
-
🔴 **
|
|
1144
|
-
|
|
1145
|
-
the
|
|
1146
|
-
|
|
1146
|
+
🔴 **OMIT `baseModelGroup` BY DEFAULT, and never pass a HARDCODED ecosystem.** It
|
|
1147
|
+
is an ecosystem-family FILTER, not a label: the host hides every resource outside
|
|
1148
|
+
the family you pass, so a literal ecosystem string makes the viewer's own valid
|
|
1149
|
+
LoRAs invisible and the picker look empty or broken. Omit it for an unconstrained
|
|
1150
|
+
pick and the viewer sees everything of that type. Pass it ONLY when the block
|
|
1151
|
+
already holds a chosen checkpoint the pick has to match, and then DERIVE it from
|
|
1152
|
+
that checkpoint (`checkpoint.baseModel`). For stack and matrix apps that pair
|
|
1153
|
+
LoRAs with a checkpoint, this is the recommended pattern: a derived family keeps
|
|
1154
|
+
incompatible LoRAs out of the picker. This hook is PAGE-ONLY, and a page slot has
|
|
1155
|
+
no `context.checkpoint` (that field lives on `ModelSlotContext` alone), so the
|
|
1156
|
+
family comes from `BlockResourceInfo.baseModel`, the `baseModel` of a Checkpoint
|
|
1157
|
+
a picker returned earlier.
|
|
1158
|
+
|
|
1159
|
+
For a complete multi-LoRA app, see
|
|
1160
|
+
[`starters/examples/generate-studio`](https://github.com/civitai/civitai-app-starters/tree/main/starters/examples/generate-studio):
|
|
1161
|
+
`src/components/ModelSection.tsx` opens the picker once per LoRA slot, up to
|
|
1162
|
+
`MAX_LORAS`, with a `baseModelGroup` derived from the selected checkpoint, and
|
|
1163
|
+
`keepCompatibleLoras` (`src/studio/setup.ts`) re-filters the stack when the
|
|
1164
|
+
checkpoint changes, dropping LoRAs made for another family.
|
|
1147
1165
|
|
|
1148
1166
|
```tsx
|
|
1149
1167
|
const { open } = useResourcePicker();
|
|
@@ -1154,12 +1172,8 @@ if (picked) {
|
|
|
1154
1172
|
}
|
|
1155
1173
|
```
|
|
1156
1174
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
🔴 **This hook is PAGE-ONLY, and a page slot has no `context.checkpoint`** — that
|
|
1160
|
-
field lives on `ModelSlotContext` alone, so the family comes from
|
|
1161
|
-
`BlockResourceInfo.baseModel`, the `baseModel` of a Checkpoint this same picker
|
|
1162
|
-
returned earlier:
|
|
1175
|
+
Constrained to the family of a checkpoint the block already holds, derived from
|
|
1176
|
+
that checkpoint, never from a literal:
|
|
1163
1177
|
|
|
1164
1178
|
```tsx
|
|
1165
1179
|
const { open } = useResourcePicker();
|
|
@@ -9,7 +9,9 @@ export interface UseBuzzPurchase {
|
|
|
9
9
|
* Opens the Civitai Buzz purchase modal on the host. Resolves with the
|
|
10
10
|
* outcome when the user closes the modal — `purchased: true` means the
|
|
11
11
|
* balance increased; the new balance is included if the host reports it.
|
|
12
|
-
*
|
|
12
|
+
* Raises the viewer's WALLET, never a spend cap: offer it when the viewer's
|
|
13
|
+
* spendable Buzz is below a quoted cost, never on a resolved `failed` submit
|
|
14
|
+
* from {@link useBuzzWorkflow} (see its `submit` docs for why).
|
|
13
15
|
*
|
|
14
16
|
* 🔴 HUMAN-GATED: the reply comes when the viewer closes the modal, and a
|
|
15
17
|
* payment flow is nowhere near a ~30s round-trip — so this passes
|
|
@@ -6,7 +6,9 @@ import { sendTypedRequest } from '../transport/transport.js';
|
|
|
6
6
|
* Opens the Civitai Buzz purchase modal on the host. Resolves with the
|
|
7
7
|
* outcome when the user closes the modal — `purchased: true` means the
|
|
8
8
|
* balance increased; the new balance is included if the host reports it.
|
|
9
|
-
*
|
|
9
|
+
* Raises the viewer's WALLET, never a spend cap: offer it when the viewer's
|
|
10
|
+
* spendable Buzz is below a quoted cost, never on a resolved `failed` submit
|
|
11
|
+
* from {@link useBuzzWorkflow} (see its `submit` docs for why).
|
|
10
12
|
*
|
|
11
13
|
* 🔴 HUMAN-GATED: the reply comes when the viewer closes the modal, and a
|
|
12
14
|
* payment flow is nowhere near a ~30s round-trip — so this passes
|
|
@@ -271,10 +271,11 @@ export type WorkflowSubmitErrorCode = 'exception' | 'workflow-failed';
|
|
|
271
271
|
*
|
|
272
272
|
* - **budget / spend-cap REJECTION** — a legitimate OUTCOME. The server quotes
|
|
273
273
|
* the price it refused to charge, so the snapshot carries a numeric
|
|
274
|
-
* `cost.total
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
274
|
+
* `cost.total`, and the block can tell the viewer what was refused and why,
|
|
275
|
+
* so it MUST keep RESOLVING. It is NOT a top-up cue: these are app and
|
|
276
|
+
* platform caps, and buying Buzz raises none of them. Every such exit on
|
|
277
|
+
* the server attaches a cost; the complete list of them, and of the other
|
|
278
|
+
* resolved `'failed'` replies, is on {@link UseBuzzWorkflow.submit}.
|
|
278
279
|
* - **caught server EXCEPTION** — `failureSnapshot(err)`, no `cost`, and the
|
|
279
280
|
* `'failed'` sentinel id. The host had no workflow to report — usually
|
|
280
281
|
* nothing was queued, but a lost response or an in-progress idempotency
|
|
@@ -322,8 +323,8 @@ export type WorkflowSubmitErrorCode = 'exception' | 'workflow-failed';
|
|
|
322
323
|
*
|
|
323
324
|
* 🔴 DO NOT "SIMPLIFY" THIS BY PUTTING `snapshot.error` BACK ON `message`, and do
|
|
324
325
|
* not widen the guard to reject a budget rejection. The first re-opens #253; the
|
|
325
|
-
* second
|
|
326
|
-
* not
|
|
326
|
+
* second turns a priced, explainable refusal into an opaque exception, which is
|
|
327
|
+
* the one thing #251 says must not happen.
|
|
327
328
|
*/
|
|
328
329
|
export declare class WorkflowSubmitError extends Error {
|
|
329
330
|
/**
|
|
@@ -503,14 +504,67 @@ export interface UseBuzzWorkflow {
|
|
|
503
504
|
* 🔴 A BUDGET / SPEND-CAP REJECTION STILL RESOLVES, and that is deliberate. It
|
|
504
505
|
* is a documented outcome, not an error: the server quotes what it refused to
|
|
505
506
|
* charge, so the resolved snapshot has `status === 'failed'` AND a numeric
|
|
506
|
-
* `cost.total`.
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
507
|
+
* `cost.total`.
|
|
508
|
+
*
|
|
509
|
+
* 🔴 WHAT A RESOLVED `status: 'failed'` CAN MEAN — THE ONE COMPLETE LIST. The
|
|
510
|
+
* `@civitai/blocks-react` README, the `civitai-block-starter` AGENTS.md and the
|
|
511
|
+
* changesets link here rather than restating it. Every case below RESOLVES
|
|
512
|
+
* (`blocks.submitWorkflow` returns it; the REST route answers it with HTTP
|
|
513
|
+
* 200):
|
|
514
|
+
*
|
|
515
|
+
* 1. **Refused before anything ran — the `'failed'` sentinel `workflowId`, a
|
|
516
|
+
* `cost.total`, nothing charged, every reservation released.**
|
|
517
|
+
* - the per-call budget, `token.buzzBudget` (`insufficient buzz budget: …`);
|
|
518
|
+
* - the viewer's daily Buzz cap — or, in its place, the review-session cap
|
|
519
|
+
* (a moderator's run-for-real) or the private-run cap;
|
|
520
|
+
* - the viewer's consent budget (the ceiling they set when they granted the
|
|
521
|
+
* scope);
|
|
522
|
+
* - the app's velocity limit or aggregate daily cap, or a fail-closed
|
|
523
|
+
* "temporarily unavailable" deny;
|
|
524
|
+
* - the dev-session cap (`dev:tunnel`);
|
|
525
|
+
* - a missing price quote for a step ("generation temporarily unavailable").
|
|
526
|
+
*
|
|
527
|
+
* A training step with no price quote is refused the same way but with NO
|
|
528
|
+
* `cost`, which on this hook REJECTS as `'exception'` instead.
|
|
529
|
+
* 2. **Not confirmed — Buzz may have been spent.** A training run whose
|
|
530
|
+
* submission the server could not confirm: the sentinel id, a `cost.total`,
|
|
531
|
+
* and `submissionUnconfirmed: true` on the REST reply. The reservation is NOT
|
|
532
|
+
* refunded and the run may be going. 🔴 The host bridge forwards only the
|
|
533
|
+
* snapshot, so on this hook that flag never arrives and the reply looks like
|
|
534
|
+
* case 1 — never tell a viewer "nothing was charged" for a resolved failed
|
|
535
|
+
* `kind: 'training'` submit; send them to their trainings instead.
|
|
536
|
+
* 3. **A run was created and came back failed — a REAL `workflowId`.** The
|
|
537
|
+
* reservation is kept and Buzz may have been spent. Priced, it resolves;
|
|
538
|
+
* unpriced, it rejects as `'workflow-failed'`.
|
|
539
|
+
*
|
|
540
|
+
* So only case 1 may be told "could not start, nothing was charged".
|
|
541
|
+
*
|
|
542
|
+
* 🔴 A RESOLVED `'failed'` IS NEVER A TOP-UP CUE — do not wire it to
|
|
543
|
+
* `useBuzzPurchase().openPurchaseModal()`. Case 1 is a set of caps that buying
|
|
544
|
+
* Buzz does not raise, and cases 2 and 3 are not about the wallet.
|
|
545
|
+
*
|
|
546
|
+
* 🔴 THE CASE A TOP-UP DOES FIX — THE VIEWER IS ACTUALLY OUT OF BUZZ — REJECTS.
|
|
547
|
+
* The orchestrator refuses the run, the server throws, and the host answers
|
|
548
|
+
* with its cost-less `failureSnapshot(err)`, so this rejects with
|
|
549
|
+
* {@link WorkflowSubmitError} code **`'exception'`** — the same code as every
|
|
550
|
+
* other thrown submit (a bad request, an outage, a rate limit, an idempotency
|
|
551
|
+
* conflict). Nothing structural marks it as insufficient funds: the code is
|
|
552
|
+
* shared, and `snapshot.error` is the upstream message, server-authored prose
|
|
553
|
+
* that is not a contract. So decide a top-up from the BALANCE, not from the
|
|
554
|
+
* rejection: on `'exception'`, read {@link useBuzzBalance} and offer
|
|
555
|
+
* `openPurchaseModal()` only if the viewer's SPENDABLE Buzz is below the quoted
|
|
556
|
+
* cost. Two things make that read correct:
|
|
557
|
+
* - `useBuzzBalance` needs `buzz:read:self` declared in `block.manifest.json`,
|
|
558
|
+
* and that scope is consent-gated (it is not consent-exempt), so request it
|
|
559
|
+
* like any other gated scope. An anonymous viewer is refused outright.
|
|
560
|
+
* - A block spends only blue plus ITS DOMAIN'S pool: blue + green under an SFW
|
|
561
|
+
* ceiling, blue + yellow under a mature one. The server keys this on the
|
|
562
|
+
* token's DOMAIN ceiling (`maxBrowsingLevel`), so compute it as
|
|
563
|
+
* `isSfwCeiling(useDomainMaturity().maxBrowsingLevel)` (from
|
|
564
|
+
* `@civitai/app-sdk/blocks`; an unknown ceiling counts as SFW) — NOT
|
|
565
|
+
* `useDomainMaturity().isSfw`, which is narrowed by the viewer's own setting —
|
|
566
|
+
* and compare `blue` plus that one pool, never blue + green + yellow, against
|
|
567
|
+
* the quoted cost.
|
|
514
568
|
*
|
|
515
569
|
* 🔴 THIS ALSO INCLUDES MODERATOR REVIEW PREVIEW. While an app is under review
|
|
516
570
|
* the host short-circuits every workflow request with
|
|
@@ -586,15 +640,18 @@ export interface UseBuzzWorkflow {
|
|
|
586
640
|
*
|
|
587
641
|
* 🔴 A BUDGET REFUSAL DOES **NOT** REJECT — IT RESOLVES. It comes back as a
|
|
588
642
|
* snapshot with `status: 'failed'`, an `error` string and the `cost` the server
|
|
589
|
-
* declined to charge
|
|
590
|
-
* `useBuzzPurchase().openPurchaseModal()
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
* 🔴
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
*
|
|
643
|
+
* declined to charge. It is NOT a cue to call
|
|
644
|
+
* `useBuzzPurchase().openPurchaseModal()`: every resolving refusal is a cap
|
|
645
|
+
* buying Buzz does not raise, or a reply that is not about the wallet at all —
|
|
646
|
+
* the complete list is on {@link UseBuzzWorkflow.submit}.
|
|
647
|
+
*
|
|
648
|
+
* 🔴 RUNNING OUT OF BUZZ REJECTS, and is not marked as such. The orchestrator's
|
|
649
|
+
* refusal reaches the block as {@link WorkflowSubmitError} code `'exception'`,
|
|
650
|
+
* shared with every other thrown submit. Offer a top-up only after checking the
|
|
651
|
+
* viewer's SPENDABLE Buzz ({@link useBuzzBalance}: blue plus the block's domain
|
|
652
|
+
* pool, `buzz:read:self` declared) against the quoted cost — see
|
|
653
|
+
* {@link UseBuzzWorkflow.submit}, which also holds the complete list of resolved
|
|
654
|
+
* refusals.
|
|
598
655
|
*
|
|
599
656
|
* AFTER `submit` FLIPS `status` TO `'polling'`, USE `watch(workflowId)`. It owns
|
|
600
657
|
* the loop, resolves on the terminal snapshot, and pushes every intermediate
|
|
@@ -672,8 +729,9 @@ export interface UseBuzzWorkflow {
|
|
|
672
729
|
* try {
|
|
673
730
|
* const snap = await submit(body); // status 'submitting' → 'polling'
|
|
674
731
|
* if (snap.status === 'failed') {
|
|
675
|
-
* // RESOLVED
|
|
676
|
-
* //
|
|
732
|
+
* // RESOLVED = a server outcome: a cap, a limit, a missing quote — none of
|
|
733
|
+
* // them fixed by buying Buzz — or a run that may have spent (see `submit`
|
|
734
|
+
* // for the complete list). Never a top-up cue.
|
|
677
735
|
* showError(submitOutcomeMessage(snap));
|
|
678
736
|
* } else {
|
|
679
737
|
* const done = await watch(snap.workflowId, { onUpdate: render }); // → terminal
|
|
@@ -300,10 +300,11 @@ const HOST_SYNTHESISED_WORKFLOW_ID = 'failed';
|
|
|
300
300
|
*
|
|
301
301
|
* - **budget / spend-cap REJECTION** — a legitimate OUTCOME. The server quotes
|
|
302
302
|
* the price it refused to charge, so the snapshot carries a numeric
|
|
303
|
-
* `cost.total
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
303
|
+
* `cost.total`, and the block can tell the viewer what was refused and why,
|
|
304
|
+
* so it MUST keep RESOLVING. It is NOT a top-up cue: these are app and
|
|
305
|
+
* platform caps, and buying Buzz raises none of them. Every such exit on
|
|
306
|
+
* the server attaches a cost; the complete list of them, and of the other
|
|
307
|
+
* resolved `'failed'` replies, is on {@link UseBuzzWorkflow.submit}.
|
|
307
308
|
* - **caught server EXCEPTION** — `failureSnapshot(err)`, no `cost`, and the
|
|
308
309
|
* `'failed'` sentinel id. The host had no workflow to report — usually
|
|
309
310
|
* nothing was queued, but a lost response or an in-progress idempotency
|
|
@@ -351,8 +352,8 @@ const HOST_SYNTHESISED_WORKFLOW_ID = 'failed';
|
|
|
351
352
|
*
|
|
352
353
|
* 🔴 DO NOT "SIMPLIFY" THIS BY PUTTING `snapshot.error` BACK ON `message`, and do
|
|
353
354
|
* not widen the guard to reject a budget rejection. The first re-opens #253; the
|
|
354
|
-
* second
|
|
355
|
-
* not
|
|
355
|
+
* second turns a priced, explainable refusal into an opaque exception, which is
|
|
356
|
+
* the one thing #251 says must not happen.
|
|
356
357
|
*/
|
|
357
358
|
export class WorkflowSubmitError extends Error {
|
|
358
359
|
/**
|
|
@@ -464,15 +465,18 @@ export class WorkflowSubmitError extends Error {
|
|
|
464
465
|
*
|
|
465
466
|
* 🔴 A BUDGET REFUSAL DOES **NOT** REJECT — IT RESOLVES. It comes back as a
|
|
466
467
|
* snapshot with `status: 'failed'`, an `error` string and the `cost` the server
|
|
467
|
-
* declined to charge
|
|
468
|
-
* `useBuzzPurchase().openPurchaseModal()
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
* 🔴
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
468
|
+
* declined to charge. It is NOT a cue to call
|
|
469
|
+
* `useBuzzPurchase().openPurchaseModal()`: every resolving refusal is a cap
|
|
470
|
+
* buying Buzz does not raise, or a reply that is not about the wallet at all —
|
|
471
|
+
* the complete list is on {@link UseBuzzWorkflow.submit}.
|
|
472
|
+
*
|
|
473
|
+
* 🔴 RUNNING OUT OF BUZZ REJECTS, and is not marked as such. The orchestrator's
|
|
474
|
+
* refusal reaches the block as {@link WorkflowSubmitError} code `'exception'`,
|
|
475
|
+
* shared with every other thrown submit. Offer a top-up only after checking the
|
|
476
|
+
* viewer's SPENDABLE Buzz ({@link useBuzzBalance}: blue plus the block's domain
|
|
477
|
+
* pool, `buzz:read:self` declared) against the quoted cost — see
|
|
478
|
+
* {@link UseBuzzWorkflow.submit}, which also holds the complete list of resolved
|
|
479
|
+
* refusals.
|
|
476
480
|
*
|
|
477
481
|
* AFTER `submit` FLIPS `status` TO `'polling'`, USE `watch(workflowId)`. It owns
|
|
478
482
|
* the loop, resolves on the terminal snapshot, and pushes every intermediate
|
|
@@ -550,8 +554,9 @@ export class WorkflowSubmitError extends Error {
|
|
|
550
554
|
* try {
|
|
551
555
|
* const snap = await submit(body); // status 'submitting' → 'polling'
|
|
552
556
|
* if (snap.status === 'failed') {
|
|
553
|
-
* // RESOLVED
|
|
554
|
-
* //
|
|
557
|
+
* // RESOLVED = a server outcome: a cap, a limit, a missing quote — none of
|
|
558
|
+
* // them fixed by buying Buzz — or a run that may have spent (see `submit`
|
|
559
|
+
* // for the complete list). Never a top-up cue.
|
|
555
560
|
* showError(submitOutcomeMessage(snap));
|
|
556
561
|
* } else {
|
|
557
562
|
* const done = await watch(snap.workflowId, { onUpdate: render }); // → terminal
|
|
@@ -699,10 +704,11 @@ export function useBuzzWorkflow() {
|
|
|
699
704
|
// `submit` half of civitai/civitai#4159). Two producers report
|
|
700
705
|
// `status:'failed'` and `status` separates neither:
|
|
701
706
|
//
|
|
702
|
-
// - a budget / spend-cap REJECTION is an OUTCOME the block
|
|
703
|
-
//
|
|
704
|
-
// charge, so `cost.total` is present. It RESOLVES —
|
|
705
|
-
// into a throw
|
|
707
|
+
// - a budget / spend-cap REJECTION is an OUTCOME the block reports (the
|
|
708
|
+
// run did not start, at this quoted price). The server quotes the price
|
|
709
|
+
// it refused to charge, so `cost.total` is present. It RESOLVES —
|
|
710
|
+
// turning this arm into a throw would erase that. (It is not a top-up
|
|
711
|
+
// cue: buying Buzz raises none of these caps.)
|
|
706
712
|
// - a failure-shaped reply with NO `cost` is not a usable outcome. It
|
|
707
713
|
// REJECTS — and the `code` says which kind, because they differ on
|
|
708
714
|
// whether money moved (see WorkflowSubmitError.code).
|
|
@@ -52,13 +52,13 @@ export interface GoodPurchaseOptions extends ConsentRetryOptions {
|
|
|
52
52
|
* fresh key here would work too, but it would make a lost response on the
|
|
53
53
|
* retry unrecoverable, which is the thing idempotency keys exist to prevent.
|
|
54
54
|
*
|
|
55
|
-
* ⚠️ RECONCILED WITH THE
|
|
56
|
-
* `useBuzzWorkflow`
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
55
|
+
* ⚠️ RECONCILED WITH THE POLICY NEXT DOOR, which a reader will hit.
|
|
56
|
+
* `useBuzzWorkflow` says a resolved-failed generation is never a top-up cue
|
|
57
|
+
* (those are caps Buzz does not raise) and that running out of Buzz there
|
|
58
|
+
* rejects without a structural marker, so the APP decides a top-up from the
|
|
59
|
+
* balance. This option does not contradict that: it defaults to `false`, and
|
|
60
|
+
* it keys on the REASON (`insufficient_funds`, which a goods purchase DOES
|
|
61
|
+
* report structurally) rather than on "the call failed", so it never offers
|
|
62
62
|
* Buzz for a failure Buzz cannot fix. What it buys, and the only reason it is
|
|
63
63
|
* in the library at all rather than left to each app, is the same-key retry
|
|
64
64
|
* above — a server contract an app gets wrong in the expensive direction.
|
package/dist/ui/styles.d.ts
CHANGED
|
@@ -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: #C1C2C5;\n}\n@property --civitai-color-text-dimmed {\n syntax: '<color>';\n inherits: true;\n initial-value: #8c8fa3;\n}\n@property --civitai-color-body {\n syntax: '<color>';\n inherits: true;\n initial-value: #1A1B1E;\n}\n@property --civitai-color-surface {\n syntax: '<color>';\n inherits: true;\n initial-value: #25262B;\n}\n@property --civitai-color-surface-2 {\n syntax: '<color>';\n inherits: true;\n initial-value: #25262B;\n}\n@property --civitai-color-border {\n syntax: '<color>';\n inherits: true;\n initial-value: #373A40;\n}\n@property --civitai-color-primary {\n syntax: '<color>';\n inherits: true;\n initial-value: #1971C2;\n}\n@property --civitai-color-primary-hover {\n syntax: '<color>';\n inherits: true;\n initial-value: #1864AB;\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.15);\n}\n@property --civitai-color-error {\n syntax: '<color>';\n inherits: true;\n initial-value: #e03131;\n}\n@property --civitai-color-success {\n syntax: '<color>';\n inherits: true;\n initial-value: #326D5C;\n}\n@property --civitai-color-warning {\n syntax: '<color>';\n inherits: true;\n initial-value: #e8590c;\n}\n@property --civitai-color-info {\n syntax: '<color>';\n inherits: true;\n initial-value: #1971C2;\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@property --civitai-color-track {\n syntax: '<color>';\n inherits: true;\n initial-value: #25262B;\n}\n@property --civitai-color-segmented-bg {\n syntax: '<color>';\n inherits: true;\n initial-value: #25262B;\n}\n@property --civitai-color-media-placeholder {\n syntax: '<color>';\n inherits: true;\n initial-value: #25262B;\n}\n\n:root {\n color-scheme: dark;\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: #C1C2C5;\n --civitai-color-text-dimmed: #8c8fa3;\n --civitai-color-body: #1A1B1E;\n --civitai-color-surface: #25262B;\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 --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 --civitai-card-border-width: 0;\n --civitai-color-track: #25262B;\n --civitai-color-segmented-bg: #25262B;\n --civitai-color-media-placeholder: #25262B;\n}\n\n[data-theme='light'] {\n color-scheme: 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 --civitai-card-border-width: 1px;\n --civitai-color-track: #e9ecef;\n --civitai-color-segmented-bg: #f1f3f5;\n --civitai-color-media-placeholder: #e9ecef;\n}\n\n[data-theme='dark'] {\n color-scheme: dark;\n --civitai-color-text: #C1C2C5;\n --civitai-color-text-dimmed: #8c8fa3;\n --civitai-color-body: #1A1B1E;\n --civitai-color-surface: #25262B;\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 --civitai-card-border-width: 0;\n --civitai-color-track: #25262B;\n --civitai-color-segmented-bg: #25262B;\n --civitai-color-media-placeholder: #25262B;\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 HERE live in `@layer civitai.components`, so a consumer's own\n * unlayered CSS always wins over THIS SHEET without specificity fights.\n * \uD83D\uDD34 That does NOT extend to the TOKENS. @civitai/theme's sheet carries no\n * layer and declares them at `:root`/`[data-theme='\u2026']` (0-1-0), so a\n * consumer's `:root { --civitai-\u2026: }` only TIES with it and stylesheet order\n * decides \u2014 silently, in the order `useBlocksStyles()` produces. Scope a token\n * override (a class on your root, or an inline style); never `:root`. Measured\n * matrix: MARKUP.md, \"Cascade / overriding\".\n * State colors are derived with `color-mix()` from base tokens (no shade\n * enumeration), and 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): light resolves surface and body to the same #fefefe, so a\n * borderless card needs a default hairline to be visible at all; dark already\n * separates them and zeroes the width. `data-with-border` is the stronger,\n * explicit, opaque border and is unconditional.\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: var(--civitai-card-border-width, 1px) solid\n 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 /* ----- 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. \uD83D\uDD34 THIS SURFACE NO LONGER EXISTS: see below.\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 \u2014 there is no single \"the\n * 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/test/presentational-parity.browser.test.ts`,\n * case **`group`** \u2014 compares `<civitai-group>` against the bare\n * `data-civitai-ui=\"group\"` markup with `flexWrap` among the properties\n * it reads. \uD83D\uDD34 It is `group`, NOT `group/nowrap`: that case sets\n * `nowrap` on BOTH arms, so both resolve to `flex-wrap: nowrap` and it\n * stays green through a deletion of the rule below. Mutate the rule and\n * check the wrong case and you will conclude it is unguarded.\n *\n * \uD83D\uDD34 THE MIDDLE SURFACE IS GONE, and so is the test that used to pin it.\n * `@civitai/components-react@0.9.0` deleted its hand-written `<Group>` when\n * the custom elements superseded the React layer, taking\n * `html-vs-react-parity.browser.test.tsx` (\"styling anchors \u2014 Group\") with\n * it. TWO surfaces now resolve against this rule, not three: bare markup,\n * and blocks-react's `<Group>`. `<civitai-group>` is a third surface that\n * does NOT read this sheet at all \u2014 shadow DOM, carrying its own\n * `flex-wrap` at `src/elements/civitai-group.ts:20`.\n *\n * That independence is why the replacement is not the weak kind of parity\n * check this comment used to warn about. The warning was right about the\n * OLD test: both its arms consumed THIS sheet, so deleting the rule moved\n * them identically and only an absolute anchor could see it.\n * `presentational-parity`'s arms are independent IMPLEMENTATIONS, so\n * deleting `flex-wrap` here moves the legacy arm alone and the comparison\n * fails. WATCHED, not inferred: deleting `flex-wrap: wrap` from this block\n * in `styles.generated.ts` (the string the browser test injects) fails\n * `light / group` and `dark / group` with\n * `flexWrap: expected 'wrap' to be 'nowrap'` \u2014 2 failed / 108 passed;\n * restoring it returns 110/110. (Re-measured after #477 added 21 Text\n * parity cases; the kill is what matters, the totals move with the file.)\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-track, var(--civitai-color-gray-2));\n cursor: pointer;\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-segmented-bg, var(--civitai-color-gray-1));\n border-radius: var(--civitai-radius);\n max-width: 100%;\n overflow-x: auto;\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-media-placeholder, var(--civitai-color-gray-2));\n border-radius: var(--civitai-radius);\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 /* ----- Text ----- */\n /*\n * The typography primitive. Alone in this sheet it prescribes NO element of\n * its own: the author writes the tag the meaning calls for \u2014 `<h1>`..`<h6>`\n * for a heading, `<p>` for a paragraph, `<span>` for inline \u2014 and this rule\n * sizes and colours whatever that is. Nothing in the visual scale implies a\n * heading level and nothing in a heading level implies a size, which is what\n * lets an `<h2>` be the small caption of a card.\n *\n * `display` is deliberately NOT set, so `<p>`/`<h2>` stay block and `<span>`\n * stays inline: the element's own semantics keep deciding its box.\n *\n * `margin: 0` IS set, and it is a decision rather than a reset for tidiness.\n * The UA gives headings and paragraphs an em-relative margin that moves with\n * every `data-size`, spacing in this pack is owned by `stack`/`group`, and\n * without it `<civitai-text as=\"h2\">` and `<h2 data-civitai-ui=\"text\">` lay\n * out differently \u2014 which is the one thing the two tracks may not do.\n *\n * Documented DEFAULTS (MARKUP.md): size=md, weight=normal, no `data-color`.\n * On the BASE rule so BARE markup renders them, exactly as button and badge\n * do; the explicit `[data-size]`/`[data-weight]` rules below OVERRIDE.\n */\n [data-civitai-ui='text'] {\n margin: 0;\n /*\n * \uD83D\uDD34 `inherit`, NOT `var(--civitai-color-text)` \u2014 see the NO COLOUR AXIS\n * note at the end of this rule. A SPECIFIED value beats an INHERITED one\n * whatever the specificity, so the token here silently cancelled the\n * ancestor route this component's whole colour story rests on.\n */\n color: inherit;\n font-size: 14px;\n font-weight: 400;\n line-height: 1.5;\n\n /*\n * THE TYPE SCALE \u2014 ONE scale, and the reason it runs this far is that the\n * package must not ship two that disagree.\n *\n * The bottom half is the UI ramp: `sm`/`md`/`lg` are byte-identical to\n * Button's own font-size ramp (13/14/16px), so one size name means one size\n * across the pack, and `xs` is the 12px the field description already uses.\n *\n * The top half is the HEADING ramp, and every step of it is a value\n * `utilities.css` already ships as `ci-fs-N` \u2014 so the two are the same\n * scale under two spellings, not two scales:\n *\n * lg 16px = ci-fs-6 2xl 24px = ci-fs-4 4xl 32px = ci-fs-2\n * xl 20px = ci-fs-5 3xl 28px = ci-fs-3 5xl 40px = ci-fs-1\n *\n * What that costs: the t-shirt names do not encode the `ci-fs-N` number,\n * and the two sequences run in OPPOSITE directions (5xl is ci-fs-1), so a\n * reader needs the mapping above. That was the cheaper trade \u2014 naming the\n * new steps `fs-1`..`fs-4` would have put two naming conventions inside one\n * attribute and reversed its direction halfway up. Nothing above `ci-fs-1`\n * is invented here; 40px is the top of both ladders.\n *\n * UNIT: this ramp is px (Button's unit), `ci-fs-*` is rem. They agree at\n * the default 16px root and diverge if a consumer changes it. Deliberate \u2014\n * mixing units within one ramp would make it non-monotonic under a changed\n * root, which is worse than this caveat. Already true of `lg`/`xl` before\n * the ramp was extended.\n *\n * Everything from `xl` up tightens its line-height to 1.25 \u2014 a 20px-plus\n * heading leaded at 1.5 reads as loose. Two leading values in total.\n */\n &[data-size='xs'] { font-size: 12px; }\n &[data-size='sm'] { font-size: 13px; }\n &[data-size='md'] { font-size: 14px; }\n &[data-size='lg'] { font-size: 16px; }\n &[data-size='xl'] { font-size: 20px; line-height: 1.25; }\n &[data-size='2xl'] { font-size: 24px; line-height: 1.25; }\n &[data-size='3xl'] { font-size: 28px; line-height: 1.25; }\n &[data-size='4xl'] { font-size: 32px; line-height: 1.25; }\n &[data-size='5xl'] { font-size: 40px; line-height: 1.25; }\n\n /* `normal` / `semibold` / `bold` are the weights `utilities.css` already\n spells (`ci-normal` 400, `ci-semibold` 600, `ci-bold` 700); `medium` is\n the 500 this sheet already uses for a radio-group and a toast title. */\n &[data-weight='normal'] { font-weight: 400; }\n &[data-weight='medium'] { font-weight: 500; }\n &[data-weight='semibold'] { font-weight: 600; }\n &[data-weight='bold'] { font-weight: 700; }\n\n /*\n * NO COLOUR AXIS, and that is the same predicate this component already\n * applies to alignment and truncation rather than a separate judgement.\n * Every value one would carry already exists as a utility that reaches this\n * element: `ci-muted` (the dimmed token), `ci-text-info` / `-success` /\n * `-warning` / `-error` (the intent enum), `ci-text-default` (the body\n * colour). `color` INHERITS, so a utility on this element or any ancestor\n * reaches the inner element of `<civitai-text>` through its shadow boundary\n * too \u2014 the inner element is `color: inherit` \u2014 which is exactly why a\n * `data-color` here would be a second copy of a predicate that already has\n * an implementation one layer down.\n *\n * \uD83D\uDD34 WHICH IS WHY THE BASE RULE SAYS `color: inherit` AND NOT THE TOKEN.\n * The ancestor half of that sentence was measurably FALSE while this rule\n * specified `var(--civitai-color-text)` \u2014 a *specified* value beats an\n * *inherited* one however far up the utility sits, so `ci-muted` on a wrapper\n * dimmed a plain `<p>` and left `<p data-civitai-ui=\"text\">` undimmed.\n * `ci-text-center` on that same wrapper always worked, because nothing here\n * re-specifies `text-align`. `<civitai-text>`'s `:host` had the identical\n * defect; both tracks changed together, as they must.\n *\n * THE TRADE, and it is NOT limited to a page that paints nothing: Text no\n * longer paints the token itself, so it takes whatever colour it inherits.\n * That decides what it renders on every page that DOES set a colour \u2014 the\n * larger population, and the one every in-repo consumer would be in once it\n * renders Text, which none does today (four starters colour `body` via\n * Tailwind; seven more colour a `[data-theme]` root).\n * Measured on both tracks, on the shape a block here actually has \u2014 a\n * `[data-theme='dark']` root carrying `color: #e6e6e6` \u2014 Text computes\n * `rgb(230, 230, 230)`, where restoring the removed declaration puts both\n * tracks back at the dark token `rgb(193, 194, 197)`. With no colour anywhere\n * it lands on the UA default `rgb(0, 0, 0)`. `ci-text-default` asks for the\n * token back, but it ships in a separate stylesheet that neither\n * `injectStyles()` nor `injectBlocksStyles()` injects \u2014 see the cost section\n * in `src/elements/civitai-text.ts` for every measurement and\n * `packages/civitai-components/MARKUP.md` for what a consumer has to load.\n * Both tracks are pinned in `test/civitai-text.browser.test.ts`.\n *\n * It is also the direction that stays open: this ships `minor` on a\n * published package, so adding the axis later is additive and taking it\n * away would not be.\n */\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";
|
|
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: #C1C2C5;\n}\n@property --civitai-color-text-dimmed {\n syntax: '<color>';\n inherits: true;\n initial-value: #8c8fa3;\n}\n@property --civitai-color-body {\n syntax: '<color>';\n inherits: true;\n initial-value: #1A1B1E;\n}\n@property --civitai-color-surface {\n syntax: '<color>';\n inherits: true;\n initial-value: #25262B;\n}\n@property --civitai-color-surface-2 {\n syntax: '<color>';\n inherits: true;\n initial-value: #25262B;\n}\n@property --civitai-color-border {\n syntax: '<color>';\n inherits: true;\n initial-value: #373A40;\n}\n@property --civitai-color-primary {\n syntax: '<color>';\n inherits: true;\n initial-value: #1971C2;\n}\n@property --civitai-color-primary-hover {\n syntax: '<color>';\n inherits: true;\n initial-value: #1864AB;\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.15);\n}\n@property --civitai-color-error {\n syntax: '<color>';\n inherits: true;\n initial-value: #e03131;\n}\n@property --civitai-color-success {\n syntax: '<color>';\n inherits: true;\n initial-value: #326D5C;\n}\n@property --civitai-color-warning {\n syntax: '<color>';\n inherits: true;\n initial-value: #e8590c;\n}\n@property --civitai-color-info {\n syntax: '<color>';\n inherits: true;\n initial-value: #1971C2;\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@property --civitai-color-track {\n syntax: '<color>';\n inherits: true;\n initial-value: #25262B;\n}\n@property --civitai-color-segmented-bg {\n syntax: '<color>';\n inherits: true;\n initial-value: #25262B;\n}\n@property --civitai-color-media-placeholder {\n syntax: '<color>';\n inherits: true;\n initial-value: #25262B;\n}\n\n:root {\n color-scheme: dark;\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: #C1C2C5;\n --civitai-color-text-dimmed: #8c8fa3;\n --civitai-color-body: #1A1B1E;\n --civitai-color-surface: #25262B;\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 --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 --civitai-card-border-width: 0;\n --civitai-color-track: #25262B;\n --civitai-color-segmented-bg: #25262B;\n --civitai-color-media-placeholder: #25262B;\n}\n\n[data-theme='light'] {\n color-scheme: 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 --civitai-card-border-width: 1px;\n --civitai-color-track: #e9ecef;\n --civitai-color-segmented-bg: #f1f3f5;\n --civitai-color-media-placeholder: #e9ecef;\n}\n\n[data-theme='dark'] {\n color-scheme: dark;\n --civitai-color-text: #C1C2C5;\n --civitai-color-text-dimmed: #8c8fa3;\n --civitai-color-body: #1A1B1E;\n --civitai-color-surface: #25262B;\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 --civitai-card-border-width: 0;\n --civitai-color-track: #25262B;\n --civitai-color-segmented-bg: #25262B;\n --civitai-color-media-placeholder: #25262B;\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 HERE live in `@layer civitai.components`, so a consumer's own\n * unlayered CSS always wins over THIS SHEET without specificity fights.\n * \uD83D\uDD34 That does NOT extend to the TOKENS. @civitai/theme's sheet carries no\n * layer and declares them at `:root`/`[data-theme='\u2026']` (0-1-0), so a\n * consumer's `:root { --civitai-\u2026: }` only TIES with it and stylesheet order\n * decides \u2014 silently, in the order `useBlocksStyles()` produces. Scope a token\n * override (a class on your root, or an inline style); never `:root`. Measured\n * matrix: MARKUP.md, \"Cascade / overriding\".\n * State colors are derived with `color-mix()` from base tokens (no shade\n * enumeration), and 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): light resolves surface and body to the same #fefefe, so a\n * borderless card needs a default hairline to be visible at all; dark already\n * separates them and zeroes the width. `data-with-border` is the stronger,\n * explicit, opaque border and is unconditional.\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: var(--civitai-card-border-width, 1px) solid\n 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 /* ----- 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. \uD83D\uDD34 THIS SURFACE NO LONGER EXISTS: see below.\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 \u2014 there is no single \"the\n * 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/test/presentational-parity.browser.test.ts`,\n * case **`group`** \u2014 compares `<civitai-group>` against the bare\n * `data-civitai-ui=\"group\"` markup with `flexWrap` among the properties\n * it reads. \uD83D\uDD34 It is `group`, NOT `group/nowrap`: that case sets\n * `nowrap` on BOTH arms, so both resolve to `flex-wrap: nowrap` and it\n * stays green through a deletion of the rule below. Mutate the rule and\n * check the wrong case and you will conclude it is unguarded.\n *\n * \uD83D\uDD34 THE MIDDLE SURFACE IS GONE, and so is the test that used to pin it.\n * `@civitai/components-react@0.9.0` deleted its hand-written `<Group>` when\n * the custom elements superseded the React layer, taking\n * `html-vs-react-parity.browser.test.tsx` (\"styling anchors \u2014 Group\") with\n * it. TWO surfaces now resolve against this rule, not three: bare markup,\n * and blocks-react's `<Group>`. `<civitai-group>` is a third surface that\n * does NOT read this sheet at all \u2014 shadow DOM, carrying its own\n * `flex-wrap` at `src/elements/civitai-group.ts:20`.\n *\n * That independence is why the replacement is not the weak kind of parity\n * check this comment used to warn about. The warning was right about the\n * OLD test: both its arms consumed THIS sheet, so deleting the rule moved\n * them identically and only an absolute anchor could see it.\n * `presentational-parity`'s arms are independent IMPLEMENTATIONS, so\n * deleting `flex-wrap` here moves the legacy arm alone and the comparison\n * fails. WATCHED, not inferred: deleting `flex-wrap: wrap` from this block\n * in `styles.generated.ts` (the string the browser test injects) fails\n * `light / group` and `dark / group` with\n * `flexWrap: expected 'wrap' to be 'nowrap'` \u2014 2 failed / 108 passed;\n * restoring it returns 110/110. (Re-measured after #477 added 21 Text\n * parity cases; the kill is what matters, the totals move with the file.)\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-track, var(--civitai-color-gray-2));\n cursor: pointer;\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-segmented-bg, var(--civitai-color-gray-1));\n border-radius: var(--civitai-radius);\n max-width: 100%;\n overflow-x: auto;\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-media-placeholder, var(--civitai-color-gray-2));\n border-radius: var(--civitai-radius);\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 /* ----- Text ----- */\n /*\n * The typography primitive. Alone in this sheet it prescribes NO element of\n * its own: the author writes the tag the meaning calls for \u2014 `<h1>`..`<h6>`\n * for a heading, `<p>` for a paragraph, `<span>` for inline \u2014 and this rule\n * sizes and colours whatever that is. Nothing in the visual scale implies a\n * heading level and nothing in a heading level implies a size, which is what\n * lets an `<h2>` be the small caption of a card.\n *\n * `display` is deliberately NOT set, so `<p>`/`<h2>` stay block and `<span>`\n * stays inline: the element's own semantics keep deciding its box.\n *\n * `margin: 0` IS set, and it is a decision rather than a reset for tidiness.\n * The UA gives headings and paragraphs an em-relative margin that moves with\n * every `data-size`, spacing in this pack is owned by `stack`/`group`, and\n * without it `<civitai-text as=\"h2\">` and `<h2 data-civitai-ui=\"text\">` lay\n * out differently \u2014 which is the one thing the two tracks may not do.\n *\n * Documented DEFAULTS (MARKUP.md): size=md, weight=normal, no `data-color`.\n * On the BASE rule so BARE markup renders them, exactly as button and badge\n * do; the explicit `[data-size]`/`[data-weight]` rules below OVERRIDE.\n */\n [data-civitai-ui='text'] {\n margin: 0;\n /*\n * \uD83D\uDD34 `inherit`, NOT `var(--civitai-color-text)` \u2014 see the NO COLOUR AXIS\n * note at the end of this rule. A SPECIFIED value beats an INHERITED one\n * whatever the specificity, so the token here silently cancelled the\n * ancestor route this component's whole colour story rests on.\n */\n color: inherit;\n font-size: 14px;\n font-weight: 400;\n line-height: 1.5;\n\n /*\n * THE TYPE SCALE \u2014 ONE scale, and the reason it runs this far is that the\n * package must not ship two that disagree.\n *\n * The bottom half is the UI ramp: `sm`/`md`/`lg` are byte-identical to\n * Button's own font-size ramp (13/14/16px), so one size name means one size\n * across the pack, and `xs` is the 12px the field description already uses.\n *\n * The top half is the HEADING ramp, and every step of it is a value\n * `utilities.css` already ships as `ci-fs-N` \u2014 so the two are the same\n * scale under two spellings, not two scales:\n *\n * lg 16px = ci-fs-6 2xl 24px = ci-fs-4 4xl 32px = ci-fs-2\n * xl 20px = ci-fs-5 3xl 28px = ci-fs-3 5xl 40px = ci-fs-1\n *\n * What that costs: the t-shirt names do not encode the `ci-fs-N` number,\n * and the two sequences run in OPPOSITE directions (5xl is ci-fs-1), so a\n * reader needs the mapping above. That was the cheaper trade \u2014 naming the\n * new steps `fs-1`..`fs-4` would have put two naming conventions inside one\n * attribute and reversed its direction halfway up. Nothing above `ci-fs-1`\n * is invented here; 40px is the top of both ladders.\n *\n * UNIT: this ramp is px (Button's unit), `ci-fs-*` is rem. They agree at\n * the default 16px root and diverge if a consumer changes it. Deliberate \u2014\n * mixing units within one ramp would make it non-monotonic under a changed\n * root, which is worse than this caveat. Already true of `lg`/`xl` before\n * the ramp was extended.\n *\n * Everything from `xl` up tightens its line-height to 1.25 \u2014 a 20px-plus\n * heading leaded at 1.5 reads as loose. Two leading values in total.\n */\n &[data-size='xs'] { font-size: 12px; }\n &[data-size='sm'] { font-size: 13px; }\n &[data-size='md'] { font-size: 14px; }\n &[data-size='lg'] { font-size: 16px; }\n &[data-size='xl'] { font-size: 20px; line-height: 1.25; }\n &[data-size='2xl'] { font-size: 24px; line-height: 1.25; }\n &[data-size='3xl'] { font-size: 28px; line-height: 1.25; }\n &[data-size='4xl'] { font-size: 32px; line-height: 1.25; }\n &[data-size='5xl'] { font-size: 40px; line-height: 1.25; }\n\n /* `normal` / `semibold` / `bold` are the weights `utilities.css` already\n spells (`ci-normal` 400, `ci-semibold` 600, `ci-bold` 700); `medium` is\n the 500 this sheet already uses for a radio-group and a toast title. */\n &[data-weight='normal'] { font-weight: 400; }\n &[data-weight='medium'] { font-weight: 500; }\n &[data-weight='semibold'] { font-weight: 600; }\n &[data-weight='bold'] { font-weight: 700; }\n\n /*\n * NO COLOUR AXIS, and that is the same predicate this component already\n * applies to alignment and truncation rather than a separate judgement.\n * Every value one would carry already exists as a utility that reaches this\n * element: `ci-muted` (the dimmed token), `ci-text-info` / `-success` /\n * `-warning` / `-error` (the intent enum), `ci-text-default` (the body\n * colour). `color` INHERITS, so a utility on this element or any ancestor\n * reaches the inner element of `<civitai-text>` through its shadow boundary\n * too \u2014 the inner element is `color: inherit` \u2014 which is exactly why a\n * `data-color` here would be a second copy of a predicate that already has\n * an implementation one layer down.\n *\n * \uD83D\uDD34 WHICH IS WHY THE BASE RULE SAYS `color: inherit` AND NOT THE TOKEN.\n * The ancestor half of that sentence was measurably FALSE while this rule\n * specified `var(--civitai-color-text)` \u2014 a *specified* value beats an\n * *inherited* one however far up the utility sits, so `ci-muted` on a wrapper\n * dimmed a plain `<p>` and left `<p data-civitai-ui=\"text\">` undimmed.\n * `ci-text-center` on that same wrapper always worked, because nothing here\n * re-specifies `text-align`. `<civitai-text>`'s `:host` had the identical\n * defect; both tracks changed together, as they must.\n *\n * THE TRADE, and it is NOT limited to a page that paints nothing: Text no\n * longer paints the token itself, so it takes whatever colour it inherits.\n * That decides what it renders on every page that DOES set a colour \u2014 the\n * larger population. Measured on both tracks, on a `[data-theme='dark']`\n * root carrying `color: #e6e6e6` (the shape this repo's blocks had when this\n * was measured; none sets that colour today): Text computes\n * `rgb(230, 230, 230)`, where restoring the removed declaration puts both\n * tracks back at the dark token `rgb(193, 194, 197)`. With no colour anywhere\n * it lands on the UA default `rgb(0, 0, 0)`.\n * In this repo: the Tailwind starters colour `body` themselves, so they\n * would show Text in their own colour once one of them renders it. Every\n * Civitai App page sets `color: var(--civitai-color-text)` itself \u2014 each\n * example's src/index.css on `:root`, the block starter's on `body` \u2014 so\n * Text there shows the token by INHERITING it rather than by naming it.\n * (See `src/elements/civitai-text.ts` for the full enumeration.)\n * `ci-text-default` asks for the\n * token back, but it ships in a separate stylesheet that neither\n * `injectStyles()` nor `injectBlocksStyles()` injects \u2014 see the cost section\n * in `src/elements/civitai-text.ts` for every measurement and\n * `packages/civitai-components/MARKUP.md` for what a consumer has to load.\n * Both tracks are pinned in `test/civitai-text.browser.test.ts`.\n *\n * It is also the direction that stays open: this ships `minor` on a\n * published package, so adding the axis later is additive and taking it\n * away would not be.\n */\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
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@civitai/blocks-react",
|
|
3
|
-
"version": "0.65.
|
|
3
|
+
"version": "0.65.2",
|
|
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",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@civitai/theme": "^0.5.2",
|
|
41
|
-
"@civitai/components": "^0.9.
|
|
41
|
+
"@civitai/components": "^0.9.3"
|
|
42
42
|
},
|
|
43
43
|
"comment-peerDependencies": "This floor is DERIVED, not chosen — the lowest published app-sdk that exports every symbol this package imports. Do not edit it without reading ./PEER_FLOOR.md (in-repo, unpublished) and tests/guards/blocks-react-peer-floor.test.mjs.",
|
|
44
44
|
"peerDependencies": {
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"typescript": "^5.9.2",
|
|
59
59
|
"vite": "^8.0.14",
|
|
60
60
|
"vitest": "^4.1.11",
|
|
61
|
-
"@civitai/app-sdk": "^0.
|
|
61
|
+
"@civitai/app-sdk": "^0.60.0"
|
|
62
62
|
},
|
|
63
63
|
"publishConfig": {
|
|
64
64
|
"access": "public"
|