* test(inspector): concept comparison for the panel and viewer details Compare the Images side panel and the viewer's full details with the concept (1600x1100 and 390x844, both themes): structure, tokens, geometry and controls, plus copy, the acknowledged pair send, the tag chip filter and Prompt history. mapInspectorConcept applies only D3, D20, D21 and D22; a mutation test shows the comparison notices a changed role label and a removed footer. The comparison showed the role copy and send names lacked the concept's ": <title>" subject, so the inspector now passes its title to the prompt blocks. It also showed that a recipe-linked image shows its recipe's own category and tags (#224), which the concept fixture's recipes lack; that is recorded as D38, and the fixture's new positive_links option links the compared image to its positive alone. * docs(inspector): renumber the recipe-category decision to D39 * feat(images): local-only Show in folder route POST /prompt_manager/images/reveal takes the listing identity {root_index, root_scope, path} and opens this computer's file manager at that file. It answers 403 not-local unless the client is loopback, no proxy header is present and Host/Origin name this computer; the scope is checked with _gallery_scope_error (409) and the path resolves only inside that root to a regular media file. Openers run as argv lists with an absolute executable, no shell and DEVNULL streams (D38). * feat(inspector): Show in folder after the File & provenance path The viewer's File & provenance offers "Show in folder" right after the Path value for a listed file only, POSTs its listing identity and toasts the outcome (opened, not-local, unavailable or failed). The browser test server stubs the opener; TESTING.md records the decision as D38. * fix(images): keep the discovery alert clear of the Rescan toast A rescan that leaves the Images discovery alert visible could show its fixed bottom toast over the alert on a short window. The concept's toast keeps its placement; the production-only alert now scrolls into view only as far as needed, with a 96px block-end scroll margin (D42). * docs(inspector): concept panel and viewer details * fix(images): open Windows folders through the shell API, not an Explorer command line Explorer splits its own command line on commas, so a gallery path such as output\a,\Windows\System32\calc.exe,b\x.png escaped the roots through [explorer, "/select,", path] (security review H1). Windows now selects the file with ILCreateFromPathW and SHOpenFolderAndSelectItems, and on any shell failure falls back to os.startfile on the folder. No process is started. * fix(images): re-check, isolate and reap the Show in folder opener - re-check that the real path is still a regular file inside its root right before launching; a changed target answers 404 (review L4) - run openers on a dedicated single worker, not ComfyUI's default executor, and make dbus-send wait for the FileManager1 reply (--print-reply --reply-timeout=2000) so a missing service falls back to xdg-open (L1) - reap launched processes on a daemon thread, give them a minimal environment with a fixed system PATH, and use /usr/bin/open on macOS and dbus-send/xdg-open resolved only on that PATH (L5) * fix(images): accept Show in folder only as a same-origin JSON request, one at a time - require an Origin equal to the request's own scheme://host:port and, when sent, Sec-Fetch-Site: same-origin; refuse every forwarding-style header (Via, X-Forwarded-*, X-Original-*, *-Client-IP, CF-Connecting-IP and the rest) as remote (security review M1, L2) - answer a non-JSON body with 415, so a cross-origin request always needs a CORS preflight (L2) - single flight plus a 1 s cooldown per process: 429 code busy (L1) - path resolution errors such as ENAMETOOLONG are a JSON 404, and neither that nor a start failure logs the client's path at error level (L3) * fix(inspector): say the folder is already opening on a busy Show in folder A 429 busy from the reveal route now toasts "The folder is already opening." The browser workflow checks that the reveal POST carries the page's own Origin and an application/json body, and TESTING.md D38 records the tightened route, the toast table and the accepted residual risk of a header-less local proxy. * test(inspector): compare the viewer details under a coarse pointer and prove its mutations The touch project measured production's D34 40px disclosure targets against the concept's fine-pointer summaries. The concept's plain-chip copy now also carries the recorded D34 coarse rule, so both documents are measured under the same emulation. The open File & provenance is compared too (D38 Show in folder and D20 Download original mapped), and the mutation check now removes the viewer's pair actions and Show in folder. * refactor(inspector): attach Show in folder to the Path value it follows The button was placed on the second fact row by position; the Path dd is now built by the caller and kept, so reordering the facts cannot move it. * test(inspector): let the uncertain-file send check fail again Inspector send buttons are named "... to ComfyUI: <title>", so the anchored regex could never match and the no-send assertion always passed. * fix(images): run the Windows folder fallback inside COM and keep desktop session variables Show in folder's os.startfile fallback now runs inside the same CoInitializeEx/CoUninitialize bracket as the shell selection, as ShellExecute callers should. The Linux opener keeps XAUTHORITY, DESKTOP_SESSION, XDG_SESSION_DESKTOP and XDG_CONFIG_HOME, so a file manager can reach the X11 display under GDM or SDDM and honour the user's mimeapps.list. A Windows-only test calls the real ILCreateFromPathW and ILFree on a temp file and pairs CoInitializeEx with CoUninitialize through the module's helper, without SHOpenFolderAndSelectItems, so no window opens on CI. * fix(images): scroll to the rescan's discovery alert only when the toast band covers it D42 scrolled the alert into view after every rescan, so a user scrolled down the grid was sent back to the top. It now scrolls only when the alert intersects the toast's band (its scroll-margin-block-end). * refactor(inspector): name prompt actions as PR #263 does The title option keeps its name, default and output ("Copy positive prompt: <title>") and now uses #263's actionName helper; the region-name arrow no longer shadows the title option. * test(inspector): the comparison opens Filters by its D44 active-count name After a tag chip applies its filter, the shared toolbar's Filters button reads "Filters · 1" (D44, #263), so the comparison opens it by that name. * fix(images): POSIX opener paths stay absolute under Python 3.13 on Windows Python 3.13 on Windows no longer counts "/usr/bin/x" as absolute, so the faked-Linux reveal tests failed on windows-latest py3.13 (the Linux and macOS openers are POSIX paths whatever host evaluates them). Use posixpath.isabs for those lookups.
227 lines
19 KiB
CSS
227 lines
19 KiB
CSS
/* Production overlay on the verbatim concept base (concept.css, D1 of #249). Only production-only states and recorded
|
||
deviations live here; a rule on a selector the concept also styles carries its decision marker. */
|
||
|
||
/* Production-only shell states. */
|
||
main:focus-visible { outline: 2px solid var(--ca-focus); outline-offset: 4px; }
|
||
/* The page title takes focus after a save or route change (concept app.js 1168); a keyboard user sees where it went. */
|
||
#page-title:focus { outline: none; }
|
||
#page-title:focus-visible { outline: 2px solid var(--ca-focus); outline-offset: 4px; }
|
||
/* D28: the sidebar shows no connection state, so its dot is neutral instead of the concept's demo green. */
|
||
.connection i { background: var(--ca-muted); }
|
||
/* D31: bounded rendering reads the grid's real width, so at extreme zoom the one-column shell must not grow to the
|
||
sidebar's min-content (the concept's `1fr`); the column is capped at the viewport as production's shell was. */
|
||
@media (max-width: 950px) {
|
||
.app-shell { grid-template-columns: minmax(0, 1fr); /* D31 */ }
|
||
}
|
||
@media (forced-colors: active) {
|
||
nav a[aria-current="page"] { border: 2px solid Highlight; color: LinkText; }
|
||
}
|
||
|
||
/* Overview: real library statistics and the legacy tool cards inside a concept `.surface`. */
|
||
.stat-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; margin: 0 0 26px; }
|
||
.stat { padding: 22px; background: var(--ca-surface); border: 1px solid var(--line); border-radius: 7px; }
|
||
.stat dt { color: var(--ca-muted); font-size: 12px; }
|
||
.stat dd { margin: 8px 0 0; font-size: 32px; font-weight: 600; }
|
||
.overview-stats > [role="status"], .overview-stats > [role="alert"], .empty-state { padding: 18px 0; color: var(--ca-muted); }
|
||
.overview-stats > [role="alert"] { color: var(--ca-error-text); }
|
||
.overview-stats > button { margin-bottom: 24px; }
|
||
.tool-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr)); gap: 12px; margin-top: 22px; }
|
||
.tool-card { border: 1px solid var(--line); background: var(--ca-field); border-radius: 8px; padding: 20px; display: flex; flex-direction: column; gap: 10px; }
|
||
.tool-card p { flex: 1; margin: 0 0 12px; }
|
||
.workspace-link { display: inline-flex; align-items: center; min-height: 44px; padding: 8px 0; font-size: 12px; overflow-wrap: anywhere; }
|
||
.handoff .workspace-link { margin-top: 12px; }
|
||
@media (max-width: 700px) {
|
||
.stat-grid { gap: 8px; }
|
||
.stat { padding: 16px; }
|
||
.stat dd { font-size: 26px; }
|
||
}
|
||
|
||
/* Tags & categories (#245): real vocabulary has unbounded names and bounded lists, which the concept's short samples
|
||
never show. Proven by the 390px layout and long-name cases in workspace-vocabulary.spec.mjs. */
|
||
.vocabulary-main :where(.two-col, .surface, .table-wrap, .operation > div) { min-width: 0; }
|
||
.vocabulary-main .operation h3 { overflow-wrap: anywhere; }
|
||
/* Long tag names wrap inside the Tag cell so the Actions column keeps its intrinsic width; on two-column screens the
|
||
minimum keeps the concept's longest sample ("architecture", about 5em) on one line. */
|
||
.vocabulary-main td:first-child { overflow-wrap: anywhere; min-width: calc(6em + 20px); }
|
||
/* Tags & categories (#245 decision 5): bounded lists continue with Load more; never a scroll anchor, so appended rows push the end away. */
|
||
.vocabulary-main .vocabulary-continuation { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; padding-block: 15px 0; overflow-anchor: none; }
|
||
.vocabulary-main .vocabulary-continuation p { margin: 0; flex: 1 1 200px; font-size: 12px; color: var(--ca-muted); }
|
||
.vocabulary-main .vocabulary-continuation .vocabulary-error { color: var(--ca-error-text); }
|
||
@media (max-width: 700px) {
|
||
/* One column: the Tag column has no fixed minimum and Actions asks only for its stacked width (the concept's 390px
|
||
rows stack Browse over Rename / merge), so a wider fallback font wraps the name instead of hiding an action. */
|
||
.vocabulary-main td:first-child { min-width: 0; }
|
||
.vocabulary-main td .row-actions { width: min-content; margin-inline-start: auto; }
|
||
}
|
||
|
||
/* Images (#249 C2). Production-only continuation states the concept never shows: a failed page and a load in flight. */
|
||
.infinite-continuation [role="alert"] { color: var(--ca-error-text); }
|
||
.infinite-continuation button[aria-disabled="true"] { opacity: .55; cursor: wait; }
|
||
/* #249 D14: the scope chip names real sources and folders, which can be far longer than "All output folders". */
|
||
.filter-row span.chip { max-width: 100%; min-width: 0; overflow-wrap: anywhere; }
|
||
/* #249 D32: at extreme zoom the 390px shell is narrower than the concept's three layout buttons; they wrap instead of
|
||
scrolling the page sideways (they never wrap while they fit). */
|
||
.segmented { flex-wrap: wrap; max-width: 100%; }
|
||
/* #249 D32: likewise the continuation's text and Load more wrap instead of pushing the button out of the column. */
|
||
.infinite-continuation { flex-wrap: wrap; }
|
||
|
||
/* #249 D18: the concept zooms a hovered card image (1.03 over 0.2s; its reduced-motion rule makes that instant). A touch
|
||
device has no real hover, so a tap must not leave the zoom stuck. */
|
||
@media (hover: none) { .image-open:hover img { transform: none; /* D18 */ } }
|
||
|
||
/* Images cards (#249 C3). #249 D3: bounded rendering mounts only the visible cards and places each one itself, in every
|
||
layout (the concept places only masonry cards), so the grid reserves the collection's height and every card is
|
||
absolute. Columns and gap still come from the concept's `.image-grid` tracks. */
|
||
.image-grid { position: relative; /* D3 */ }
|
||
.image-grid > .media-card { position: absolute; }
|
||
/* #249 D3: a list row's info keeps its own height so it can be measured; centred, it looks as the stretched concept row.
|
||
#249 D31: it may also shrink below a long nowrap title (the h3 ellipsis then applies, as in the grid). */
|
||
.list-grid .media-info { align-self: center; min-width: 0; /* D3, D31 */ }
|
||
/* #249 D31: at extreme zoom a list row is narrower than the concept's 160px art; the art yields so the info stays inside. */
|
||
.list-grid .media-art { width: min(160px, 45%); /* D31 */ }
|
||
/* #249 D31: at extreme zoom the subtitle wraps inside its column instead of running under the Inspect button. */
|
||
.media-info p { overflow-wrap: anywhere; /* D31 */ }
|
||
/* #249 D31: below the narrowest bounded card (120px, reached only at extreme zoom; images.mjs marks the grid) the info
|
||
row stacks its text above Inspect, so neither the title nor the subtitle collapses to nothing beside the button. */
|
||
.narrow-cards .media-info { flex-wrap: wrap; }
|
||
.narrow-cards .media-info > div { flex: 1 0 100%; }
|
||
/* A list row beside its art keeps room for Inspect. */
|
||
.narrow-cards.list-grid .media-art { width: 40%; }
|
||
.narrow-cards.list-grid .media-info { padding-inline: 4px; }
|
||
|
||
/* Legacy video media (the mock does not drop it) takes the concept's image rules in each layout. */
|
||
.image-open video { display: block; width: 100%; height: auto; aspect-ratio: 1.33; object-fit: cover; }
|
||
.masonry-grid .image-open video { aspect-ratio: var(--media-ratio); object-fit: contain; }
|
||
.list-grid .image-open video { height: 95px; aspect-ratio: auto; }
|
||
|
||
/* Images viewer (#249 C6): the concept's `#viewer-dialog` shell (s1008–1058, ≤700 s1456–1474). #249 D34: real media,
|
||
titles and states that the concept's one demo image never shows. */
|
||
/* #249 D34: a real title can wrap (long linked titles and filenames). Above 700px the dialog stacks its header and the
|
||
concept's `calc(100% - 72px)` layout in a column, so a taller header shrinks the layout instead of pushing the toolbar
|
||
out of view; a one-line title gives the concept's boxes. */
|
||
@media (min-width: 701px) {
|
||
.ca-tools #viewer-dialog[open] { display: flex; flex-direction: column; /* D34 */ }
|
||
}
|
||
/* #249 D34: under 200% page zoom the concept's 94dvh outgrows the viewport; the dialog keeps within it. */
|
||
.ca-tools #viewer-dialog { max-height: min(94dvh, 94%); /* D34 */ }
|
||
#viewer-dialog .dialog-header > div { min-width: 0; }
|
||
#viewer-title { overflow-wrap: anywhere; /* D34 */ }
|
||
#viewer-dialog, #viewer-dialog::backdrop { overscroll-behavior: contain; }
|
||
#viewer-details { overscroll-behavior: contain; /* D34 */ }
|
||
/* #249 D34: Viewer.js's stylesheet styles every `.viewer-toolbar` (`position: absolute; bottom: 0; left: 0; right: 0`),
|
||
which lifts the concept's toolbar out of `.viewer-left` to span the whole dialog over the details' last lines. It stays
|
||
in the column under the stage, where the concept's `.viewer-left` flex column and border place it. */
|
||
#viewer-toolbar { position: static; /* D34 */ }
|
||
/* #249 D34: Viewer.js's own toolbar stays hidden (`toolbar: false`); the concept's `.viewer-toolbar` rule would show it. */
|
||
.viewer-container .viewer-hide { display: none; /* D34 */ }
|
||
/* #249 D34: the stage is focusable for arrow-key navigation; a video plays with native controls, an image without
|
||
Viewer.js shows itself (the concept hides it under the viewer), and a load failure covers the stage with Retry. */
|
||
#viewer-stage:focus-visible { outline: 2px solid var(--ca-focus); outline-offset: -2px; }
|
||
#viewer-stage > :is(video, img:not([hidden])) { position: absolute; inset: 0; display: block; width: 100%; height: 100%; object-fit: contain; visibility: visible; /* D34 */ }
|
||
.media-failure { position: absolute; inset: 0; z-index: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px; background: var(--ca-field); text-align: center; font-size: 12px; }
|
||
/* #249 D34: Download stays the original file's link, drawn as the concept's toolbar button. */
|
||
#viewer-toolbar > .media-download { display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-height: 32px; padding: 4px 10px; border: 1px solid var(--ca-border); border-radius: 5px; background: var(--ca-control); color: var(--ca-text); font-size: 12px; white-space: nowrap; /* D34 */ }
|
||
#viewer-toolbar > .media-download:hover:not([aria-disabled="true"]) { background: var(--ca-hover); }
|
||
#viewer-toolbar > [aria-disabled="true"] { opacity: .55; cursor: not-allowed; }
|
||
/* #249 D34: after Next → the position status and the unavailability reasons, each on its own centred line. */
|
||
#viewer-toolbar > p { flex-basis: 100%; margin: 0; text-align: center; font-size: 11px; line-height: 1.6; overflow-wrap: anywhere; }
|
||
@media (pointer: coarse) {
|
||
/* #249 D34: coarse pointers keep 40px square viewer targets (the concept's "−" and "+" are narrower). */
|
||
#viewer-dialog :is(.dialog-header > button, #viewer-toolbar > button, #viewer-toolbar > a) { min-width: 40px; min-height: 40px; /* D34 */ }
|
||
/* #249 D34: the details' disclosure summaries and "Download original" keep 40px targets too (the concept's coarse
|
||
rule covers buttons only); the summary keeps its disclosure marker. */
|
||
#viewer-details summary { box-sizing: border-box; min-block-size: 40px; padding-block: 10px; /* D34 */ }
|
||
#viewer-details a { display: inline-flex; align-items: center; min-block-size: 40px; /* D34 */ }
|
||
}
|
||
@media (max-width: 700px) {
|
||
#viewer-toolbar > .media-download { font-size: 10px; /* D34: as the concept's `.viewer-toolbar button` */ }
|
||
}
|
||
|
||
/* Interim page guards: concept element rules (`.ca-tools dialog`, `pre`) now reach production dialogs and prompt text
|
||
that keep their own design until their page PR rebuilds them in concept markup. */
|
||
.ca-tools dialog.prompt-sender { width: min(760px, calc(100% - 32px)); max-width: none; max-height: calc(100dvh - 32px); padding: 22px; border-color: var(--ca-line); border-radius: 12px; } /* PR D */
|
||
.ca-tools dialog.prompt-sender::backdrop { background: #000b; } /* PR D */
|
||
.ca-tools dialog.recorded-run-dialog { width: min(1100px, calc(100% - 32px)); max-width: calc(100% - 32px); max-height: calc(100dvh - 32px); padding: 22px; border-radius: 7px; } /* PR F */
|
||
.prompt-text[tabindex]:focus-visible { outline: 2px solid var(--ca-focus); outline-offset: 3px; } /* production-only: a confirmed prompt stays keyboard-reachable (#249 D22) */
|
||
@media (pointer: coarse) {
|
||
.ca-tools .recorded-history .history-toolbar button { min-height: 44px; } /* PR F: concept coarse buttons are 40px */
|
||
}
|
||
@media (max-width: 700px) {
|
||
.ca-tools dialog.recorded-run-dialog { padding: 16px; } /* PR F */
|
||
}
|
||
|
||
/* Images side panel (#249 D2), production-only. Real prompts can be far longer than the concept's samples, so the sticky
|
||
panel scrolls within the window and its footer stays reachable; where the concept unsticks it, it grows instead. */
|
||
.detail-panel { min-inline-size: 0; max-block-size: calc(100dvh - 40px); overflow: auto; /* D2 */ }
|
||
@media (max-width: 700px) { .detail-panel { max-block-size: none; /* D2 */ } }
|
||
@container images-page (inline-size < 600px) { .detail-panel { position: static; max-block-size: none; /* D2 */ } }
|
||
/* #249 D2/D20: the prompt blocks' mount point is transparent, so their sections sit in `.detail-inner` (panel) and the
|
||
viewer details' wrapper as the concept's do. */
|
||
:is(.detail-inner, #viewer-details > div) > .prompt-inspector { display: contents; /* D20 */ }
|
||
/* #249 D2: real titles and filenames can be one unbroken word; they wrap inside the 310px panel. */
|
||
.panel-heading h2, .detail-inner > p.small { min-inline-size: 0; overflow-wrap: anywhere; }
|
||
|
||
/* #249 D3: production-only rules kept from the retired images.css and inspector.css. Each has a live consumer. */
|
||
/* The inspector wraps real unbroken literals (model paths, raw records) inside the 310px panel and the 340px viewer
|
||
details, and an unavailable download stays visibly inert next to its reason. */
|
||
.file-inspector { min-inline-size: 0; overflow-wrap: anywhere; }
|
||
.file-inspector [aria-disabled="true"] { opacity: .6; cursor: not-allowed; }
|
||
/* #249 D20: the viewer details' pair send and history buttons wrap inside the narrow rail at 200% zoom with wider fonts,
|
||
where the toolkit's `nowrap` would push them past its edge. */
|
||
.ca-tools #viewer-details > button { white-space: normal; /* D20 */ }
|
||
/* The Images page narrows to one column inside a narrow container (the Library-embedded gallery). */
|
||
.images-main { container: images-page / inline-size; }
|
||
@container images-page (inline-size < 600px) {
|
||
.image-workspace { grid-template-columns: minmax(0, 1fr); }
|
||
}
|
||
/* #249 D42: a discovery alert a rescan leaves scrolls clear of the concept's fixed toast (22px inset, 12px at 700px and
|
||
below, two 12px lines at 390px), never under it; the toast itself keeps the concept's placement. */
|
||
.options-status { font-size: 12px; padding-inline: 4px; scroll-margin-block-end: 96px; /* D42 */ }
|
||
/* Until their pages rebuild (PR F history): the history page's and the recorded run's prompt blocks keep their
|
||
spacing (on the shelf's cards `.prompt-inspector` is `display: contents`, so its margin is inert there), the Archive
|
||
negative dialog keeps `section.prompt-block`, and the shared prompt sender keeps its dialog and send button. */
|
||
.prompt-inspector { margin-block-start: 22px; min-inline-size: 0; }
|
||
.prompt-block { border-block-start: 1px solid var(--ca-line); padding-block: 13px; min-inline-size: 0; }
|
||
.prompt-negative { --prompt-role: #ff8c98; }
|
||
[data-ca-theme="light"] .prompt-negative { --prompt-role: #b71931; }
|
||
.prompt-block-heading { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; }
|
||
.prompt-block h3 { font-size: 10px; text-transform: uppercase; letter-spacing: .5px; margin: 0; color: var(--prompt-role); }
|
||
.prompt-empty, .prompt-block > .muted { font-size: 10px; }
|
||
/* PR E/F: the Archive dialog and the frozen field provenance keep `pre.prompt-text`, guarded against the concept `pre` rule. */
|
||
pre.prompt-text { font: inherit; font-size: 12px; line-height: 1.75; white-space: pre-wrap; overflow-wrap: anywhere; margin-block: 10px; background: none; padding: 0; border: 0; border-radius: 0; max-height: none; overflow: visible; }
|
||
.prompt-send-button { display: inline-flex; align-items: center; justify-content: center; gap: 7px; color: var(--ca-focus); }
|
||
.prompt-send-button svg { inline-size: 16px; block-size: 16px; flex-shrink: 0; }
|
||
.prompt-sender { background: var(--ca-surface); color: var(--ca-text); border: 1px solid var(--ca-line); border-radius: 12px; padding: 22px; inline-size: min(760px, calc(100% - 32px)); max-inline-size: none; max-block-size: calc(100dvh - 32px); overflow: auto; }
|
||
.prompt-sender::backdrop { background: #000b; }
|
||
.prompt-sender-header, .prompt-sender-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; }
|
||
.prompt-sender-header h2 { font-size: 20px; margin: 0; }
|
||
.prompt-sender-options { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; margin-block: 20px; }
|
||
.prompt-sender-options .ca-field { display: flex; flex-direction: column; align-items: stretch; gap: 6px; }
|
||
.prompt-sender select { inline-size: 100%; min-block-size: 38px; }
|
||
.prompt-sender-status { font-size: 13px; line-height: 1.6; }
|
||
.prompt-sender-status[role="alert"] { color: var(--ca-error-text); }
|
||
.prompt-sender-preview { margin-block: 20px; min-inline-size: 0; }
|
||
.prompt-sender-field { border: 1px solid var(--ca-line); border-radius: 7px; padding: 14px; margin-block: 12px; min-inline-size: 0; }
|
||
.prompt-sender-field :where(pre, textarea), .prompt-sender-composition { white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; font-size: 12px; line-height: 1.65; max-inline-size: 100%; }
|
||
.prompt-sender-field textarea { inline-size: 100%; min-block-size: 100px; resize: vertical; }
|
||
.prompt-sender-composition { padding: 12px; background: var(--ca-field); border-radius: 6px; }
|
||
.prompt-sender-actions { justify-content: flex-end; margin-block-start: 18px; }
|
||
@media (max-width: 780px) {
|
||
.prompt-sender-options { grid-template-columns: minmax(0, 1fr); }
|
||
}
|
||
|
||
/* Prompt library cards (#249 PR E): the concept's `article.prompt-card` (s653–708) with real prompts, titles and media. */
|
||
/* #249 D23: real prompts are far longer than the concept's samples, so the card clamps the positive to two lines; copy
|
||
and send keep the full text, and history shows all of it. */
|
||
.prompt-card .prompt-text { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; /* D23 */ }
|
||
/* #249 D23: the prompt blocks' mount point is transparent, so the section sits in `.card-content` as the concept's does. */
|
||
.card-content > .prompt-inspector { display: contents; /* D23 */ }
|
||
/* #249 D23: a real title can be long or one unbroken word; it wraps inside the title row (the toolkit's buttons never
|
||
wrap), while the category chip keeps whole words beside it and wraps only a name wider than its share of the row. */
|
||
.card-title button { min-width: 0; white-space: normal; overflow-wrap: anywhere; /* D23 */ }
|
||
.card-title .chip { flex-shrink: 0; max-width: 60%; overflow-wrap: anywhere; /* D23 */ }
|
||
.prompt-card .tags .chip { max-width: 100%; overflow-wrap: anywhere; /* D23 */ }
|
||
/* #249 D23: a parent with one preview, or none, fills the art row instead of leaving the concept's second cell empty. */
|
||
.prompt-card .card-art > :only-child { grid-column: 1 / -1; /* D23 */ }
|
||
.prompt-card .card-art > .small { display: grid; place-content: center; min-width: 0; color: var(--ca-muted); /* D23 */ }
|