refactor: Update bottom controls for improved styling and functionality
- Adjusted button styles across various components for consistency and better UX. - Enhanced layout of action controls to utilize whitespace more effectively. - Updated slider styles for a more modern appearance and improved usability. - Refined input fields and labels for better accessibility and readability. - Introduced new app surface styles for a cohesive design across the application. - Added tests for canvas cursor behavior to ensure correct cursor display during operations.
This commit is contained in:
@@ -54,11 +54,11 @@ export function BrushControls({ tool, settings, editingMask = false, maskViewMod
|
||||
min={1}
|
||||
max={200}
|
||||
value={settings.size}
|
||||
className="w-48"
|
||||
className="min-w-10 w-[clamp(2.5rem,9vw,7rem)] shrink"
|
||||
aria-label={`${tool} size`}
|
||||
onValueChange={(size) => dispatch(commandIds.toolSetBrushSettings, { size })}
|
||||
/>
|
||||
<span className="w-12 text-right text-base text-white">{Math.round(settings.size)}</span>
|
||||
<span className="w-9 text-right text-xs font-medium text-white/85">{Math.round(settings.size)}</span>
|
||||
</label>
|
||||
<BottomControlDivider />
|
||||
<label className={bottomControlFieldClass()}>
|
||||
@@ -67,11 +67,11 @@ export function BrushControls({ tool, settings, editingMask = false, maskViewMod
|
||||
min={0}
|
||||
max={100}
|
||||
value={settings.hardness}
|
||||
className="w-48"
|
||||
className="min-w-10 w-[clamp(2.5rem,9vw,7rem)] shrink"
|
||||
aria-label={`${tool} hardness`}
|
||||
onValueChange={(hardness) => dispatch(commandIds.toolSetBrushSettings, { hardness })}
|
||||
/>
|
||||
<span className="w-12 text-right text-base text-white">{Math.round(settings.hardness)}</span>
|
||||
<span className="w-9 text-right text-xs font-medium text-white/85">{Math.round(settings.hardness)}</span>
|
||||
</label>
|
||||
{editingMask ? (
|
||||
<>
|
||||
@@ -88,7 +88,7 @@ export function BrushControls({ tool, settings, editingMask = false, maskViewMod
|
||||
<BottomControlDivider />
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full bg-white px-5 py-2 text-base font-medium text-black transition hover:bg-white/90"
|
||||
className="rounded-md bg-sky-300 px-3 py-1.5 text-xs font-semibold text-slate-950 transition hover:bg-sky-200"
|
||||
onClick={() => dispatch(commandIds.toolExitMaskEdit, undefined)}
|
||||
>
|
||||
Done
|
||||
@@ -100,5 +100,5 @@ export function BrushControls({ tool, settings, editingMask = false, maskViewMod
|
||||
}
|
||||
|
||||
function maskModeButtonClass(active: boolean) {
|
||||
return `rounded-full px-4 py-2 text-base font-medium transition ${active ? "bg-white text-black" : "bg-white/10 text-white hover:bg-white/15"}`;
|
||||
return `rounded-md px-3 py-1.5 text-xs font-medium transition ${active ? "bg-sky-300 text-slate-950" : "bg-white/[0.06] text-white/70 hover:bg-white/10"}`;
|
||||
}
|
||||
|
||||
@@ -66,11 +66,11 @@ export function ChromaKeyControls({ document, selection, settings, dispatch }: C
|
||||
min={0}
|
||||
max={255}
|
||||
value={settings.tolerance}
|
||||
className="w-36"
|
||||
className="min-w-7 w-[clamp(1.75rem,5vw,4.5rem)] shrink"
|
||||
aria-label="Chroma key tolerance"
|
||||
onValueChange={(tolerance) => dispatch(commandIds.toolSetChromaKeySettings, { tolerance })}
|
||||
/>
|
||||
<span className="w-10 text-right text-base text-white">{Math.round(settings.tolerance)}</span>
|
||||
<span className="w-8 text-right text-xs font-medium text-white/85">{Math.round(settings.tolerance)}</span>
|
||||
</label>
|
||||
<BottomControlDivider />
|
||||
<label className={bottomControlFieldClass()}>
|
||||
@@ -79,11 +79,11 @@ export function ChromaKeyControls({ document, selection, settings, dispatch }: C
|
||||
min={0}
|
||||
max={255}
|
||||
value={settings.softness}
|
||||
className="w-36"
|
||||
className="min-w-7 w-[clamp(1.75rem,5vw,4.5rem)] shrink"
|
||||
aria-label="Chroma key edge softness"
|
||||
onValueChange={(softness) => dispatch(commandIds.toolSetChromaKeySettings, { softness })}
|
||||
/>
|
||||
<span className="w-10 text-right text-base text-white">{Math.round(settings.softness)}</span>
|
||||
<span className="w-8 text-right text-xs font-medium text-white/85">{Math.round(settings.softness)}</span>
|
||||
</label>
|
||||
<BottomControlDivider />
|
||||
<label className={bottomControlFieldClass()}>
|
||||
@@ -92,11 +92,11 @@ export function ChromaKeyControls({ document, selection, settings, dispatch }: C
|
||||
min={0}
|
||||
max={20}
|
||||
value={settings.feather}
|
||||
className="w-32"
|
||||
className="min-w-7 w-[clamp(1.75rem,5vw,4rem)] shrink"
|
||||
aria-label="Chroma key mask feather"
|
||||
onValueChange={(feather) => dispatch(commandIds.toolSetChromaKeySettings, { feather })}
|
||||
/>
|
||||
<span className="w-8 text-right text-base text-white">{Math.round(settings.feather)}</span>
|
||||
<span className="w-7 text-right text-xs font-medium text-white/85">{Math.round(settings.feather)}</span>
|
||||
</label>
|
||||
<BottomControlDivider />
|
||||
<label className={bottomControlFieldClass()}>
|
||||
@@ -105,11 +105,11 @@ export function ChromaKeyControls({ document, selection, settings, dispatch }: C
|
||||
min={-20}
|
||||
max={20}
|
||||
value={settings.choke}
|
||||
className="w-32"
|
||||
className="min-w-7 w-[clamp(1.75rem,5vw,4rem)] shrink"
|
||||
aria-label="Chroma key mask choke or expand"
|
||||
onValueChange={(choke) => dispatch(commandIds.toolSetChromaKeySettings, { choke })}
|
||||
/>
|
||||
<span className="w-8 text-right text-base text-white">{Math.round(settings.choke)}</span>
|
||||
<span className="w-7 text-right text-xs font-medium text-white/85">{Math.round(settings.choke)}</span>
|
||||
</label>
|
||||
<BottomControlDivider />
|
||||
<label className={bottomControlFieldClass()}>
|
||||
@@ -118,11 +118,11 @@ export function ChromaKeyControls({ document, selection, settings, dispatch }: C
|
||||
min={0}
|
||||
max={20}
|
||||
value={settings.despeckle}
|
||||
className="w-32"
|
||||
className="min-w-7 w-[clamp(1.75rem,5vw,4rem)] shrink"
|
||||
aria-label="Chroma key mask despeckle"
|
||||
onValueChange={(despeckle) => dispatch(commandIds.toolSetChromaKeySettings, { despeckle })}
|
||||
/>
|
||||
<span className="w-8 text-right text-base text-white">{Math.round(settings.despeckle)}</span>
|
||||
<span className="w-7 text-right text-xs font-medium text-white/85">{Math.round(settings.despeckle)}</span>
|
||||
</label>
|
||||
<BottomControlDivider />
|
||||
<label className={bottomControlFieldClass()}>
|
||||
@@ -131,16 +131,16 @@ export function ChromaKeyControls({ document, selection, settings, dispatch }: C
|
||||
min={0}
|
||||
max={100}
|
||||
value={settings.spill}
|
||||
className="w-36"
|
||||
className="min-w-7 w-[clamp(1.75rem,5vw,4.5rem)] shrink"
|
||||
aria-label="Chroma key spill suppression"
|
||||
onValueChange={(spill) => dispatch(commandIds.toolSetChromaKeySettings, { spill })}
|
||||
/>
|
||||
<span className="w-10 text-right text-base text-white">{Math.round(settings.spill)}</span>
|
||||
<span className="w-8 text-right text-xs font-medium text-white/85">{Math.round(settings.spill)}</span>
|
||||
</label>
|
||||
<BottomControlDivider />
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full bg-white px-5 py-2 text-base font-medium text-black transition hover:bg-white/90 disabled:pointer-events-none disabled:opacity-35"
|
||||
className="rounded-md bg-sky-300 px-4 py-1.5 text-xs font-semibold text-slate-950 transition hover:bg-sky-200 disabled:pointer-events-none disabled:opacity-35"
|
||||
disabled={!target}
|
||||
title={target ? "Create or update a layer mask from the chroma key" : "Select one image or raster layer to create a chroma key mask"}
|
||||
onClick={() => target && void applyChromaKeyMask(target, settings, dispatch)}
|
||||
|
||||
@@ -29,12 +29,12 @@ export function BottomControlColorPicker({ value, onValueChange, ...props }: Bot
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
className="grid size-10 place-items-center rounded-full border border-white/40 bg-transparent text-white transition hover:border-white focus:outline-none focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-white"
|
||||
className="grid size-8 place-items-center rounded-md border border-white/20 bg-white/[0.03] text-white transition hover:border-white/40 focus:outline-none focus-visible:ring-2 focus-visible:ring-sky-300/50"
|
||||
aria-label={props["aria-label"]}
|
||||
title={value}
|
||||
onClick={() => inputRef.current?.click()}
|
||||
>
|
||||
<span className="size-6 rounded-full border border-white/25" style={{ backgroundColor: value }} />
|
||||
<span className="size-5 rounded border border-white/20" style={{ backgroundColor: value }} />
|
||||
</button>
|
||||
<input
|
||||
ref={inputRef}
|
||||
@@ -45,7 +45,7 @@ export function BottomControlColorPicker({ value, onValueChange, ...props }: Bot
|
||||
onChange={(event) => onValueChange(event.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="h-10 w-24 bg-transparent px-2 font-mono text-base uppercase text-white outline-none transition placeholder:text-white/30 focus:text-white"
|
||||
className="h-8 w-20 bg-transparent px-2 font-mono text-xs uppercase text-white outline-none transition placeholder:text-white/25 focus:text-white"
|
||||
value={draft}
|
||||
aria-label={`${props["aria-label"]} hex value`}
|
||||
spellCheck={false}
|
||||
|
||||
@@ -23,11 +23,11 @@ export function GenerateActionControls({ settings, generation, dispatch, workflo
|
||||
const repair = precondition.ready ? undefined : precondition.repair;
|
||||
|
||||
return (
|
||||
<div className="flex max-w-[calc(100vw-2rem)] flex-wrap items-center justify-center gap-2 px-2">
|
||||
<div className="flex max-w-[calc(100vw-2rem)] flex-nowrap items-center justify-start gap-2 whitespace-nowrap px-2">
|
||||
<button
|
||||
type="button"
|
||||
disabled={!canGenerate}
|
||||
className="h-12 rounded-full bg-white px-7 text-base font-semibold !text-black transition hover:bg-white/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/40 disabled:pointer-events-none disabled:opacity-35"
|
||||
className="h-9 rounded-lg bg-sky-300 px-5 text-xs font-semibold !text-slate-950 transition hover:bg-sky-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-sky-300/50 disabled:pointer-events-none disabled:opacity-35"
|
||||
title={job?.status === "failed" ? job.error : preconditionMessage ?? "Generate with ComfyUI"}
|
||||
onClick={() => {
|
||||
void generate();
|
||||
@@ -36,17 +36,17 @@ export function GenerateActionControls({ settings, generation, dispatch, workflo
|
||||
{busy && job?.kind === "generate" ? "Generating..." : "Generate"}
|
||||
</button>
|
||||
{preconditionMessage ? (
|
||||
<span className="flex max-w-80 items-center gap-2 rounded-full bg-amber-300/10 px-3 py-2 text-xs text-amber-100/75" role="status">
|
||||
<span className="flex max-w-80 items-center gap-2 rounded-md border border-amber-300/10 bg-amber-300/[0.07] px-2.5 py-1.5 text-xs text-amber-100/70" role="status">
|
||||
<span>{preconditionMessage}</span>
|
||||
{repair === "add-mask" ? (
|
||||
<button type="button" className="shrink-0 rounded-full bg-white px-3 py-1 font-semibold text-black" onClick={() => void workflow.prepareInpaintMask()}>Add mask</button>
|
||||
<button type="button" className="shrink-0 rounded-md bg-white px-2.5 py-1 font-semibold text-black" onClick={() => void workflow.prepareInpaintMask()}>Add mask</button>
|
||||
) : repair === "set-outpaint-padding" ? (
|
||||
<button type="button" className="shrink-0 rounded-full bg-white px-3 py-1 font-semibold text-black" onClick={() => dispatch(commandIds.toolSetGenerateSettings, { outpaint: { ...settings.outpaint, left: 128, right: 128 } })}>Add padding</button>
|
||||
<button type="button" className="shrink-0 rounded-md bg-white px-2.5 py-1 font-semibold text-black" onClick={() => dispatch(commandIds.toolSetGenerateSettings, { outpaint: { ...settings.outpaint, left: 128, right: 128 } })}>Add padding</button>
|
||||
) : null}
|
||||
</span>
|
||||
) : null}
|
||||
<GenerationJobStatus generation={generation} />
|
||||
{busy ? <button type="button" className="h-9 rounded-full bg-white/5 px-3 text-xs font-semibold text-white/70 hover:bg-white/10 hover:text-white" onClick={workflow.cancel}>Cancel</button> : null}
|
||||
{busy ? <button type="button" className="h-8 rounded-md bg-white/[0.05] px-2.5 text-xs font-semibold text-white/65 hover:bg-white/[0.09] hover:text-white" onClick={workflow.cancel}>Cancel</button> : null}
|
||||
{candidate ? (
|
||||
<>
|
||||
<CandidatePicker generation={generation} dispatch={dispatch} />
|
||||
@@ -66,19 +66,19 @@ export function GenerateActionControls({ settings, generation, dispatch, workflo
|
||||
|
||||
function CandidatePicker({ generation, dispatch }: { generation: GenerationState; dispatch: AppStore["dispatch"] }) {
|
||||
return (
|
||||
<div className="subtle-scrollbar flex max-w-[min(34rem,calc(100vw-2rem))] items-stretch gap-2 overflow-x-auto rounded-[1.25rem] bg-black/20 p-2 ring-1 ring-white/[0.08]" role="group" aria-label="Provisional generation candidates">
|
||||
<div className="subtle-scrollbar flex max-w-[min(34rem,calc(100vw-2rem))] items-stretch gap-1.5 overflow-x-auto border-l border-white/[0.08] pl-2" role="group" aria-label="Provisional generation candidates">
|
||||
{generation.candidates.map((candidate) => {
|
||||
const selected = candidate.id === (generation.selectedCandidateId ?? generation.candidates[0]?.id);
|
||||
return (
|
||||
<button
|
||||
key={candidate.id}
|
||||
type="button"
|
||||
className={`relative h-16 w-16 shrink-0 overflow-hidden rounded-xl ring-2 transition ${selected ? "ring-white" : "ring-white/10 hover:ring-white/40"}`}
|
||||
className={`relative h-16 w-16 shrink-0 overflow-hidden rounded-lg border transition ${selected ? "border-sky-300 shadow-[0_0_0_1px_rgba(125,211,252,0.35)]" : "border-white/10 hover:border-white/30"}`}
|
||||
title={`Provisional candidate · seed ${candidate.seed}`}
|
||||
onClick={() => dispatch(commandIds.generationSelectCandidate, { candidateId: candidate.id })}
|
||||
>
|
||||
<img src={candidate.source} alt="" className="h-full w-full object-cover" />
|
||||
{selected ? <span className="absolute inset-x-1 bottom-1 rounded-full bg-black/70 px-1 py-0.5 text-[0.6rem] font-semibold text-white">Reviewing</span> : null}
|
||||
{selected ? <span className="absolute inset-x-1 bottom-1 rounded bg-black/75 px-1 py-0.5 text-[0.58rem] font-semibold uppercase tracking-wide text-sky-100">Reviewing</span> : null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -116,7 +116,7 @@ function CandidateControls({
|
||||
const disabled = busy;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-center gap-1 rounded-[1.25rem] bg-white/[0.04] px-2 py-2 ring-1 ring-white/[0.05]">
|
||||
<div className="flex flex-nowrap items-center justify-center gap-1 border-l border-white/[0.08] pl-2">
|
||||
<CandidatePreview candidate={candidate} />
|
||||
<span className="px-2 text-xs font-medium text-white/55" title={`${candidate.settings.model} · ${candidate.mode}`}><strong className="block font-semibold text-white/75">Provisional result</strong>Seed {candidate.seed}</span>
|
||||
<CandidateCompareControls compareMode={compareMode} disabled={disabled} dispatch={dispatch} />
|
||||
@@ -170,14 +170,14 @@ function CandidateControls({
|
||||
|
||||
function CandidateCompareControls({ compareMode, disabled, dispatch }: { compareMode: GenerationCompareMode; disabled: boolean; dispatch: AppStore["dispatch"] }) {
|
||||
return (
|
||||
<span className="flex items-center gap-1 rounded-full bg-black/20 p-1" aria-label="Compare candidate">
|
||||
<span className="flex items-center gap-0.5 border-l border-white/[0.08] pl-1" aria-label="Compare candidate">
|
||||
{generationCompareOptions.map((option) => {
|
||||
const active = compareMode === option.mode;
|
||||
return (
|
||||
<button
|
||||
key={option.mode}
|
||||
type="button"
|
||||
className={`h-7 rounded-full px-2 text-[0.7rem] font-semibold transition ${
|
||||
className={`h-7 rounded px-2 text-[0.68rem] font-semibold transition ${
|
||||
active ? "bg-white text-black" : "text-white/55 hover:bg-white/10 hover:text-white"
|
||||
} disabled:pointer-events-none disabled:opacity-35`}
|
||||
disabled={disabled}
|
||||
@@ -200,14 +200,14 @@ const generationCompareOptions: Array<{ mode: GenerationCompareMode; label: stri
|
||||
|
||||
function CandidatePreview({ candidate }: { candidate: GenerationCandidate }) {
|
||||
if (!candidate.inputImage) {
|
||||
return <img src={candidate.source} alt="" className="h-10 w-10 rounded-full bg-black/25 object-cover ring-1 ring-white/10" />;
|
||||
return <img src={candidate.source} alt="" className="h-10 w-10 rounded-md bg-black/25 object-cover ring-1 ring-white/10" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="flex items-center -space-x-2" title={candidate.maskImage ? "Input crop, normalized mask, generated candidate" : "Before and generated candidate"}>
|
||||
<img src={candidate.inputImage} alt="" className="h-10 w-10 rounded-full bg-black/25 object-cover ring-1 ring-white/10" />
|
||||
{candidate.maskImage ? <img src={candidate.maskImage} alt="" className="h-10 w-10 rounded-full bg-black/25 object-cover ring-1 ring-white/20" /> : null}
|
||||
<img src={candidate.source} alt="" className="h-10 w-10 rounded-full bg-black/25 object-cover ring-2 ring-white/40" />
|
||||
<img src={candidate.inputImage} alt="" className="h-10 w-10 rounded-md bg-black/25 object-cover ring-1 ring-white/10" />
|
||||
{candidate.maskImage ? <img src={candidate.maskImage} alt="" className="h-10 w-10 rounded-md bg-black/25 object-cover ring-1 ring-white/20" /> : null}
|
||||
<img src={candidate.source} alt="" className="h-10 w-10 rounded-md bg-black/25 object-cover ring-1 ring-sky-300/40" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -216,7 +216,7 @@ function CandidateButton({ label, title, disabled, busy, onClick }: { label: str
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="h-9 rounded-full bg-white/5 px-3 text-xs font-semibold text-white/70 transition hover:bg-white/10 hover:text-white disabled:pointer-events-none disabled:opacity-35"
|
||||
className="h-8 rounded-md bg-white/[0.05] px-2.5 text-xs font-semibold text-white/65 transition hover:bg-white/[0.09] hover:text-white disabled:pointer-events-none disabled:opacity-35"
|
||||
disabled={disabled}
|
||||
title={title}
|
||||
onClick={onClick}
|
||||
|
||||
@@ -72,11 +72,11 @@ export function GenerateControls({ settings, resources, dispatch }: GenerateCont
|
||||
|
||||
return (
|
||||
<div className="grid gap-5 pb-2 tabular-nums">
|
||||
{resources.error ? <p className="rounded-full bg-red-500/10 px-3 py-2 text-xs text-red-200">{resources.error}</p> : null}
|
||||
{resources.error ? <p className="rounded-md border border-red-400/15 bg-red-500/10 px-2.5 py-2 text-xs text-red-200">{resources.error}</p> : null}
|
||||
|
||||
<section className={panelSectionClass()}>
|
||||
<div className="px-1">
|
||||
<h2 className="text-base font-semibold text-white/90">Edit with AI</h2>
|
||||
<h2 className="text-sm font-semibold text-white/90">Edit with AI</h2>
|
||||
<p className="mt-1 text-xs leading-5 text-white/45">Choose an outcome. Image Studio configures the matching generation workflow.</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
@@ -85,14 +85,14 @@ export function GenerateControls({ settings, resources, dispatch }: GenerateCont
|
||||
key={intent.value}
|
||||
type="button"
|
||||
aria-pressed={settings.mode === intent.mode}
|
||||
className={`rounded-[1.25rem] px-3 py-3 text-left transition focus:outline-none focus-visible:ring-2 focus-visible:ring-white/30 ${settings.mode === intent.mode ? "bg-white text-black" : "bg-white/[0.05] text-white/75 hover:bg-white/10"}`}
|
||||
className={`border-l-2 px-2.5 py-1.5 text-left transition focus:outline-none focus-visible:ring-2 focus-visible:ring-sky-300/50 ${settings.mode === intent.mode ? "border-sky-300 bg-sky-300/10 text-sky-100" : "border-transparent text-white/60 hover:border-white/20 hover:text-white"}`}
|
||||
onClick={() => dispatch(commandIds.toolChooseGenerateIntent, { intent: intent.value })}
|
||||
>
|
||||
<span className="block text-sm font-semibold">{intent.label}</span>
|
||||
<span className={`mt-1 block text-xs leading-4 ${settings.mode === intent.mode ? "text-black/55" : "text-white/40"}`}>{intent.description}</span>
|
||||
<span className={`mt-1 block text-xs leading-4 ${settings.mode === intent.mode ? "text-sky-100/70" : "text-white/40"}`}>{intent.description}</span>
|
||||
</button>
|
||||
))}
|
||||
<button type="button" disabled className="rounded-[1.25rem] bg-white/[0.025] px-3 py-3 text-left text-white/30" title="Dedicated object removal is not supported by the current backend. Use Replace and describe the desired background.">
|
||||
<button type="button" disabled className="border-l-2 border-transparent px-2.5 py-1.5 text-left text-white/25" title="Dedicated object removal is not supported by the current backend. Use Replace and describe the desired background.">
|
||||
<span className="block text-sm font-semibold">Remove</span>
|
||||
<span className="mt-1 block text-xs leading-4">Use Replace for now; describe what should fill the area.</span>
|
||||
</button>
|
||||
@@ -154,7 +154,7 @@ export function GenerateControls({ settings, resources, dispatch }: GenerateCont
|
||||
<PanelSelect label="VAE" value={settings.vae} options={supportOptions.vaes} ariaLabel="Generate VAE" onValueChange={(vae) => dispatch(commandIds.toolSetGenerateSettings, { vae })} />
|
||||
</>
|
||||
) : null}
|
||||
<PanelNumber label="Seed" aria-label="Generate seed" min={-1} max={Number.MAX_SAFE_INTEGER} value={settings.seed} onValueChange={(seed) => dispatch(commandIds.toolSetGenerateSettings, { seed })} />
|
||||
<PanelNumber label="Seed" aria-label="Generate seed" value={settings.seed} onValueChange={(seed) => dispatch(commandIds.toolSetGenerateSettings, { seed })} />
|
||||
<PanelSelect label="Sampler" value={settings.sampler} options={samplerOptions} ariaLabel="Generate sampler" onValueChange={(sampler) => dispatch(commandIds.toolSetGenerateSettings, { sampler })} />
|
||||
<PanelSelect label="Scheduler" value={settings.scheduler} options={schedulerOptions} ariaLabel="Generate scheduler" onValueChange={(scheduler) => dispatch(commandIds.toolSetGenerateSettings, { scheduler })} />
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
@@ -211,12 +211,12 @@ export function GenerateControls({ settings, resources, dispatch }: GenerateCont
|
||||
<span className="min-w-0 flex-1 text-right text-white">{settings.inpaint.maskedAreaOnly ? "Mask crop" : "Full layer"}</span>
|
||||
</button>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<PanelNumber label="Pad" aria-label="Inpaint crop padding" value={settings.inpaint.cropPadding} max={2048} onValueChange={(cropPadding) => dispatch(commandIds.toolSetGenerateSettings, { inpaint: { ...settings.inpaint, cropPadding } })} />
|
||||
<PanelNumber label="Grow" aria-label="Inpaint backend grow mask" value={settings.inpaint.growMaskBy} max={256} onValueChange={(growMaskBy) => dispatch(commandIds.toolSetGenerateSettings, { inpaint: { ...settings.inpaint, growMaskBy } })} />
|
||||
<PanelNumber label="Expand" aria-label="Inpaint mask expand" value={settings.inpaint.maskExpand} min={-256} max={256} onValueChange={(maskExpand) => dispatch(commandIds.toolSetGenerateSettings, { inpaint: { ...settings.inpaint, maskExpand } })} />
|
||||
<PanelNumber label="Feather" aria-label="Inpaint mask feather" value={settings.inpaint.maskFeather} max={256} onValueChange={(maskFeather) => dispatch(commandIds.toolSetGenerateSettings, { inpaint: { ...settings.inpaint, maskFeather } })} />
|
||||
<PanelNumber label="Blur" aria-label="Inpaint mask blur" value={settings.inpaint.maskBlur} max={256} onValueChange={(maskBlur) => dispatch(commandIds.toolSetGenerateSettings, { inpaint: { ...settings.inpaint, maskBlur } })} />
|
||||
<PanelNumber label="Clean" aria-label="Inpaint mask despeckle" value={settings.inpaint.maskDespeckle} max={64} onValueChange={(maskDespeckle) => dispatch(commandIds.toolSetGenerateSettings, { inpaint: { ...settings.inpaint, maskDespeckle } })} />
|
||||
<PanelNumber label="Pad" aria-label="Inpaint crop padding" value={settings.inpaint.cropPadding} onValueChange={(cropPadding) => dispatch(commandIds.toolSetGenerateSettings, { inpaint: { ...settings.inpaint, cropPadding } })} />
|
||||
<PanelNumber label="Grow" aria-label="Inpaint backend grow mask" value={settings.inpaint.growMaskBy} onValueChange={(growMaskBy) => dispatch(commandIds.toolSetGenerateSettings, { inpaint: { ...settings.inpaint, growMaskBy } })} />
|
||||
<PanelNumber label="Expand" aria-label="Inpaint mask expand" value={settings.inpaint.maskExpand} onValueChange={(maskExpand) => dispatch(commandIds.toolSetGenerateSettings, { inpaint: { ...settings.inpaint, maskExpand } })} />
|
||||
<PanelNumber label="Feather" aria-label="Inpaint mask feather" value={settings.inpaint.maskFeather} onValueChange={(maskFeather) => dispatch(commandIds.toolSetGenerateSettings, { inpaint: { ...settings.inpaint, maskFeather } })} />
|
||||
<PanelNumber label="Blur" aria-label="Inpaint mask blur" value={settings.inpaint.maskBlur} onValueChange={(maskBlur) => dispatch(commandIds.toolSetGenerateSettings, { inpaint: { ...settings.inpaint, maskBlur } })} />
|
||||
<PanelNumber label="Clean" aria-label="Inpaint mask despeckle" value={settings.inpaint.maskDespeckle} onValueChange={(maskDespeckle) => dispatch(commandIds.toolSetGenerateSettings, { inpaint: { ...settings.inpaint, maskDespeckle } })} />
|
||||
</div>
|
||||
</div>
|
||||
</section> : null}
|
||||
@@ -236,26 +236,26 @@ function SectionTitle({ title }: { title: string }) {
|
||||
}
|
||||
|
||||
function panelSectionClass() {
|
||||
return "grid gap-3 rounded-[1.75rem] bg-white/[0.035] p-3 ring-1 ring-white/[0.04]";
|
||||
return "grid gap-2 border-b border-white/[0.07] px-0.5 pb-3 last:border-b-0 last:pb-0";
|
||||
}
|
||||
|
||||
function sectionToggleClass() {
|
||||
return "flex w-full items-center justify-between gap-3 rounded-[1.25rem] px-2 py-1 text-left transition hover:bg-white/[0.04] focus:outline-none focus-visible:ring-2 focus-visible:ring-white/30";
|
||||
return "flex w-full items-center justify-between gap-2 rounded-md px-1 py-0.5 text-left transition hover:bg-white/[0.04] focus:outline-none focus-visible:ring-2 focus-visible:ring-sky-300/50";
|
||||
}
|
||||
|
||||
function PanelSelect<TValue extends string>({ label, value, options, ariaLabel, onValueChange }: { label: string; value: TValue; options: readonly BottomControlSelectOption<TValue>[]; ariaLabel: string; onValueChange: (value: TValue) => void }) {
|
||||
return (
|
||||
<div className="rounded-[1.25rem] bg-white/[0.04] p-1">
|
||||
<div className="p-0.5">
|
||||
<BottomControlSelectMenu label={label} value={value} options={options} aria-label={ariaLabel} placement="inline" onValueChange={onValueChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PanelNumber({ label, value, onValueChange, min = 0, max = 4096, ...props }: { label: string; "aria-label": string; value: number; min?: number; max?: number; onValueChange: (value: number) => void }) {
|
||||
function PanelNumber({ label, value, onValueChange, ...props }: { label: string; "aria-label": string; value: number; onValueChange: (value: number) => void }) {
|
||||
return (
|
||||
<label className={panelRowClass()}>
|
||||
<span className={panelLabelClass()}>{label}</span>
|
||||
<NumberInput {...props} min={min} max={max} value={value} onValueChange={onValueChange} />
|
||||
<NumberInput {...props} value={value} onValueChange={onValueChange} />
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -269,14 +269,14 @@ function SizeControl({ refRoot, open, setOpen, settings, dispatch }: { refRoot:
|
||||
<CaretDown size={16} weight="bold" />
|
||||
</button>
|
||||
{open ? (
|
||||
<div className="absolute right-0 top-full z-30 mt-2 grid w-full gap-3 rounded-[1.5rem] bg-slate-950/90 p-3 text-white shadow-2xl ring-1 ring-white/10 backdrop-blur-xl">
|
||||
<div className="app-surface absolute right-0 top-full z-30 mt-1.5 grid w-full gap-2 rounded-lg p-2.5 text-white shadow-2xl">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<PanelNumber label="W" aria-label="Generate width" value={settings.width} onValueChange={(width) => dispatch(commandIds.toolSetGenerateSettings, { width })} />
|
||||
<PanelNumber label="H" aria-label="Generate height" value={settings.height} onValueChange={(height) => dispatch(commandIds.toolSetGenerateSettings, { height })} />
|
||||
</div>
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
{sizePresets.map((preset) => (
|
||||
<button key={preset.label} type="button" className="rounded-full bg-white/5 px-2 py-2 text-xs text-white/75 transition hover:bg-white/10 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-white/30" onClick={() => dispatch(commandIds.toolSetGenerateSettings, { width: preset.w, height: preset.h })}>
|
||||
<button key={preset.label} type="button" className="rounded-md bg-white/[0.04] px-2 py-1.5 text-xs text-white/65 transition hover:bg-white/[0.08] hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-sky-300/50" onClick={() => dispatch(commandIds.toolSetGenerateSettings, { width: preset.w, height: preset.h })}>
|
||||
{preset.label}
|
||||
</button>
|
||||
))}
|
||||
@@ -287,12 +287,19 @@ function SizeControl({ refRoot, open, setOpen, settings, dispatch }: { refRoot:
|
||||
);
|
||||
}
|
||||
|
||||
function NumberInput({ value, onValueChange, min = 0, max = 4096, ...props }: { "aria-label": string; value: number; min?: number; max?: number; onValueChange: (value: number) => void }) {
|
||||
return <input {...props} type="number" min={min} max={max} step={1} value={Math.round(value)} className="h-9 w-16 rounded-full bg-white/5 px-2 text-right text-sm text-white outline-none transition hover:bg-white/10 focus:bg-white/10 focus:ring-2 focus:ring-white/30" onChange={(event) => onValueChange(Number(event.currentTarget.value))} />;
|
||||
function NumberInput({ value, onValueChange, ...props }: { "aria-label": string; value: number; onValueChange: (value: number) => void }) {
|
||||
const [draft, setDraft] = useState(String(Math.round(value)));
|
||||
useEffect(() => setDraft(String(Math.round(value))), [value]);
|
||||
const commit = () => {
|
||||
const next = Number(draft.trim());
|
||||
if (draft.trim() !== "" && Number.isFinite(next)) onValueChange(next);
|
||||
else setDraft(String(Math.round(value)));
|
||||
};
|
||||
return <input {...props} type="text" inputMode="numeric" value={draft} className="h-7 w-12 rounded bg-transparent px-1.5 text-right font-mono text-xs text-white/90 outline-none transition hover:bg-white/[0.04] focus:bg-white/[0.06] focus:ring-1 focus:ring-sky-300/50" onFocus={(event) => event.currentTarget.select()} onChange={(event) => setDraft(event.currentTarget.value)} onBlur={commit} onKeyDown={(event) => { event.stopPropagation(); if (event.key === "Enter") event.currentTarget.blur(); if (event.key === "Escape") { setDraft(String(Math.round(value))); event.currentTarget.blur(); } }} />;
|
||||
}
|
||||
|
||||
function panelRowClass() {
|
||||
return "flex min-h-11 items-center justify-between gap-3 rounded-full bg-white/[0.04] px-4";
|
||||
return "flex min-h-9 items-center justify-between gap-2 border-b border-white/[0.045] px-1 last:border-b-0";
|
||||
}
|
||||
|
||||
function compactRowButtonClass() {
|
||||
@@ -304,5 +311,5 @@ function panelLabelClass() {
|
||||
}
|
||||
|
||||
function panelTextAreaClass(extra = "") {
|
||||
return `${extra} resize-none rounded-[1.25rem] bg-white/5 px-4 py-3 text-sm text-white outline-none transition placeholder:text-white/25 focus:bg-white/[0.07] focus:ring-2 focus:ring-white/30`;
|
||||
return `${extra} resize-none rounded-lg border border-white/[0.05] bg-white/[0.035] px-3 py-2 text-xs text-white outline-none transition placeholder:text-white/25 focus:bg-white/[0.06] focus:ring-1 focus:ring-sky-300/50`;
|
||||
}
|
||||
|
||||
@@ -19,16 +19,19 @@ export function MagicWandControls({ settings, dispatch }: { settings: MagicWandS
|
||||
<BottomControlDivider />
|
||||
<Slider label="Clean" value={settings.despeckle} min={0} max={20} onChange={(despeckle) => dispatch(commandIds.toolSetMagicWandSettings, { despeckle })} />
|
||||
<BottomControlDivider />
|
||||
<button type="button" className={`rounded-full px-4 py-2 text-base font-medium transition ${settings.contiguous ? "bg-white text-black" : "bg-white/10 text-white"}`} aria-label="Contiguous selection" aria-pressed={settings.contiguous} title="Select only connected pixels" onClick={() => dispatch(commandIds.toolSetMagicWandSettings, { contiguous: !settings.contiguous })}>Contiguous</button>
|
||||
<button type="button" className={`rounded-md px-3 py-1.5 text-xs font-medium transition ${settings.contiguous ? "bg-sky-300 text-slate-950" : "bg-white/[0.06] text-white/70"}`} aria-label="Contiguous selection" aria-pressed={settings.contiguous} title="Select only connected pixels" onClick={() => dispatch(commandIds.toolSetMagicWandSettings, { contiguous: !settings.contiguous })}>Contiguous</button>
|
||||
<BottomControlDivider />
|
||||
{(["replace", "add", "subtract"] as const).map((mode) => (
|
||||
<button key={mode} type="button" className={`rounded-full px-4 py-2 text-base font-medium capitalize transition ${settings.mode === mode ? "bg-white text-black" : "bg-white/10 text-white"}`} aria-pressed={settings.mode === mode} onClick={() => dispatch(commandIds.toolSetMagicWandSettings, { mode })}>{mode}</button>
|
||||
<button key={mode} type="button" className={`rounded-md px-3 py-1.5 text-xs font-medium capitalize transition ${settings.mode === mode ? "bg-sky-300 text-slate-950" : "bg-white/[0.06] text-white/70"}`} aria-pressed={settings.mode === mode} onClick={() => dispatch(commandIds.toolSetMagicWandSettings, { mode })}>{mode}</button>
|
||||
))}
|
||||
<span className="px-2 text-sm text-white/60">Shift-click adds, Alt-click subtracts</span>
|
||||
<span className="flex items-center gap-1 px-1 text-[0.65rem] text-white/45" aria-label="Shift click adds; Alt click subtracts" title="Shift-click adds · Alt-click subtracts">
|
||||
<kbd className="rounded border border-white/10 px-1 py-0.5 font-sans text-white/65">⇧ +</kbd>
|
||||
<kbd className="rounded border border-white/10 px-1 py-0.5 font-sans text-white/65">⌥ −</kbd>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Slider({ label, value, min, max, onChange }: { label: string; value: number; min: number; max: number; onChange: (value: number) => void }) {
|
||||
return <label className={bottomControlFieldClass()}><span className={bottomControlLabelClass()}>{label}</span><BottomControlSlider min={min} max={max} value={value} className="w-32" aria-label={`Magic wand ${label}`} onValueChange={onChange} /><span className="w-8 text-right text-base text-white">{Math.round(value)}</span></label>;
|
||||
return <label className={`${bottomControlFieldClass()} min-w-0`}><span className={bottomControlLabelClass()}>{label}</span><BottomControlSlider min={min} max={max} value={value} className="min-w-8 w-[clamp(2rem,7vw,5rem)] shrink" aria-label={`Magic wand ${label}`} onValueChange={onChange} /><span className="w-7 shrink-0 text-right text-xs font-medium text-white/85">{Math.round(value)}</span></label>;
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ export function BottomControlSelectMenu<TValue extends string>({ value, options,
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
className={`inline-flex h-10 min-w-32 max-w-full items-center rounded-full text-base text-white/85 transition hover:bg-white/10 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-white/30 ${placement === "inline" ? "w-full px-4" : ""}`}
|
||||
className={`inline-flex h-8 min-w-28 max-w-full items-center rounded-md text-xs text-white/75 transition hover:bg-white/[0.07] hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-sky-300/50 ${placement === "inline" ? "w-full px-2.5" : ""}`}
|
||||
aria-label={props["aria-label"]}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={open}
|
||||
@@ -112,7 +112,7 @@ export function BottomControlSelectMenu<TValue extends string>({ value, options,
|
||||
value={value}
|
||||
onValueChange={onValueChange}
|
||||
setOpen={setOpen}
|
||||
className="subtle-scrollbar mt-2 max-h-56 w-full overflow-auto rounded-[1.25rem] bg-white/[0.04] p-1 text-white ring-1 ring-white/10"
|
||||
className="subtle-scrollbar mt-1.5 max-h-56 w-full overflow-auto border-t border-white/[0.07] pt-1 text-white"
|
||||
aria-label={props["aria-label"]}
|
||||
/>
|
||||
) : open ? (
|
||||
@@ -124,7 +124,7 @@ export function BottomControlSelectMenu<TValue extends string>({ value, options,
|
||||
value={value}
|
||||
onValueChange={onValueChange}
|
||||
setOpen={setOpen}
|
||||
className="subtle-scrollbar z-50 overflow-auto rounded-[1.5rem] bg-slate-950/90 p-1 text-white shadow-2xl ring-1 ring-white/10 backdrop-blur-xl"
|
||||
className="app-surface subtle-scrollbar z-50 overflow-auto rounded-lg p-1 text-white shadow-2xl"
|
||||
style={menuStyle}
|
||||
aria-label={props["aria-label"]}
|
||||
/>,
|
||||
@@ -174,7 +174,7 @@ function SelectOptions<TValue extends string>({ menuRef, id, options, value, cla
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
className={`flex h-10 w-full items-center gap-3 rounded-full px-3 text-left text-sm transition ${selected ? "bg-white !text-black" : "text-white/80 hover:bg-white/10 hover:text-white"}`}
|
||||
className={`flex h-8 w-full items-center gap-2 rounded-md px-2.5 text-left text-xs transition ${selected ? "bg-sky-300 !text-slate-950" : "text-white/70 hover:bg-white/[0.07] hover:text-white"}`}
|
||||
role="option"
|
||||
aria-selected={selected}
|
||||
tabIndex={selected ? 0 : -1}
|
||||
|
||||
@@ -6,6 +6,7 @@ export type BottomControlSliderProps = {
|
||||
max: number;
|
||||
value: number;
|
||||
step?: number;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
onValueChange: (value: number) => void;
|
||||
};
|
||||
|
||||
@@ -138,14 +138,14 @@ export function TransformControls({ bounds, target, documentIndex, layerInfo, di
|
||||
type CropDraft = Record<BoundsField, string>;
|
||||
|
||||
function CropEditor({ draft, setDraft, onApply, onCancel, onReset }: { draft: CropDraft; setDraft: Dispatch<SetStateAction<CropDraft>>; onApply: () => void; onCancel: () => void; onReset: () => void }) {
|
||||
return <div className="flex items-center gap-2 rounded-2xl bg-black/25 px-3 py-2" aria-label="Crop source pixels">
|
||||
return <div className="flex items-center gap-2 border-l border-white/[0.08] pl-3" aria-label="Crop source pixels">
|
||||
{(["x", "y", "w", "h"] as const).map((field) => <label key={field} className={bottomControlFieldClass()}><span className={bottomControlLabelClass()}>{field.toUpperCase()}</span><input className={bottomControlInputClass()} inputMode="decimal" value={draft[field]} aria-label={`Crop ${field}`} onChange={(event) => setDraft((current) => ({ ...current, [field]: event.target.value }))} onKeyDown={(event) => event.stopPropagation()} /></label>)}
|
||||
<button type="button" className={actionButtonClass()} onClick={onApply}>Apply</button><button type="button" className={actionButtonClass()} onClick={onCancel}>Cancel</button><button type="button" className={actionButtonClass()} onClick={onReset}>Reset</button>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function ArtboardResizeEditor({ draft, setDraft, onApply, onCancel }: { draft: { w: string; h: string; scaleContents: boolean }; setDraft: Dispatch<SetStateAction<{ w: string; h: string; scaleContents: boolean }>>; onApply: () => void; onCancel: () => void }) {
|
||||
return <div className="flex items-center gap-2 rounded-2xl bg-black/25 px-3 py-2" aria-label="Resize artboard canvas">
|
||||
return <div className="flex items-center gap-2 border-l border-white/[0.08] pl-3" aria-label="Resize artboard canvas">
|
||||
{(["w", "h"] as const).map((field) => <label key={field} className={bottomControlFieldClass()}><span className={bottomControlLabelClass()}>{field.toUpperCase()}</span><input className={bottomControlInputClass()} inputMode="decimal" value={draft[field]} aria-label={`Canvas ${field}`} onChange={(event) => setDraft((current) => ({ ...current, [field]: event.target.value }))} onKeyDown={(event) => event.stopPropagation()} /></label>)}
|
||||
<label className="flex items-center gap-2 text-xs text-white/70"><input type="checkbox" checked={draft.scaleContents} onChange={(event) => setDraft((current) => ({ ...current, scaleContents: event.target.checked }))} />Scale contents</label>
|
||||
<span className="max-w-40 text-[10px] text-white/45">Off changes canvas bounds only.</span>
|
||||
@@ -182,7 +182,7 @@ function rotationDegrees(layerInfo?: IndexedLayerInfo) {
|
||||
}
|
||||
|
||||
function actionButtonClass() {
|
||||
return "inline-flex h-10 items-center gap-2 rounded-full px-3 text-xs font-semibold text-white/70 transition hover:bg-white/10 hover:text-white disabled:pointer-events-none disabled:opacity-35";
|
||||
return "inline-flex h-8 items-center gap-1.5 rounded-md px-2.5 text-xs font-semibold text-white/65 transition hover:bg-white/[0.07] hover:text-white disabled:pointer-events-none disabled:opacity-35";
|
||||
}
|
||||
|
||||
function BoundsInput({
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export function bottomControlMenuClass() {
|
||||
return "flex w-full items-center justify-center gap-4 tabular-nums";
|
||||
return "flex max-w-full min-w-0 flex-nowrap items-center justify-center gap-2 whitespace-nowrap tabular-nums";
|
||||
}
|
||||
|
||||
export function bottomControlFieldClass() {
|
||||
return "flex items-center gap-3";
|
||||
return "flex min-w-0 shrink items-center gap-2 whitespace-nowrap";
|
||||
}
|
||||
|
||||
export function bottomControlDividerClass() {
|
||||
@@ -11,19 +11,19 @@ export function bottomControlDividerClass() {
|
||||
}
|
||||
|
||||
export function bottomControlLabelClass() {
|
||||
return "text-sm text-white/60";
|
||||
return "shrink-0 text-xs text-white/60";
|
||||
}
|
||||
|
||||
export function bottomControlValueClass() {
|
||||
return "min-w-16 text-center text-base text-white";
|
||||
return "min-w-12 text-center text-xs font-medium text-white/85";
|
||||
}
|
||||
|
||||
export function bottomControlInputClass() {
|
||||
return "h-10 w-20 px-2 text-center text-base text-white outline-none transition placeholder:text-white/30 focus:text-white";
|
||||
return "h-7 w-12 rounded bg-transparent px-1.5 text-right font-mono text-xs text-white/90 outline-none transition placeholder:text-white/25 hover:bg-white/[0.04] focus:bg-white/[0.06] focus:ring-1 focus:ring-sky-300/50";
|
||||
}
|
||||
|
||||
export function bottomControlIconSlotClass() {
|
||||
return "grid size-12 place-items-center rounded-full text-white/80";
|
||||
return "grid size-8 place-items-center rounded-lg text-white/70";
|
||||
}
|
||||
|
||||
export function bottomControlButtonClass() {
|
||||
|
||||
Reference in New Issue
Block a user