adf0d17497
publish / version_or_publish (push) Has been cancelled
storybook-build / changes (push) Has been cancelled
storybook-build / :storybook-build (push) Has been cancelled
Sync Gradio Skills to Hugging Face / sync-skills (push) Has been cancelled
functional / changes (push) Has been cancelled
functional / build-frontend (push) Has been cancelled
functional / functional-test-SSR=false (push) Has been cancelled
functional / functional-reload (push) Has been cancelled
js / changes (push) Has been cancelled
js / js-test (push) Has been cancelled
docs-build / changes (push) Has been cancelled
docs-build / docs-build (push) Has been cancelled
docs-build / website-build (push) Has been cancelled
functional / functional-test-SSR=true (push) Has been cancelled
hygiene / hygiene-test (push) Has been cancelled
python / changes (push) Has been cancelled
python / build (push) Has been cancelled
python / test-ubuntu-latest-flaky (push) Has been cancelled
python / test-ubuntu-latest-not-flaky (push) Has been cancelled
python / test-windows-latest-flaky (push) Has been cancelled
python / test-windows-latest-not-flaky (push) Has been cancelled
775 lines
18 KiB
Svelte
775 lines
18 KiB
Svelte
<script lang="ts">
|
|
import type {
|
|
WFNode,
|
|
PortType,
|
|
NodeDataValue,
|
|
FileValue
|
|
} from "./workflow-types";
|
|
import { BaseTextbox } from "@gradio/textbox";
|
|
import { BaseStaticImage } from "@gradio/image";
|
|
import DownloadIcon from "./icons/DownloadIcon.svelte";
|
|
|
|
interface Props {
|
|
node: WFNode;
|
|
widgetPortId: string;
|
|
widgetType: PortType;
|
|
isReadonly: boolean;
|
|
ondatachange: (
|
|
nodeId: string,
|
|
portId: string,
|
|
value: NodeDataValue
|
|
) => void;
|
|
}
|
|
|
|
let { node, widgetPortId, widgetType, isReadonly, ondatachange }: Props =
|
|
$props();
|
|
|
|
const widgetPort = $derived(
|
|
node.outputs.find((p) => p.id === widgetPortId) ??
|
|
node.inputs.find((p) => p.id === widgetPortId)
|
|
);
|
|
const choices = $derived(widgetPort?.choices ?? null);
|
|
const hasChoices = $derived(!!choices?.length);
|
|
const multiselect = $derived(!!widgetPort?.multiselect);
|
|
|
|
let fileInputEl: HTMLInputElement | undefined = $state();
|
|
|
|
function getTextValue(): string {
|
|
const v = node.data?.[widgetPortId];
|
|
return typeof v === "string" ? v : "";
|
|
}
|
|
|
|
function getFileValue(): FileValue | null {
|
|
const v = node.data?.[widgetPortId];
|
|
return v && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
}
|
|
|
|
function getNumberValue(): number {
|
|
const v = node.data?.[widgetPortId];
|
|
return typeof v === "number" ? v : 0;
|
|
}
|
|
|
|
function getBooleanValue(): boolean {
|
|
const v = node.data?.[widgetPortId];
|
|
return typeof v === "boolean" ? v : false;
|
|
}
|
|
|
|
function handleNumberInput(e: Event) {
|
|
const target = e.currentTarget as HTMLInputElement;
|
|
ondatachange(node.id, widgetPortId, parseFloat(target.value) || 0);
|
|
}
|
|
|
|
function handleBooleanInput(e: Event) {
|
|
const target = e.currentTarget as HTMLInputElement;
|
|
ondatachange(node.id, widgetPortId, target.checked);
|
|
}
|
|
|
|
function revoke_old_blob(): void {
|
|
const old = getFileValue();
|
|
if (old?.url?.startsWith("blob:")) URL.revokeObjectURL(old.url);
|
|
}
|
|
|
|
function adopt_file(file: File): void {
|
|
revoke_old_blob();
|
|
ondatachange(node.id, widgetPortId, {
|
|
name: file.name,
|
|
url: URL.createObjectURL(file),
|
|
mime: file.type
|
|
});
|
|
}
|
|
|
|
function handleFileSelect(e: Event) {
|
|
const file = (e.currentTarget as HTMLInputElement).files?.[0];
|
|
if (file) adopt_file(file);
|
|
}
|
|
|
|
function handleFileDrop(e: DragEvent) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
const file = e.dataTransfer?.files?.[0];
|
|
if (file) adopt_file(file);
|
|
}
|
|
|
|
function clearFile() {
|
|
revoke_old_blob();
|
|
ondatachange(node.id, widgetPortId, null);
|
|
}
|
|
|
|
/**
|
|
* Download the current file value. Same-origin and blob/data URLs work
|
|
* via a direct `<a download>`. Cross-origin URLs (e.g. HF Inference
|
|
* results) often ignore the download attribute and navigate instead,
|
|
* so we fetch into a blob first and then trigger the link on the
|
|
* resulting object URL — that forces a save instead of a tab swap.
|
|
*/
|
|
async function downloadFile() {
|
|
const v = getFileValue();
|
|
if (!v?.url) return;
|
|
const name = v.name || "download";
|
|
const sameOrigin =
|
|
v.url.startsWith("blob:") ||
|
|
v.url.startsWith("data:") ||
|
|
(v.url.startsWith("http") &&
|
|
new URL(v.url, window.location.origin).origin ===
|
|
window.location.origin) ||
|
|
!v.url.startsWith("http");
|
|
try {
|
|
const url = sameOrigin
|
|
? v.url
|
|
: await fetch(v.url)
|
|
.then((r) => r.blob())
|
|
.then((b) => URL.createObjectURL(b));
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = name;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
if (!sameOrigin) setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
} catch {
|
|
// Fall back to navigating; user can right-click → save.
|
|
window.open(v.url, "_blank", "noopener");
|
|
}
|
|
}
|
|
|
|
const i18n = (key: string) => key;
|
|
async function stubUpload(files: File[]): Promise<any[]> {
|
|
return files.map((f) => ({
|
|
url: URL.createObjectURL(f),
|
|
orig_name: f.name,
|
|
path: f.name,
|
|
mime_type: f.type,
|
|
size: f.size
|
|
}));
|
|
}
|
|
async function stubStream(): Promise<any> {
|
|
return;
|
|
}
|
|
</script>
|
|
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<div
|
|
class="widget-zone nodrag nopan"
|
|
class:text-full={(widgetType === "text" || widgetType === "json") &&
|
|
!hasChoices}
|
|
onmousedown={(e) => e.stopPropagation()}
|
|
onpointerdown={(e) => e.stopPropagation()}
|
|
>
|
|
{#if hasChoices && choices}
|
|
{@const rawValue = node.data?.[widgetPortId]}
|
|
{@const selected = multiselect && Array.isArray(rawValue) ? rawValue : []}
|
|
{@const current =
|
|
!multiselect && typeof rawValue === "string" ? rawValue : ""}
|
|
<div class="widget-choices">
|
|
{#each choices as choice}
|
|
<label class="widget-choice-row">
|
|
{#if multiselect}
|
|
<input
|
|
type="checkbox"
|
|
class="widget-checkbox widget-checkbox-choice"
|
|
checked={selected.includes(choice)}
|
|
disabled={isReadonly}
|
|
onchange={(e) =>
|
|
ondatachange(
|
|
node.id,
|
|
widgetPortId,
|
|
e.currentTarget.checked
|
|
? [...selected, choice]
|
|
: selected.filter((c) => c !== choice)
|
|
)}
|
|
/>
|
|
{:else}
|
|
<input
|
|
type="radio"
|
|
class="widget-radio"
|
|
name="{node.id}-{widgetPortId}"
|
|
value={choice}
|
|
checked={current === choice}
|
|
disabled={isReadonly}
|
|
onchange={() => ondatachange(node.id, widgetPortId, choice)}
|
|
/>
|
|
{/if}
|
|
<span class="widget-checkbox-label">{choice}</span>
|
|
</label>
|
|
{/each}
|
|
</div>
|
|
{:else if widgetType === "text" || widgetType === "json"}
|
|
<div class="widget-text-wrap">
|
|
<div class="widget-gradio-wrap">
|
|
<BaseTextbox
|
|
value={getTextValue()}
|
|
label="text"
|
|
show_label={false}
|
|
lines={widgetType === "json" ? 4 : 3}
|
|
max_lines={8}
|
|
placeholder={isReadonly
|
|
? "Waiting for output..."
|
|
: widgetType === "json"
|
|
? '{"key": "value"}'
|
|
: "Enter text..."}
|
|
disabled={isReadonly}
|
|
onchange={(val) => {
|
|
if (node.data?.[widgetPortId] !== val)
|
|
ondatachange(node.id, widgetPortId, val);
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
{:else if widgetType === "number"}
|
|
<div class="widget-number-wrap">
|
|
{#if isReadonly}
|
|
<div class="widget-text-display">
|
|
{getNumberValue()}
|
|
</div>
|
|
{:else}
|
|
<input
|
|
class="widget-number"
|
|
type="number"
|
|
value={getNumberValue()}
|
|
oninput={handleNumberInput}
|
|
step="any"
|
|
/>
|
|
{/if}
|
|
</div>
|
|
{:else if widgetType === "boolean"}
|
|
<div class="widget-bool-wrap">
|
|
<label class="widget-checkbox-row">
|
|
<input
|
|
class="widget-checkbox"
|
|
type="checkbox"
|
|
checked={getBooleanValue()}
|
|
disabled={isReadonly}
|
|
onchange={handleBooleanInput}
|
|
/>
|
|
<span class="widget-checkbox-label"
|
|
>{getBooleanValue() ? "On" : "Off"}</span
|
|
>
|
|
</label>
|
|
</div>
|
|
{:else if widgetType === "image" || widgetType === "audio" || widgetType === "video" || widgetType === "file" || widgetType === "gallery" || widgetType === "model3d"}
|
|
{@const fileVal = getFileValue()}
|
|
{#if fileVal}
|
|
<div class="widget-preview">
|
|
{#if (widgetType === "image" || widgetType === "gallery") && isReadonly}
|
|
<div class="widget-gradio-wrap widget-gradio-image">
|
|
<BaseStaticImage
|
|
value={{
|
|
url: fileVal.url,
|
|
orig_name: fileVal.name,
|
|
path: fileVal.url,
|
|
mime_type: fileVal.mime,
|
|
meta: { _type: "gradio.FileData" }
|
|
}}
|
|
show_label={false}
|
|
{i18n}
|
|
buttons={[]}
|
|
/>
|
|
</div>
|
|
{:else if widgetType === "image" || widgetType === "gallery"}
|
|
<img class="widget-img" src={fileVal.url} alt={fileVal.name} />
|
|
{:else if widgetType === "audio"}
|
|
<audio class="widget-audio" controls src={fileVal.url}></audio>
|
|
{:else if widgetType === "video"}
|
|
<video class="widget-video" controls src={fileVal.url}></video>
|
|
{:else}
|
|
<div class="widget-file-info">
|
|
<span class="widget-file-name">{fileVal.name}</span>
|
|
</div>
|
|
{/if}
|
|
<div class="widget-preview-actions">
|
|
<button
|
|
class="widget-action"
|
|
onclick={downloadFile}
|
|
title="Download {fileVal.name}"
|
|
aria-label="Download"
|
|
>
|
|
<DownloadIcon />
|
|
</button>
|
|
{#if !isReadonly}
|
|
<button
|
|
class="widget-action widget-clear"
|
|
onclick={clearFile}
|
|
title="Clear"
|
|
aria-label="Clear">×</button
|
|
>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{:else if isReadonly}
|
|
<div class="widget-placeholder">Waiting for output...</div>
|
|
{:else}
|
|
<!-- svelte-ignore a11y_interactive_supports_focus -->
|
|
<div
|
|
class="widget-file-drop nodrag nopan"
|
|
role="button"
|
|
tabindex="0"
|
|
onclick={() => fileInputEl?.click()}
|
|
onkeydown={(e) => {
|
|
if (e.key === "Enter" || e.key === " ") fileInputEl?.click();
|
|
}}
|
|
onmousedown={(e) => e.stopPropagation()}
|
|
onpointerdown={(e) => e.stopPropagation()}
|
|
ondragover={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}}
|
|
ondrop={handleFileDrop}
|
|
>
|
|
<input
|
|
bind:this={fileInputEl}
|
|
type="file"
|
|
accept={widgetType === "image"
|
|
? "image/*"
|
|
: widgetType === "audio"
|
|
? "audio/*"
|
|
: widgetType === "video"
|
|
? "video/*"
|
|
: widgetType === "model3d"
|
|
? ".glb,.gltf,.obj,.stl"
|
|
: "*"}
|
|
onchange={handleFileSelect}
|
|
style="display: none"
|
|
/>
|
|
<span class="widget-drop-text">
|
|
Drop {widgetType} or click
|
|
</span>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
|
|
<style>
|
|
/* ─── Gradio Component Wrapper ─── */
|
|
.widget-text-wrap,
|
|
.widget-number-wrap,
|
|
.widget-bool-wrap {
|
|
padding: 6px 12px 8px;
|
|
}
|
|
|
|
.widget-gradio-wrap {
|
|
font-size: 12px;
|
|
--input-text-size: 11px;
|
|
--input-text-weight: 400;
|
|
--input-padding: 8px 10px;
|
|
--input-background-fill: #101118;
|
|
--input-background-fill-focus: #101118;
|
|
--input-border-color: #1e1f2a;
|
|
--input-border-color-focus: var(--accent);
|
|
--input-border-width: 1px;
|
|
--input-radius: 6px;
|
|
--input-shadow: none;
|
|
--input-shadow-focus: 0 0 0 2px var(--accent-dim);
|
|
--input-placeholder-color: #4a4b58;
|
|
--body-text-color: #c8c9d2;
|
|
--font-sans: "JetBrains Mono", monospace;
|
|
--line-sm: 1.4;
|
|
--spacing-sm: 4px;
|
|
--weight-semibold: 600;
|
|
--layer-1: #101118;
|
|
--shadow-inset: none;
|
|
--button-secondary-background-fill: #1e1f2a;
|
|
--button-secondary-background-fill-hover: #2a2b36;
|
|
--button-secondary-text-color: #8b8d98;
|
|
--button-shadow-active: none;
|
|
--error-icon-color: #ef4444;
|
|
}
|
|
|
|
.widget-gradio-wrap :global(textarea),
|
|
.widget-gradio-wrap :global(input) {
|
|
font-family: "JetBrains Mono", monospace !important;
|
|
font-size: 11px !important;
|
|
line-height: 1.4 !important;
|
|
background: #101118 !important;
|
|
color: #c8c9d2 !important;
|
|
border: 1px solid #1e1f2a !important;
|
|
border-radius: 6px !important;
|
|
padding: 8px 10px !important;
|
|
outline: none !important;
|
|
box-shadow: none !important;
|
|
}
|
|
|
|
.widget-gradio-wrap :global(textarea:focus),
|
|
.widget-gradio-wrap :global(input:focus) {
|
|
border-color: var(--accent) !important;
|
|
box-shadow: 0 0 0 2px var(--accent-dim) !important;
|
|
}
|
|
|
|
.widget-gradio-wrap :global(textarea::placeholder),
|
|
.widget-gradio-wrap :global(input::placeholder) {
|
|
color: #4a4b58 !important;
|
|
}
|
|
|
|
.widget-gradio-wrap :global(.block),
|
|
.widget-gradio-wrap :global(.wrap),
|
|
.widget-gradio-wrap :global(.container) {
|
|
background: transparent !important;
|
|
border: none !important;
|
|
box-shadow: none !important;
|
|
padding: 0 !important;
|
|
margin: 0 !important;
|
|
gap: 0 !important;
|
|
}
|
|
|
|
.widget-gradio-wrap :global(.block.padded) {
|
|
padding: 0 !important;
|
|
}
|
|
|
|
.widget-gradio-wrap :global(.label-wrap),
|
|
.widget-gradio-wrap :global(.info-text),
|
|
.widget-gradio-wrap :global(.icon-button-wrapper),
|
|
.widget-gradio-wrap :global(.icon-buttons) {
|
|
display: none !important;
|
|
}
|
|
|
|
.widget-gradio-image {
|
|
overflow: hidden;
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.widget-gradio-image :global(img) {
|
|
max-height: 140px !important;
|
|
width: 100% !important;
|
|
object-fit: contain !important;
|
|
display: block !important;
|
|
}
|
|
|
|
.widget-gradio-image :global(.image-container) {
|
|
max-height: 140px !important;
|
|
overflow: hidden !important;
|
|
}
|
|
|
|
.widget-gradio-image :global(.empty-wrapper) {
|
|
display: none !important;
|
|
}
|
|
|
|
.widget-gradio-wrap :global(textarea) {
|
|
min-height: 60px !important;
|
|
height: auto !important;
|
|
resize: vertical !important;
|
|
}
|
|
|
|
.widget-gradio-wrap :global(label) {
|
|
display: block !important;
|
|
}
|
|
|
|
.widget-gradio-wrap :global(.input-container) {
|
|
display: flex !important;
|
|
}
|
|
|
|
/* ─── Widget Zone ─── */
|
|
.widget-zone {
|
|
padding: 0;
|
|
border-top: 1px solid #1e1f2a;
|
|
/* Canvas blocks user-select to stop dbl-click selecting random
|
|
* UI text; opt the widget back in so users can highlight + copy
|
|
* text inside textboxes / display zones. */
|
|
user-select: text;
|
|
-webkit-user-select: text;
|
|
}
|
|
|
|
.widget-zone.text-full {
|
|
border-top: none;
|
|
}
|
|
|
|
.widget-zone.text-full .widget-text-wrap {
|
|
padding: 0;
|
|
}
|
|
|
|
.widget-zone.text-full :global(textarea) {
|
|
border-radius: 0 0 9px 9px !important;
|
|
border-top: 1px solid #1e1f2a !important;
|
|
border-left: none !important;
|
|
border-right: none !important;
|
|
border-bottom: none !important;
|
|
min-height: 120px !important;
|
|
width: 100% !important;
|
|
box-sizing: border-box !important;
|
|
resize: vertical !important;
|
|
}
|
|
|
|
.widget-zone.text-full :global(textarea:focus) {
|
|
border-top-color: var(--accent) !important;
|
|
box-shadow: none !important;
|
|
}
|
|
|
|
.widget-text-display {
|
|
font-family: "JetBrains Mono", monospace;
|
|
font-size: 11px;
|
|
line-height: 1.4;
|
|
padding: 8px 10px;
|
|
border: 1px solid #1e1f2a;
|
|
border-radius: 6px;
|
|
background: #101118;
|
|
color: #5c5e6a;
|
|
min-height: 42px;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.widget-number {
|
|
width: 100%;
|
|
font-family: "JetBrains Mono", monospace;
|
|
font-size: 12px;
|
|
border: 1px solid #1e1f2a;
|
|
border-radius: 6px;
|
|
padding: 8px 10px;
|
|
background: #101118;
|
|
color: #c8c9d2;
|
|
outline: none;
|
|
box-sizing: border-box;
|
|
transition: border-color 0.15s;
|
|
}
|
|
|
|
.widget-number:focus {
|
|
border-color: var(--accent);
|
|
box-shadow: 0 0 0 2px var(--accent-dim);
|
|
}
|
|
|
|
.widget-checkbox-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 6px 0;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.widget-choices {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
padding: 8px 12px 10px;
|
|
}
|
|
|
|
.widget-choice-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.widget-radio {
|
|
width: 14px;
|
|
height: 14px;
|
|
accent-color: var(--accent);
|
|
cursor: pointer;
|
|
border-radius: 50%;
|
|
appearance: auto;
|
|
-webkit-appearance: radio;
|
|
}
|
|
|
|
.widget-checkbox-choice {
|
|
width: 14px;
|
|
height: 14px;
|
|
accent-color: var(--accent);
|
|
cursor: pointer;
|
|
appearance: auto;
|
|
-webkit-appearance: checkbox;
|
|
}
|
|
|
|
.widget-checkbox {
|
|
width: 16px;
|
|
height: 16px;
|
|
accent-color: var(--accent);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.widget-checkbox-label {
|
|
font-family: "JetBrains Mono", monospace;
|
|
font-size: 11px;
|
|
color: #8b8d98;
|
|
}
|
|
|
|
.widget-file-info {
|
|
padding: 10px 12px;
|
|
}
|
|
|
|
.widget-file-name {
|
|
font-family: "JetBrains Mono", monospace;
|
|
font-size: 10.5px;
|
|
color: #8b8d98;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.widget-file-drop {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 80px;
|
|
border: none;
|
|
border-radius: 0 0 10px 10px;
|
|
background: #101118;
|
|
cursor: pointer;
|
|
transition: background 0.15s;
|
|
}
|
|
|
|
.widget-file-drop:hover {
|
|
background: #14151a;
|
|
}
|
|
|
|
.widget-file-drop input {
|
|
display: none;
|
|
}
|
|
|
|
.widget-drop-text {
|
|
font-size: 10.5px;
|
|
color: #4a4b58;
|
|
}
|
|
|
|
.widget-placeholder {
|
|
font-family: "JetBrains Mono", monospace;
|
|
font-size: 10px;
|
|
color: #2e2f3d;
|
|
text-align: center;
|
|
padding: 24px 0;
|
|
background: #101118;
|
|
border-radius: 0 0 10px 10px;
|
|
}
|
|
|
|
.widget-preview {
|
|
position: relative;
|
|
overflow: hidden;
|
|
border-radius: 0 0 10px 10px;
|
|
}
|
|
|
|
.widget-img {
|
|
display: block;
|
|
width: 100%;
|
|
max-height: 320px;
|
|
object-fit: contain;
|
|
background: #101118;
|
|
}
|
|
|
|
.widget-audio {
|
|
display: block;
|
|
width: 100%;
|
|
height: 36px;
|
|
border-radius: 5px;
|
|
}
|
|
|
|
.widget-video {
|
|
display: block;
|
|
width: 100%;
|
|
max-height: 100px;
|
|
object-fit: contain;
|
|
border-radius: 5px;
|
|
}
|
|
|
|
.widget-preview-actions {
|
|
position: absolute;
|
|
top: 4px;
|
|
right: 4px;
|
|
display: flex;
|
|
gap: 4px;
|
|
opacity: 0;
|
|
transition: opacity 0.15s;
|
|
}
|
|
|
|
.widget-preview:hover .widget-preview-actions {
|
|
opacity: 1;
|
|
}
|
|
|
|
.widget-action {
|
|
width: 22px;
|
|
height: 22px;
|
|
border-radius: 50%;
|
|
border: none;
|
|
background: rgba(0, 0, 0, 0.6);
|
|
color: #d5d6de;
|
|
font-size: 13px;
|
|
line-height: 1;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 0;
|
|
transition:
|
|
background 0.15s,
|
|
color 0.15s;
|
|
}
|
|
|
|
.widget-action:hover {
|
|
background: rgba(0, 0, 0, 0.85);
|
|
color: #fff;
|
|
}
|
|
|
|
.widget-clear:hover {
|
|
background: rgba(239, 68, 68, 0.85);
|
|
color: #fff;
|
|
}
|
|
|
|
/* ─── Light mode ─── */
|
|
:global(body:not(.dark)) .widget-zone {
|
|
border-top-color: #e2e4ea;
|
|
}
|
|
|
|
:global(body:not(.dark)) .widget-gradio-wrap {
|
|
--input-background-fill: #f8f9fb;
|
|
--input-background-fill-focus: #ffffff;
|
|
--input-border-color: #e2e4ea;
|
|
--input-placeholder-color: #c0c2cc;
|
|
--body-text-color: #1a1b25;
|
|
--layer-1: #f8f9fb;
|
|
--button-secondary-background-fill: #f0f1f5;
|
|
--button-secondary-background-fill-hover: #e2e4ea;
|
|
--button-secondary-text-color: #6b6e78;
|
|
}
|
|
|
|
:global(body:not(.dark)) .widget-gradio-wrap :global(textarea),
|
|
:global(body:not(.dark)) .widget-gradio-wrap :global(input) {
|
|
background: #f8f9fb !important;
|
|
color: #1a1b25 !important;
|
|
border-color: #e2e4ea !important;
|
|
}
|
|
|
|
:global(body:not(.dark)) .widget-gradio-wrap :global(textarea::placeholder),
|
|
:global(body:not(.dark)) .widget-gradio-wrap :global(input::placeholder) {
|
|
color: #c0c2cc !important;
|
|
}
|
|
|
|
:global(body:not(.dark)) .widget-text-display {
|
|
background: #f8f9fb;
|
|
border-color: #e2e4ea;
|
|
color: #6b6e78;
|
|
}
|
|
|
|
:global(body:not(.dark)) .widget-number {
|
|
background: #f8f9fb;
|
|
border-color: #e2e4ea;
|
|
color: #1a1b25;
|
|
}
|
|
|
|
:global(body:not(.dark)) .widget-checkbox-label {
|
|
color: #6b6e78;
|
|
}
|
|
|
|
:global(body:not(.dark)) .widget-file-name {
|
|
color: #6b6e78;
|
|
}
|
|
|
|
:global(body:not(.dark)) .widget-file-drop {
|
|
background: #f8f9fb;
|
|
border-color: #d0d2dc;
|
|
}
|
|
|
|
:global(body:not(.dark)) .widget-file-drop:hover {
|
|
background: #f0f1f5;
|
|
}
|
|
|
|
:global(body:not(.dark)) .widget-drop-text {
|
|
color: #b0b2bc;
|
|
}
|
|
|
|
:global(body:not(.dark)) .widget-placeholder {
|
|
background: #f8f9fb;
|
|
border-color: #e2e4ea;
|
|
color: #b0b2bc;
|
|
}
|
|
|
|
:global(body:not(.dark)) .widget-preview {
|
|
background: #f8f9fb;
|
|
border-color: #e2e4ea;
|
|
}
|
|
</style>
|