Match desktop app styling to the webapp (#1963)
This commit is contained in:
parent
1b27c31531
commit
90c7a8a118
|
|
@ -31,7 +31,7 @@
|
|||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap"
|
||||
<link href="https://fonts.googleapis.com/css2?family=Archivo:wdth,wght@62..125,100..900&family=Instrument+Serif:ital@0;1&family=Geist+Mono:wght@300..800&display=swap"
|
||||
rel="stylesheet" />
|
||||
<!-- <link rel="stylesheet" href="/src/styles.css" /> -->
|
||||
</head>
|
||||
|
|
|
|||
|
|
@ -191,11 +191,11 @@ export function GlobalFileDropHandler() {
|
|||
return (
|
||||
<>
|
||||
{isDragging && modalType === null && (
|
||||
<div className="pointer-events-none fixed inset-0 z-[9999] flex items-center justify-center bg-black/40">
|
||||
<div className="flex flex-col items-center gap-3 rounded-2xl border-2 border-dashed border-white/60 bg-black/30 px-16 py-12 text-white backdrop-blur-sm">
|
||||
<Maximize2Icon className="text-4xl opacity-80" />
|
||||
<div className="text-xl font-semibold">Drop to Upload</div>
|
||||
<div className="text-sm opacity-60">GLB, PNG, JPG, JPEG, SPZ</div>
|
||||
<div className="pointer-events-none fixed inset-0 z-[9999] flex items-center justify-center bg-black/70">
|
||||
<div className="flex flex-col items-center gap-3 border border-dashed border-white/60 bg-ui-panel px-16 py-12 text-white">
|
||||
<Maximize2Icon className="text-3xl opacity-60" />
|
||||
<div className="font-display text-2xl tracking-tight">Drop to Upload</div>
|
||||
<div className="hud-label text-white/50">GLB, PNG, JPG, JPEG, SPZ</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import {
|
|||
useCostBreakdownModalStore,
|
||||
} from "@storyteller/ui-pricing-modal";
|
||||
import { HelpMenuButton } from "@storyteller/ui-help-menu";
|
||||
import { CreateEmptyState } from "~/components/generation-feed/CreateEmptyState";
|
||||
|
||||
type Mode = "image" | "text";
|
||||
type Variant = "object" | "world";
|
||||
|
|
@ -550,9 +551,9 @@ export const ImageTo3DExperience = ({
|
|||
role="button"
|
||||
tabIndex={0}
|
||||
className={twMerge(
|
||||
"flex flex-col items-center justify-center rounded-2xl border-[3px] border-dashed border-primary/40 bg-primary/5 text-center text-xs transition-all hover:border-primary hover:bg-primary/10 focus:outline-none focus:ring-2 focus:ring-primary/40",
|
||||
"flex flex-col items-center justify-center border border-dashed border-white/20 bg-white/[0.015] text-center text-xs transition-all hover:border-white/40 hover:bg-white/5 focus:border-white/60 focus:outline-none",
|
||||
hasResults ? "aspect-square w-24" : "aspect-square w-48",
|
||||
dragActive && "border-primary bg-primary/10",
|
||||
dragActive && "border-white/60 bg-white/5",
|
||||
)}
|
||||
onDragEnter={(event) => {
|
||||
event.preventDefault();
|
||||
|
|
@ -582,11 +583,11 @@ export const ImageTo3DExperience = ({
|
|||
<PlusIcon
|
||||
|
||||
className={twMerge(
|
||||
"text-base-fg opacity-90 drop-shadow",
|
||||
"text-base-fg opacity-80",
|
||||
hasResults ? "text-2xl" : "text-4xl",
|
||||
)} />
|
||||
{!hasResults && (
|
||||
<span className="mt-3 text-[15px] font-medium text-base-fg opacity-60">
|
||||
<span className="mt-3 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">
|
||||
Add Image
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -599,7 +600,7 @@ export const ImageTo3DExperience = ({
|
|||
{uploadedPreview ? (
|
||||
<div
|
||||
className={twMerge(
|
||||
"group relative cursor-pointer overflow-hidden rounded-2xl border-[3px] border-primary/40 bg-black/30 transition-all",
|
||||
"group relative cursor-pointer overflow-hidden border border-white/30 bg-black/30 transition-all hover:border-white/60",
|
||||
hasResults ? "aspect-square w-24" : "aspect-square w-48",
|
||||
)}
|
||||
onClick={() => !isUploading && fileInputRef.current?.click()}
|
||||
|
|
@ -614,13 +615,13 @@ export const ImageTo3DExperience = ({
|
|||
/>
|
||||
{isUploading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-[3px] border-white/30 border-t-primary" />
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-[3px] border-white/30 border-t-white" />
|
||||
</div>
|
||||
)}
|
||||
{!isUploading && (
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-2 top-2 flex h-6 w-6 items-center justify-center rounded-full bg-black/60 text-white opacity-0 transition-opacity group-hover:opacity-100"
|
||||
className="absolute right-2 top-2 flex h-6 w-6 items-center justify-center rounded-[3px] bg-black/60 text-white opacity-0 transition-all hover:bg-black group-hover:opacity-100"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setUploadedPreview(null);
|
||||
|
|
@ -646,7 +647,7 @@ export const ImageTo3DExperience = ({
|
|||
ref={textareaRef}
|
||||
id={promptInputId}
|
||||
rows={1}
|
||||
className="text-md max-h-[5.5em] w-full resize-none overflow-y-auto rounded bg-transparent pr-2 pt-1 text-base-fg placeholder-base-fg/60 focus:outline-none"
|
||||
className="text-md max-h-[5.5em] w-full resize-none overflow-y-auto rounded-[3px] bg-transparent pr-2 pt-1 text-base-fg placeholder-base-fg/60 focus:outline-none"
|
||||
value={prompt}
|
||||
placeholder="Describe any object you want to generate from scratch..."
|
||||
onChange={(event) => setPrompt(event.target.value)}
|
||||
|
|
@ -692,8 +693,8 @@ export const ImageTo3DExperience = ({
|
|||
role="button"
|
||||
tabIndex={0}
|
||||
className={twMerge(
|
||||
"flex h-32 w-32 flex-col items-center justify-center rounded-xl border-2 border-dashed border-primary/40 bg-primary/5 transition-all hover:border-primary hover:bg-primary/10 focus:outline-none focus:ring-2 focus:ring-primary/40",
|
||||
dragActive && "border-primary bg-primary/10",
|
||||
"flex h-32 w-32 flex-col items-center justify-center border border-dashed border-white/20 bg-white/[0.015] transition-all hover:border-white/40 hover:bg-white/5 focus:border-white/60 focus:outline-none",
|
||||
dragActive && "border-white/60 bg-white/5",
|
||||
)}
|
||||
onDragEnter={(e) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -731,8 +732,8 @@ export const ImageTo3DExperience = ({
|
|||
>
|
||||
<PlusIcon
|
||||
|
||||
className="text-3xl text-base-fg opacity-90 drop-shadow" />
|
||||
<span className="mt-2 text-sm font-medium text-base-fg/50">
|
||||
className="text-3xl text-base-fg opacity-80" />
|
||||
<span className="mt-2 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">
|
||||
Add Images
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -741,7 +742,7 @@ export const ImageTo3DExperience = ({
|
|||
<textarea
|
||||
ref={worldTextareaRef}
|
||||
rows={2}
|
||||
className="w-full resize-none overflow-y-auto rounded-lg bg-white/5 px-3 py-2.5 text-base text-base-fg placeholder-base-fg/60 outline-none ring-2 ring-transparent transition-all focus:ring-primary/80"
|
||||
className="w-full resize-none overflow-y-auto rounded-[3px] border border-white/15 bg-ui-controls px-3 py-2.5 text-base text-base-fg placeholder-base-fg/60 transition-colors focus:border-white/60 focus:outline-none"
|
||||
style={{ maxHeight: "5em" }}
|
||||
value={worldPrompt}
|
||||
placeholder="Describe your 3D world (optional)..."
|
||||
|
|
@ -755,7 +756,7 @@ export const ImageTo3DExperience = ({
|
|||
<div
|
||||
key={img.id}
|
||||
className={twMerge(
|
||||
"glass group relative aspect-square overflow-hidden rounded-lg border-2 border-white/30 transition-all",
|
||||
"group relative aspect-square overflow-hidden rounded-[3px] border border-white/30 bg-black/30 transition-all",
|
||||
hasResults ? "w-10" : "w-auto",
|
||||
img.isUploading
|
||||
? "cursor-default"
|
||||
|
|
@ -773,13 +774,13 @@ export const ImageTo3DExperience = ({
|
|||
/>
|
||||
{img.isUploading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/20">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-primary" />
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
|
||||
</div>
|
||||
)}
|
||||
{!img.isUploading && (
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-[2px] top-[2px] flex h-4 w-4 cursor-pointer items-center justify-center rounded-full bg-black/50 text-white opacity-0 backdrop-blur-md transition-colors hover:bg-red/70 group-hover:opacity-100"
|
||||
className="absolute right-[2px] top-[2px] flex h-4 w-4 cursor-pointer items-center justify-center rounded-[3px] bg-black/60 text-white opacity-0 transition-colors hover:bg-red/70 group-hover:opacity-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeWorldImage(img.id);
|
||||
|
|
@ -821,7 +822,7 @@ export const ImageTo3DExperience = ({
|
|||
<Button
|
||||
variant="action"
|
||||
className={twMerge(
|
||||
"aspect-square overflow-hidden rounded-lg border-2 border-dashed border-black/5 bg-ui-controls/40 transition-all hover:bg-ui-controls/60 dark:border-white/25",
|
||||
"aspect-square overflow-hidden rounded-[3px] border border-dashed border-black/15 bg-white/5 transition-all hover:border-white/40 hover:bg-white/10 dark:border-white/25",
|
||||
hasResults ? "w-10" : "w-14",
|
||||
)}
|
||||
onClick={() => worldFileInputRef.current?.click()}
|
||||
|
|
@ -837,7 +838,7 @@ export const ImageTo3DExperience = ({
|
|||
);
|
||||
|
||||
const imageCountRow = (
|
||||
<div className="flex items-center justify-center gap-2 text-xs text-base-fg/40">
|
||||
<div className="flex items-center justify-center gap-2 font-mono text-[11px] tabular-nums text-base-fg/40">
|
||||
<span>
|
||||
{worldImages.length}/{MAX_WORLD_IMAGES} images
|
||||
</span>
|
||||
|
|
@ -866,7 +867,7 @@ export const ImageTo3DExperience = ({
|
|||
<textarea
|
||||
ref={worldTextareaRef}
|
||||
rows={1}
|
||||
className="flex-1 resize-none overflow-y-auto rounded-lg bg-white/5 px-3 py-2 text-sm text-base-fg placeholder-base-fg/60 focus:outline-none focus:ring-2 focus:ring-primary/60"
|
||||
className="flex-1 resize-none overflow-y-auto rounded-[3px] border border-white/15 bg-ui-controls px-3 py-2 text-sm text-base-fg placeholder-base-fg/60 transition-colors focus:border-white/60 focus:outline-none"
|
||||
value={worldPrompt}
|
||||
placeholder="Describe world (optional)..."
|
||||
onChange={(e) => setWorldPrompt(e.target.value)}
|
||||
|
|
@ -888,7 +889,7 @@ export const ImageTo3DExperience = ({
|
|||
<textarea
|
||||
ref={worldTextareaRef}
|
||||
rows={2}
|
||||
className="w-full resize-none overflow-y-auto rounded-lg bg-white/5 px-3 py-2.5 text-base text-base-fg placeholder-base-fg/60 focus:outline-none focus:ring-2 focus:ring-primary/60"
|
||||
className="w-full resize-none overflow-y-auto rounded-[3px] border border-white/15 bg-ui-controls px-3 py-2.5 text-base text-base-fg placeholder-base-fg/60 transition-colors focus:border-white/60 focus:outline-none"
|
||||
style={{ maxHeight: "5em" }}
|
||||
value={worldPrompt}
|
||||
placeholder="Describe your 3D world (optional)..."
|
||||
|
|
@ -908,7 +909,7 @@ export const ImageTo3DExperience = ({
|
|||
results.find((r) => r.id === selectedResultId) || results[0];
|
||||
|
||||
return (
|
||||
<div className="bg-ui-panel-gradient flex h-[calc(100vh-56px)] w-full bg-ui-panel text-base-fg">
|
||||
<div className="flex h-[calc(100vh-56px)] w-full bg-ui-background text-base-fg">
|
||||
{backgroundImage && !hasResults && (
|
||||
<>
|
||||
<div className="pointer-events-none fixed inset-0 z-[1] overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,_transparent_49%,_rgb(var(--st-controls-rgb)_/_var(--st-gallery-vignette-alpha))_100%)]" />
|
||||
|
|
@ -929,9 +930,12 @@ export const ImageTo3DExperience = ({
|
|||
|
||||
<div className="relative z-10 h-full w-full p-8">
|
||||
{!hasResults && (
|
||||
<div className="pointer-events-none absolute left-0 top-[calc(50%-280px)] w-full text-center">
|
||||
<h1 className="mb-3 text-7xl font-bold tracking-tight">{title}</h1>
|
||||
<p className="text-xl text-base-fg/70">{subtitle}</p>
|
||||
<div className="pointer-events-none absolute left-0 top-[calc(50%-280px)] flex w-full justify-center">
|
||||
<CreateEmptyState
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
className="py-8 sm:py-8"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -945,7 +949,7 @@ export const ImageTo3DExperience = ({
|
|||
style={{ height: `calc(100vh - ${bottomOffsetPx + 80}px)` }}
|
||||
>
|
||||
{/* Left: Viewer */}
|
||||
<div className="glass relative h-full overflow-hidden rounded-xl border border-ui-panel-border">
|
||||
<div className="relative h-full overflow-hidden border border-ui-panel-border bg-ui-panel">
|
||||
<Viewer3D
|
||||
key={activeResult?.id}
|
||||
modelUrl={activeResult?.modelUrl}
|
||||
|
|
@ -1003,13 +1007,15 @@ export const ImageTo3DExperience = ({
|
|||
</div>
|
||||
|
||||
{/* Right: History List */}
|
||||
<div className="glass flex h-full flex-col overflow-hidden rounded-xl border border-ui-panel-border">
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<h3 className="font-semibold text-base-fg/80">History</h3>
|
||||
<div className="flex h-full flex-col overflow-hidden border border-ui-panel-border bg-ui-panel">
|
||||
<div className="flex items-center justify-between border-b border-ui-panel-border px-4 py-3">
|
||||
<h3 className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">
|
||||
History
|
||||
</h3>
|
||||
{results.length > 0 && (
|
||||
<button
|
||||
onClick={resetResults}
|
||||
className="rounded-md bg-red/20 px-3 py-1 text-xs text-white/70 transition-colors hover:bg-red/30"
|
||||
className="rounded-[3px] border border-red/40 bg-red/10 px-3 py-1 text-xs text-white/70 transition-colors hover:bg-red/20"
|
||||
>
|
||||
Clear Session
|
||||
</button>
|
||||
|
|
@ -1025,13 +1031,13 @@ export const ImageTo3DExperience = ({
|
|||
key={result.id}
|
||||
onClick={() => setSelectedResultId(result.id)}
|
||||
className={twMerge(
|
||||
"group flex w-full items-center gap-3 rounded-xl border p-2 text-left transition-all hover:bg-ui-controls/40",
|
||||
"group flex w-full items-center gap-3 rounded-none border p-2 text-left transition-all hover:bg-white/5",
|
||||
isSelected
|
||||
? "border-primary/50 bg-primary/10"
|
||||
: "border-transparent bg-ui-controls/20",
|
||||
? "border-white bg-white/10"
|
||||
: "border-white/10 bg-transparent hover:border-white/30",
|
||||
)}
|
||||
>
|
||||
<div className="relative aspect-square h-14 w-14 shrink-0 overflow-hidden rounded-lg bg-black/30 ring-1 ring-white/5">
|
||||
<div className="relative aspect-square h-14 w-14 shrink-0 overflow-hidden border border-white/10 bg-black/30">
|
||||
{result.previewUrl ? (
|
||||
<img
|
||||
src={result.previewUrl}
|
||||
|
|
@ -1050,7 +1056,7 @@ export const ImageTo3DExperience = ({
|
|||
)}
|
||||
{isPending && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/30">
|
||||
<div className="h-5 w-5 animate-spin rounded-full border-2 border-white/20 border-t-primary" />
|
||||
<div className="h-5 w-5 animate-spin rounded-full border-2 border-white/20 border-t-white" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -1064,7 +1070,7 @@ export const ImageTo3DExperience = ({
|
|||
Generating…
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-base-fg/45">
|
||||
<span className="font-mono tabular-nums text-base-fg/45">
|
||||
{formatTime(result.timestamp)}
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -1094,7 +1100,7 @@ export const ImageTo3DExperience = ({
|
|||
<div ref={promptContentRef}>
|
||||
<div
|
||||
className={twMerge(
|
||||
"glass w-full rounded-xl shadow-2xl ring-1 ring-white/10",
|
||||
"glass w-full",
|
||||
hasResults ? "p-3" : "p-5",
|
||||
)}
|
||||
>
|
||||
|
|
@ -1139,7 +1145,7 @@ export const ImageTo3DExperience = ({
|
|||
activeTab={activeMode}
|
||||
onTabChange={(tabId) => setActiveMode(tabId as Mode)}
|
||||
className="w-fit"
|
||||
indicatorClassName="bg-primary/25"
|
||||
indicatorClassName="bg-white/15"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -1196,12 +1202,12 @@ export const ImageTo3DExperience = ({
|
|||
|
||||
{previewImage && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm"
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/90"
|
||||
onClick={() => setPreviewImage(null)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-4 top-4 flex h-8 w-8 items-center justify-center rounded-full bg-black/60 text-white transition-colors hover:bg-black/80"
|
||||
className="absolute right-4 top-4 flex h-8 w-8 items-center justify-center rounded-[3px] border border-white/15 bg-black/60 text-white transition-colors hover:border-white/30 hover:bg-black"
|
||||
onClick={() => setPreviewImage(null)}
|
||||
>
|
||||
<XIcon className="h-4 w-4" />
|
||||
|
|
@ -1209,7 +1215,7 @@ export const ImageTo3DExperience = ({
|
|||
<img
|
||||
src={previewImage}
|
||||
alt="Preview"
|
||||
className="max-h-[80vh] max-w-[80vw] rounded-xl object-contain shadow-2xl"
|
||||
className="max-h-[80vh] max-w-[80vw] border border-white/15 object-contain"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
import { type ReactNode } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
const CORNERS = [
|
||||
"top-0 left-0",
|
||||
"top-0 right-0",
|
||||
"bottom-0 left-0",
|
||||
"bottom-0 right-0",
|
||||
];
|
||||
|
||||
interface CreateEmptyStateProps {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
children?: ReactNode;
|
||||
compact?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/** A quiet editorial section with a fine border and corner registration marks. */
|
||||
export function CreateEmptyState({
|
||||
title,
|
||||
subtitle,
|
||||
children,
|
||||
compact = false,
|
||||
className,
|
||||
}: CreateEmptyStateProps) {
|
||||
return (
|
||||
<section
|
||||
className={twMerge(
|
||||
"create-empty-section relative w-full max-w-3xl px-6 text-left sm:px-10",
|
||||
compact ? "py-8" : "py-10 sm:py-14",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<h1
|
||||
className={twMerge(
|
||||
"text-balance font-display leading-[1.05] tracking-[-0.035em] text-ui-ink",
|
||||
compact ? "text-2xl" : "text-3xl md:text-5xl",
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
<p className="mt-4 max-w-xl text-pretty text-sm leading-relaxed text-white/60 sm:text-base">
|
||||
{subtitle}
|
||||
</p>
|
||||
{children && <div className="mt-6">{children}</div>}
|
||||
{CORNERS.map((corner) => (
|
||||
<span
|
||||
key={corner}
|
||||
aria-hidden="true"
|
||||
className={`frame-corner-mark ${corner}`}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { CreateEmptyState } from "./CreateEmptyState";
|
||||
|
||||
// Shared page chrome for the desktop Generate Image / Generate Video pages,
|
||||
// mirroring the webapp's CreateMediaPageShell: a centered hero over the
|
||||
// decorative background, and a scrollable generation feed (padded past the
|
||||
// fixed promptbox) once there is anything to show.
|
||||
// mirroring the webapp's CreateMediaPageShell: a centered empty-state panel,
|
||||
// and a scrollable generation feed (padded past the fixed promptbox) once
|
||||
// there is anything to show.
|
||||
//
|
||||
// The hero + background behave like a splash screen: they stay up while the
|
||||
// feed is still loading (pages remount on tab switches, so this covers the
|
||||
// reload) and fade out once content arrives. With a genuinely empty library
|
||||
// they simply stay — they ARE the empty state.
|
||||
// The empty state (and optional background) behave like a splash screen:
|
||||
// they stay up while the feed is still loading (pages remount on tab
|
||||
// switches, so this covers the reload) and fade out once content arrives.
|
||||
// With a genuinely empty library they simply stay.
|
||||
|
||||
const SPLASH_FADE_MS = 700;
|
||||
|
||||
|
|
@ -17,7 +18,8 @@ interface DesktopCreatePageShellProps {
|
|||
hasContent: boolean;
|
||||
emptyStateTitle: string;
|
||||
emptyStateSubtitle: string;
|
||||
/** Decorative page background (fixed-position); fades out with the hero. */
|
||||
/** Optional decorative page background (fixed-position); fades out with
|
||||
* the empty state. */
|
||||
background?: ReactNode;
|
||||
/** Height of the fixed promptbox area, used to pad the feed's bottom. */
|
||||
bottomOffset: number;
|
||||
|
|
@ -55,11 +57,11 @@ export function DesktopCreatePageShell({
|
|||
}, [hasContent, splashState]);
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-56px)] w-full bg-ui-background">
|
||||
<div className="flex h-[calc(100vh-56px)] w-full bg-ui-background text-white">
|
||||
<div className="relative h-full w-full">
|
||||
{hasContent && (
|
||||
<div
|
||||
className="h-full w-full overflow-y-auto pt-4"
|
||||
className="h-full w-full overflow-y-auto pt-0.5"
|
||||
style={{ paddingBottom: bottomOffset + 24 }}
|
||||
>
|
||||
{listContent}
|
||||
|
|
@ -73,15 +75,14 @@ export function DesktopCreatePageShell({
|
|||
splashState === "fading" ? "opacity-0" : "opacity-100",
|
||||
)}
|
||||
>
|
||||
<div className="flex h-full w-full flex-col items-center justify-center pb-52">
|
||||
<div className="relative z-20 flex flex-col items-center justify-center text-center drop-shadow-xl">
|
||||
<h1 className="text-7xl font-bold text-base-fg">
|
||||
{emptyStateTitle}
|
||||
</h1>
|
||||
<span className="pt-2 text-xl text-base-fg opacity-80">
|
||||
{emptyStateSubtitle}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="relative z-20 flex h-full w-full items-center justify-center px-6"
|
||||
style={{ paddingBottom: bottomOffset }}
|
||||
>
|
||||
<CreateEmptyState
|
||||
title={emptyStateTitle}
|
||||
subtitle={emptyStateSubtitle}
|
||||
/>
|
||||
</div>
|
||||
{background}
|
||||
</div>
|
||||
|
|
@ -92,7 +93,7 @@ export function DesktopCreatePageShell({
|
|||
{hasContent && (
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 z-10 h-64 bg-gradient-to-t from-ui-background to-transparent"
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 z-10 h-48 bg-gradient-to-t from-ui-background to-transparent"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ function SelectionDownloadBar({
|
|||
type="button"
|
||||
onClick={handleDownload}
|
||||
disabled={isDownloading || selectedItems.length === 0}
|
||||
className="flex items-center gap-2 rounded-full bg-ui-controls/60 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-ui-controls/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:opacity-60"
|
||||
className="flex items-center gap-2 border border-white/15 bg-ui-controls/60 px-3 py-1.5 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white transition-colors hover:bg-ui-controls/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-white/60 disabled:opacity-60"
|
||||
>
|
||||
<DynamicIcon
|
||||
icon={isDownloading ? LoaderCircleIcon : ArrowDownToLineIcon}
|
||||
|
|
@ -226,8 +226,8 @@ function ItemActions({
|
|||
|
||||
const buttonClass =
|
||||
variant === "card"
|
||||
? "flex h-7 w-7 items-center justify-center rounded-md text-white/85 transition-colors hover:bg-white/15 hover:text-white disabled:opacity-60"
|
||||
: "flex h-8 w-8 items-center justify-center rounded-md text-white/70 transition-colors hover:bg-white/10 hover:text-white disabled:opacity-60";
|
||||
? "flex h-7 w-7 items-center justify-center text-white/85 transition-colors hover:bg-white/15 hover:text-white disabled:opacity-60"
|
||||
: "flex h-8 w-8 items-center justify-center text-white/70 transition-colors hover:bg-white/10 hover:text-white disabled:opacity-60";
|
||||
|
||||
const handleRecreate = useCallback(
|
||||
async (e: React.MouseEvent) => {
|
||||
|
|
@ -305,7 +305,7 @@ function ItemActions({
|
|||
aria-label="Make Video"
|
||||
className={buttonClass}
|
||||
>
|
||||
<VideoIcon className="text-sm" />
|
||||
<VideoIcon className="text-sm" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -106,8 +106,8 @@ export const UploadEntryCard = ({
|
|||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"bg-ui-background/60 relative flex h-full flex-col items-center justify-center gap-8 overflow-hidden rounded-2xl border-2 border-dashed border-ui-panel-border p-10 text-center transition-colors",
|
||||
isDragActive && "border-primary/80 bg-primary/5",
|
||||
"relative flex h-full flex-col items-center justify-center gap-6 overflow-hidden rounded-[3px] border border-dashed border-white/15 bg-ui-background p-8 text-center transition-colors hover:border-white/40",
|
||||
isDragActive && "border-white bg-white/10 hover:border-white",
|
||||
disabled && "pointer-events-none opacity-60",
|
||||
)}
|
||||
onDragEnter={handleDragEnter}
|
||||
|
|
@ -124,35 +124,28 @@ export const UploadEntryCard = ({
|
|||
onChange={handleFileChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-6">
|
||||
<div className="relative">
|
||||
<div
|
||||
className={twMerge(
|
||||
"relative flex h-32 w-32 items-center justify-center rounded-2xl border-2 shadow-xl backdrop-blur-sm",
|
||||
accentBackgroundClass,
|
||||
accentBorderClass,
|
||||
)}
|
||||
>
|
||||
<DynamicIcon
|
||||
icon={icon}
|
||||
className="text-5xl text-white drop-shadow-lg"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex h-14 w-14 items-center justify-center border",
|
||||
accentBackgroundClass,
|
||||
accentBorderClass,
|
||||
)}
|
||||
>
|
||||
<DynamicIcon icon={icon} className="text-xl text-white" />
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-4xl font-bold tracking-tight text-base-fg">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="mx-auto max-w-md text-base leading-relaxed text-base-fg/70">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-base-fg">{title}</h3>
|
||||
<p className="mx-auto mt-1 max-w-md text-sm text-base-fg/60">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap justify-center gap-4">
|
||||
<div className="mt-2 flex flex-wrap justify-center gap-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
icon={primaryIcon}
|
||||
onClick={handlePrimaryClick}
|
||||
className="px-8 py-3 text-base font-semibold shadow-lg"
|
||||
className="px-3 py-2"
|
||||
disabled={disabled}
|
||||
>
|
||||
{primaryLabel}
|
||||
|
|
@ -162,7 +155,7 @@ export const UploadEntryCard = ({
|
|||
variant="action"
|
||||
icon={secondaryIcon}
|
||||
onClick={onSecondaryClick}
|
||||
className="border-2 px-8 py-3 text-base font-semibold"
|
||||
className="px-3 py-2"
|
||||
disabled={disabled}
|
||||
>
|
||||
{secondaryLabel}
|
||||
|
|
@ -173,7 +166,7 @@ export const UploadEntryCard = ({
|
|||
variant="action"
|
||||
icon={tertiaryIcon}
|
||||
onClick={onTertiaryClick}
|
||||
className="border-2 px-8 py-3 text-base font-semibold"
|
||||
className="px-3 py-2"
|
||||
disabled={disabled}
|
||||
>
|
||||
{tertiaryLabel}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export const ConfirmationModal = ({
|
|||
onClose,
|
||||
onOk,
|
||||
okText = "OK",
|
||||
okColor = "bg-brand-success",
|
||||
okColor = "",
|
||||
onCancel,
|
||||
cancelText = "Cancel",
|
||||
canHide,
|
||||
|
|
@ -52,7 +52,7 @@ export const ConfirmationModal = ({
|
|||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black/40" />
|
||||
<div className="fixed inset-0 bg-black/80" />
|
||||
</Transition.Child>
|
||||
<div className="fixed inset-0 flex w-screen items-center justify-center p-4">
|
||||
<Transition.Child
|
||||
|
|
@ -64,15 +64,15 @@ export const ConfirmationModal = ({
|
|||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-xl border border-ui-panel-border bg-ui-panel p-5 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden border border-white/15 bg-ui-modal p-5 text-left align-middle transition-all">
|
||||
<Dialog.Title
|
||||
as="h4"
|
||||
className="mb-4 text-xl font-bold text-white"
|
||||
className="mb-4 font-display text-xl tracking-tight text-white"
|
||||
>
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
|
||||
<div className="mt-2">{text}</div>
|
||||
<div className="mt-2 text-sm leading-relaxed text-white/70">{text}</div>
|
||||
{canHide && (
|
||||
<div className="mt-2">
|
||||
<Checkbox id="hide-dialog" label="Do not show this again" />
|
||||
|
|
@ -84,7 +84,7 @@ export const ConfirmationModal = ({
|
|||
<Button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="rounded-lg px-3 py-2"
|
||||
className="px-3 py-2"
|
||||
variant="secondary"
|
||||
>
|
||||
{cancelText}
|
||||
|
|
@ -94,7 +94,7 @@ export const ConfirmationModal = ({
|
|||
<Button
|
||||
type="button"
|
||||
onClick={handleOk}
|
||||
className={[okColor, "rounded-lg px-3 py-2"].join(" ")}
|
||||
className={[okColor, "px-3 py-2"].join(" ").trim()}
|
||||
>
|
||||
{okText}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -37,11 +37,11 @@ export const CostModal = ({ credits = 1, onClose }: CostModalProps) => {
|
|||
transition={{ duration: 0.1, ease: "easeOut" }}
|
||||
drag
|
||||
dragMomentum={false}
|
||||
className="pointer-events-auto z-10 flex w-72 flex-col overflow-hidden rounded-xl border border-ui-panel-border bg-ui-panel shadow-2xl"
|
||||
className="pointer-events-auto z-10 flex w-72 flex-col overflow-hidden border border-white/15 bg-ui-panel"
|
||||
>
|
||||
<div className="bg-ui-panel-header flex cursor-move select-none items-center justify-between border-b border-ui-panel-border px-4 py-3">
|
||||
<div className="flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-base-fg">
|
||||
<CoinsIcon className="text-yellow-400" />
|
||||
<div className="flex cursor-move select-none items-center justify-between border-b border-white/15 px-4 py-3">
|
||||
<div className="flex items-center gap-2 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg">
|
||||
<CoinsIcon className="text-primary" />
|
||||
Cost Breakdown
|
||||
</div>
|
||||
<button
|
||||
|
|
@ -53,10 +53,10 @@ export const CostModal = ({ credits = 1, onClose }: CostModalProps) => {
|
|||
</div>
|
||||
|
||||
<div className="space-y-4 bg-ui-panel p-4">
|
||||
<div className="border-ui-controls-border rounded-lg border bg-ui-controls/50 p-3">
|
||||
<div className="border border-white/15 bg-white/5 p-3">
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<span className="text-sm text-base-fg/80">Total Cost</span>
|
||||
<span className="text-lg font-bold text-base-fg">
|
||||
<span className="text-lg font-semibold tabular-nums tracking-tight text-base-fg">
|
||||
{credits} Credits
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -66,7 +66,7 @@ export const CostModal = ({ credits = 1, onClose }: CostModalProps) => {
|
|||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-medium text-base-fg/80">
|
||||
<label className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">
|
||||
Currency
|
||||
</label>
|
||||
<Select
|
||||
|
|
@ -77,7 +77,7 @@ export const CostModal = ({ credits = 1, onClose }: CostModalProps) => {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 border-t border-ui-panel-border pt-3 text-center text-[10px] text-base-fg/40">
|
||||
<div className="mt-2 border-t border-white/15 pt-3 text-center font-mono text-[10px] uppercase tracking-[0.12em] text-base-fg/40">
|
||||
1 Credit = $0.01 USD
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export const UploadFilesMedia = ({
|
|||
<h6 className="z-10 text-red">{assetFile.error}</h6>
|
||||
)}
|
||||
|
||||
<div className="relative m-auto aspect-square w-full overflow-hidden rounded-lg bg-brand-secondary">
|
||||
<div className="relative m-auto aspect-square w-full overflow-hidden border border-white/15 bg-ui-controls">
|
||||
{!assetFile.value && (
|
||||
<h6 className="absolute left-0 top-1/2 -mt-5 w-full text-center">
|
||||
File Preview
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export const UploadImageMedia = ({
|
|||
<H6 className="z-10 text-red"> {assetFile.error} </H6>
|
||||
)}
|
||||
|
||||
<div className="relative m-auto aspect-square w-full overflow-hidden rounded-lg bg-brand-secondary">
|
||||
<div className="relative m-auto aspect-square w-full overflow-hidden border border-white/15 bg-ui-controls">
|
||||
{!assetFile.value && (
|
||||
<H6 className="absolute left-0 top-1/2 -mt-5 w-full text-center">
|
||||
File Preview
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export function UploadImageMediaModal({
|
|||
return (
|
||||
<>
|
||||
<LoadingDots className="mb-1 bg-transparent" />
|
||||
<div className="w-100 text-center opacity-50">Uploading...</div>
|
||||
<div className="hud-label w-full text-center text-base-fg/50">Uploading...</div>
|
||||
</>
|
||||
);
|
||||
case UploaderStates.success:
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export function UploadModalMedia({
|
|||
return (
|
||||
<>
|
||||
<LoadingDots className="mb-1 bg-transparent" />
|
||||
<div className="w-100 text-center opacity-50">Uploading...</div>
|
||||
<div className="hud-label w-full text-center text-base-fg/50">Uploading...</div>
|
||||
</>
|
||||
);
|
||||
case UploaderStates.success:
|
||||
|
|
|
|||
|
|
@ -20,14 +20,14 @@ export function ErrorDialog() {
|
|||
<Modal
|
||||
title={errorDialogTitle}
|
||||
titleIcon={TriangleAlertIcon}
|
||||
titleIconClassName="text-brand-primary"
|
||||
titleIconClassName="text-red-400"
|
||||
isOpen={showErrorDialog}
|
||||
onClose={() => setShowErrorDialog(false)}
|
||||
showClose={false}
|
||||
>
|
||||
<div>
|
||||
{errorDialogMessage}
|
||||
<div className="flex justify-end">
|
||||
<div className="mt-4 flex justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setShowErrorDialog(false)}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ import { twMerge } from "tailwind-merge";
|
|||
import {
|
||||
useGenerateApps,
|
||||
useEditApps,
|
||||
getAppCardPalette,
|
||||
getBadgeStyles,
|
||||
goToApp,
|
||||
type FullAppItem,
|
||||
} from "~/config/appMenu";
|
||||
|
||||
export const AppsQuickMenu = () => {
|
||||
|
|
@ -12,101 +14,68 @@ export const AppsQuickMenu = () => {
|
|||
const editApps = useEditApps();
|
||||
return (
|
||||
<div className="grid w-[680px] grid-cols-2 gap-3">
|
||||
<div>
|
||||
<h3 className="mb-2 px-2 text-xs font-semibold opacity-50">Create</h3>
|
||||
<div className="space-y-0.5">
|
||||
{generateApps.map((app) => (
|
||||
<button
|
||||
key={app.id}
|
||||
onClick={() => goToApp(app.action)}
|
||||
disabled={!app.action}
|
||||
className={twMerge(
|
||||
"group flex w-full items-center gap-3 rounded-md px-2 py-2 text-left transition-colors",
|
||||
app.action
|
||||
? "cursor-pointer hover:bg-base-fg/10"
|
||||
: "cursor-default opacity-60",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex h-8 w-8 shrink-0 items-center justify-center rounded-md transition-colors",
|
||||
app.color || "bg-ui-panel",
|
||||
)}
|
||||
>
|
||||
<DynamicIcon icon={app.icon} className="text-sm" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="truncate text-[13px] font-medium">
|
||||
{app.label}
|
||||
</div>
|
||||
{app.badge && (
|
||||
<span
|
||||
className={twMerge(
|
||||
"shrink-0 rounded-full px-2 py-1 text-[9px] font-bold uppercase tracking-wider",
|
||||
getBadgeStyles(app.badge),
|
||||
)}
|
||||
>
|
||||
{app.badge}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="truncate text-[11px] opacity-60">
|
||||
{app.description}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="mb-2 px-2 text-xs font-semibold opacity-50">Edit</h3>
|
||||
<div className="space-y-0.5">
|
||||
{editApps.map((app) => (
|
||||
<button
|
||||
key={app.id}
|
||||
onClick={() => goToApp(app.action)}
|
||||
disabled={!app.action}
|
||||
className={twMerge(
|
||||
"group flex w-full items-center gap-3 rounded-md px-2 py-2 text-left transition-colors",
|
||||
app.action
|
||||
? "cursor-pointer hover:bg-base-fg/10"
|
||||
: "cursor-default opacity-60",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex h-8 w-8 shrink-0 items-center justify-center rounded-md transition-colors",
|
||||
app.color || "bg-ui-panel",
|
||||
)}
|
||||
>
|
||||
<DynamicIcon icon={app.icon} className="text-sm" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="truncate text-[13px] font-medium">
|
||||
{app.label}
|
||||
</div>
|
||||
{app.badge && (
|
||||
<span
|
||||
className={twMerge(
|
||||
"shrink-0 rounded-full px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider",
|
||||
getBadgeStyles(app.badge),
|
||||
)}
|
||||
>
|
||||
{app.badge}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="truncate text-[11px] opacity-60">
|
||||
{app.description}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<AppsQuickMenuSection title="Create" apps={generateApps} />
|
||||
<AppsQuickMenuSection title="Edit" apps={editApps} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AppsQuickMenuSection = ({
|
||||
title,
|
||||
apps,
|
||||
}: {
|
||||
title: string;
|
||||
apps: FullAppItem[];
|
||||
}) => (
|
||||
<div>
|
||||
<h3 className="hud-label mb-2 px-2 text-base-fg/50">{title}</h3>
|
||||
<div className="space-y-0.5">
|
||||
{apps.map((app) => {
|
||||
const palette = getAppCardPalette(app.id);
|
||||
return (
|
||||
<button
|
||||
key={app.id}
|
||||
onClick={() => goToApp(app.action)}
|
||||
disabled={!app.action}
|
||||
className={twMerge(
|
||||
"group flex w-full items-center gap-3 rounded-[3px] px-2 py-2 text-left transition-colors",
|
||||
app.action
|
||||
? "cursor-pointer hover:bg-white/[0.08]"
|
||||
: "cursor-default opacity-60",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex h-8 w-8 shrink-0 items-center justify-center border transition-colors",
|
||||
palette.iconBg,
|
||||
palette.iconColor,
|
||||
)}
|
||||
>
|
||||
<DynamicIcon icon={app.icon} className="text-sm" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="truncate text-[13px] font-medium">
|
||||
{app.label}
|
||||
</div>
|
||||
{app.badge && (
|
||||
<span
|
||||
className={twMerge(
|
||||
"shrink-0 border px-1.5 py-0.5 font-mono text-[9px] font-semibold uppercase leading-none tracking-[0.12em]",
|
||||
getBadgeStyles(app.badge),
|
||||
)}
|
||||
>
|
||||
{app.badge}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="truncate text-[11px] text-base-fg/60">
|
||||
{app.description}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { AUTH_STATUS } from "~/enums";
|
||||
import { authentication } from "~/signals";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
// import ProfileDropdown from "./ProfileDropdown";
|
||||
|
||||
export const AuthButtons = ({
|
||||
|
|
@ -19,15 +18,14 @@ export const AuthButtons = ({
|
|||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-white/20">|</span>
|
||||
<Button
|
||||
className="h-[38px]"
|
||||
<button
|
||||
className="flex h-8 items-center gap-1.5 rounded-[3px] bg-white px-3.5 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-black transition-colors hover:bg-white/80"
|
||||
onClick={() => {
|
||||
loginSignUpPressed();
|
||||
}}
|
||||
>
|
||||
Login / Sign Up
|
||||
</Button>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -56,20 +56,20 @@ export default function ProfileDropdown() {
|
|||
>
|
||||
<Menu.Items
|
||||
static
|
||||
className="absolute right-[-5px] mt-2.5 w-36 overflow-hidden rounded-lg bg-brand-secondary shadow-lg focus:outline-none"
|
||||
className="absolute right-0 z-50 mt-2 w-48 origin-top-right overflow-hidden rounded-[3px] border border-white/15 bg-[#101014] focus:outline-none"
|
||||
>
|
||||
<Menu.Item key={0}>
|
||||
{({ active }) => (
|
||||
<a
|
||||
className={twMerge(
|
||||
"duration-50 group flex w-full items-center gap-2 bg-action/60 px-3 py-[10px] text-start text-sm font-medium text-white transition-all",
|
||||
active && "bg-action-500/80",
|
||||
"group flex w-full items-center gap-2 px-4 py-2 text-start text-sm text-white/70 transition-colors",
|
||||
active && "bg-white/10",
|
||||
)}
|
||||
href={profileUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<UserIcon />
|
||||
<UserIcon className="text-[11px] text-white/50" />
|
||||
My Profile
|
||||
</a>
|
||||
)}
|
||||
|
|
@ -79,12 +79,15 @@ export default function ProfileDropdown() {
|
|||
{({ active }) => (
|
||||
<button
|
||||
className={twMerge(
|
||||
"duration-50 group flex w-full items-center gap-2 bg-action/60 px-3 py-[10px] text-start text-sm font-medium text-white transition-all",
|
||||
active && "bg-action-500/80",
|
||||
"group flex w-full items-center gap-2 px-4 py-2 text-start text-sm text-white/70 transition-colors",
|
||||
active && "bg-white/10",
|
||||
)}
|
||||
onClick={() => option.onClick()}
|
||||
>
|
||||
<DynamicIcon icon={option.icon} />
|
||||
<DynamicIcon
|
||||
icon={option.icon}
|
||||
className="text-[11px] text-white/50"
|
||||
/>
|
||||
{option.label}
|
||||
</button>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import { useState } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { LoaderCircleIcon, PencilIcon } from "lucide-react";
|
||||
import { ChevronRightIcon, LoaderCircleIcon, PencilIcon } from "lucide-react";
|
||||
import { scene, signalScene, authentication } from "~/signals";
|
||||
import { usePageSceneStore } from "@storyteller/ui-pagescene";
|
||||
import { Input } from "@storyteller/ui-input";
|
||||
import { MediaFilesApi } from "~/Classes/ApiManager/MediaFilesApi";
|
||||
import { getCreatorIcon, ModelCreator } from "@storyteller/model-list";
|
||||
|
||||
interface Props {
|
||||
pageName: string;
|
||||
|
|
@ -95,27 +94,29 @@ export const SceneTitleInput = ({ pageName }: Props) => {
|
|||
data-tauri-drag-region
|
||||
>
|
||||
{!showInput && (
|
||||
<div className="flex items-center gap-2.5" data-tauri-drag-region>
|
||||
{getCreatorIcon(
|
||||
ModelCreator.ArtCraft,
|
||||
"h-5 w-5 icon-auto-contrast opacity-60",
|
||||
)}
|
||||
<span className="text-nowrap opacity-60">{pageName}</span>
|
||||
<span className="opacity-60">/</span>
|
||||
<div className="flex items-center gap-1.5" data-tauri-drag-region>
|
||||
<span
|
||||
className="hud-label text-nowrap text-base-fg/70"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
{pageName}
|
||||
</span>
|
||||
<ChevronRightIcon
|
||||
aria-hidden="true"
|
||||
className="h-3 w-3 shrink-0 text-base-fg/50"
|
||||
/>
|
||||
|
||||
{isSceneOwner ? (
|
||||
<button
|
||||
className="ml-0.5 rounded-md px-2 py-1 transition-all hover:cursor-text hover:bg-white/[8%]"
|
||||
className="flex max-w-[280px] items-center border border-transparent px-3 py-1.5 text-sm font-semibold text-white transition-colors hover:cursor-text hover:bg-white/10"
|
||||
onClick={handleShowInput}
|
||||
data-tauri-drag-region="false"
|
||||
>
|
||||
{scene.value.title || ""}
|
||||
<PencilIcon
|
||||
|
||||
className="ml-2 text-sm opacity-50" />
|
||||
<span className="truncate">{scene.value.title || ""}</span>
|
||||
<PencilIcon className="ml-2 shrink-0 text-sm opacity-50" />
|
||||
</button>
|
||||
) : (
|
||||
<div className="ml-0.5 rounded-md px-2 py-1">
|
||||
<div className="max-w-[280px] truncate border border-transparent px-3 py-1.5 text-sm font-semibold text-white/80">
|
||||
{scene.value.title || ""}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -128,8 +129,8 @@ export const SceneTitleInput = ({ pageName }: Props) => {
|
|||
disabled={scene.value.ownerToken !== userInfo.value?.user_token}
|
||||
className="w-[420px]"
|
||||
inputClassName={twMerge(
|
||||
"text-center h-8 focus:outline-brand-primary",
|
||||
isSaving && "outline-brand-secondary",
|
||||
"text-center h-[34px] text-sm font-semibold focus:outline-white/60",
|
||||
isSaving && "outline-white/30",
|
||||
)}
|
||||
isError={!isValid}
|
||||
value={scene.value.title || ""}
|
||||
|
|
@ -152,7 +153,9 @@ export const SceneTitleInput = ({ pageName }: Props) => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{isSaving && <LoaderCircleIcon className="animate-spin" />}
|
||||
{isSaving && (
|
||||
<LoaderCircleIcon className="shrink-0 animate-spin text-sm opacity-70" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import { coverImageCache } from "~/pages/PageImageTo3DObject/ImageTo3DStore";
|
|||
import { useCreditsState } from "@storyteller/credits";
|
||||
import { getMetaForTask, cleanupOldEntries } from "./taskEnqueueMeta";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
type InProgressTask = {
|
||||
id: string;
|
||||
|
|
@ -159,7 +160,7 @@ const CopyPromptButton = ({ prompt }: { prompt: string }) => {
|
|||
delay={300}
|
||||
>
|
||||
<button
|
||||
className="flex h-6 w-6 items-center justify-center rounded-full text-base-fg/60 hover:bg-ui-controls"
|
||||
className="flex h-6 w-6 items-center justify-center text-base-fg/60 hover:bg-ui-controls"
|
||||
aria-label="Copy prompt"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -195,7 +196,7 @@ const InProgressCard = ({
|
|||
const hasRefImages = task.refImageUrls && task.refImageUrls.length > 0;
|
||||
|
||||
const thumbnailContent = hasRefImages ? (
|
||||
<div className="relative h-[86px] w-[86px] shrink-0 overflow-hidden rounded">
|
||||
<div className="relative h-[72px] w-[72px] shrink-0 overflow-hidden">
|
||||
<img
|
||||
src={task.refImageUrls![0]}
|
||||
alt="Reference"
|
||||
|
|
@ -212,13 +213,13 @@ const InProgressCard = ({
|
|||
size="lg" />
|
||||
</div>
|
||||
{task.refImageUrls!.length > 1 && (
|
||||
<div className="absolute bottom-0.5 right-0.5 rounded bg-black/60 px-1 text-[9px] text-white/80">
|
||||
<div className="absolute bottom-0.5 right-0.5 bg-black/60 px-1 text-[9px] text-white/80">
|
||||
+{task.refImageUrls!.length - 1}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-[86px] w-[86px] shrink-0 items-center justify-center overflow-hidden rounded bg-ui-controls">
|
||||
<div className="flex h-[72px] w-[72px] shrink-0 items-center justify-center overflow-hidden bg-ui-controls">
|
||||
<LoaderCircleIcon
|
||||
|
||||
className="animate-spin text-base-fg/60"
|
||||
|
|
@ -227,7 +228,7 @@ const InProgressCard = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="rounded-md p-2 transition-colors hover:bg-ui-controls/40">
|
||||
<div className="p-2 transition-colors hover:bg-ui-controls/40">
|
||||
<div className="flex items-center gap-2.5">
|
||||
{thumbnailContent}
|
||||
<div className="min-w-0 flex-1">
|
||||
|
|
@ -239,7 +240,7 @@ const InProgressCard = ({
|
|||
content="Seedance 2.0 is in Early Alpha. Generations may be slow and may experience outages."
|
||||
position="top"
|
||||
strategy="fixed"
|
||||
className="w-[200px] text-wrap bg-yellow-400/60 backdrop-blur-3xl"
|
||||
className="w-[200px] text-wrap bg-yellow-400/60"
|
||||
zIndex={50}
|
||||
delay={100}
|
||||
>
|
||||
|
|
@ -249,7 +250,7 @@ const InProgressCard = ({
|
|||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-2 shrink-0 text-[11px] tabular-nums text-base-fg/60">
|
||||
<div className="ml-2 shrink-0 text-xs tabular-nums text-base-fg/60">
|
||||
{progressPercent}%
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -259,9 +260,9 @@ const InProgressCard = ({
|
|||
</div>
|
||||
)}
|
||||
<div className="mt-1.5 flex items-center gap-2">
|
||||
<div className="h-1.5 min-w-0 flex-1 rounded bg-ui-controls">
|
||||
<div className="h-1.5 min-w-0 flex-1 bg-ui-controls">
|
||||
<div
|
||||
className="h-1.5 rounded bg-brand-primary-400"
|
||||
className="h-1.5 bg-white"
|
||||
style={{
|
||||
width: `${Math.max(0, Math.min(100, task.progress))}%`,
|
||||
}}
|
||||
|
|
@ -269,7 +270,7 @@ const InProgressCard = ({
|
|||
</div>
|
||||
</div>
|
||||
{timeLabel && (
|
||||
<div className="mt-1 text-[11px] text-base-fg/50">{timeLabel}</div>
|
||||
<div className="mt-1 text-xs text-base-fg/50">{timeLabel}</div>
|
||||
)}
|
||||
{task.prompt && <PromptLine prompt={task.prompt} className="mt-0" />}
|
||||
</div>
|
||||
|
|
@ -277,7 +278,7 @@ const InProgressCard = ({
|
|||
{task.prompt && <CopyPromptButton prompt={task.prompt} />}
|
||||
{onDismiss && (
|
||||
<button
|
||||
className="flex h-6 w-6 items-center justify-center rounded-full text-base-fg/60 hover:bg-ui-controls"
|
||||
className="flex h-6 w-6 items-center justify-center text-base-fg/60 hover:bg-ui-controls"
|
||||
aria-label="Dismiss"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -304,12 +305,12 @@ const CompletedCard = ({
|
|||
}) => {
|
||||
return (
|
||||
<div
|
||||
className="flex cursor-pointer items-center gap-2.5 rounded-md p-2 transition-colors hover:bg-ui-controls/40"
|
||||
className="flex cursor-pointer items-center gap-2.5 p-2 transition-colors hover:bg-ui-controls/40"
|
||||
onClick={onClick}
|
||||
role={onClick ? "button" : undefined}
|
||||
tabIndex={onClick ? 0 : -1}
|
||||
>
|
||||
<div className="h-[86px] w-[86px] shrink-0 overflow-hidden rounded bg-ui-controls">
|
||||
<div className="h-[72px] w-[72px] shrink-0 overflow-hidden bg-ui-controls">
|
||||
{task.thumbnailUrl ? (
|
||||
<img
|
||||
src={task.thumbnailUrl}
|
||||
|
|
@ -342,7 +343,7 @@ const CompletedCard = ({
|
|||
)}
|
||||
{task.completedAt && (
|
||||
<div className="text-xs text-base-fg opacity-60">
|
||||
{task.completedAt.toISOString()}
|
||||
{dayjs(task.completedAt).format("MMM D, h:mm A")}
|
||||
</div>
|
||||
)}
|
||||
{task.prompt && <PromptLine prompt={task.prompt} />}
|
||||
|
|
@ -351,7 +352,7 @@ const CompletedCard = ({
|
|||
{task.prompt && <CopyPromptButton prompt={task.prompt} />}
|
||||
{onDismiss && (
|
||||
<button
|
||||
className="flex h-6 w-6 items-center justify-center rounded-full text-base-fg/60 hover:bg-ui-controls"
|
||||
className="flex h-6 w-6 items-center justify-center text-base-fg/60 hover:bg-ui-controls"
|
||||
aria-label="Dismiss"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -398,7 +399,7 @@ const FailedCard = ({
|
|||
const hasRefImages = task.refImageUrls && task.refImageUrls.length > 0;
|
||||
|
||||
const thumbnailContent = hasRefImages ? (
|
||||
<div className="relative h-[86px] w-[86px] shrink-0 overflow-hidden rounded">
|
||||
<div className="relative h-[72px] w-[72px] shrink-0 overflow-hidden">
|
||||
<img
|
||||
src={task.refImageUrls![0]}
|
||||
alt="Reference"
|
||||
|
|
@ -415,13 +416,13 @@ const FailedCard = ({
|
|||
size="lg" />
|
||||
</div>
|
||||
{task.refImageUrls!.length > 1 && (
|
||||
<div className="absolute bottom-0.5 right-0.5 rounded bg-black/60 px-1 text-[9px] text-white/80">
|
||||
<div className="absolute bottom-0.5 right-0.5 bg-black/60 px-1 text-[9px] text-white/80">
|
||||
+{task.refImageUrls!.length - 1}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-[86px] w-[86px] shrink-0 items-center justify-center overflow-hidden rounded bg-red-500/10">
|
||||
<div className="flex h-[72px] w-[72px] shrink-0 items-center justify-center overflow-hidden bg-red-500/10">
|
||||
<CircleAlertIcon
|
||||
|
||||
className="text-red-400"
|
||||
|
|
@ -430,7 +431,7 @@ const FailedCard = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="rounded-md p-2 transition-colors hover:bg-ui-controls/40">
|
||||
<div className="p-2 transition-colors hover:bg-ui-controls/40">
|
||||
<div className="flex items-center gap-2.5">
|
||||
{thumbnailContent}
|
||||
<div className="min-w-0 flex-1">
|
||||
|
|
@ -445,7 +446,7 @@ const FailedCard = ({
|
|||
</div>
|
||||
)}
|
||||
<div className="mt-1 flex min-w-0 items-center gap-1.5 overflow-hidden">
|
||||
<span className="shrink-0 rounded bg-red-500/15 px-1.5 py-0 text-[11px] font-medium text-red-400">
|
||||
<span className="shrink-0 bg-red-500/15 px-1.5 py-0 text-[11px] font-medium text-red-400">
|
||||
{statusLabel}
|
||||
</span>
|
||||
{task.failureReason && (
|
||||
|
|
@ -478,13 +479,18 @@ const FailedCard = ({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
{task.failedAt && (
|
||||
<div className="mt-0.5 text-[11px] text-base-fg/40">
|
||||
{dayjs(task.failedAt).format("MMM D, h:mm A")}
|
||||
</div>
|
||||
)}
|
||||
{task.prompt && <PromptLine prompt={task.prompt} />}
|
||||
</div>
|
||||
<div className="ml-auto flex shrink-0 items-center gap-1">
|
||||
{task.prompt && <CopyPromptButton prompt={task.prompt} />}
|
||||
{onDismiss && (
|
||||
<button
|
||||
className="flex h-6 w-6 items-center justify-center rounded-full text-base-fg/60 hover:bg-ui-controls"
|
||||
className="flex h-6 w-6 items-center justify-center text-base-fg/60 hover:bg-ui-controls"
|
||||
aria-label="Dismiss"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -1002,23 +1008,21 @@ export const TaskQueue = () => {
|
|||
<Tooltip content="Task Queue" position="bottom" closeOnClick={true}>
|
||||
<div className="relative">
|
||||
{badgeCount > 0 && (
|
||||
<div className="absolute -right-1 -top-1 z-20 flex h-[17px] w-[17px] items-center justify-center rounded-full bg-brand-primary-400 text-[13px] font-medium text-white">
|
||||
<div className="absolute -right-1 -top-1 z-20 flex h-[15px] min-w-[15px] items-center justify-center bg-white px-1 font-mono text-[10px] font-semibold text-black ring-2 ring-[#0b0b0c]">
|
||||
{badgeCount}
|
||||
</div>
|
||||
)}
|
||||
<PopoverMenu
|
||||
mode="default"
|
||||
buttonClassName="h-[34px] w-[34px] !p-0 relative"
|
||||
panelClassName="w-[400px] p-2 bg-ui-panel mt-2.5"
|
||||
buttonClassName="h-8 w-8 !p-0 relative bg-transparent hover:bg-white/10 border border-white/15 hover:border-white/30 text-white/80 rounded-[3px] shadow-none"
|
||||
panelClassName="w-[400px] p-2 bg-[#101014] border border-white/15 mt-2 rounded-[3px]"
|
||||
position="bottom"
|
||||
align="end"
|
||||
triggerIcon={
|
||||
inProgressCount > 0 ? (
|
||||
<LoaderCircleIcon
|
||||
|
||||
className="animate-spin" />
|
||||
<LoaderCircleIcon className="animate-spin text-[11px]" />
|
||||
) : (
|
||||
<ListChecksIcon />
|
||||
<ListChecksIcon className="text-[11px]" />
|
||||
)
|
||||
}
|
||||
onOpenChange={handleOpenChange}
|
||||
|
|
@ -1037,7 +1041,7 @@ export const TaskQueue = () => {
|
|||
<div>
|
||||
{inProgress.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<div className="mb-1 px-1 text-xs uppercase tracking-wide text-base-fg/50">
|
||||
<div className="mb-1 px-1 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/50">
|
||||
In Progress
|
||||
</div>
|
||||
{inProgress.map((t) => (
|
||||
|
|
@ -1056,7 +1060,7 @@ export const TaskQueue = () => {
|
|||
{failed.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<div className="mb-1 flex items-center justify-between px-1">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-red-400/70">
|
||||
<div className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-red-400/70">
|
||||
Failed
|
||||
</div>
|
||||
<button
|
||||
|
|
@ -1081,7 +1085,7 @@ export const TaskQueue = () => {
|
|||
{completed.length > 0 && (
|
||||
<div>
|
||||
<div className="mb-1 flex items-center justify-between px-1">
|
||||
<div className="text-xs font-semibold uppercase tracking-wide text-base-fg/50">
|
||||
<div className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/50">
|
||||
Completed
|
||||
</div>
|
||||
<button
|
||||
|
|
@ -1159,7 +1163,7 @@ export const TaskQueue = () => {
|
|||
showClose={false}
|
||||
>
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="rounded-t-xl border-ui-panel-border bg-ui-panel">
|
||||
<div className="border-b border-ui-panel-border bg-ui-panel">
|
||||
<div className="flex items-center justify-between p-3">
|
||||
<h2 className="text-lg font-semibold">Task Queue</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -1207,7 +1211,7 @@ export const TaskQueue = () => {
|
|||
<div>
|
||||
{inProgress.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<div className="mb-2 px-1 text-xs uppercase tracking-wide text-base-fg/50">
|
||||
<div className="mb-1 px-1 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/50">
|
||||
In Progress
|
||||
</div>
|
||||
{inProgress.map((t) => (
|
||||
|
|
@ -1223,7 +1227,7 @@ export const TaskQueue = () => {
|
|||
)}
|
||||
{failed.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<div className="mb-2 px-1 text-xs uppercase tracking-wide text-red-400/70">
|
||||
<div className="mb-1 px-1 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-red-400/70">
|
||||
Failed
|
||||
</div>
|
||||
{failed.map((t) => (
|
||||
|
|
@ -1237,7 +1241,7 @@ export const TaskQueue = () => {
|
|||
)}
|
||||
{completed.length > 0 && (
|
||||
<div>
|
||||
<div className="mb-2 px-1 text-xs uppercase tracking-wide text-base-fg/50">
|
||||
<div className="mb-1 px-1 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/50">
|
||||
Completed
|
||||
</div>
|
||||
{completed.map((t) => (
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import { CalculatorIcon, CheckIcon, CircleAlertIcon, CoinsIcon, GemIcon, HouseIcon, ImagesIcon, MinusIcon, PictureInPicture2Icon, SettingsIcon, SquareIcon, XIcon } from "lucide-react";
|
||||
import { CalculatorIcon, CheckIcon, ChevronRightIcon, CircleAlertIcon, CoinsIcon, GemIcon, HouseIcon, ImagesIcon, MinusIcon, PictureInPicture2Icon, SettingsIcon, SquareIcon, XIcon } from "lucide-react";
|
||||
import { DynamicIcon } from "@storyteller/icons";
|
||||
import { signal } from "@preact/signals-react";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { getCreatorIcon, ModelCreator } from "@storyteller/model-list";
|
||||
import { useCreditsState, type CreditsIconStatus } from "@storyteller/credits";
|
||||
import { gtagEvent } from "@storyteller/google-analytics";
|
||||
import { ProviderBillingModal } from "@storyteller/provider-billing-modal";
|
||||
|
|
@ -42,7 +41,7 @@ import {
|
|||
} from "@storyteller/ui-generation-list";
|
||||
import { SettingsModal } from "@storyteller/ui-settings-modal";
|
||||
import { Tooltip } from "@storyteller/ui-tooltip";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Fragment, useEffect, useRef, useState } from "react";
|
||||
import { APP_DESCRIPTORS, goToApp } from "~/config/appMenu";
|
||||
import {
|
||||
applyMakeVideoFromImage,
|
||||
|
|
@ -167,10 +166,10 @@ const CreditsCoinWithStatus = ({
|
|||
}
|
||||
>
|
||||
<span className="relative inline-flex">
|
||||
<CoinsIcon className="text-primary" />
|
||||
<CoinsIcon className="text-[11px] text-primary" />
|
||||
{showBadge && (
|
||||
<span
|
||||
className={`absolute -right-1.5 -top-1.5 flex h-3 w-3 items-center justify-center rounded-full ring-1 ring-ui-background ${badgeColorClass}`}
|
||||
className={`absolute -right-1.5 -top-1.5 flex h-3 w-3 items-center justify-center ring-1 ring-ui-panel ${badgeColorClass}`}
|
||||
>
|
||||
<DynamicIcon icon={badgeIconDef} className="text-[7px]" />
|
||||
</span>
|
||||
|
|
@ -347,40 +346,50 @@ export const TopBar = ({ pageName }: Props) => {
|
|||
}
|
||||
};
|
||||
|
||||
const getPageTitle = (): string => {
|
||||
const getPageCrumbs = (): string[] => {
|
||||
switch (tabStore.activeTabId) {
|
||||
case "2D":
|
||||
return "Canvas";
|
||||
return ["Studio", "Canvas"];
|
||||
case "3D":
|
||||
return "3D Editor";
|
||||
return ["Studio", "3D Editor"];
|
||||
case "IMAGE":
|
||||
return "Create Image";
|
||||
return ["Create", "Image"];
|
||||
case "VIDEO":
|
||||
return "Create Video";
|
||||
return ["Create", "Video"];
|
||||
case "AUDIO":
|
||||
return "Create Audio";
|
||||
return ["Create", "Audio"];
|
||||
case "EDIT":
|
||||
return "Edit Image";
|
||||
return ["Studio", "Edit Image"];
|
||||
case "VIDEO_FRAME_EXTRACTOR":
|
||||
return "Video Frame Extractor";
|
||||
return ["Studio", "Frame Extractor"];
|
||||
case "VIDEO_WATERMARK_REMOVAL":
|
||||
return "Video Watermark Remover";
|
||||
return ["Studio", "Video Watermark Remover"];
|
||||
case "IMAGE_WATERMARK_REMOVAL":
|
||||
return "Image Watermark Remover";
|
||||
return ["Studio", "Image Watermark Remover"];
|
||||
case "IMAGE_TO_3D_OBJECT":
|
||||
return "Image to 3D Object";
|
||||
return ["Create", "3D Object"];
|
||||
case "IMAGE_TO_3D_WORLD":
|
||||
return "Image to 3D World";
|
||||
case "APPS":
|
||||
return "ArtCraft";
|
||||
return ["Create", "3D World"];
|
||||
case "REMOVE_BACKGROUND":
|
||||
return ["Studio", "Remove Background"];
|
||||
case "ANGLES":
|
||||
return ["Create", "Angles"];
|
||||
case "STORYBOARD":
|
||||
return ["Create", "Storyboard"];
|
||||
case "BACKGROUND_CHANGE":
|
||||
return "Background Change";
|
||||
return ["Studio", "Background Change"];
|
||||
case "VIDEO_EDITOR":
|
||||
return ["Studio", "Edit Video"];
|
||||
case "MOODBOARD":
|
||||
return ["Studio", "Moodboard"];
|
||||
case "APPS":
|
||||
return ["Home"];
|
||||
default:
|
||||
return "Artcraft";
|
||||
return ["ArtCraft"];
|
||||
}
|
||||
};
|
||||
|
||||
const pageTitle = getPageTitle();
|
||||
const pageCrumbs = getPageCrumbs();
|
||||
|
||||
const { toggleModal: toggleSubscriptionModal } = usePricingModalStore();
|
||||
const { toggleModal: toggleCreditsModal } = useCreditsModalStore();
|
||||
|
|
@ -418,7 +427,7 @@ export const TopBar = ({ pageName }: Props) => {
|
|||
return (
|
||||
<>
|
||||
<header
|
||||
className="fixed left-0 top-0 z-[60] w-full border-b border-ui-panel-border bg-ui-background"
|
||||
className="fixed left-0 top-0 z-[60] w-full border-b border-ui-border bg-ui-panel"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<nav
|
||||
|
|
@ -479,27 +488,47 @@ export const TopBar = ({ pageName }: Props) => {
|
|||
</div>
|
||||
|
||||
<div
|
||||
className={`${tabStore.activeTabId === "3D" ? "no-drag" : ""} flex items-center justify-center gap-2 font-medium`}
|
||||
className={`${tabStore.activeTabId === "3D" ? "no-drag" : ""} flex min-w-0 items-center justify-center gap-2 font-medium`}
|
||||
data-tauri-drag-region
|
||||
>
|
||||
{tabStore.activeTabId === "3D" ? (
|
||||
<SceneTitleInput pageName={pageName} />
|
||||
) : (
|
||||
<h1
|
||||
className="flex items-center gap-2.5 text-base-fg"
|
||||
className="hud-label flex min-w-0 items-center gap-1.5 text-base-fg/70"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
{getCreatorIcon(
|
||||
ModelCreator.ArtCraft,
|
||||
"h-5 w-5 icon-auto-contrast opacity-50",
|
||||
)}
|
||||
{pageTitle}
|
||||
{pageCrumbs.map((crumb, i) => {
|
||||
const isLast = i === pageCrumbs.length - 1;
|
||||
return (
|
||||
<Fragment key={`${crumb}-${i}`}>
|
||||
{i > 0 && (
|
||||
<ChevronRightIcon
|
||||
aria-hidden="true"
|
||||
className="h-3 w-3 shrink-0 text-base-fg/50"
|
||||
data-tauri-drag-region
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
className={
|
||||
isLast
|
||||
? "truncate bg-ui-ink px-1.5 py-1 text-ui-panel"
|
||||
: "truncate"
|
||||
}
|
||||
aria-current={isLast ? "page" : undefined}
|
||||
data-tauri-drag-region
|
||||
>
|
||||
{crumb}
|
||||
</span>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</h1>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2" data-tauri-drag-region>
|
||||
<div className="no-drag flex items-center gap-1.5">
|
||||
<div className="no-drag flex items-center gap-1.5 sm:gap-2">
|
||||
{(tabStore.activeTabId === "IMAGE" ||
|
||||
tabStore.activeTabId === "VIDEO") && <GallerySelectToggle />}
|
||||
{tabStore.activeTabId === "VIDEO" && <GalleryAutoplayToggle />}
|
||||
|
|
@ -508,26 +537,26 @@ export const TopBar = ({ pageName }: Props) => {
|
|||
tabStore.activeTabId === "AUDIO") && <GalleryViewToggle />}
|
||||
<PopoverMenu
|
||||
position="bottom"
|
||||
align="center"
|
||||
align="end"
|
||||
triggerIcon={
|
||||
<CreditsCoinWithStatus iconStatus={creditsIconStatus} />
|
||||
}
|
||||
triggerLabel={
|
||||
<span className="whitespace-nowrap text-sm font-medium">
|
||||
{sumTotalCredits} Credits
|
||||
{sumTotalCredits.toLocaleString()}
|
||||
</span>
|
||||
}
|
||||
buttonClassName="h-[30px] px-2 ps-1.5 bg-transparent hover:bg-ui-controls/30 border-0 shadow-none"
|
||||
panelClassName="mt-3 bg-ui-panel border border-ui-panel-border text-base-fg"
|
||||
buttonClassName="h-8 px-3 ps-2.5 bg-transparent hover:bg-white/10 border border-white/15 hover:border-white/30 shadow-none text-white/80 rounded-[3px] gap-1.5"
|
||||
panelClassName="mt-2 bg-[#101014] border border-white/15 text-white rounded-[3px]"
|
||||
>
|
||||
{(close) => (
|
||||
<div className="w-72 p-2.5 text-base-fg">
|
||||
<div className="w-72 p-3 text-white">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="flex items-center gap-1.5 text-sm font-medium text-base-fg/80">
|
||||
<span className="text-sm font-medium text-white/70">
|
||||
Your credit balance
|
||||
</span>
|
||||
<button
|
||||
className="text-sm font-medium text-primary-400 transition-all hover:text-primary-300"
|
||||
className="text-sm font-medium text-primary transition-colors hover:text-primary-300"
|
||||
onClick={() => {
|
||||
close();
|
||||
toggleCreditsModal();
|
||||
|
|
@ -536,15 +565,13 @@ export const TopBar = ({ pageName }: Props) => {
|
|||
Buy credits
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-4xl font-bold text-base-fg">
|
||||
<CoinsIcon
|
||||
|
||||
className="text-2xl text-primary" />
|
||||
{sumTotalCredits}
|
||||
<div className="flex items-center gap-2 text-3xl font-semibold tracking-tight text-white">
|
||||
<CoinsIcon className="text-xl text-primary" />
|
||||
{sumTotalCredits.toLocaleString()}
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="mt-2 flex items-center gap-1.5 text-xs text-base-fg/50 transition-colors hover:text-primary"
|
||||
className="mt-2 flex items-center gap-1.5 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/50 transition-colors hover:text-white"
|
||||
onClick={() => {
|
||||
close();
|
||||
useCostBreakdownModalStore.getState().openModal();
|
||||
|
|
@ -556,7 +583,7 @@ export const TopBar = ({ pageName }: Props) => {
|
|||
|
||||
<div className="mt-3 flex gap-2">
|
||||
<Button
|
||||
variant="action"
|
||||
variant="secondary"
|
||||
className="h-9 grow"
|
||||
onClick={() => {
|
||||
close();
|
||||
|
|
@ -586,21 +613,35 @@ export const TopBar = ({ pageName }: Props) => {
|
|||
variant="primary"
|
||||
icon={GemIcon}
|
||||
onClick={toggleSubscriptionModal}
|
||||
className="transition-all duration-300"
|
||||
className="h-8 px-3"
|
||||
>
|
||||
Upgrade
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<UploadImagesButton className="h-[34px] w-[34px]" />
|
||||
<TaskQueue />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleOpenGalleryModal}
|
||||
aria-label="My Library"
|
||||
className="flex h-8 items-center gap-1.5 rounded-[3px] border border-white/15 px-3 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/80 transition-colors hover:border-white/30 hover:bg-white/10"
|
||||
>
|
||||
<ImagesIcon className="text-[11px]" />
|
||||
<span className="hidden whitespace-nowrap xl:block">
|
||||
My Library
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<UploadImagesButton />
|
||||
|
||||
<Tooltip content="Settings" position="bottom" delay={300}>
|
||||
<Button
|
||||
aria-label="Settings"
|
||||
variant="secondary"
|
||||
icon={SettingsIcon}
|
||||
iconClassName="h-5 w-5 shrink-0"
|
||||
className="h-[34px] w-[34px] p-0"
|
||||
iconClassName="h-4 w-4 shrink-0"
|
||||
className="h-8 w-8 border-white/15 bg-transparent p-0 text-white/80 hover:border-white/30 hover:bg-white/10"
|
||||
onClick={() => {
|
||||
setSettingsSection("general");
|
||||
setIsSettingsModalOpen(true);
|
||||
|
|
@ -608,19 +649,6 @@ export const TopBar = ({ pageName }: Props) => {
|
|||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
icon={ImagesIcon}
|
||||
onClick={handleOpenGalleryModal}
|
||||
>
|
||||
<span className="hidden whitespace-nowrap text-base-fg xl:block">
|
||||
My Library
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
{/* <Activity /> */}
|
||||
<TaskQueue />
|
||||
</div>
|
||||
|
||||
<div className="no-drag">
|
||||
|
|
@ -631,14 +659,14 @@ export const TopBar = ({ pageName }: Props) => {
|
|||
<div className="no-drag flex items-center">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="h-[32px] w-[44px] rounded-none border-0 bg-transparent text-base-fg opacity-70 shadow-none hover:bg-ui-controls/20 hover:opacity-100"
|
||||
className="h-[32px] w-[44px] rounded-none border-0 bg-transparent p-0 text-base-fg opacity-70 shadow-none hover:bg-white/10 hover:opacity-100"
|
||||
onClick={minimize}
|
||||
>
|
||||
<MinusIcon className="text-xs" />
|
||||
<MinusIcon className="text-xs" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="h-[32px] w-[44px] rounded-none border-0 bg-transparent text-base-fg opacity-70 shadow-none hover:bg-ui-controls/20 hover:opacity-100"
|
||||
className="h-[32px] w-[44px] rounded-none border-0 bg-transparent p-0 text-base-fg opacity-70 shadow-none hover:bg-white/10 hover:opacity-100"
|
||||
onClick={toggleMaximize}
|
||||
>
|
||||
<DynamicIcon
|
||||
|
|
@ -648,10 +676,10 @@ export const TopBar = ({ pageName }: Props) => {
|
|||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="h-[32px] w-[44px] rounded-none border-0 bg-transparent text-base-fg opacity-70 shadow-none hover:bg-red/10 hover:text-red"
|
||||
className="h-[32px] w-[44px] rounded-none border-0 bg-transparent p-0 text-base-fg opacity-70 shadow-none hover:bg-red hover:text-white hover:opacity-100"
|
||||
onClick={close}
|
||||
>
|
||||
<XIcon className="text-lg" />
|
||||
<XIcon className="text-lg" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,11 @@ export const UploadImagesButton = ({ className }: Props) => {
|
|||
<Button
|
||||
variant="secondary"
|
||||
icon={UploadIcon}
|
||||
iconClassName="h-5 w-5 shrink-0"
|
||||
className={twMerge("h-[38px] w-[38px] p-0", className)}
|
||||
iconClassName="h-4 w-4 shrink-0"
|
||||
className={twMerge(
|
||||
"h-8 w-8 rounded-[3px] border border-white/15 bg-transparent p-0 text-white/80 shadow-none hover:border-white/30 hover:bg-white/10",
|
||||
className,
|
||||
)}
|
||||
onClick={() => setIsOpen(true)}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
|
|
|||
|
|
@ -83,98 +83,122 @@ export interface FullAppItem {
|
|||
}
|
||||
|
||||
// Per-app card palette for the Apps page (webapp-home-style cards). Derived
|
||||
// from each app's Tailwind color family so the icon box, its border, and the
|
||||
// hover gradient all share a hue. Keyed by app id; falls back to a neutral
|
||||
// slate when an id is missing.
|
||||
// from each app's Tailwind color family so the icon tile, its border, and the
|
||||
// hover state all share a hue. Keyed by app id; falls back to a neutral tone
|
||||
// when an id is missing.
|
||||
interface AppCardPalette {
|
||||
/** Hover gradient overlay, e.g. "from-blue-500/20 to-blue-500/0". */
|
||||
/** Hover tint for the card, e.g. "from-blue-500/20 to-blue-500/0". Legacy. */
|
||||
accent: string;
|
||||
/** Icon box background + border, e.g. "bg-blue-500/20 border-blue-400/30". */
|
||||
/** Icon tile background + border, e.g. "bg-blue-500/20 border-blue-400/40". */
|
||||
iconBg: string;
|
||||
/** Icon glyph color, e.g. "text-blue-300". */
|
||||
iconColor: string;
|
||||
/** Card hover border + background, e.g. "hover:border-blue-400/60 hover:bg-blue-500/10". */
|
||||
hoverStyle: string;
|
||||
}
|
||||
|
||||
const APP_CARD_PALETTES: Record<string, AppCardPalette> = {
|
||||
"text-to-image": {
|
||||
accent: "from-blue-500/20 to-blue-500/0",
|
||||
iconBg: "bg-blue-500/20 border-blue-400/30",
|
||||
iconBg: "bg-blue-500/20 border-blue-400/40",
|
||||
iconColor: "text-blue-300",
|
||||
hoverStyle: "hover:border-blue-400/60 hover:bg-blue-500/10",
|
||||
},
|
||||
"image-to-video": {
|
||||
accent: "from-amber-500/20 to-amber-500/0",
|
||||
iconBg: "bg-amber-500/20 border-amber-400/30",
|
||||
iconColor: "text-amber-300",
|
||||
accent: "from-purple-500/20 to-purple-500/0",
|
||||
iconBg: "bg-purple-500/20 border-purple-400/40",
|
||||
iconColor: "text-purple-300",
|
||||
hoverStyle: "hover:border-purple-400/60 hover:bg-purple-500/10",
|
||||
},
|
||||
"create-audio": {
|
||||
accent: "from-pink-500/20 to-pink-500/0",
|
||||
iconBg: "bg-pink-500/20 border-pink-400/30",
|
||||
iconBg: "bg-pink-500/20 border-pink-400/40",
|
||||
iconColor: "text-pink-300",
|
||||
hoverStyle: "hover:border-pink-400/60 hover:bg-pink-500/10",
|
||||
},
|
||||
"image-to-3d-object": {
|
||||
accent: "from-emerald-500/20 to-emerald-500/0",
|
||||
iconBg: "bg-emerald-500/20 border-emerald-400/30",
|
||||
iconColor: "text-emerald-300",
|
||||
accent: "from-cyan-500/20 to-cyan-500/0",
|
||||
iconBg: "bg-cyan-500/20 border-cyan-400/40",
|
||||
iconColor: "text-cyan-300",
|
||||
hoverStyle: "hover:border-cyan-400/60 hover:bg-cyan-500/10",
|
||||
},
|
||||
"image-to-3d-world": {
|
||||
accent: "from-teal-500/20 to-teal-500/0",
|
||||
iconBg: "bg-teal-500/20 border-teal-400/40",
|
||||
iconColor: "text-teal-300",
|
||||
hoverStyle: "hover:border-teal-400/60 hover:bg-teal-500/10",
|
||||
},
|
||||
"3d-editor": {
|
||||
accent: "from-amber-500/20 to-amber-500/0",
|
||||
iconBg: "bg-amber-500/20 border-amber-400/40",
|
||||
iconColor: "text-amber-300",
|
||||
hoverStyle: "hover:border-amber-400/60 hover:bg-amber-500/10",
|
||||
},
|
||||
"background-change": {
|
||||
accent: "from-emerald-500/20 to-emerald-500/0",
|
||||
iconBg: "bg-emerald-500/20 border-emerald-400/40",
|
||||
iconColor: "text-emerald-300",
|
||||
hoverStyle: "hover:border-emerald-400/60 hover:bg-emerald-500/10",
|
||||
},
|
||||
"video-editor": {
|
||||
accent: "from-rose-500/20 to-rose-500/0",
|
||||
iconBg: "bg-rose-500/20 border-rose-400/40",
|
||||
iconColor: "text-rose-300",
|
||||
hoverStyle: "hover:border-rose-400/60 hover:bg-rose-500/10",
|
||||
},
|
||||
moodboard: {
|
||||
accent: "from-indigo-500/20 to-indigo-500/0",
|
||||
iconBg: "bg-indigo-500/20 border-indigo-400/40",
|
||||
iconColor: "text-indigo-300",
|
||||
hoverStyle: "hover:border-indigo-400/60 hover:bg-indigo-500/10",
|
||||
},
|
||||
"video-frame-extractor": {
|
||||
accent: "from-orange-500/20 to-orange-500/0",
|
||||
iconBg: "bg-orange-500/20 border-orange-400/40",
|
||||
iconColor: "text-orange-300",
|
||||
hoverStyle: "hover:border-orange-400/60 hover:bg-orange-500/10",
|
||||
},
|
||||
"edit-image": {
|
||||
accent: "from-sky-500/20 to-sky-500/0",
|
||||
iconBg: "bg-sky-500/20 border-sky-400/30",
|
||||
iconBg: "bg-sky-500/20 border-sky-400/40",
|
||||
iconColor: "text-sky-300",
|
||||
},
|
||||
angles: {
|
||||
accent: "from-lime-500/20 to-lime-500/0",
|
||||
iconBg: "bg-lime-500/20 border-lime-400/30",
|
||||
iconColor: "text-lime-300",
|
||||
},
|
||||
storyboard: {
|
||||
accent: "from-fuchsia-500/20 to-fuchsia-500/0",
|
||||
iconBg: "bg-fuchsia-500/20 border-fuchsia-400/30",
|
||||
iconColor: "text-fuchsia-300",
|
||||
hoverStyle: "hover:border-sky-400/60 hover:bg-sky-500/10",
|
||||
},
|
||||
"2d-canvas": {
|
||||
accent: "from-sky-500/20 to-sky-500/0",
|
||||
iconBg: "bg-sky-500/20 border-sky-400/30",
|
||||
iconBg: "bg-sky-500/20 border-sky-400/40",
|
||||
iconColor: "text-sky-300",
|
||||
},
|
||||
"3d-editor": {
|
||||
accent: "from-emerald-500/20 to-emerald-500/0",
|
||||
iconBg: "bg-emerald-500/20 border-emerald-400/30",
|
||||
iconColor: "text-emerald-300",
|
||||
},
|
||||
"edit-image": {
|
||||
accent: "from-purple-500/20 to-purple-500/0",
|
||||
iconBg: "bg-purple-500/20 border-purple-400/30",
|
||||
iconColor: "text-purple-300",
|
||||
hoverStyle: "hover:border-sky-400/60 hover:bg-sky-500/10",
|
||||
},
|
||||
"remove-background": {
|
||||
accent: "from-violet-500/20 to-violet-500/0",
|
||||
iconBg: "bg-violet-500/20 border-violet-400/30",
|
||||
iconBg: "bg-violet-500/20 border-violet-400/40",
|
||||
iconColor: "text-violet-300",
|
||||
hoverStyle: "hover:border-violet-400/60 hover:bg-violet-500/10",
|
||||
},
|
||||
"video-frame-extractor": {
|
||||
accent: "from-rose-500/20 to-rose-500/0",
|
||||
iconBg: "bg-rose-500/20 border-rose-400/30",
|
||||
iconColor: "text-rose-300",
|
||||
angles: {
|
||||
accent: "from-lime-500/20 to-lime-500/0",
|
||||
iconBg: "bg-lime-500/20 border-lime-400/40",
|
||||
iconColor: "text-lime-300",
|
||||
hoverStyle: "hover:border-lime-400/60 hover:bg-lime-500/10",
|
||||
},
|
||||
"background-change": {
|
||||
accent: "from-orange-500/20 to-orange-500/0",
|
||||
iconBg: "bg-orange-500/20 border-orange-400/30",
|
||||
iconColor: "text-orange-300",
|
||||
},
|
||||
"video-editor": {
|
||||
accent: "from-teal-500/20 to-teal-500/0",
|
||||
iconBg: "bg-teal-500/20 border-teal-400/30",
|
||||
iconColor: "text-teal-300",
|
||||
storyboard: {
|
||||
accent: "from-fuchsia-500/20 to-fuchsia-500/0",
|
||||
iconBg: "bg-fuchsia-500/20 border-fuchsia-400/40",
|
||||
iconColor: "text-fuchsia-300",
|
||||
hoverStyle: "hover:border-fuchsia-400/60 hover:bg-fuchsia-500/10",
|
||||
},
|
||||
"video-watermark-removal": {
|
||||
accent: "from-cyan-500/20 to-cyan-500/0",
|
||||
iconBg: "bg-cyan-500/20 border-cyan-400/30",
|
||||
iconColor: "text-cyan-300",
|
||||
accent: "from-yellow-500/20 to-yellow-500/0",
|
||||
iconBg: "bg-yellow-500/20 border-yellow-400/40",
|
||||
iconColor: "text-yellow-300",
|
||||
hoverStyle: "hover:border-yellow-400/60 hover:bg-yellow-500/10",
|
||||
},
|
||||
"image-watermark-removal": {
|
||||
accent: "from-indigo-500/20 to-indigo-500/0",
|
||||
iconBg: "bg-indigo-500/20 border-indigo-400/30",
|
||||
iconColor: "text-indigo-300",
|
||||
accent: "from-red-500/20 to-red-500/0",
|
||||
iconBg: "bg-red-500/20 border-red-400/40",
|
||||
iconColor: "text-red-300",
|
||||
hoverStyle: "hover:border-red-400/60 hover:bg-red-500/10",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -182,6 +206,7 @@ const FALLBACK_APP_CARD_PALETTE: AppCardPalette = {
|
|||
accent: "from-white/10 to-white/0",
|
||||
iconBg: "bg-ui-controls border-ui-controls-border",
|
||||
iconColor: "text-base-fg",
|
||||
hoverStyle: "hover:border-white/30 hover:bg-white/10",
|
||||
};
|
||||
|
||||
export const getAppCardPalette = (id: string): AppCardPalette =>
|
||||
|
|
@ -387,13 +412,13 @@ export const useEditApps = (): FullAppItem[] => {
|
|||
export const getBadgeStyles = (badge?: string) => {
|
||||
switch (badge) {
|
||||
case "NEW":
|
||||
return "bg-teal-600 text-white";
|
||||
return "border-purple-400/40 text-purple-300";
|
||||
case "BEST":
|
||||
return "bg-primary text-white";
|
||||
return "border-primary/30 text-ui-accent-ink";
|
||||
case "SOON":
|
||||
return "bg-gray-600 text-white";
|
||||
return "border-white/20 text-base-fg/50";
|
||||
case "BETA":
|
||||
return "bg-amber-500 text-white";
|
||||
return "border-amber-400/40 text-amber-300";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -481,7 +481,7 @@ export const Angles = () => {
|
|||
/* ──── Upload screen ──── */
|
||||
<div className="flex flex-1 items-center justify-center p-8 lg:p-16">
|
||||
<div className="w-full max-w-5xl">
|
||||
<div className="relative aspect-video overflow-hidden rounded-2xl border border-ui-panel-border bg-ui-panel shadow-lg">
|
||||
<div className="relative aspect-video overflow-hidden border border-ui-panel-border bg-ui-panel">
|
||||
<UploadEntryCard
|
||||
icon={CrosshairIcon}
|
||||
title="Angles"
|
||||
|
|
@ -497,7 +497,7 @@ export const Angles = () => {
|
|||
disabled={isLoadingImage}
|
||||
/>
|
||||
{isLoadingImage && (
|
||||
<div className="bg-ui-panel/80 absolute inset-0 flex items-center justify-center backdrop-blur-sm">
|
||||
<div className="bg-ui-panel/80 absolute inset-0 flex items-center justify-center">
|
||||
<LoadingSpinner className="h-12 w-12" />
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -518,7 +518,7 @@ export const Angles = () => {
|
|||
{/* Full-bleed image display */}
|
||||
<div className="flex h-full w-full items-center justify-center px-16 pb-56 pt-16">
|
||||
<div
|
||||
className="relative overflow-hidden rounded-xl shadow-lg"
|
||||
className="relative overflow-hidden"
|
||||
style={imageContainerStyle}
|
||||
>
|
||||
{activeAngle ? (
|
||||
|
|
@ -583,12 +583,12 @@ export const Angles = () => {
|
|||
|
||||
{/* ── Floating bottom angle controls ── */}
|
||||
<div className="absolute bottom-4 left-1/2 z-10 w-[calc(100%-32px)] max-w-[860px] -translate-x-1/2">
|
||||
<div className="glass flex items-center gap-4 rounded-xl px-4 lg:gap-5 lg:px-5">
|
||||
<div className="glass flex items-center gap-4 rounded-none px-4 lg:gap-5 lg:px-5">
|
||||
{/* Orbit sphere */}
|
||||
<div className="relative shrink-0 px-5 py-5">
|
||||
<button
|
||||
onClick={() => handleTiltStep(1)}
|
||||
className="absolute left-1/2 top-0 z-10 -translate-x-1/2 p-1 text-base-fg/40 transition-colors hover:text-base-fg/80"
|
||||
className="absolute left-1/2 top-0 z-10 -translate-x-1/2 rounded-[3px] p-1 text-base-fg/40 transition-colors hover:text-base-fg/80"
|
||||
>
|
||||
<ChevronUpIcon
|
||||
|
||||
|
|
@ -596,7 +596,7 @@ export const Angles = () => {
|
|||
</button>
|
||||
<button
|
||||
onClick={() => handleTiltStep(-1)}
|
||||
className="absolute bottom-0 left-1/2 z-10 -translate-x-1/2 p-1 text-base-fg/40 transition-colors hover:text-base-fg/80"
|
||||
className="absolute bottom-0 left-1/2 z-10 -translate-x-1/2 rounded-[3px] p-1 text-base-fg/40 transition-colors hover:text-base-fg/80"
|
||||
>
|
||||
<ChevronDownIcon
|
||||
|
||||
|
|
@ -604,7 +604,7 @@ export const Angles = () => {
|
|||
</button>
|
||||
<button
|
||||
onClick={() => handleRotationStep(-1)}
|
||||
className="absolute left-0 top-1/2 z-10 -translate-y-1/2 p-1 text-base-fg/40 transition-colors hover:text-base-fg/80"
|
||||
className="absolute left-0 top-1/2 z-10 -translate-y-1/2 rounded-[3px] p-1 text-base-fg/40 transition-colors hover:text-base-fg/80"
|
||||
>
|
||||
<ChevronLeftIcon
|
||||
|
||||
|
|
@ -612,7 +612,7 @@ export const Angles = () => {
|
|||
</button>
|
||||
<button
|
||||
onClick={() => handleRotationStep(1)}
|
||||
className="absolute right-0 top-1/2 z-10 -translate-y-1/2 p-1 text-base-fg/40 transition-colors hover:text-base-fg/80"
|
||||
className="absolute right-0 top-1/2 z-10 -translate-y-1/2 rounded-[3px] p-1 text-base-fg/40 transition-colors hover:text-base-fg/80"
|
||||
>
|
||||
<ChevronRightIcon
|
||||
|
||||
|
|
@ -632,7 +632,7 @@ export const Angles = () => {
|
|||
{/* Sliders group */}
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-14 shrink-0 text-xs font-medium text-base-fg">
|
||||
<span className="w-16 shrink-0 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">
|
||||
Rotation
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
|
|
@ -645,13 +645,13 @@ export const Angles = () => {
|
|||
suffix="°"
|
||||
/>
|
||||
</div>
|
||||
<span className="w-10 shrink-0 text-left text-xs tabular-nums text-base-fg/70">
|
||||
<span className="w-10 shrink-0 text-left font-mono text-xs tabular-nums text-base-fg/70">
|
||||
{rotationToDisplay(angleConfig.rotation)}°
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-14 shrink-0 text-xs font-medium text-base-fg">
|
||||
<span className="w-16 shrink-0 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">
|
||||
Tilt
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
|
|
@ -664,13 +664,13 @@ export const Angles = () => {
|
|||
suffix="°"
|
||||
/>
|
||||
</div>
|
||||
<span className="w-9 shrink-0 text-left text-xs tabular-nums text-base-fg/70">
|
||||
<span className="w-9 shrink-0 text-left font-mono text-xs tabular-nums text-base-fg/70">
|
||||
{angleConfig.tilt}°
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-14 shrink-0 text-xs font-medium text-base-fg">
|
||||
<span className="w-16 shrink-0 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">
|
||||
Zoom
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
|
|
@ -682,7 +682,7 @@ export const Angles = () => {
|
|||
onChange={handleZoomSlider}
|
||||
/>
|
||||
</div>
|
||||
<span className="w-9 shrink-0 text-left text-xs tabular-nums text-base-fg/70">
|
||||
<span className="w-9 shrink-0 text-left font-mono text-xs tabular-nums text-base-fg/70">
|
||||
{angleConfig.zoom}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -23,28 +23,35 @@ export const AppsIndexPage = () => {
|
|||
// flush against the right edge; horizontal padding lives on the inner
|
||||
// wrapper instead.
|
||||
<div className="fixed inset-0 overflow-y-auto bg-ui-background pt-[56px] text-base-fg">
|
||||
<main className="mx-auto max-w-6xl px-6 pb-20 pt-12 sm:pt-20">
|
||||
<h1 className="mx-auto text-center text-4xl font-semibold tracking-tight sm:text-6xl">
|
||||
What will you <span className="text-primary">craft</span> today?
|
||||
<main className="mx-auto w-full max-w-6xl px-5 pb-20 pt-8 sm:px-8 sm:pt-12">
|
||||
<p className="hud-label mb-4 text-ui-accent-ink">
|
||||
Your creative workspace
|
||||
</p>
|
||||
<h1 className="max-w-3xl font-display text-3xl leading-tight tracking-tight sm:text-5xl">
|
||||
What will you <span className="text-ui-accent-ink">craft</span> today?
|
||||
</h1>
|
||||
<p className="mx-auto mt-3 max-w-xl text-center text-lg text-base-fg/55">
|
||||
Every tool in your toolbox - pick one and start creating.
|
||||
<p className="mt-4 max-w-xl text-sm leading-relaxed text-base-fg/70 sm:text-base">
|
||||
Start with an idea. Choose a tool to bring it to life.
|
||||
</p>
|
||||
|
||||
<div className="mt-12 space-y-10">
|
||||
{categories.map((category) => (
|
||||
<section key={category.title}>
|
||||
<h2 className="mb-4 text-sm font-semibold text-base-fg/85">
|
||||
{category.title}
|
||||
</h2>
|
||||
<div className="grid auto-rows-min gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{category.apps.map((app) => (
|
||||
<AppCard key={app.id} app={app} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
{categories.map((category, index) => (
|
||||
<section
|
||||
key={category.title}
|
||||
className={twMerge(
|
||||
"border-t border-ui-border pt-6",
|
||||
index === 0 ? "mt-8 sm:mt-10" : "mt-10",
|
||||
)}
|
||||
>
|
||||
<h2 className="hud-label mb-4 text-base-fg/70">
|
||||
{category.title}
|
||||
</h2>
|
||||
<div className="grid auto-rows-fr gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
{category.apps.map((app) => (
|
||||
<AppCard key={app.id} app={app} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -59,27 +66,14 @@ function AppCard({ app }: { app: FullAppItem }) {
|
|||
onClick={() => goToApp(app.action)}
|
||||
disabled={!enabled}
|
||||
className={twMerge(
|
||||
"group relative block overflow-hidden rounded-2xl bg-ui-controls/50 p-5 text-left transition-colors",
|
||||
enabled
|
||||
? "cursor-pointer hover:bg-ui-controls"
|
||||
: "cursor-default opacity-60",
|
||||
"group relative flex h-full rounded-[3px] border border-ui-border bg-white/5 p-5 text-left transition-colors duration-150 focus-visible:border-primary",
|
||||
enabled ? twMerge("cursor-pointer", palette.hoverStyle) : "cursor-default opacity-60",
|
||||
)}
|
||||
>
|
||||
{/* Hover accent — gradient wash matching the app's hue. */}
|
||||
{enabled && (
|
||||
<div
|
||||
aria-hidden
|
||||
className={twMerge(
|
||||
"pointer-events-none absolute -inset-px rounded-2xl bg-gradient-to-br opacity-0 transition-opacity group-hover:opacity-100",
|
||||
palette.accent,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="relative flex items-start gap-4">
|
||||
<div className="relative flex w-full items-start gap-4">
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex h-11 w-11 shrink-0 items-center justify-center rounded-xl border",
|
||||
"flex h-10 w-10 shrink-0 items-center justify-center border",
|
||||
palette.iconBg,
|
||||
palette.iconColor,
|
||||
)}
|
||||
|
|
@ -88,15 +82,17 @@ function AppCard({ app }: { app: FullAppItem }) {
|
|||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<h3 className="truncate text-base font-semibold text-base-fg">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<h3 className="text-base font-bold uppercase tracking-tight text-ui-ink">
|
||||
{app.label}
|
||||
</h3>
|
||||
{app.badge && (
|
||||
<span
|
||||
className={twMerge(
|
||||
"shrink-0 rounded-full px-1.5 py-0.5 text-[10px] font-bold uppercase leading-none tracking-wider",
|
||||
getBadgeStyles(app.badge),
|
||||
"shrink-0 border px-1.5 py-1 font-mono text-[9px] font-medium leading-none tracking-wider",
|
||||
app.badge === "SOON"
|
||||
? getBadgeStyles(app.badge)
|
||||
: "border-primary/30 text-ui-accent-ink",
|
||||
)}
|
||||
>
|
||||
{app.badge}
|
||||
|
|
@ -104,12 +100,12 @@ function AppCard({ app }: { app: FullAppItem }) {
|
|||
)}
|
||||
</div>
|
||||
{enabled && (
|
||||
<ArrowRightIcon
|
||||
|
||||
className="text-sm text-base-fg/40 transition-all group-hover:translate-x-0.5 group-hover:text-base-fg/70" />
|
||||
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-[3px] border border-white/15 text-white/40 transition-colors duration-150 group-hover:border-white/40 group-hover:text-white">
|
||||
<ArrowRightIcon className="text-xs" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-1 text-sm leading-snug text-base-fg/55">
|
||||
<p className="mt-2 text-sm leading-relaxed text-base-fg/70">
|
||||
{app.description}
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import {
|
|||
FilterMediaClasses,
|
||||
} from "@storyteller/api";
|
||||
import { useOmniGenAudioModels, useAudioCostEstimate } from "@storyteller/omni-gen";
|
||||
import BackgroundGallery from "../PageImage/BackgroundGallery";
|
||||
import {
|
||||
galleryModalLightboxImage,
|
||||
galleryModalLightboxMediaId,
|
||||
|
|
@ -149,7 +148,6 @@ const CreateAudio = () => {
|
|||
hasContent={hasContent}
|
||||
emptyStateTitle="Create Audio"
|
||||
emptyStateSubtitle="Describe a song, a sound, or a sample"
|
||||
background={<BackgroundGallery />}
|
||||
bottomOffset={promptHeight + 40}
|
||||
listContent={
|
||||
<DesktopGenerationGallery
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { SparklesIcon } from "lucide-react";
|
||||
import { toast } from "@storyteller/ui-toaster";
|
||||
import { UploadImageMedia, UploadVideoMedia } from "@storyteller/api";
|
||||
import {
|
||||
PromptBoxVFX,
|
||||
SubTabStrip,
|
||||
TruchetPattern,
|
||||
VFXResultCard,
|
||||
VFXShowcaseView,
|
||||
newIdempotencyToken,
|
||||
|
|
@ -14,6 +12,7 @@ import {
|
|||
VFX_NOT_AVAILABLE_ERROR,
|
||||
VFX_SHOWCASE,
|
||||
} from "@storyteller/ui-vfx";
|
||||
import { CreateEmptyState } from "~/components/generation-feed/CreateEmptyState";
|
||||
|
||||
export const PageBackgroundChange = () => {
|
||||
const subTab = useVFXStore((s) => s.subTab);
|
||||
|
|
@ -113,34 +112,17 @@ export const PageBackgroundChange = () => {
|
|||
className="relative flex flex-1 items-center justify-center px-6"
|
||||
style={{ paddingBottom: Math.max(promptBoxHeight + 36, 240) }}
|
||||
>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0 z-0"
|
||||
style={{
|
||||
maskImage:
|
||||
"radial-gradient(ellipse 60% 60% at 50% 45%, black 30%, transparent 85%)",
|
||||
WebkitMaskImage:
|
||||
"radial-gradient(ellipse 60% 60% at 50% 45%, black 30%, transparent 85%)",
|
||||
}}
|
||||
>
|
||||
<TruchetPattern
|
||||
intensity={0.8}
|
||||
className="absolute inset-0 h-full w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-10">
|
||||
<EmptyState
|
||||
title="No background changes yet"
|
||||
subtitle="Upload a source video and a reference image, then optionally add a prompt."
|
||||
/>
|
||||
</div>
|
||||
<CreateEmptyState
|
||||
title="No background changes yet"
|
||||
subtitle="Upload a source video and a reference image, then optionally add a prompt."
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="flex-1 overflow-y-auto"
|
||||
style={{ paddingBottom: Math.max(promptBoxHeight + 36, 240) }}
|
||||
>
|
||||
<div className="flex flex-col gap-10 px-6 pt-6">
|
||||
<div className="flex flex-col items-center gap-10 px-6 pt-6">
|
||||
{history.map((r) => (
|
||||
<VFXResultCard
|
||||
key={r.id}
|
||||
|
|
@ -155,6 +137,7 @@ export const PageBackgroundChange = () => {
|
|||
failureReason: r.failureReason,
|
||||
}}
|
||||
onDismiss={() => dismissResult(r.id)}
|
||||
className="w-[min(960px,calc(100vw-32px))]"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -163,7 +146,7 @@ export const PageBackgroundChange = () => {
|
|||
|
||||
<div
|
||||
aria-hidden
|
||||
className="via-ui-background/85 pointer-events-none fixed bottom-0 left-0 right-0 z-20 h-72 bg-gradient-to-t from-ui-background to-transparent"
|
||||
className="pointer-events-none fixed bottom-0 left-0 right-0 z-20 h-72 bg-gradient-to-t from-ui-background via-ui-background/85 to-transparent"
|
||||
/>
|
||||
|
||||
<div
|
||||
|
|
@ -184,19 +167,4 @@ export const PageBackgroundChange = () => {
|
|||
);
|
||||
};
|
||||
|
||||
interface EmptyStateProps {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
}
|
||||
|
||||
const EmptyState = ({ title, subtitle }: EmptyStateProps) => (
|
||||
<div className="flex max-w-md flex-col items-center gap-4 text-center">
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-base-fg/5 ring-1 ring-base-fg/10">
|
||||
<SparklesIcon className="text-2xl" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-base-fg">{title}</h3>
|
||||
<p className="max-w-xs text-sm text-base-fg/60">{subtitle}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default PageBackgroundChange;
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
.stretch-background-image {
|
||||
background-image:
|
||||
linear-gradient(
|
||||
0deg,
|
||||
rgb(var(--st-photo-tint-rgb) / var(--st-photo-tint-alpha)),
|
||||
rgb(var(--st-photo-tint-rgb) / var(--st-photo-tint-alpha))
|
||||
),
|
||||
url("/resources/images/digital-mountains.webp");
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
filter: grayscale(var(--st-photo-grayscale));
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import "./Background.css";
|
||||
|
||||
export default function GalleryBackground() {
|
||||
return (
|
||||
<>
|
||||
<div className="pointer-events-none fixed inset-0 z-[1] overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,_transparent_49%,_rgb(var(--st-controls-rgb)_/_var(--st-gallery-vignette-alpha))_100%)]" />
|
||||
<div className="fixed inset-0 z-0 overflow-hidden">
|
||||
<div
|
||||
className={`stretch-background-image h-full w-full opacity-30 transition-opacity duration-1000`}
|
||||
>
|
||||
<div className="absolute inset-0 grid grid-cols-5 gap-6 px-6 opacity-35" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|||
import { JobContextType } from "@storyteller/common";
|
||||
import { PromptBoxImage } from "@storyteller/ui-promptbox";
|
||||
import { UploadImageMedia, FilterMediaClasses } from "@storyteller/api";
|
||||
import BackgroundGallery from "./BackgroundGallery";
|
||||
import {
|
||||
useTextToImagePageModelList,
|
||||
ModelPage,
|
||||
|
|
@ -158,7 +157,6 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
|
|||
hasContent={hasContent}
|
||||
emptyStateTitle="Create Image"
|
||||
emptyStateSubtitle="Add a prompt, then generate"
|
||||
background={<BackgroundGallery />}
|
||||
bottomOffset={promptHeight + 40}
|
||||
listContent={
|
||||
<DesktopGenerationGallery
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ export const ImageTo3DObject = () => {
|
|||
title="Generate 3D Object"
|
||||
subtitle="Transform your image into a 3D object with textures."
|
||||
variant="object"
|
||||
backgroundImage="/resources/images/floating-cubes.webp"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ export const ImageTo3DWorld = () => {
|
|||
title="Image to 3D World"
|
||||
subtitle="Generate 3D worlds from an image with World Labs"
|
||||
variant="world"
|
||||
backgroundImage="/resources/images/room-of-items.webp"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ export const ImageWatermarkRemover = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-ui-panel-gradient flex h-[calc(100vh-56px)] w-full overflow-hidden bg-ui-panel text-base-fg">
|
||||
<div className="flex h-[calc(100vh-56px)] w-full overflow-hidden bg-ui-background text-base-fg">
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<main
|
||||
className={
|
||||
|
|
@ -115,7 +115,7 @@ export const ImageWatermarkRemover = () => {
|
|||
>
|
||||
{!imageUrl ? (
|
||||
<div className="w-full max-w-5xl">
|
||||
<div className="aspect-video overflow-hidden rounded-2xl border border-ui-panel-border bg-ui-background shadow-lg">
|
||||
<div className="aspect-video overflow-hidden border border-ui-panel-border bg-ui-panel">
|
||||
<UploadEntryCard
|
||||
icon={DropletIcon}
|
||||
title="Remove Image Watermark"
|
||||
|
|
@ -133,7 +133,7 @@ export const ImageWatermarkRemover = () => {
|
|||
</div>
|
||||
) : (
|
||||
<div className="flex w-full max-w-5xl flex-col gap-5">
|
||||
<div className="w-full overflow-hidden rounded-2xl border border-ui-panel-border bg-ui-background shadow-lg">
|
||||
<div className="w-full overflow-hidden border border-ui-panel-border bg-ui-panel">
|
||||
<div className="relative w-full bg-black">
|
||||
<Button
|
||||
icon={RotateCwIcon}
|
||||
|
|
@ -142,7 +142,7 @@ export const ImageWatermarkRemover = () => {
|
|||
setImageUrl("");
|
||||
setImageDimensions(null);
|
||||
}}
|
||||
className="absolute right-3 top-3 z-10 border-2 border-red/50 px-3 py-1.5 text-sm hover:border-red/80 hover:bg-red/80"
|
||||
className="absolute right-3 top-3 z-10 border border-red/50 px-3 py-1.5 text-sm hover:border-red/80 hover:bg-red/80"
|
||||
>
|
||||
Switch Image
|
||||
</Button>
|
||||
|
|
@ -151,7 +151,7 @@ export const ImageWatermarkRemover = () => {
|
|||
ref={imageRef}
|
||||
src={imageUrl}
|
||||
alt="Selected pic"
|
||||
className="max-h-[70vh] max-w-full rounded-lg object-contain shadow-2xl"
|
||||
className="max-h-[70vh] max-w-full object-contain"
|
||||
onLoad={(e) => {
|
||||
const img = e.currentTarget;
|
||||
setImageDimensions({
|
||||
|
|
@ -177,12 +177,12 @@ export const ImageWatermarkRemover = () => {
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-ui-panel-border bg-ui-background p-6 shadow-lg">
|
||||
<div className="border border-ui-panel-border bg-ui-panel p-6">
|
||||
<div>
|
||||
<div className="mb-4 flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-base-fg/60">
|
||||
<div className="mb-4 flex items-center gap-2 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">
|
||||
<ImagesIcon
|
||||
|
||||
className="text-primary" />
|
||||
className="text-ui-accent-ink" />
|
||||
Image Information
|
||||
</div>
|
||||
{imageDimensions ? (
|
||||
|
|
@ -191,7 +191,7 @@ export const ImageWatermarkRemover = () => {
|
|||
<span className="font-medium text-base-fg/70">
|
||||
Resolution
|
||||
</span>
|
||||
<span className="font-mono text-lg font-bold text-base-fg">
|
||||
<span className="font-mono text-lg font-semibold tabular-nums text-base-fg">
|
||||
{imageDimensions.width} × {imageDimensions.height}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -199,7 +199,7 @@ export const ImageWatermarkRemover = () => {
|
|||
<span className="font-medium text-base-fg/70">
|
||||
Aspect Ratio
|
||||
</span>
|
||||
<span className="font-mono text-lg font-bold text-base-fg">
|
||||
<span className="font-mono text-lg font-semibold tabular-nums text-base-fg">
|
||||
{(
|
||||
imageDimensions.width / imageDimensions.height
|
||||
).toFixed(2)}
|
||||
|
|
|
|||
|
|
@ -543,12 +543,12 @@ export const RemoveBackground = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-ui-panel-gradient flex h-[calc(100vh-56px)] w-full overflow-hidden bg-ui-panel text-base-fg">
|
||||
<div className="flex h-[calc(100vh-56px)] w-full overflow-hidden bg-ui-panel text-base-fg">
|
||||
<div className="flex flex-1 items-center justify-center overflow-y-auto p-16">
|
||||
<main className="flex h-full w-full flex-col items-center justify-center">
|
||||
{showUploadScreen ? (
|
||||
<div className="w-full max-w-5xl">
|
||||
<div className="relative aspect-video overflow-hidden rounded-2xl border border-ui-panel-border bg-ui-background shadow-lg">
|
||||
<div className="relative aspect-video overflow-hidden border border-ui-panel-border bg-ui-background">
|
||||
<UploadEntryCard
|
||||
icon={WandSparklesIcon}
|
||||
title="Remove Background"
|
||||
|
|
@ -564,7 +564,7 @@ export const RemoveBackground = () => {
|
|||
disabled={isLoadingImage}
|
||||
/>
|
||||
{isLoadingImage && (
|
||||
<div className="bg-ui-panel/80 absolute inset-0 flex items-center justify-center backdrop-blur-sm">
|
||||
<div className="bg-ui-panel/80 absolute inset-0 flex items-center justify-center">
|
||||
<LoadingSpinner className="h-12 w-12" />
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -580,9 +580,9 @@ export const RemoveBackground = () => {
|
|||
onMouseDown={handleCompareMouseDown}
|
||||
disabled={!activeImage || isProcessing}
|
||||
className={twMerge(
|
||||
"border-ui-controls-border select-none border-2 px-6 py-2.5 text-sm font-semibold transition-all",
|
||||
"border-ui-controls-border select-none border px-6 py-2.5 text-sm font-semibold transition-all",
|
||||
isHoldingCompare
|
||||
? "border-primary bg-primary/20"
|
||||
? "border-white bg-white/10"
|
||||
: "border-ui-controls-border",
|
||||
(!activeImage || isProcessing) &&
|
||||
"cursor-not-allowed opacity-50",
|
||||
|
|
@ -598,7 +598,7 @@ export const RemoveBackground = () => {
|
|||
onClick={handleDownload}
|
||||
disabled={!activeImage || isProcessing}
|
||||
className={twMerge(
|
||||
"select-none border-2 border-primary px-6 py-2.5 text-sm font-semibold transition-all",
|
||||
"select-none px-6 py-2.5 text-sm font-semibold transition-all",
|
||||
(!activeImage || isProcessing) &&
|
||||
"cursor-not-allowed opacity-50",
|
||||
)}
|
||||
|
|
@ -610,11 +610,11 @@ export const RemoveBackground = () => {
|
|||
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<div
|
||||
className="relative overflow-hidden rounded-2xl border border-ui-panel-border shadow-xl"
|
||||
className="relative overflow-hidden border border-ui-panel-border"
|
||||
style={imageContainerStyle}
|
||||
>
|
||||
{isProcessing && (
|
||||
<div className="absolute inset-0 z-20 flex flex-col items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||
<div className="absolute inset-0 z-20 flex flex-col items-center justify-center bg-black/70">
|
||||
{currentOriginalUrl && (
|
||||
<img
|
||||
src={currentOriginalUrl}
|
||||
|
|
@ -624,12 +624,12 @@ export const RemoveBackground = () => {
|
|||
)}
|
||||
<div className="relative z-10 flex flex-col items-center gap-4">
|
||||
<div className="relative">
|
||||
<div className="h-16 w-16 animate-spin rounded-full border-4 border-primary-500/30 border-t-primary-500" />
|
||||
<div className="h-16 w-16 animate-spin rounded-full border-2 border-white/20 border-t-white" />
|
||||
<WandSparklesIcon
|
||||
|
||||
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-2xl text-primary-400" />
|
||||
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-2xl text-white" />
|
||||
</div>
|
||||
<span className="text-lg font-semibold text-white">
|
||||
<span className="font-mono text-sm font-semibold uppercase tracking-[0.12em] text-white">
|
||||
Removing Background...
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -679,7 +679,7 @@ export const RemoveBackground = () => {
|
|||
|
||||
<div
|
||||
ref={progressBarRef}
|
||||
className="absolute bottom-0 top-0 w-1 bg-primary-500 shadow-lg shadow-primary-500/50"
|
||||
className="absolute bottom-0 top-0 w-1 bg-white"
|
||||
style={{
|
||||
left: `${revealProgressRef.current}%`,
|
||||
transform: "translateX(-50%)",
|
||||
|
|
@ -695,7 +695,7 @@ export const RemoveBackground = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto flex shrink-0 items-center gap-3 rounded-xl border border-ui-panel-border bg-ui-background p-2">
|
||||
<div className="mt-auto flex shrink-0 items-center gap-3 border border-ui-panel-border bg-ui-background p-2">
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
|
|
@ -711,7 +711,7 @@ export const RemoveBackground = () => {
|
|||
position="top"
|
||||
showIconsInList
|
||||
buttonClassName={twMerge(
|
||||
"h-14 w-14 border-2 border-dashed border-ui-panel-border bg-ui-controls/50",
|
||||
"h-14 w-14 rounded-[3px] border border-dashed border-white/20 bg-white/[0.015] hover:border-white/40 hover:bg-white/5",
|
||||
isProcessing && "cursor-not-allowed opacity-50",
|
||||
)}
|
||||
triggerIcon={
|
||||
|
|
@ -724,10 +724,10 @@ export const RemoveBackground = () => {
|
|||
key={img.id}
|
||||
onClick={() => handleThumbnailClick(img)}
|
||||
className={twMerge(
|
||||
"relative h-14 w-14 overflow-hidden rounded-lg border-2 transition-all",
|
||||
"relative h-14 w-14 overflow-hidden rounded-[3px] border transition-all",
|
||||
img.id === activeImageId
|
||||
? "border-primary ring-2 ring-primary/30"
|
||||
: "border-transparent hover:border-primary/50",
|
||||
? "border-white"
|
||||
: "border-white/15 hover:border-white/30",
|
||||
)}
|
||||
>
|
||||
<img
|
||||
|
|
|
|||
|
|
@ -40,11 +40,11 @@ const formatTime = (s: number): string => {
|
|||
const EmptyState = ({ onAdd }: { onAdd: () => void }) => (
|
||||
<div className="flex h-[calc(100vh-56px)] w-full items-center justify-center bg-ui-background">
|
||||
<div className="flex flex-col items-center gap-4 text-center">
|
||||
<div className="flex h-20 w-20 items-center justify-center rounded-2xl bg-white/5">
|
||||
<div className="flex h-20 w-20 items-center justify-center border border-white/15 bg-white/[0.015]">
|
||||
<ImagesIcon className="text-4xl text-base-fg/40" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-lg font-semibold text-base-fg">No shots yet</p>
|
||||
<p className="font-display text-2xl tracking-tight text-base-fg">No shots yet</p>
|
||||
<p className="mt-1 text-sm text-base-fg/50">Add your first shot to get started</p>
|
||||
</div>
|
||||
<Button variant="primary" icon={PlusIcon} onClick={onAdd}>
|
||||
|
|
@ -68,63 +68,63 @@ const MetadataEditor = ({ board, onUpdate, onDelete, onUpload, onSketch }: Metad
|
|||
<aside className="flex w-[280px] shrink-0 flex-col border-r border-ui-panel-border bg-ui-panel">
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="text-xs font-medium uppercase tracking-wider text-base-fg/40">
|
||||
<div className="hud-label text-base-fg/40">
|
||||
Shot {board.shotNumber}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-base-fg/60">Title</label>
|
||||
<label className="mb-1 block font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={board.title}
|
||||
onChange={(e) => onUpdate({ title: e.target.value })}
|
||||
placeholder="Shot title"
|
||||
className="w-full rounded-md border border-ui-panel-border bg-ui-background px-3 py-2 text-sm text-base-fg placeholder:text-base-fg/30 focus:border-primary focus:outline-none"
|
||||
className="w-full rounded-[3px] border border-white/15 bg-ui-controls px-3 py-2 text-sm text-base-fg placeholder:text-base-fg/30 focus:border-white/60 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-base-fg/60">Dialogue</label>
|
||||
<label className="mb-1 block font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">Dialogue</label>
|
||||
<textarea
|
||||
value={board.dialogue}
|
||||
onChange={(e) => onUpdate({ dialogue: e.target.value })}
|
||||
placeholder="Character dialogue..."
|
||||
rows={3}
|
||||
className="w-full resize-none rounded-md border border-ui-panel-border bg-ui-background px-3 py-2 text-sm text-base-fg placeholder:text-base-fg/30 focus:border-primary focus:outline-none"
|
||||
className="w-full resize-none rounded-[3px] border border-white/15 bg-ui-controls px-3 py-2 text-sm text-base-fg placeholder:text-base-fg/30 focus:border-white/60 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-base-fg/60">Action</label>
|
||||
<label className="mb-1 block font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">Action</label>
|
||||
<textarea
|
||||
value={board.action}
|
||||
onChange={(e) => onUpdate({ action: e.target.value })}
|
||||
placeholder="On-screen action..."
|
||||
rows={3}
|
||||
className="w-full resize-none rounded-md border border-ui-panel-border bg-ui-background px-3 py-2 text-sm text-base-fg placeholder:text-base-fg/30 focus:border-primary focus:outline-none"
|
||||
className="w-full resize-none rounded-[3px] border border-white/15 bg-ui-controls px-3 py-2 text-sm text-base-fg placeholder:text-base-fg/30 focus:border-white/60 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-base-fg/60">Notes</label>
|
||||
<label className="mb-1 block font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">Notes</label>
|
||||
<textarea
|
||||
value={board.notes}
|
||||
onChange={(e) => onUpdate({ notes: e.target.value })}
|
||||
placeholder="Director notes..."
|
||||
rows={3}
|
||||
className="w-full resize-none rounded-md border border-ui-panel-border bg-ui-background px-3 py-2 text-sm text-base-fg placeholder:text-base-fg/30 focus:border-primary focus:outline-none"
|
||||
className="w-full resize-none rounded-[3px] border border-white/15 bg-ui-controls px-3 py-2 text-sm text-base-fg placeholder:text-base-fg/30 focus:border-white/60 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-base-fg/60">Duration (seconds)</label>
|
||||
<label className="mb-1 block font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">Duration (seconds)</label>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
step={0.5}
|
||||
value={board.duration}
|
||||
onChange={(e) => onUpdate({ duration: parseFloat(e.target.value) || 0 })}
|
||||
className="w-full rounded-md border border-ui-panel-border bg-ui-background px-3 py-2 text-sm text-base-fg focus:border-primary focus:outline-none"
|
||||
className="w-full rounded-[3px] border border-white/15 bg-ui-controls px-3 py-2 font-mono text-sm tabular-nums text-base-fg focus:border-white/60 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -170,7 +170,7 @@ const MainPreview = ({
|
|||
style={{ pointerEvents: "none" }}
|
||||
/>
|
||||
{board.title && (
|
||||
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 rounded-lg bg-black/60 px-4 py-2 backdrop-blur-sm">
|
||||
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 border border-white/15 bg-black/70 px-4 py-2">
|
||||
<p className="text-sm font-medium text-white">{board.title}</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -178,7 +178,7 @@ const MainPreview = ({
|
|||
) : (
|
||||
<button
|
||||
onClick={onUploadClick}
|
||||
className="flex flex-col items-center gap-3 rounded-xl border-2 border-dashed border-ui-panel-border p-12 text-base-fg/40 transition-colors hover:border-primary/50 hover:text-base-fg/70"
|
||||
className="flex flex-col items-center gap-3 border border-dashed border-white/20 bg-white/[0.015] p-12 text-base-fg/40 transition-colors hover:border-white/40 hover:bg-white/5 hover:text-base-fg/70"
|
||||
>
|
||||
<UploadIcon className="text-3xl" />
|
||||
<span className="text-sm">Click to upload an image</span>
|
||||
|
|
@ -332,9 +332,9 @@ const SketchCanvas = ({ initialImageDataUrl, onSave, onExit }: SketchCanvasProps
|
|||
onClick={() => setTool(t)}
|
||||
title={label}
|
||||
className={twMerge(
|
||||
"flex h-8 w-8 items-center justify-center rounded-md transition-colors",
|
||||
"flex h-8 w-8 items-center justify-center rounded-[3px] transition-colors",
|
||||
tool === t
|
||||
? "bg-primary text-white"
|
||||
? "bg-white text-black"
|
||||
: "text-base-fg/60 hover:bg-white/10 hover:text-base-fg",
|
||||
)}
|
||||
>
|
||||
|
|
@ -352,7 +352,7 @@ const SketchCanvas = ({ initialImageDataUrl, onSave, onExit }: SketchCanvasProps
|
|||
<div className="mx-1 h-5 w-px bg-ui-panel-border" />
|
||||
|
||||
{/* Color picker */}
|
||||
<label className="relative flex h-8 w-8 cursor-pointer items-center justify-center rounded-md border border-ui-panel-border" title="Stroke color">
|
||||
<label className="relative flex h-8 w-8 cursor-pointer items-center justify-center rounded-[3px] border border-white/15 hover:border-white/30" title="Stroke color">
|
||||
<span
|
||||
className="h-4 w-4 rounded-sm border border-white/20"
|
||||
style={{ background: color }}
|
||||
|
|
@ -367,16 +367,16 @@ const SketchCanvas = ({ initialImageDataUrl, onSave, onExit }: SketchCanvasProps
|
|||
|
||||
{/* Brush size */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[10px] text-base-fg/40">Size</span>
|
||||
<span className="font-mono text-[10px] uppercase tracking-[0.12em] text-base-fg/40">Size</span>
|
||||
<input
|
||||
type="range"
|
||||
min={1}
|
||||
max={32}
|
||||
value={brushSize}
|
||||
onChange={(e) => setBrushSize(Number(e.target.value))}
|
||||
className="w-20 accent-primary"
|
||||
className="w-20 accent-white"
|
||||
/>
|
||||
<span className="w-5 text-center text-[10px] text-base-fg/50">{brushSize}</span>
|
||||
<span className="w-5 text-center font-mono text-[10px] tabular-nums text-base-fg/50">{brushSize}</span>
|
||||
</div>
|
||||
|
||||
<div className="mx-1 h-5 w-px bg-ui-panel-border" />
|
||||
|
|
@ -385,7 +385,7 @@ const SketchCanvas = ({ initialImageDataUrl, onSave, onExit }: SketchCanvasProps
|
|||
onClick={handleUndo}
|
||||
disabled={undoStack.length === 0}
|
||||
title="Undo"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-md text-base-fg/60 transition-colors hover:bg-white/10 hover:text-base-fg disabled:opacity-30"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-[3px] text-base-fg/60 transition-colors hover:bg-white/10 hover:text-base-fg disabled:opacity-30"
|
||||
>
|
||||
<RotateCcwIcon className="text-sm" />
|
||||
</button>
|
||||
|
|
@ -393,7 +393,7 @@ const SketchCanvas = ({ initialImageDataUrl, onSave, onExit }: SketchCanvasProps
|
|||
<button
|
||||
onClick={handleClear}
|
||||
title="Clear canvas"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-md text-base-fg/60 transition-colors hover:bg-white/10 hover:text-red-400"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-[3px] text-base-fg/60 transition-colors hover:bg-white/10 hover:text-red-400"
|
||||
>
|
||||
<XIcon className="text-sm" />
|
||||
</button>
|
||||
|
|
@ -401,7 +401,7 @@ const SketchCanvas = ({ initialImageDataUrl, onSave, onExit }: SketchCanvasProps
|
|||
<button
|
||||
onClick={onExit}
|
||||
title="Done sketching"
|
||||
className="ml-auto flex h-8 items-center gap-1.5 rounded-md bg-primary px-3 text-xs font-medium text-white transition-colors hover:bg-primary-400"
|
||||
className="ml-auto flex h-8 items-center gap-1.5 rounded-[3px] bg-white px-3 text-xs font-medium text-black transition-colors hover:bg-white/85"
|
||||
>
|
||||
<CheckIcon className="text-xs" />
|
||||
Done
|
||||
|
|
@ -421,7 +421,7 @@ const SketchCanvas = ({ initialImageDataUrl, onSave, onExit }: SketchCanvasProps
|
|||
touchAction: "none",
|
||||
display: "block",
|
||||
}}
|
||||
className="rounded shadow-xl"
|
||||
className="border border-white/15"
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
|
|
@ -467,11 +467,11 @@ const ThumbnailItem = ({
|
|||
onDragEnd={onDragEnd}
|
||||
onClick={onSelect}
|
||||
className={twMerge(
|
||||
"relative flex h-[60px] w-[100px] shrink-0 cursor-pointer overflow-hidden rounded-md border-2 transition-all select-none",
|
||||
"relative flex h-[60px] w-[100px] shrink-0 cursor-pointer overflow-hidden rounded-[3px] border transition-all select-none",
|
||||
isSelected
|
||||
? "border-primary ring-2 ring-primary/30"
|
||||
: "border-transparent hover:border-white/20",
|
||||
isDragOver && "border-primary/60 opacity-60",
|
||||
? "border-white"
|
||||
: "border-white/15 hover:border-white/30",
|
||||
isDragOver && "border-white/60 opacity-60",
|
||||
)}
|
||||
>
|
||||
{board.imageDataUrl ? (
|
||||
|
|
@ -482,7 +482,7 @@ const ThumbnailItem = ({
|
|||
</div>
|
||||
)}
|
||||
<div className="absolute bottom-0 left-0 right-0 bg-black/60 px-1.5 py-0.5">
|
||||
<span className="text-[10px] text-white/70">{board.shotNumber}</span>
|
||||
<span className="font-mono text-[10px] tabular-nums text-white/85">{board.shotNumber}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -515,7 +515,7 @@ const Filmstrip = ({
|
|||
<div className="flex h-[80px] shrink-0 items-center gap-2 overflow-x-auto border-t border-ui-panel-border bg-ui-panel px-3">
|
||||
<button
|
||||
onClick={onAdd}
|
||||
className="flex h-[60px] w-[60px] shrink-0 items-center justify-center rounded-md border-2 border-dashed border-ui-panel-border bg-transparent text-base-fg/40 transition-colors hover:border-primary/50 hover:text-primary"
|
||||
className="flex h-[60px] w-[60px] shrink-0 items-center justify-center rounded-[3px] border border-dashed border-white/20 bg-white/[0.015] text-base-fg/40 transition-colors hover:border-white/40 hover:bg-white/5 hover:text-base-fg"
|
||||
title="Add shot"
|
||||
>
|
||||
<PlusIcon />
|
||||
|
|
@ -705,12 +705,12 @@ const Timeline = ({
|
|||
const playheadLeft = currentTimeSeconds * PIXELS_PER_SECOND;
|
||||
|
||||
return (
|
||||
<div className="shrink-0 border-t border-ui-panel-border bg-[#1a1a1a]">
|
||||
<div className="shrink-0 border-t border-ui-panel-border bg-ui-panel">
|
||||
{/* Header row */}
|
||||
<div className="flex items-center justify-between border-b border-ui-panel-border/40 px-3 py-1">
|
||||
<span className="font-mono text-xs text-base-fg/50">{formatTime(currentTimeSeconds)}</span>
|
||||
<span className="text-xs text-base-fg/25">Timeline</span>
|
||||
<span className="font-mono text-xs text-base-fg/50">{formatTime(totalDuration)}</span>
|
||||
<span className="font-mono text-xs tabular-nums text-base-fg/50">{formatTime(currentTimeSeconds)}</span>
|
||||
<span className="hud-label text-base-fg/25">Timeline</span>
|
||||
<span className="font-mono text-xs tabular-nums text-base-fg/50">{formatTime(totalDuration)}</span>
|
||||
</div>
|
||||
|
||||
{/* Lane */}
|
||||
|
|
@ -729,7 +729,7 @@ const Timeline = ({
|
|||
style={{ left: i * PIXELS_PER_SECOND }}
|
||||
>
|
||||
<div className="h-2 w-px bg-white/10" />
|
||||
<span className="text-[9px] text-base-fg/25">{i}s</span>
|
||||
<span className="font-mono text-[9px] tabular-nums text-base-fg/25">{i}s</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
|
@ -743,9 +743,9 @@ const Timeline = ({
|
|||
<div
|
||||
key={board.id}
|
||||
className={twMerge(
|
||||
"absolute top-6 bottom-1 flex items-stretch overflow-hidden rounded border transition-colors",
|
||||
"absolute top-6 bottom-1 flex items-stretch overflow-hidden border transition-colors",
|
||||
isSelected
|
||||
? "border-primary bg-primary/25"
|
||||
? "border-white bg-white/15"
|
||||
: "border-white/15 bg-white/5 hover:bg-white/10",
|
||||
)}
|
||||
style={{
|
||||
|
|
@ -776,13 +776,13 @@ const Timeline = ({
|
|||
"{board.dialogue}"
|
||||
</span>
|
||||
)}
|
||||
<span className="text-[9px] text-base-fg/30">{board.duration.toFixed(1)}s</span>
|
||||
<span className="font-mono text-[9px] tabular-nums text-base-fg/30">{board.duration.toFixed(1)}s</span>
|
||||
</div>
|
||||
|
||||
{/* Resize handle */}
|
||||
<div
|
||||
data-resize="true"
|
||||
className="absolute right-0 top-0 bottom-0 w-2 rounded-r opacity-0 transition-opacity hover:bg-white/30 hover:opacity-100"
|
||||
className="absolute right-0 top-0 bottom-0 w-2 opacity-0 transition-opacity hover:bg-white/30 hover:opacity-100"
|
||||
style={{ cursor: "ew-resize" }}
|
||||
onPointerDown={(e) => handleResizePointerDown(e, board.id, board.duration)}
|
||||
title="Drag to resize duration"
|
||||
|
|
@ -804,10 +804,10 @@ const Timeline = ({
|
|||
|
||||
{/* Playhead */}
|
||||
<div
|
||||
className="pointer-events-none absolute top-0 bottom-0 z-20 w-px bg-primary"
|
||||
className="pointer-events-none absolute top-0 bottom-0 z-20 w-px bg-white"
|
||||
style={{ left: playheadLeft }}
|
||||
>
|
||||
<div className="absolute -left-[4px] top-0 h-0 w-0 border-x-[4px] border-t-[7px] border-x-transparent border-t-primary" />
|
||||
<div className="absolute -left-[4px] top-0 h-0 w-0 border-x-[4px] border-t-[7px] border-x-transparent border-t-white" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -845,14 +845,14 @@ const PlaybackControls = ({
|
|||
onNextScene,
|
||||
}: PlaybackControlsProps) => {
|
||||
const transportBtnClass =
|
||||
"flex h-9 w-9 items-center justify-center rounded-md text-base-fg/70 transition-colors hover:bg-white/10 hover:text-base-fg active:scale-95";
|
||||
"flex h-9 w-9 items-center justify-center rounded-[3px] text-base-fg/70 transition-colors hover:bg-white/10 hover:text-base-fg active:scale-95";
|
||||
|
||||
return (
|
||||
<div className="flex h-[52px] shrink-0 items-center border-t border-ui-panel-border bg-[#111] px-4">
|
||||
<div className="flex h-[52px] shrink-0 items-center border-t border-ui-panel-border bg-ui-background px-4">
|
||||
{/* Left stats */}
|
||||
<div className="flex w-32 flex-col">
|
||||
<span className="font-mono text-xs text-base-fg/60">{formatTime(currentTimeSeconds)}</span>
|
||||
<span className="text-[10px] text-base-fg/30">
|
||||
<span className="font-mono text-xs tabular-nums text-base-fg/60">{formatTime(currentTimeSeconds)}</span>
|
||||
<span className="font-mono text-[10px] uppercase tracking-[0.12em] text-base-fg/30">
|
||||
{currentBoardNumber !== null ? `Shot ${currentBoardNumber}` : "—"}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -876,7 +876,7 @@ const PlaybackControls = ({
|
|||
|
||||
{/* Play / Pause */}
|
||||
<button
|
||||
className="mx-1 flex h-10 w-10 items-center justify-center rounded-full bg-primary text-white transition-all hover:bg-primary-400 active:scale-95"
|
||||
className="mx-1 flex h-10 w-10 items-center justify-center rounded-[3px] bg-white text-black transition-all hover:bg-white/85 active:scale-95"
|
||||
onClick={isPlaying ? onPause : onPlay}
|
||||
title={isPlaying ? "Pause" : "Play"}
|
||||
>
|
||||
|
|
@ -901,8 +901,8 @@ const PlaybackControls = ({
|
|||
|
||||
{/* Right stats */}
|
||||
<div className="flex w-32 flex-col items-end">
|
||||
<span className="font-mono text-xs text-base-fg/60">{formatTime(totalDuration)}</span>
|
||||
<span className="text-[10px] text-base-fg/30">{totalBoards} shot{totalBoards !== 1 ? "s" : ""}</span>
|
||||
<span className="font-mono text-xs tabular-nums text-base-fg/60">{formatTime(totalDuration)}</span>
|
||||
<span className="font-mono text-[10px] uppercase tracking-[0.12em] text-base-fg/30">{totalBoards} shot{totalBoards !== 1 ? "s" : ""}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
.video-background-image {
|
||||
background-image:
|
||||
linear-gradient(
|
||||
0deg,
|
||||
rgb(var(--st-photo-tint-rgb) / var(--st-photo-tint-alpha)),
|
||||
rgb(var(--st-photo-tint-rgb) / var(--st-photo-tint-alpha))
|
||||
),
|
||||
url("/resources/images/forest-trees.webp");
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
filter: grayscale(var(--st-photo-grayscale));
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import "./Background.css";
|
||||
|
||||
export default function GalleryBackground() {
|
||||
return (
|
||||
<>
|
||||
<div className="pointer-events-none fixed inset-0 z-[1] overflow-hidden bg-[radial-gradient(50%_50%_at_50%_50%,_transparent_49%,_rgb(var(--st-controls-rgb)_/_var(--st-gallery-vignette-alpha))_100%)]" />
|
||||
<div className="fixed inset-0 z-0 overflow-hidden">
|
||||
<div
|
||||
className={`video-background-image h-full w-full opacity-30 transition-opacity duration-1000`}
|
||||
>
|
||||
<div className="absolute inset-0 grid grid-cols-5 gap-6 px-6 opacity-35" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -10,7 +10,6 @@ import {
|
|||
UploadAudioMedia,
|
||||
FilterMediaClasses,
|
||||
} from "@storyteller/api";
|
||||
import BackgroundGallery from "./BackgroundGallery";
|
||||
import {
|
||||
ClassyModelSelector,
|
||||
useImageToVideoPageModelList,
|
||||
|
|
@ -186,7 +185,6 @@ const ImageToVideo = ({ imageMediaId, imageUrl }: ImageToVideoProps) => {
|
|||
hasContent={hasContent}
|
||||
emptyStateTitle="Create Video"
|
||||
emptyStateSubtitle="Choose an image, add a prompt, then generate"
|
||||
background={<BackgroundGallery />}
|
||||
bottomOffset={promptHeight + 40}
|
||||
listContent={
|
||||
<DesktopGenerationGallery
|
||||
|
|
|
|||
|
|
@ -457,7 +457,7 @@ export const VideoFrameExtractor = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-ui-panel-gradient flex h-[calc(100vh-56px)] w-full overflow-hidden bg-ui-panel text-base-fg">
|
||||
<div className="flex h-[calc(100vh-56px)] w-full overflow-hidden bg-ui-background text-base-fg">
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<main
|
||||
className={
|
||||
|
|
@ -468,7 +468,7 @@ export const VideoFrameExtractor = () => {
|
|||
>
|
||||
{!videoUrl ? (
|
||||
<div className="w-full max-w-5xl">
|
||||
<div className="aspect-video overflow-hidden rounded-2xl border border-ui-panel-border bg-ui-background shadow-lg">
|
||||
<div className="aspect-video overflow-hidden border border-ui-panel-border bg-ui-panel">
|
||||
<UploadEntryCard
|
||||
icon={ImagesIcon}
|
||||
title="Extract Video Frames"
|
||||
|
|
@ -486,7 +486,7 @@ export const VideoFrameExtractor = () => {
|
|||
</div>
|
||||
) : (
|
||||
<div className="flex w-full max-w-5xl flex-col gap-5">
|
||||
<div className="w-full overflow-hidden rounded-xl border border-ui-panel-border bg-ui-controls/40">
|
||||
<div className="w-full overflow-hidden border border-ui-panel-border bg-ui-panel">
|
||||
<div className="relative aspect-video w-full bg-black">
|
||||
<Button
|
||||
icon={RotateCwIcon}
|
||||
|
|
@ -500,7 +500,7 @@ export const VideoFrameExtractor = () => {
|
|||
setSavedFrames(new Set());
|
||||
setConvertingFrames(new Set());
|
||||
}}
|
||||
className="absolute right-3 top-3 z-10 border-2 border-red/50 px-3 py-1.5 text-sm hover:border-red/80 hover:bg-red/80"
|
||||
className="absolute right-3 top-3 z-10 border border-red/50 px-3 py-1.5 text-sm hover:border-red/80 hover:bg-red/80"
|
||||
>
|
||||
Switch Video
|
||||
</Button>
|
||||
|
|
@ -516,17 +516,17 @@ export const VideoFrameExtractor = () => {
|
|||
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 to-transparent p-4">
|
||||
<div
|
||||
ref={progressBarRef}
|
||||
className="group relative mb-3 h-3 cursor-pointer rounded-full bg-white/20"
|
||||
className="group relative mb-3 h-3 cursor-pointer bg-white/15"
|
||||
onClick={handleProgressBarClick}
|
||||
>
|
||||
<div
|
||||
className="absolute h-full overflow-hidden rounded-full bg-primary"
|
||||
className="absolute h-full overflow-hidden bg-white"
|
||||
style={{
|
||||
width: `${duration ? (currentTime / duration) * 100 : 0}%`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="absolute top-1/2 h-6 w-1.5 -translate-y-1/2 cursor-ew-resize rounded-full bg-yellow-400 shadow-lg"
|
||||
className="absolute top-1/2 h-6 w-1.5 -translate-y-1/2 cursor-ew-resize bg-yellow-400"
|
||||
style={{
|
||||
left: `${duration ? (startTime / duration) * 100 : 0}%`,
|
||||
}}
|
||||
|
|
@ -570,7 +570,7 @@ export const VideoFrameExtractor = () => {
|
|||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => seekToFrame("first")}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full bg-white/10 hover:bg-white/20"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-[3px] bg-white/10 transition-colors hover:bg-white/20"
|
||||
title="First Frame"
|
||||
>
|
||||
<SkipBackIcon
|
||||
|
|
@ -579,7 +579,7 @@ export const VideoFrameExtractor = () => {
|
|||
</button>
|
||||
<button
|
||||
onClick={togglePlayPause}
|
||||
className="flex h-9 w-9 items-center justify-center rounded-full bg-white/20 hover:bg-white/30"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-[3px] bg-white/20 transition-colors hover:bg-white/30"
|
||||
>
|
||||
<DynamicIcon
|
||||
icon={isPlaying ? PauseIcon : PlayIcon}
|
||||
|
|
@ -588,7 +588,7 @@ export const VideoFrameExtractor = () => {
|
|||
</button>
|
||||
<button
|
||||
onClick={() => seekToFrame("last")}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full bg-white/10 hover:bg-white/20"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-[3px] bg-white/10 transition-colors hover:bg-white/20"
|
||||
title="Last Frame"
|
||||
>
|
||||
<SkipForwardIcon
|
||||
|
|
@ -598,7 +598,7 @@ export const VideoFrameExtractor = () => {
|
|||
<div className="mx-1 h-6 w-px bg-white/20" />
|
||||
<button
|
||||
onClick={toggleMute}
|
||||
className="flex h-7 w-7 items-center justify-center rounded-full bg-white/10 hover:bg-white/20"
|
||||
className="flex h-7 w-7 items-center justify-center rounded-[3px] bg-white/10 transition-colors hover:bg-white/20"
|
||||
>
|
||||
<DynamicIcon
|
||||
icon={isMuted ? VolumeXIcon : Volume2Icon}
|
||||
|
|
@ -617,7 +617,7 @@ export const VideoFrameExtractor = () => {
|
|||
className="w-20 accent-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="font-mono text-sm text-white">
|
||||
<div className="font-mono text-sm tabular-nums text-white">
|
||||
{formatTime(currentTime)} / {formatTime(duration)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -640,24 +640,24 @@ export const VideoFrameExtractor = () => {
|
|||
|
||||
<div className="grid w-full grid-cols-1 gap-5 md:grid-cols-2">
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="group rounded-2xl border border-ui-panel-border bg-ui-background p-6 shadow-lg">
|
||||
<div className="group border border-ui-panel-border bg-ui-panel p-6">
|
||||
<div>
|
||||
<div className="mb-2 text-xs font-semibold uppercase tracking-wider text-base-fg/60">
|
||||
<div className="mb-2 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">
|
||||
Start Time
|
||||
</div>
|
||||
<div className="font-mono text-3xl font-bold text-primary drop-shadow-sm">
|
||||
<div className="font-mono text-3xl font-semibold tabular-nums text-ui-accent-ink">
|
||||
{formatTimePrecise(startTime)}
|
||||
</div>
|
||||
<div className="mt-3 flex items-center gap-2 text-xs text-base-fg/60">
|
||||
<div className="h-2.5 w-2.5 animate-pulse rounded-full bg-yellow-400 shadow-lg shadow-yellow-400/50" />
|
||||
<div className="h-2.5 w-2.5 animate-pulse rounded-full bg-yellow-400" />
|
||||
<span>Adjust via the yellow marker on the video</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="group rounded-2xl border border-ui-panel-border bg-ui-background p-6 shadow-lg">
|
||||
<div className="group border border-ui-panel-border bg-ui-panel p-6">
|
||||
<div>
|
||||
<div className="mb-4 text-xs font-semibold uppercase tracking-wider text-base-fg/60">
|
||||
<div className="mb-4 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">
|
||||
Video Information
|
||||
</div>
|
||||
<div className="space-y-3 text-sm">
|
||||
|
|
@ -665,7 +665,7 @@ export const VideoFrameExtractor = () => {
|
|||
<span className="font-medium text-base-fg/70">
|
||||
Duration
|
||||
</span>
|
||||
<span className="font-mono text-lg font-bold text-base-fg">
|
||||
<span className="font-mono text-lg font-semibold tabular-nums text-base-fg">
|
||||
{formatTime(duration)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -674,7 +674,7 @@ export const VideoFrameExtractor = () => {
|
|||
<span className="font-medium text-base-fg/70">
|
||||
Resolution
|
||||
</span>
|
||||
<span className="font-mono font-bold text-base-fg">
|
||||
<span className="font-mono font-semibold tabular-nums text-base-fg">
|
||||
{videoRef.current.videoWidth} ×{" "}
|
||||
{videoRef.current.videoHeight}
|
||||
</span>
|
||||
|
|
@ -685,12 +685,12 @@ export const VideoFrameExtractor = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-ui-panel-border bg-ui-background p-6 shadow-lg">
|
||||
<div className="border border-ui-panel-border bg-ui-panel p-6">
|
||||
<div>
|
||||
<h3 className="mb-5 flex items-center gap-2 text-sm font-semibold uppercase tracking-wider text-base-fg/60">
|
||||
<h3 className="mb-5 flex items-center gap-2 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">
|
||||
<SparklesIcon
|
||||
|
||||
className="text-primary" />
|
||||
className="text-ui-accent-ink" />
|
||||
Extraction Settings
|
||||
</h3>
|
||||
|
||||
|
|
@ -698,7 +698,7 @@ export const VideoFrameExtractor = () => {
|
|||
<div>
|
||||
<label
|
||||
htmlFor="numFrames"
|
||||
className="mb-2.5 block text-sm font-semibold text-base-fg"
|
||||
className="mb-1.5 block text-xs font-medium text-base-fg/60"
|
||||
>
|
||||
Number of Frames
|
||||
</label>
|
||||
|
|
@ -716,14 +716,14 @@ export const VideoFrameExtractor = () => {
|
|||
),
|
||||
)
|
||||
}
|
||||
className="border-ui-controls-border w-full rounded-xl border-2 bg-ui-controls px-4 py-3.5 text-base-fg focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
className="w-full rounded-[3px] border border-white/15 bg-ui-controls px-3 py-2 text-sm tabular-nums text-base-fg focus:border-white/60 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="frameDistance"
|
||||
className="mb-2.5 block text-sm font-semibold text-base-fg"
|
||||
className="mb-1.5 block text-xs font-medium text-base-fg/60"
|
||||
>
|
||||
Distance (ms)
|
||||
</label>
|
||||
|
|
@ -738,11 +738,11 @@ export const VideoFrameExtractor = () => {
|
|||
Math.max(1, parseInt(e.target.value) || 1),
|
||||
)
|
||||
}
|
||||
className="border-ui-controls-border w-full rounded-xl border-2 bg-ui-controls px-4 py-3.5 text-base-fg focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
className="w-full rounded-[3px] border border-white/15 bg-ui-controls px-3 py-2 text-sm tabular-nums text-base-fg focus:border-white/60 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="text-xs leading-relaxed text-base-fg/80">
|
||||
<div className="text-xs leading-relaxed text-base-fg/60">
|
||||
Extracting multiple frames helps capture a sharp,
|
||||
non-blurred frame.
|
||||
</div>
|
||||
|
|
@ -754,26 +754,26 @@ export const VideoFrameExtractor = () => {
|
|||
{extractedFrames.length > 0 && (
|
||||
<div
|
||||
ref={extractedFramesRef}
|
||||
className="rounded-2xl border border-ui-panel-border bg-ui-controls p-6 shadow-lg"
|
||||
className="border border-ui-panel-border bg-ui-panel p-6"
|
||||
>
|
||||
<div>
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<h3 className="flex items-center gap-2 text-xl font-bold uppercase tracking-wider text-base-fg">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h3 className="flex items-center gap-2 text-sm font-semibold text-base-fg">
|
||||
<ImagesIcon
|
||||
|
||||
className="text-primary" />
|
||||
className="text-ui-accent-ink" />
|
||||
Extracted Frames
|
||||
</h3>
|
||||
<div className="bg-ui-badge rounded-full border-2 border-primary/30 px-5 py-2 text-sm font-bold text-base-fg shadow-lg">
|
||||
<div className="border border-white/20 px-1.5 py-0.5 font-mono text-[9px] font-semibold uppercase leading-none tracking-[0.12em] text-base-fg/70">
|
||||
{extractedFrames.length}{" "}
|
||||
{extractedFrames.length === 1 ? "Frame" : "Frames"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-5">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{extractedFrames.map((frame, index) => (
|
||||
<div
|
||||
key={frame.id}
|
||||
className="group relative overflow-hidden rounded-xl border-2 border-ui-panel-border bg-ui-controls"
|
||||
className="group relative overflow-hidden border border-ui-panel-border bg-ui-background transition-colors hover:border-white/30"
|
||||
>
|
||||
<div className="aspect-video overflow-hidden bg-black">
|
||||
<img
|
||||
|
|
@ -782,12 +782,12 @@ export const VideoFrameExtractor = () => {
|
|||
className="h-full w-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2 bg-ui-background p-3">
|
||||
<div className="space-y-2 border-t border-ui-panel-border/60 bg-ui-background p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="bg-ui-badge rounded border border-ui-panel-border px-2 py-1 text-xs font-bold text-base-fg">
|
||||
<span className="border border-white/20 px-1.5 py-0.5 font-mono text-[9px] font-semibold uppercase leading-none tracking-[0.12em] text-base-fg/70">
|
||||
#{index + 1}
|
||||
</span>
|
||||
<div className="font-mono text-xs font-semibold text-base-fg/70">
|
||||
<div className="font-mono text-xs tabular-nums text-base-fg/70">
|
||||
{formatTimePrecise(frame.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ export const VideoWatermarkRemover = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-ui-panel-gradient flex h-[calc(100vh-56px)] w-full overflow-hidden bg-ui-panel text-base-fg">
|
||||
<div className="flex h-[calc(100vh-56px)] w-full overflow-hidden bg-ui-background text-base-fg">
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<main
|
||||
className={
|
||||
|
|
@ -214,7 +214,7 @@ export const VideoWatermarkRemover = () => {
|
|||
>
|
||||
{!videoUrl ? (
|
||||
<div className="w-full max-w-5xl">
|
||||
<div className="aspect-video overflow-hidden rounded-2xl border border-ui-panel-border bg-ui-background shadow-lg">
|
||||
<div className="aspect-video overflow-hidden border border-ui-panel-border bg-ui-panel">
|
||||
<UploadEntryCard
|
||||
icon={DropletIcon}
|
||||
title="Remove Video Watermark"
|
||||
|
|
@ -232,7 +232,7 @@ export const VideoWatermarkRemover = () => {
|
|||
</div>
|
||||
) : (
|
||||
<div className="flex w-full max-w-5xl flex-col gap-5">
|
||||
<div className="w-full overflow-hidden rounded-2xl border border-ui-panel-border bg-ui-background shadow-lg">
|
||||
<div className="w-full overflow-hidden border border-ui-panel-border bg-ui-panel">
|
||||
<div className="relative aspect-video w-full bg-black">
|
||||
<Button
|
||||
icon={RotateCwIcon}
|
||||
|
|
@ -241,7 +241,7 @@ export const VideoWatermarkRemover = () => {
|
|||
setVideoUrl("");
|
||||
setCurrentTime(0);
|
||||
}}
|
||||
className="absolute right-3 top-3 z-10 border-2 border-red/50 px-3 py-1.5 text-sm hover:border-red/80 hover:bg-red/80"
|
||||
className="absolute right-3 top-3 z-10 border border-red/50 px-3 py-1.5 text-sm hover:border-red/80 hover:bg-red/80"
|
||||
>
|
||||
Switch Video
|
||||
</Button>
|
||||
|
|
@ -256,11 +256,11 @@ export const VideoWatermarkRemover = () => {
|
|||
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 to-transparent p-4">
|
||||
<div
|
||||
ref={progressBarRef}
|
||||
className="group relative mb-3 h-3 cursor-pointer rounded-full bg-white/20"
|
||||
className="group relative mb-3 h-3 cursor-pointer bg-white/15"
|
||||
onClick={handleProgressBarClick}
|
||||
>
|
||||
<div
|
||||
className="absolute h-full overflow-hidden rounded-full bg-primary"
|
||||
className="absolute h-full overflow-hidden bg-white"
|
||||
style={{
|
||||
width: `${duration ? (currentTime / duration) * 100 : 0}%`,
|
||||
}}
|
||||
|
|
@ -270,7 +270,7 @@ export const VideoWatermarkRemover = () => {
|
|||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => seekToFrame("first")}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full bg-white/10 hover:bg-white/20"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-[3px] bg-white/10 transition-colors hover:bg-white/20"
|
||||
title="First Frame"
|
||||
>
|
||||
<SkipBackIcon
|
||||
|
|
@ -279,7 +279,7 @@ export const VideoWatermarkRemover = () => {
|
|||
</button>
|
||||
<button
|
||||
onClick={togglePlayPause}
|
||||
className="flex h-9 w-9 items-center justify-center rounded-full bg-white/20 hover:bg-white/30"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-[3px] bg-white/20 transition-colors hover:bg-white/30"
|
||||
>
|
||||
<DynamicIcon
|
||||
icon={isPlaying ? PauseIcon : PlayIcon}
|
||||
|
|
@ -288,7 +288,7 @@ export const VideoWatermarkRemover = () => {
|
|||
</button>
|
||||
<button
|
||||
onClick={() => seekToFrame("last")}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full bg-white/10 hover:bg-white/20"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-[3px] bg-white/10 transition-colors hover:bg-white/20"
|
||||
title="Last Frame"
|
||||
>
|
||||
<SkipForwardIcon
|
||||
|
|
@ -298,7 +298,7 @@ export const VideoWatermarkRemover = () => {
|
|||
<div className="mx-1 h-6 w-px bg-white/20" />
|
||||
<button
|
||||
onClick={toggleMute}
|
||||
className="flex h-7 w-7 items-center justify-center rounded-full bg-white/10 hover:bg-white/20"
|
||||
className="flex h-7 w-7 items-center justify-center rounded-[3px] bg-white/10 transition-colors hover:bg-white/20"
|
||||
>
|
||||
<DynamicIcon
|
||||
icon={isMuted ? VolumeXIcon : Volume2Icon}
|
||||
|
|
@ -317,7 +317,7 @@ export const VideoWatermarkRemover = () => {
|
|||
className="w-20 accent-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="font-mono text-sm text-white">
|
||||
<div className="font-mono text-sm tabular-nums text-white">
|
||||
{formatTime(currentTime)} / {formatTime(duration)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -338,12 +338,12 @@ export const VideoWatermarkRemover = () => {
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-ui-panel-border bg-ui-background p-6 shadow-lg">
|
||||
<div className="border border-ui-panel-border bg-ui-panel p-6">
|
||||
<div>
|
||||
<div className="mb-4 flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-base-fg/60">
|
||||
<div className="mb-4 flex items-center gap-2 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">
|
||||
<VideoIcon
|
||||
|
||||
className="text-primary" />
|
||||
className="text-ui-accent-ink" />
|
||||
Video Information
|
||||
</div>
|
||||
<div className="space-y-3 text-sm">
|
||||
|
|
@ -351,7 +351,7 @@ export const VideoWatermarkRemover = () => {
|
|||
<span className="font-medium text-base-fg/70">
|
||||
Duration
|
||||
</span>
|
||||
<span className="font-mono text-lg font-bold text-base-fg">
|
||||
<span className="font-mono text-lg font-semibold tabular-nums text-base-fg">
|
||||
{formatTime(duration)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -360,7 +360,7 @@ export const VideoWatermarkRemover = () => {
|
|||
<span className="font-medium text-base-fg/70">
|
||||
Resolution
|
||||
</span>
|
||||
<span className="font-mono font-bold text-base-fg">
|
||||
<span className="font-mono font-semibold tabular-nums text-base-fg">
|
||||
{videoRef.current.videoWidth} ×{" "}
|
||||
{videoRef.current.videoHeight}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export const links: LinksFunction = () => [
|
|||
},
|
||||
{
|
||||
rel: "stylesheet",
|
||||
href: "https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap",
|
||||
href: "https://fonts.googleapis.com/css2?family=Archivo:wdth,wght@62..125,100..900&family=Instrument+Serif:ital@0;1&family=Geist+Mono:wght@300..800&display=swap",
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #363636 #171717;
|
||||
scrollbar-color: #2c2c2f #0b0b0c;
|
||||
}
|
||||
|
||||
/* Works on Chrome, Edge, and Safari */
|
||||
|
|
@ -9,13 +9,12 @@
|
|||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: #171717;
|
||||
background: #0b0b0c;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: #363636;
|
||||
border-radius: 4px;
|
||||
border: 2px solid #171717;
|
||||
background-color: #2c2c2f;
|
||||
border: 2px solid #0b0b0c;
|
||||
}
|
||||
|
||||
/* Light theme: the scrollbar track/thumb must be light, not the dark defaults. */
|
||||
|
|
@ -46,18 +45,37 @@ input[type="number"] {
|
|||
appearance: textfield;
|
||||
}
|
||||
|
||||
/* Display face: Archivo, matching artcraft-webapp. */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: "Outfit", sans-serif;
|
||||
font-family:
|
||||
"Archivo",
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
Roboto,
|
||||
"Helvetica Neue",
|
||||
Arial,
|
||||
sans-serif;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
/* Only display-scale headings run expanded on the wdth axis; compact
|
||||
headings (card titles, section labels) stay at normal width so they
|
||||
don't truncate in tight layouts. */
|
||||
h1,
|
||||
h2 {
|
||||
font-stretch: 118%;
|
||||
font-weight: 620;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family:
|
||||
"Source Sans 3",
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
|
|
@ -70,22 +88,61 @@ body {
|
|||
background-color: var(--st-bg);
|
||||
color-scheme: var(--st-color-scheme, dark);
|
||||
user-select: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: #2d81ff;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid #74aaff;
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
/* Mono HUD micro-label, matching the webapp/marketing site's .hud-label. */
|
||||
.hud-label {
|
||||
font-family:
|
||||
"Geist Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.font-serif-italic {
|
||||
font-family: "Instrument Serif", Georgia, "Times New Roman", serif;
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* Display utility carries the ax-display treatment (Archivo at weight 620,
|
||||
118% width). Plain (unlayered) rule so it wins over font-weight
|
||||
utilities like font-semibold. */
|
||||
.font-display {
|
||||
font-stretch: 118%;
|
||||
font-weight: 620;
|
||||
}
|
||||
|
||||
/* ===================== THEME VARIABLES ===================== */
|
||||
/* The default (the "gray" theme id) is the brutalist system shared with
|
||||
artcraft-webapp: near-black surfaces, white/15 hairline borders. */
|
||||
:root {
|
||||
--st-bg: #121212;
|
||||
/* gray theme default */
|
||||
--st-panel: #1f1f1f;
|
||||
--st-panel-border: #3f3f3f;
|
||||
--st-modal: #1f1f1f;
|
||||
--st-bg: #121316;
|
||||
--st-panel: #101014;
|
||||
--st-panel-border: rgba(255, 255, 255, 0.15);
|
||||
--st-modal: #101014;
|
||||
/* default modal surface (can be gradient per theme) */
|
||||
--st-controls: #3e3e41;
|
||||
--st-controls-rgb: 62 62 65;
|
||||
--st-controls-border: #3f3f3f;
|
||||
--st-fg: #ffffff;
|
||||
--st-fg-rgb: 255 255 255;
|
||||
--st-divider: #515168;
|
||||
--st-controls: #1e1f22;
|
||||
--st-controls-rgb: 30 31 34;
|
||||
--st-controls-border: rgba(255, 255, 255, 0.15);
|
||||
--st-sunken: #060607;
|
||||
--st-fg: #f2f1ee;
|
||||
--st-fg-rgb: 242 241 238;
|
||||
--st-divider: rgba(255, 255, 255, 0.15);
|
||||
--st-gallery-vignette-alpha: 0.6;
|
||||
/* Photo tint (default neutral) */
|
||||
--st-photo-tint-rgb: 0 0 0;
|
||||
|
|
@ -95,28 +152,45 @@ body {
|
|||
--st-badge-alpha: 0.4;
|
||||
|
||||
/* shadcn design tokens consumed by @storyteller/ui-video-editor.
|
||||
Dark-theme palette — the editor only runs in dark mode today.
|
||||
--primary matches the webapp's brand blue (#2d81ff) so the
|
||||
selection ring is consistent across hosts. */
|
||||
--background: 0 0% 9%;
|
||||
Same values as artcraft-webapp's styles.css. */
|
||||
--background: 225 10% 8%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 12%;
|
||||
--card: 240 11% 7%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 12%;
|
||||
--popover: 240 11% 7%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 216 100% 59%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 24%;
|
||||
--secondary: 240 4% 9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 18%;
|
||||
--muted: 240 4% 9%;
|
||||
--muted-foreground: 0 0% 64%;
|
||||
--accent: 0 0% 16%;
|
||||
--accent: 240 4% 9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 70% 50%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 18%;
|
||||
--input: 0 0% 18%;
|
||||
--ring: 217 91% 60%;
|
||||
--border: 0 0% 100% / 0.15;
|
||||
--input: 0 0% 100% / 0.15;
|
||||
--ring: 217 100% 73%;
|
||||
|
||||
/* Motion tokens, mirrored from artcraft-webapp. */
|
||||
--ease-out: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
|
||||
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
--ease-emphasis: cubic-bezier(0.32, 0.72, 0, 1);
|
||||
--dur-fast: 140ms;
|
||||
--dur-base: 220ms;
|
||||
--dur-slow: 340ms;
|
||||
--dur-page: 260ms;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:root {
|
||||
--dur-fast: 1ms;
|
||||
--dur-base: 1ms;
|
||||
--dur-slow: 1ms;
|
||||
--dur-page: 1ms;
|
||||
}
|
||||
}
|
||||
|
||||
/* Light theme */
|
||||
|
|
@ -130,6 +204,7 @@ body {
|
|||
--st-controls-rgb: 238 238 239;
|
||||
--st-controls-border: #dcdcdf;
|
||||
/* requested light button border */
|
||||
--st-sunken: #ececef;
|
||||
--st-fg: #111827;
|
||||
--st-fg-rgb: 17 24 39;
|
||||
--st-divider: #d1d5db;
|
||||
|
|
@ -141,18 +216,19 @@ body {
|
|||
--st-badge-alpha: 0.1;
|
||||
}
|
||||
|
||||
/* Gray (current) */
|
||||
/* Default dark (brutalist, matches artcraft-webapp) */
|
||||
.theme-gray {
|
||||
--st-bg: #121212;
|
||||
--st-panel: #1f1f1f;
|
||||
--st-panel-border: #272727;
|
||||
--st-modal: #1f1f1f;
|
||||
--st-controls: #3e3e41;
|
||||
--st-controls-rgb: 62 62 65;
|
||||
--st-controls-border: #464646;
|
||||
--st-fg: #ffffff;
|
||||
--st-fg-rgb: 255 255 255;
|
||||
--st-divider: #272727;
|
||||
--st-bg: #121316;
|
||||
--st-panel: #101014;
|
||||
--st-panel-border: rgba(255, 255, 255, 0.15);
|
||||
--st-modal: #101014;
|
||||
--st-controls: #1e1f22;
|
||||
--st-controls-rgb: 30 31 34;
|
||||
--st-controls-border: rgba(255, 255, 255, 0.15);
|
||||
--st-sunken: #060607;
|
||||
--st-fg: #f2f1ee;
|
||||
--st-fg-rgb: 242 241 238;
|
||||
--st-divider: rgba(255, 255, 255, 0.15);
|
||||
--st-color-scheme: dark;
|
||||
--st-gallery-vignette-alpha: 0.3;
|
||||
--st-photo-tint-rgb: 0 0 0;
|
||||
|
|
@ -165,11 +241,12 @@ body {
|
|||
.theme-black {
|
||||
--st-bg: #000000;
|
||||
--st-panel: #0a0a0a;
|
||||
--st-panel-border: #242424;
|
||||
--st-panel-border: rgba(255, 255, 255, 0.15);
|
||||
--st-modal: #0a0a0a;
|
||||
--st-controls: #111111;
|
||||
--st-controls-rgb: 17 17 17;
|
||||
--st-controls-border: #242424;
|
||||
--st-controls-border: rgba(255, 255, 255, 0.15);
|
||||
--st-sunken: #000000;
|
||||
--st-fg: #ffffff;
|
||||
--st-fg-rgb: 255 255 255;
|
||||
--st-divider: #303030;
|
||||
|
|
@ -193,7 +270,8 @@ body {
|
|||
radial-gradient(520px 280px at 110% 120%, #1cb6be33, transparent), #0f1220;
|
||||
--st-controls: #1a1f35;
|
||||
--st-controls-rgb: 26 31 53;
|
||||
--st-controls-border: #1e2237;
|
||||
--st-controls-border: rgba(255, 255, 255, 0.15);
|
||||
--st-sunken: #05070d;
|
||||
--st-fg: #f8fafc;
|
||||
--st-fg-rgb: 248 250 252;
|
||||
--st-divider: #2a3152;
|
||||
|
|
@ -217,7 +295,8 @@ body {
|
|||
radial-gradient(700px 360px at 115% 120%, #fb923c88, transparent), #1a121a;
|
||||
--st-controls: #271927;
|
||||
--st-controls-rgb: 39 25 39;
|
||||
--st-controls-border: #3a253a;
|
||||
--st-controls-border: rgba(255, 255, 255, 0.15);
|
||||
--st-sunken: #0d070b;
|
||||
--st-fg: #f8fafc;
|
||||
--st-fg-rgb: 248 250 252;
|
||||
--st-divider: #4a2e3a;
|
||||
|
|
@ -238,14 +317,10 @@ body {
|
|||
background: var(--st-panel);
|
||||
}
|
||||
|
||||
/* Panel surface. Brutalist system: flat, no gradient (class name kept
|
||||
for lib compatibility). */
|
||||
.bg-ui-panel-gradient {
|
||||
background:
|
||||
radial-gradient(
|
||||
60.68% 60.68% at 50% 0%,
|
||||
rgba(255, 255, 255, 0.08) 0%,
|
||||
rgba(0, 0, 0, 0) 70%
|
||||
),
|
||||
var(--st-panel);
|
||||
background: var(--st-panel);
|
||||
background-color: var(--st-panel);
|
||||
}
|
||||
|
||||
|
|
@ -331,7 +406,6 @@ img {
|
|||
|
||||
.promptbox-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.promptbox-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
|
|
@ -730,3 +804,60 @@ img {
|
|||
.promptbox-drop-chip {
|
||||
animation: promptbox-drop-chip-in 0.25s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||
}
|
||||
|
||||
/* ===================== Brutalist extras (from artcraft-webapp) ========== */
|
||||
|
||||
.liquid-glass {
|
||||
background: rgba(11, 11, 12, 0.92);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
@keyframes toast-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
translate: 0 8px;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
translate: 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-toast-in {
|
||||
animation: toast-in 0.2s ease-out both;
|
||||
}
|
||||
|
||||
.bg-dots {
|
||||
background-image: radial-gradient(#ffffff20 1px, transparent 0);
|
||||
background-color: var(--st-panel);
|
||||
background-size: 33px 33px;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
/* Fine registration marks echo the landing page section corners. */
|
||||
.frame-corner-mark {
|
||||
position: absolute;
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
margin: -5px;
|
||||
pointer-events: none;
|
||||
background:
|
||||
linear-gradient(rgba(242, 241, 238, 0.4), rgba(242, 241, 238, 0.4))
|
||||
center / 1px 11px no-repeat,
|
||||
linear-gradient(rgba(242, 241, 238, 0.4), rgba(242, 241, 238, 0.4))
|
||||
center / 11px 1px no-repeat;
|
||||
}
|
||||
|
||||
/* Contained section borders match the landing without decorative extensions. */
|
||||
.create-empty-section {
|
||||
margin-inline: 12px;
|
||||
border: 1px solid rgba(242, 241, 238, 0.2);
|
||||
background: rgba(242, 241, 238, 0.015);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.create-empty-section {
|
||||
margin-inline: 32px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,29 +2,46 @@
|
|||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Subtle 3px corners on interactive controls (matches artcraft-webapp).
|
||||
The brutalist system keeps large surfaces (panels, modals, tables),
|
||||
passive labels, and media tiles square; anything pressable gets a soft
|
||||
corner for affordance. Tailwind radius utilities (rounded-full,
|
||||
rounded-none) still win — the utilities layer is emitted after base. */
|
||||
@layer base {
|
||||
button,
|
||||
select,
|
||||
textarea,
|
||||
input:not([type="radio"]):not([type="range"]),
|
||||
[role="button"] {
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.filter-tab {
|
||||
@apply text-nowrap rounded-lg border border-[#363636] bg-ui-controls-button px-2.5 py-1 text-[13px] font-medium text-white/80 transition-all duration-150 hover:border-brand-primary/50 hover:bg-assets-background/80;
|
||||
@apply text-nowrap rounded-[3px] border border-white/15 bg-white/5 px-2.5 py-1 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/70 transition-colors duration-150 hover:border-white/25 hover:bg-white/[0.07] hover:text-white;
|
||||
}
|
||||
|
||||
.filter-tab.active {
|
||||
@apply border border-brand-primary text-white hover:border-brand-primary hover:bg-ui-controls-button;
|
||||
@apply border-white bg-white text-black hover:border-white hover:bg-white hover:text-black;
|
||||
}
|
||||
|
||||
.filter-tab:disabled {
|
||||
@apply border-transparent bg-ui-controls-button/50 hover:border-transparent disabled:text-white/30;
|
||||
@apply border-transparent bg-white/[0.03] text-white/30 hover:border-transparent hover:bg-white/[0.03];
|
||||
}
|
||||
|
||||
/* Brutalist system: former "glass" surfaces are flat near-black panels
|
||||
with white/15 hairline borders (class name kept for lib compatibility). */
|
||||
.glass {
|
||||
@apply relative border border-white/10 shadow-lg backdrop-blur-xl transition-colors duration-300;
|
||||
@apply bg-[#303032]/90 !important;
|
||||
@apply relative rounded-[3px] border border-white/15 backdrop-blur-sm transition-colors duration-300;
|
||||
@apply bg-[#1E1F22]/95 !important;
|
||||
}
|
||||
|
||||
/* Light theme: the glass surface must be a light panel, not the dark
|
||||
hard-coded fill above. Tint + border swapped to read on a light bg. */
|
||||
.theme-light .glass {
|
||||
@apply border-black/10 !important;
|
||||
background-color: rgb(248 248 248 / 0.92) !important;
|
||||
@apply border-black/15 !important;
|
||||
background-color: rgb(248 248 248 / 0.95) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,36 @@ export default {
|
|||
extend: {
|
||||
fontFamily: {
|
||||
"custom-font": ["Fira Sans", "sans-serif"],
|
||||
// Brutalist type system, matching artcraft-webapp.
|
||||
mono: [
|
||||
"Geist Mono",
|
||||
"ui-monospace",
|
||||
"SFMono-Regular",
|
||||
"Menlo",
|
||||
"Consolas",
|
||||
"monospace",
|
||||
],
|
||||
display: [
|
||||
"Archivo",
|
||||
"system-ui",
|
||||
"-apple-system",
|
||||
"BlinkMacSystemFont",
|
||||
"Segoe UI",
|
||||
"Roboto",
|
||||
"Helvetica Neue",
|
||||
"Arial",
|
||||
"sans-serif",
|
||||
],
|
||||
sans: [
|
||||
"system-ui",
|
||||
"-apple-system",
|
||||
"BlinkMacSystemFont",
|
||||
"Segoe UI",
|
||||
"Roboto",
|
||||
"Helvetica Neue",
|
||||
"Arial",
|
||||
"sans-serif",
|
||||
],
|
||||
},
|
||||
keyframes: {
|
||||
fadeIn: {
|
||||
|
|
@ -225,12 +255,20 @@ export default {
|
|||
wrapper: "#39394d",
|
||||
},
|
||||
ui: {
|
||||
// Brutalist system (matches artcraft-webapp): near-black surfaces
|
||||
// with white/15 hairline borders. Values come from the --st-* theme
|
||||
// variables in app/src/styles/base.css.
|
||||
background: "var(--st-bg)",
|
||||
panel: "var(--st-panel)",
|
||||
"panel-border": "var(--st-panel-border)",
|
||||
border: "var(--st-panel-border)",
|
||||
controls: "rgb(var(--st-controls-rgb) / <alpha-value>)",
|
||||
"controls-button": "rgb(var(--st-controls-rgb) / <alpha-value>)",
|
||||
"controls-border": "var(--st-controls-border)",
|
||||
divider: "var(--st-divider)",
|
||||
ink: "rgb(var(--st-fg-rgb) / <alpha-value>)",
|
||||
"accent-ink": "#74aaff",
|
||||
sunken: "var(--st-sunken)",
|
||||
},
|
||||
"base-fg": "rgb(var(--st-fg-rgb) / <alpha-value>)",
|
||||
media: {
|
||||
|
|
@ -288,6 +326,9 @@ export default {
|
|||
},
|
||||
plugins: [],
|
||||
safelist: [
|
||||
// lucide-react adds this class at runtime; keep the base-layer sizing rule
|
||||
// in styles/tailwind.css from being purged.
|
||||
"lucide",
|
||||
"bg-character-selected",
|
||||
"bg-character-unselected",
|
||||
"bg-character-clip",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export const Badge = ({ label, className, icon }: BadgeProps) => {
|
|||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex items-center gap-1 rounded-[6px] bg-ui-controls/45 px-[4px] py-[1px] text-[10px] shadow-sm",
|
||||
"flex items-center gap-1 rounded-none border border-ui-controls-border bg-black/40 px-1.5 py-px font-mono text-[10px] font-medium uppercase tracking-[0.1em]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ export const ButtonDropdown = ({
|
|||
<Menu.Items
|
||||
static
|
||||
className={twMerge(
|
||||
"absolute z-20 mt-1 w-max divide-y divide-gray-100 overflow-hidden rounded-lg bg-brand-secondary py-1.5 shadow-xl focus:outline-none",
|
||||
"absolute z-20 mt-2 w-max min-w-48 overflow-hidden rounded-[3px] border border-ui-panel-border bg-ui-controls p-1.5 text-base-fg focus:outline-none",
|
||||
align === "left" ? "left-0" : "right-0",
|
||||
)}
|
||||
>
|
||||
|
|
@ -109,19 +109,19 @@ export const ButtonDropdown = ({
|
|||
{options.map((option, index) => (
|
||||
<Fragment key={index}>
|
||||
{option.divider && (
|
||||
<div className="my-1.5 border-t border-white/5" />
|
||||
<div className="my-1.5 border-t border-white/15" />
|
||||
)}
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
disabled={option.disabled}
|
||||
className={twMerge(
|
||||
"duration-50 bg-brand-secondary font-medium text-white transition-all",
|
||||
active ? "bg-brand-secondary-800" : "",
|
||||
"rounded-[3px] bg-transparent font-medium text-base-fg transition-colors duration-150",
|
||||
active ? "bg-white/10" : "",
|
||||
option.disabled
|
||||
? "pointer-events-none opacity-40"
|
||||
: "",
|
||||
"group flex w-full items-center py-1.5 pl-7 pr-4 text-sm",
|
||||
"group flex w-full items-center gap-2 px-2.5 py-2 text-sm",
|
||||
option.className,
|
||||
)}
|
||||
onClick={() => handleOptionClick(index)}
|
||||
|
|
@ -130,11 +130,11 @@ export const ButtonDropdown = ({
|
|||
{option.icon && (
|
||||
<DynamicIcon
|
||||
icon={option.icon}
|
||||
className="mr-2"
|
||||
className="mr-2 h-4 w-4 shrink-0"
|
||||
/>
|
||||
)}
|
||||
<div className="grow text-start">{option.label}</div>
|
||||
<div className="ml-10 font-normal text-white/75">
|
||||
<div className="ml-6 font-mono text-[11px] font-normal text-white/45">
|
||||
{option.description && option.description}
|
||||
</div>
|
||||
{showSelected && (
|
||||
|
|
|
|||
|
|
@ -15,12 +15,14 @@ interface ButtonIconSelectProps {
|
|||
options: Option[];
|
||||
onOptionChange?: (value: string) => void;
|
||||
selectedOption?: string;
|
||||
buttonClassName?: string;
|
||||
}
|
||||
|
||||
export function ButtonIconSelect({
|
||||
options,
|
||||
onOptionChange,
|
||||
selectedOption,
|
||||
buttonClassName,
|
||||
}: ButtonIconSelectProps) {
|
||||
const [internalSelectedOption, setInternalSelectedOption] = useState<string>(
|
||||
selectedOption || options[0].value
|
||||
|
|
@ -54,7 +56,8 @@ export function ButtonIconSelect({
|
|||
text ? "h-auto w-auto gap-2 px-3 py-1.5" : "w-9",
|
||||
internalSelectedOption === value
|
||||
? "border-brand-primary bg-brand-primary/20"
|
||||
: "border-transparent hover:bg-ui-panel/[0.4]"
|
||||
: "border-transparent hover:bg-ui-panel/[0.4]",
|
||||
buttonClassName,
|
||||
)}
|
||||
onClick={() => handleOptionChange(value)}
|
||||
>
|
||||
|
|
@ -72,7 +75,8 @@ export function ButtonIconSelect({
|
|||
text ? "h-auto w-auto gap-2 px-3 py-1.5" : "w-9",
|
||||
internalSelectedOption === value
|
||||
? "border-brand-primary bg-brand-primary/20"
|
||||
: "border-transparent hover:bg-ui-panel/[0.4]"
|
||||
: "border-transparent hover:bg-ui-panel/[0.4]",
|
||||
buttonClassName,
|
||||
)}
|
||||
onClick={() => handleOptionChange(value)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -20,12 +20,12 @@ export const ButtonIcon = ({
|
|||
...rest
|
||||
}: ButtonIconProps) => {
|
||||
const className = twMerge(
|
||||
"box-content flex h-8 w-8 items-center justify-center rounded-lg transition-all duration-150",
|
||||
"box-content flex h-8 w-8 items-center justify-center rounded-[3px] transition-colors duration-150",
|
||||
bgFill
|
||||
? "bg-ui-controls-button hover:bg-ui-controls-button/[0.75]"
|
||||
: "bg-transparent hover:bg-ui-panel/[0.4]",
|
||||
disabled && "opacity-50 hover:bg-transparent",
|
||||
propsClassName
|
||||
propsClassName,
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ export const GenerateButton = ({
|
|||
disabled && "opacity-50",
|
||||
)}
|
||||
>
|
||||
<CoinsIcon className="text-xs text-white" />
|
||||
<span className="text-[13px] font-bold text-white/90">
|
||||
<CoinsIcon className="text-xs" />
|
||||
<span className="text-[13px] font-bold">
|
||||
{credits}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { LucideIcon } from "lucide-react";
|
||||
import { DynamicIcon } from "@storyteller/icons";
|
||||
import { Button } from "./button";
|
||||
import { Button, NEUTRAL_BUTTON_HOVER_CLASSES } from "./button";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
interface ToggleButtonProps {
|
||||
|
|
@ -26,14 +26,15 @@ export const ToggleButton = ({
|
|||
return (
|
||||
<Button
|
||||
className={twMerge(
|
||||
// 34px matches the sibling toolbar controls (GenerateButton, the
|
||||
// PopoverMenu triggers); the 2px border is inside the box.
|
||||
"flex h-[34px] items-center justify-center rounded-lg border-2 border-transparent text-sm text-white backdrop-blur-lg transition-all",
|
||||
hasLabel ? "px-3" : "w-[34px]",
|
||||
// Same box and type as the PopoverMenu triggers beside it (34px,
|
||||
// text-sm sans, flat control surface + hairline border), overriding
|
||||
// the Button base's mono uppercase label.
|
||||
"flex h-[34px] items-center justify-center rounded-[3px] border border-ui-controls-border bg-ui-controls py-0 font-sans text-sm font-medium normal-case tracking-normal text-base-fg transition-colors",
|
||||
hasLabel ? "px-3" : "w-[34px] p-0",
|
||||
isActive
|
||||
? "border-white/20 bg-brand-primary/40 hover:border-white/30 hover:bg-brand-primary/40"
|
||||
: "bg-[#5F5F68]/60 hover:bg-[#5F5F68]/90",
|
||||
className
|
||||
? "border-white/40 bg-white/10 hover:border-white/40 hover:bg-white/15"
|
||||
: NEUTRAL_BUTTON_HOVER_CLASSES,
|
||||
className,
|
||||
)}
|
||||
variant="secondary"
|
||||
onClick={onClick}
|
||||
|
|
@ -46,7 +47,7 @@ export const ToggleButton = ({
|
|||
/>
|
||||
)}
|
||||
{label && (
|
||||
<span className="text-sm font-medium text-white/90">{label}</span>
|
||||
<span className="whitespace-nowrap">{label}</span>
|
||||
)}
|
||||
</span>
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import { DynamicIcon } from "@storyteller/icons";
|
|||
import { twMerge } from "tailwind-merge";
|
||||
import { ButtonHTMLAttributes, AnchorHTMLAttributes } from "react";
|
||||
|
||||
export const NEUTRAL_BUTTON_HOVER_CLASSES =
|
||||
"hover:bg-white/[0.07] hover:border-white/25";
|
||||
|
||||
type AnchorProps = Omit<
|
||||
AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||
keyof ButtonHTMLAttributes<HTMLButtonElement>
|
||||
|
|
@ -39,20 +42,22 @@ export const Button = ({
|
|||
function getVariantClassNames(variant: string) {
|
||||
switch (variant) {
|
||||
case "secondary": {
|
||||
return "bg-ui-controls text-base-fg border border-ui-controls-border hover:bg-ui-controls/80 focus-visible:outline-secondary";
|
||||
// Resting bg-white/5 tint (matching the home grid cards) so the
|
||||
// button reads as a surface, not a bare outline on the page bg.
|
||||
return "bg-white/5 text-base-fg border border-ui-controls-border";
|
||||
}
|
||||
case "action": {
|
||||
return "bg-ui-controls text-base-fg border border-ui-controls-border hover:bg-ui-controls/80 focus-visible:outline-action";
|
||||
return "bg-ui-controls text-base-fg border border-ui-controls-border";
|
||||
}
|
||||
case "destructive": {
|
||||
return "bg-red hover:bg-red/90 text-white focus-visible:outline-red";
|
||||
return "bg-red-500 hover:bg-red-400 text-white";
|
||||
}
|
||||
case "ghost": {
|
||||
return "bg-transparent text-base-fg border border-ui-controls-border/70 hover:bg-ui-controls/30 focus-visible:outline-primary-600";
|
||||
return "bg-transparent text-base-fg/70 hover:text-base-fg";
|
||||
}
|
||||
case "primary":
|
||||
default: {
|
||||
return "bg-primary hover:bg-primary-400 text-white focus-visible:outline-primary-600";
|
||||
return "bg-white hover:bg-white/90 text-black font-bold";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -62,8 +67,10 @@ export const Button = ({
|
|||
);
|
||||
|
||||
const className = twMerge(
|
||||
"w-fit text-sm font-medium rounded-lg px-3 py-1.5 border border-transparent shadow-sm focus-visible:outline focus-visible:outline-0 focus-visible:outline-offset-0 transition-all duration-150 flex gap-2 items-center justify-center active:scale-95 transform",
|
||||
"w-fit rounded-[3px] font-mono text-xs font-semibold uppercase tracking-[0.12em] px-3.5 py-2 border border-transparent focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-white/60 transition-colors duration-150 flex gap-2 items-center justify-center",
|
||||
getVariantClassNames(propsVariant),
|
||||
(propsVariant === "secondary" || propsVariant === "action" || propsVariant === "ghost") &&
|
||||
NEUTRAL_BUTTON_HOVER_CLASSES,
|
||||
propsClassName,
|
||||
disabledClass,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -115,25 +115,27 @@ export const CameraSettingsModal = ({
|
|||
showClose={false}
|
||||
>
|
||||
<div className="grid h-full grid-cols-12 gap-3">
|
||||
<div className="relative col-span-4 p-3 pt-2 after:absolute after:right-0 after:top-0 after:h-full after:w-px after:bg-gray-200 after:dark:bg-white/10">
|
||||
<div className="relative col-span-4 p-3 pt-2 after:absolute after:right-0 after:top-0 after:h-full after:w-px after:bg-white/15">
|
||||
<div className="flex items-center justify-between gap-2.5 py-0.5">
|
||||
<h2 className="text-[18px] font-semibold opacity-80">Camera</h2>
|
||||
<h2 className="hud-label text-base-fg/60">Camera</h2>
|
||||
<Tooltip content="Add camera" position="top" delay={200}>
|
||||
<button
|
||||
className="h-6 w-6 rounded-full text-white/70 transition-colors hover:text-white/100"
|
||||
className="flex h-6 w-6 items-center justify-center rounded-[3px] text-white/70 transition-colors hover:bg-white/10 hover:text-white"
|
||||
onClick={onAddCamera}
|
||||
>
|
||||
<PlusIcon className="text-xl" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<hr className="my-2 w-full border-white/10" />
|
||||
<hr className="my-2 w-full border-white/15" />
|
||||
<div className="space-y-1">
|
||||
{cameras.map((camera) => (
|
||||
<button
|
||||
key={camera.id}
|
||||
className={`h-9 w-full rounded-lg p-2 text-left transition-colors duration-100 hover:bg-[#63636B]/40 ${
|
||||
camera.id === selectedCameraId ? "bg-[#63636B]/40" : ""
|
||||
className={`h-9 w-full rounded-[3px] border-l-2 p-2 text-left transition-colors duration-100 ${
|
||||
camera.id === selectedCameraId
|
||||
? "border-white bg-white/10"
|
||||
: "border-transparent hover:bg-white/5"
|
||||
}`}
|
||||
onClick={() => handleCameraSelect(camera)}
|
||||
>
|
||||
|
|
@ -151,7 +153,7 @@ export const CameraSettingsModal = ({
|
|||
<div className="flex h-full flex-col">
|
||||
<div>
|
||||
<div className="flex items-center justify-between gap-2.5 py-0.5 opacity-100">
|
||||
<h2 className="text-[18px] font-semibold">
|
||||
<h2 className="font-display text-[18px] font-semibold">
|
||||
{selectedCamera?.label || "Camera"}
|
||||
</h2>
|
||||
<Tooltip
|
||||
|
|
@ -164,10 +166,10 @@ export const CameraSettingsModal = ({
|
|||
delay={200}
|
||||
>
|
||||
<button
|
||||
className={`h-6 w-6 rounded-lg transition-colors ${
|
||||
className={`flex h-6 w-6 items-center justify-center rounded-[3px] transition-colors ${
|
||||
selectedCamera?.id === "main"
|
||||
? "cursor-not-allowed text-white/30"
|
||||
: "text-white/60 hover:text-white/100"
|
||||
: "text-white/60 hover:bg-white/10 hover:text-white"
|
||||
}`}
|
||||
onClick={() =>
|
||||
selectedCamera &&
|
||||
|
|
@ -180,10 +182,10 @@ export const CameraSettingsModal = ({
|
|||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<hr className="my-2 w-full border-white/10" />
|
||||
<hr className="my-2 w-full border-white/15" />
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="camera-name" className="text-sm opacity-70">
|
||||
<Label htmlFor="camera-name">
|
||||
Name
|
||||
</Label>
|
||||
<Input
|
||||
|
|
@ -199,7 +201,7 @@ export const CameraSettingsModal = ({
|
|||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="focal-length" className="text-sm opacity-70">
|
||||
<Label htmlFor="focal-length">
|
||||
Focal Length
|
||||
</Label>
|
||||
<div
|
||||
|
|
@ -220,7 +222,7 @@ export const CameraSettingsModal = ({
|
|||
showIncrement={true}
|
||||
className="w-full"
|
||||
/>
|
||||
<span className="min-w-[60px] text-sm">
|
||||
<span className="min-w-[60px] font-mono text-sm tabular-nums">
|
||||
{selectedCamera?.focalLength || 35}mm
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -97,12 +97,12 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
|
|||
<span
|
||||
aria-hidden
|
||||
className={twMerge(
|
||||
"relative inline-block shrink-0 rounded-[4px] border transition-colors",
|
||||
"relative inline-block shrink-0 rounded-[3px] border transition-colors",
|
||||
boxDims,
|
||||
isChecked
|
||||
? "border-primary bg-primary text-white"
|
||||
: "border-ui-controls-border bg-ui-controls hover:border-primary/60",
|
||||
"peer-focus-visible:ring-2 peer-focus-visible:ring-primary/40",
|
||||
? "border-white bg-white text-black"
|
||||
: "border-ui-controls-border bg-transparent hover:border-white/60",
|
||||
"peer-focus-visible:ring-1 peer-focus-visible:ring-white/60",
|
||||
checkboxClassName,
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@ export const CloseButton = ({
|
|||
<button
|
||||
onClick={onClick}
|
||||
className={twMerge(
|
||||
"flex items-center justify-center rounded-full bg-black/40 text-white/60 transition-all hover:bg-black/70 hover:text-white",
|
||||
"flex items-center justify-center rounded-[3px] bg-black/40 text-white/60 transition-colors hover:bg-white hover:text-black",
|
||||
sizeClasses[size],
|
||||
className
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<XIcon />
|
||||
|
|
|
|||
|
|
@ -13,14 +13,14 @@ export const DragAndDropZone = ({
|
|||
}) => {
|
||||
if (files.length === 0) {
|
||||
return (
|
||||
<div className="flex cursor-pointer items-center gap-3.5 rounded-lg border border-dashed border-[#363636] bg-brand-secondary p-3">
|
||||
<FileUpIcon className="text-4xl" />
|
||||
<div className="flex cursor-pointer items-center gap-3.5 rounded-none border border-dashed border-white/20 bg-white/[0.02] p-3 transition-colors hover:border-white/40 hover:bg-white/[0.05]">
|
||||
<FileUpIcon className="text-4xl text-white/60" />
|
||||
<div className="flex flex-col gap-0">
|
||||
<p className="font-medium">
|
||||
<u>Upload a file</u> or drop it here
|
||||
</p>
|
||||
|
||||
<p className="flex items-center gap-2 text-sm font-normal opacity-50">
|
||||
<p className="flex items-center gap-2 font-mono text-[11px] font-normal uppercase tracking-[0.12em] opacity-50">
|
||||
{fileTypes.join(", ").toString()} supported
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -37,7 +37,7 @@ export const DragAndDropZone = ({
|
|||
: `${Math.floor(file.size / 1024)} KB`;
|
||||
|
||||
return (
|
||||
<div className="flex cursor-pointer items-center justify-between gap-3.5 rounded-lg border border-dashed border-[#363636] bg-brand-secondary p-3">
|
||||
<div className="flex cursor-pointer items-center justify-between gap-3.5 rounded-none border border-dashed border-white/20 bg-white/[0.02] p-3 transition-colors hover:border-white/40 hover:bg-white/[0.05]">
|
||||
<DynamicIcon icon={fileIcon} className="text-4xl" />
|
||||
<div className="flex grow flex-col gap-0">
|
||||
<p className="font-medium">
|
||||
|
|
@ -55,7 +55,7 @@ export const DragAndDropZone = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex cursor-pointer items-center justify-between gap-3.5 rounded-lg border border-dashed border-[#363636] bg-brand-secondary p-3">
|
||||
<div className="flex cursor-pointer items-center justify-between gap-3.5 rounded-none border border-dashed border-white/20 bg-white/[0.02] p-3 transition-colors hover:border-white/40 hover:bg-white/[0.05]">
|
||||
<DynamicIcon icon={fileIcon} className="text-4xl" />
|
||||
<div className="flex grow flex-col gap-0">
|
||||
<p className="font-medium">{files.length} files selected</p>
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ export function DragGhost({
|
|||
zIndex: 10000,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
className="flex cursor-grabbing flex-col overflow-hidden rounded-xl shadow-[0_18px_40px_-12px_rgba(0,0,0,0.7)] ring-1 ring-white/20"
|
||||
className="flex cursor-grabbing flex-col overflow-hidden shadow-[0_18px_40px_-12px_rgba(0,0,0,0.7)] ring-1 ring-white/20"
|
||||
>
|
||||
<img
|
||||
src={state.thumbnail}
|
||||
|
|
@ -143,7 +143,7 @@ export function DragGhost({
|
|||
/>
|
||||
{state.indicator && (
|
||||
<div
|
||||
className={`absolute left-1.5 top-1.5 flex h-5 w-5 items-center justify-center rounded-full shadow-md ring-1 ring-black/30 ${
|
||||
className={`absolute left-1.5 top-1.5 flex h-5 w-5 items-center justify-center shadow-md ring-1 ring-black/30 ${
|
||||
state.indicator === "ok" ? "bg-emerald-500" : "bg-red-500"
|
||||
}`}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export const FolderColorRow: React.FC<FolderColorRowProps> = ({
|
|||
onClick={() => onSetColor(c)}
|
||||
style={{ backgroundColor: c }}
|
||||
className={twMerge(
|
||||
"h-5 w-5 rounded-full border border-black/20 transition-transform hover:scale-110",
|
||||
"h-5 w-5 rounded-[3px] border border-black/20 transition-transform hover:scale-110",
|
||||
colorCode === c &&
|
||||
"ring-2 ring-white ring-offset-1 ring-offset-ui-panel",
|
||||
)}
|
||||
|
|
@ -37,16 +37,16 @@ export const FolderColorRow: React.FC<FolderColorRowProps> = ({
|
|||
aria-label="Clear color"
|
||||
onClick={() => onSetColor(null)}
|
||||
className={twMerge(
|
||||
"flex h-5 w-5 items-center justify-center rounded-full border border-ui-panel-border text-base-fg/50 hover:text-base-fg",
|
||||
"flex h-5 w-5 items-center justify-center rounded-[3px] border border-ui-panel-border text-base-fg/50 hover:text-base-fg",
|
||||
!colorCode && "ring-2 ring-white ring-offset-1 ring-offset-ui-panel",
|
||||
)}
|
||||
>
|
||||
<XIcon className="text-[10px]" />
|
||||
<XIcon className="text-[10px]" />
|
||||
</button>
|
||||
{/* Custom hex (native picker) */}
|
||||
<label
|
||||
title="Custom color"
|
||||
className="relative h-5 w-5 cursor-pointer overflow-hidden rounded-full border border-ui-panel-border"
|
||||
className="relative h-5 w-5 cursor-pointer overflow-hidden rounded-[3px] border border-ui-panel-border"
|
||||
style={{
|
||||
background:
|
||||
"conic-gradient(red, orange, yellow, lime, cyan, blue, magenta, red)",
|
||||
|
|
@ -55,7 +55,9 @@ export const FolderColorRow: React.FC<FolderColorRowProps> = ({
|
|||
<input
|
||||
type="color"
|
||||
value={
|
||||
colorCode && /^#[0-9a-fA-F]{6}$/.test(colorCode) ? colorCode : "#888888"
|
||||
colorCode && /^#[0-9a-fA-F]{6}$/.test(colorCode)
|
||||
? colorCode
|
||||
: "#888888"
|
||||
}
|
||||
onChange={(e) => onSetColor(e.target.value)}
|
||||
className="absolute -inset-1 h-[150%] w-[150%] cursor-pointer opacity-0"
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export const FolderNameDialog: React.FC<FolderNameDialogProps> = ({
|
|||
onClose={onClose}
|
||||
title={title}
|
||||
accessibleTitle={title}
|
||||
className="w-80 max-w-[90vw]"
|
||||
className="w-80 max-w-[90vw] [&_h2]:font-sans [&_h2]:text-sm [&_h2]:font-semibold [&_h2]:tracking-normal [&_h2]:[font-stretch:normal]"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{subtitle && (
|
||||
|
|
@ -57,26 +57,27 @@ export const FolderNameDialog: React.FC<FolderNameDialogProps> = ({
|
|||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
aria-label="Folder name"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") submit();
|
||||
}}
|
||||
className="w-full rounded-md border border-ui-panel-border bg-ui-controls/40 px-3 py-1.5 text-sm text-base-fg outline-none focus:ring-1 focus:ring-primary/50"
|
||||
className="w-full rounded-[3px] border border-ui-panel-border bg-ui-controls/40 px-3 py-1.5 text-sm text-base-fg outline-none focus:ring-1 focus:ring-primary/50"
|
||||
autoFocus
|
||||
/>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="action"
|
||||
onClick={onClose}
|
||||
className="px-3 py-1 text-sm"
|
||||
className="px-3 py-1.5 text-[11px]"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={submit}
|
||||
disabled={!value.trim()}
|
||||
className="px-3 py-1 text-sm"
|
||||
className="px-3 py-1.5 text-[11px]"
|
||||
>
|
||||
{confirmLabel}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,4 @@
|
|||
import React, {
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { CheckIcon, EllipsisIcon, MusicIcon, UploadIcon } from "lucide-react";
|
||||
import { LoadingSpinner } from "@storyteller/ui-loading-spinner";
|
||||
|
|
@ -248,7 +243,7 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
|
|||
type="button"
|
||||
tabIndex={-1}
|
||||
className={twMerge(
|
||||
"w-full group relative overflow-visible rounded-md border-[3px] transition-colors outline-none focus:outline-none focus-visible:outline-none active:outline-none aspect-square",
|
||||
"w-full group relative overflow-visible border-[3px] transition-colors outline-none focus:outline-none focus-visible:outline-none active:outline-none aspect-square",
|
||||
selected || bulkSelected
|
||||
? "border-primary"
|
||||
: disabled
|
||||
|
|
@ -272,10 +267,8 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
|
|||
{isAudio ? (
|
||||
<div className="flex h-full w-full flex-col bg-gradient-to-br from-white/[0.08] via-white/[0.04] to-white/[0.02]">
|
||||
<div className="flex min-h-0 flex-1 items-center justify-center">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-full bg-white/10 ring-1 ring-white/15">
|
||||
<MusicIcon
|
||||
|
||||
className="text-lg text-white/70" />
|
||||
<div className="flex h-11 w-11 items-center justify-center bg-white/10 ring-1 ring-white/15">
|
||||
<MusicIcon className="text-lg text-white/70" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="line-clamp-2 shrink-0 px-2.5 pb-2.5 text-left text-xs leading-snug text-white/75">
|
||||
|
|
@ -316,12 +309,12 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
|
|||
</>
|
||||
)}
|
||||
{selected && (
|
||||
<div className="absolute left-2 top-2 flex h-6 w-6 items-center justify-center rounded-full bg-primary">
|
||||
<CheckIcon className="text-sm" />
|
||||
<div className="absolute left-2 top-2 flex h-6 w-6 items-center justify-center rounded-[3px] bg-primary">
|
||||
<CheckIcon className="text-sm" />
|
||||
</div>
|
||||
)}
|
||||
{disabled && (
|
||||
<div className="absolute left-2 top-2 rounded-full bg-black/70 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-white/80">
|
||||
<div className="absolute left-2 top-2 bg-black/70 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-white/80">
|
||||
Added
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -356,10 +349,8 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
|
|||
position="bottom"
|
||||
align="end"
|
||||
mode="default"
|
||||
triggerIcon={
|
||||
<EllipsisIcon className="text-base-fg" />
|
||||
}
|
||||
buttonClassName="h-7 w-7 p-0 rounded-full bg-ui-controls/60 hover:bg-ui-controls/90 text-base-fg border border-ui-controls-border"
|
||||
triggerIcon={<EllipsisIcon className="text-base-fg" />}
|
||||
buttonClassName="h-7 w-7 p-0 bg-ui-controls/60 hover:bg-ui-controls/90 text-base-fg border border-ui-controls-border"
|
||||
panelClassName="w-max min-w-44 p-1"
|
||||
closeOnUnhover
|
||||
>
|
||||
|
|
@ -394,7 +385,7 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
|
|||
/>
|
||||
<div
|
||||
ref={ctxPanelRef}
|
||||
className="fixed z-[9999] w-max min-w-44 rounded-lg border border-ui-panel-border bg-ui-panel p-1 shadow-xl"
|
||||
className="fixed z-[9999] w-max min-w-44 rounded-[3px] border border-ui-panel-border bg-ui-panel p-1 shadow-xl"
|
||||
style={{ left: ctxMenu.x, top: ctxMenu.y }}
|
||||
>
|
||||
<GalleryItemMenuItems
|
||||
|
|
@ -418,7 +409,7 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
|
|||
{mode !== "select" && onBulkSelectToggle && (
|
||||
<div
|
||||
className={twMerge(
|
||||
"absolute left-2 top-2 z-[2] flex h-5 w-5 items-center justify-center rounded border-2 cursor-pointer transition-all duration-100",
|
||||
"absolute left-2 top-2 z-[2] flex h-5 w-5 items-center justify-center rounded-[3px] border-2 cursor-pointer transition-all duration-100",
|
||||
bulkSelected
|
||||
? "bg-primary border-primary"
|
||||
: "border-white/60 bg-black/40 hover:border-white",
|
||||
|
|
@ -432,16 +423,12 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
|
|||
onBulkSelectToggle?.();
|
||||
}}
|
||||
>
|
||||
{bulkSelected && (
|
||||
<CheckIcon
|
||||
|
||||
className="text-[10px] text-white" />
|
||||
)}
|
||||
{bulkSelected && <CheckIcon className="text-[10px] text-white" />}
|
||||
</div>
|
||||
)}
|
||||
{/* Media class badge on hover — bottom-left */}
|
||||
{!disableTooltipAndBadge && item.mediaClass && (
|
||||
<div className="pointer-events-none absolute left-2 bottom-2 z-[1] rounded-full bg-black/50 backdrop-blur-lg px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-white opacity-0 group-hover:opacity-100 transition-opacity duration-150">
|
||||
<div className="pointer-events-none absolute left-2 bottom-2 z-[1] bg-black/50 backdrop-blur-lg px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-white opacity-0 group-hover:opacity-100 transition-opacity duration-150">
|
||||
{item.mediaClass === "dimensional" ||
|
||||
item.mediaClass === "mesh" ||
|
||||
item.mediaClass === "splat"
|
||||
|
|
@ -451,8 +438,8 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
|
|||
)}
|
||||
{/* Upload badge — bottom-right (always visible, even in select mode) */}
|
||||
{item.isUpload && (
|
||||
<div className="pointer-events-none absolute right-2 bottom-2 z-[1] flex h-5 w-5 items-center justify-center rounded-full bg-black/50 backdrop-blur-lg text-white">
|
||||
<UploadIcon className="text-[10px]" />
|
||||
<div className="pointer-events-none absolute right-2 bottom-2 z-[1] flex h-5 w-5 items-center justify-center bg-black/50 backdrop-blur-lg text-white">
|
||||
<UploadIcon className="text-[10px]" />
|
||||
</div>
|
||||
)}
|
||||
{/* Conditionally wrap with Tooltip — hidden when selecting or in bulk mode */}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export const GalleryFolderChip: React.FC<GalleryFolderChipProps> = ({
|
|||
}}
|
||||
style={colorCode ? { borderColor: colorCode } : undefined}
|
||||
className={twMerge(
|
||||
"group/chip relative w-full aspect-square overflow-hidden rounded-md border-2 border-ui-controls/40 bg-ui-controls/20 transition-[transform,border-color,background-color,box-shadow] duration-200 ease-out cursor-pointer hover:border-primary/60",
|
||||
"group/chip relative w-full aspect-square overflow-hidden rounded-[3px] border-2 border-ui-controls/40 bg-ui-controls/20 transition-[transform,border-color,background-color,box-shadow] duration-200 ease-out cursor-pointer hover:border-primary/60",
|
||||
// A dragged item hovering the folder makes it lift and glow — clear,
|
||||
// tactile "drop here" feedback rather than a flat color swap.
|
||||
"[&.folder-drag-over]:scale-[1.04] [&.folder-drag-over]:border-primary [&.folder-drag-over]:bg-primary/20 [&.folder-drag-over]:ring-2 [&.folder-drag-over]:ring-primary/40",
|
||||
|
|
@ -168,7 +168,7 @@ export const GalleryFolderChip: React.FC<GalleryFolderChipProps> = ({
|
|||
e.stopPropagation();
|
||||
openMenuAt(e.currentTarget as HTMLElement);
|
||||
}}
|
||||
className="absolute right-1.5 top-1.5 flex h-7 w-7 items-center justify-center rounded-full bg-black/40 text-white opacity-0 transition-opacity hover:bg-black/60 group-hover/chip:opacity-100 [@media(pointer:coarse)]:opacity-100"
|
||||
className="absolute right-1.5 top-1.5 flex h-7 w-7 items-center justify-center rounded-[3px] bg-black/40 text-white opacity-0 transition-opacity hover:bg-black/60 group-hover/chip:opacity-100 [@media(pointer:coarse)]:opacity-100"
|
||||
>
|
||||
<EllipsisIcon className="text-sm" />
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
import React, { useState } from "react";
|
||||
import { ChevronRightIcon, EyeIcon, FolderIcon, FolderMinusIcon, FolderPlusIcon, PencilIcon, PlusIcon, Trash2Icon } from "lucide-react";
|
||||
import {
|
||||
ChevronRightIcon,
|
||||
EyeIcon,
|
||||
FolderIcon,
|
||||
FolderMinusIcon,
|
||||
FolderPlusIcon,
|
||||
PencilIcon,
|
||||
PlusIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { GalleryItem } from "./gallery-modal";
|
||||
import { GalleryFolder } from "./GalleryDraggableItem";
|
||||
|
|
@ -28,7 +37,7 @@ export interface GalleryItemMenuItemsProps {
|
|||
}
|
||||
|
||||
const ROW =
|
||||
"flex w-full items-center gap-2 px-2 py-2 rounded-md hover:bg-ui-controls/60 text-base-fg text-sm whitespace-nowrap";
|
||||
"flex w-full items-center gap-2 rounded-[3px] px-2 py-2 hover:bg-ui-controls/60 text-base-fg text-sm whitespace-nowrap";
|
||||
|
||||
/**
|
||||
* The gallery-tile menu body (Open / Edit / Add to Folder ▸ / Remove from
|
||||
|
|
@ -60,7 +69,7 @@ export const GalleryItemMenuItems: React.FC<GalleryItemMenuItemsProps> = ({
|
|||
onOpen();
|
||||
}}
|
||||
>
|
||||
<EyeIcon className="text-base-fg w-4" />
|
||||
<EyeIcon className="text-base-fg w-4" />
|
||||
<span>
|
||||
{item.mediaClass === "video"
|
||||
? "View video"
|
||||
|
|
@ -85,7 +94,7 @@ export const GalleryItemMenuItems: React.FC<GalleryItemMenuItemsProps> = ({
|
|||
close();
|
||||
}}
|
||||
>
|
||||
<PencilIcon className="text-base-fg w-4" />
|
||||
<PencilIcon className="text-base-fg w-4" />
|
||||
<span>Edit image</span>
|
||||
</button>
|
||||
)}
|
||||
|
|
@ -103,28 +112,28 @@ export const GalleryItemMenuItems: React.FC<GalleryItemMenuItemsProps> = ({
|
|||
>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between gap-2 px-2 py-2 rounded-md hover:bg-ui-controls/60 text-base-fg text-sm whitespace-nowrap"
|
||||
className="flex w-full items-center justify-between gap-2 rounded-[3px] px-2 py-2 hover:bg-ui-controls/60 text-base-fg text-sm whitespace-nowrap"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setFolderSubmenuOpen((v) => !v);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FolderPlusIcon className="text-base-fg w-4" />
|
||||
<FolderPlusIcon className="text-base-fg w-4" />
|
||||
<span>Add to Folder</span>
|
||||
</div>
|
||||
<ChevronRightIcon
|
||||
|
||||
className={twMerge(
|
||||
"text-[10px] text-base-fg/50 transition-transform",
|
||||
COARSE_POINTER && folderSubmenuOpen && "rotate-90",
|
||||
)} />
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
{folderSubmenuOpen &&
|
||||
(COARSE_POINTER ? (
|
||||
// Touch: expand inline (accordion) — a fly-out would overflow
|
||||
// the screen edge on narrow viewports.
|
||||
<div className="mb-1 max-h-48 overflow-y-auto rounded-md bg-ui-controls/20 p-1">
|
||||
<div className="mb-1 max-h-48 overflow-y-auto bg-ui-controls/20 p-1">
|
||||
<FolderList
|
||||
item={item}
|
||||
folders={folders}
|
||||
|
|
@ -138,7 +147,7 @@ export const GalleryItemMenuItems: React.FC<GalleryItemMenuItemsProps> = ({
|
|||
</div>
|
||||
) : (
|
||||
<div className="absolute left-full top-0 -ml-1 pl-2 z-50">
|
||||
<div className="max-h-64 overflow-y-auto w-max min-w-36 rounded-lg border border-ui-panel-border bg-ui-panel p-1 shadow-xl">
|
||||
<div className="max-h-64 overflow-y-auto w-max min-w-36 rounded-[3px] border border-ui-panel-border bg-ui-panel p-1 shadow-xl">
|
||||
<FolderList
|
||||
item={item}
|
||||
folders={folders}
|
||||
|
|
@ -165,7 +174,7 @@ export const GalleryItemMenuItems: React.FC<GalleryItemMenuItemsProps> = ({
|
|||
close();
|
||||
}}
|
||||
>
|
||||
<FolderMinusIcon className="text-base-fg w-4" />
|
||||
<FolderMinusIcon className="text-base-fg w-4" />
|
||||
<span>Remove from folder</span>
|
||||
</button>
|
||||
)}
|
||||
|
|
@ -173,14 +182,14 @@ export const GalleryItemMenuItems: React.FC<GalleryItemMenuItemsProps> = ({
|
|||
{onDelete && (
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 px-2 py-2 rounded-md hover:bg-ui-controls/60 text-sm whitespace-nowrap"
|
||||
className="flex w-full items-center gap-2 rounded-[3px] px-2 py-2 hover:bg-ui-controls/60 text-sm whitespace-nowrap"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
close();
|
||||
onDelete();
|
||||
}}
|
||||
>
|
||||
<Trash2Icon className="text-red w-4" />
|
||||
<Trash2Icon className="text-red w-4" />
|
||||
<span className="text-red">Delete</span>
|
||||
</button>
|
||||
)}
|
||||
|
|
@ -209,7 +218,7 @@ const FolderList = ({
|
|||
<button
|
||||
key={folder.id}
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 px-2 py-1.5 rounded-md hover:bg-ui-controls/60 text-base-fg text-sm"
|
||||
className="flex w-full items-center gap-2 rounded-[3px] px-2 py-1.5 hover:bg-ui-controls/60 text-base-fg text-sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onAddToFolder?.([item.id], folder.id);
|
||||
|
|
@ -217,9 +226,9 @@ const FolderList = ({
|
|||
}}
|
||||
>
|
||||
<FolderIcon
|
||||
|
||||
className={folder.colorCode ? "text-xs" : "text-primary text-xs"}
|
||||
style={folder.colorCode ? { color: folder.colorCode } : undefined} />
|
||||
style={folder.colorCode ? { color: folder.colorCode } : undefined}
|
||||
/>
|
||||
<span className="truncate">{folder.name}</span>
|
||||
</button>
|
||||
))}
|
||||
|
|
@ -228,14 +237,14 @@ const FolderList = ({
|
|||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 px-2 py-1.5 rounded-md hover:bg-ui-controls/60 text-base-fg/70 text-sm whitespace-nowrap"
|
||||
className="flex w-full items-center gap-2 rounded-[3px] px-2 py-1.5 hover:bg-ui-controls/60 text-base-fg/70 text-sm whitespace-nowrap"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
closeAll();
|
||||
onCreateFolderFromMenu?.();
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="text-xs w-4" />
|
||||
<PlusIcon className="text-xs w-4" />
|
||||
<span>New Folder</span>
|
||||
</button>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ const SkeletonGrid = ({ columns }: { columns: number }) => {
|
|||
<div className="space-y-6 p-4 animate-in fade-in duration-200">
|
||||
{/* Fake date heading */}
|
||||
<div>
|
||||
<div className="h-4 w-24 rounded bg-white/[0.06] mb-3" />
|
||||
<div className="h-4 w-24 bg-white/[0.06] mb-3" />
|
||||
<div
|
||||
className={twMerge("grid", gapClass)}
|
||||
style={{
|
||||
|
|
@ -143,7 +143,7 @@ const SkeletonGrid = ({ columns }: { columns: number }) => {
|
|||
}}
|
||||
>
|
||||
{Array.from({ length: SKELETON_COUNT }).map((_, i) => (
|
||||
<div key={i} className="aspect-square rounded-md overflow-hidden">
|
||||
<div key={i} className="aspect-square overflow-hidden">
|
||||
<div
|
||||
className="h-full w-full bg-white/[0.06]"
|
||||
style={{
|
||||
|
|
@ -477,7 +477,7 @@ const BulkThumb = ({
|
|||
const showImage = !!thumbnail && !failed;
|
||||
|
||||
return (
|
||||
<div className="h-8 w-8 rounded overflow-hidden border-2 border-ui-panel bg-black/30 flex-shrink-0">
|
||||
<div className="h-8 w-8 overflow-hidden border-2 border-ui-panel bg-black/30 flex-shrink-0">
|
||||
{showImage ? (
|
||||
<img
|
||||
src={thumbnail}
|
||||
|
|
@ -523,14 +523,14 @@ const FolderContextMenuItems = ({
|
|||
}) => (
|
||||
<div
|
||||
className={twMerge(
|
||||
"min-w-44 rounded-lg border border-ui-panel-border bg-ui-panel p-1 shadow-xl z-[59]",
|
||||
"min-w-44 rounded-[3px] border border-ui-panel-border bg-ui-panel p-1 shadow-xl z-[59]",
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 px-2 py-2 rounded-md hover:bg-ui-controls/60 text-sm text-base-fg"
|
||||
className="flex w-full items-center gap-2 rounded-[3px] px-2 py-2 hover:bg-ui-controls/60 text-sm text-base-fg"
|
||||
onClick={() => onToggleStar(folderId, !hasStar)}
|
||||
>
|
||||
<StarIcon
|
||||
|
|
@ -548,7 +548,7 @@ const FolderContextMenuItems = ({
|
|||
<div className="mx-1.5 my-1 border-t border-ui-panel-border" />
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 px-2 py-2 rounded-md hover:bg-ui-controls/60 text-sm text-base-fg"
|
||||
className="flex w-full items-center gap-2 rounded-[3px] px-2 py-2 hover:bg-ui-controls/60 text-sm text-base-fg"
|
||||
onClick={() => onNewSubfolder(folderId)}
|
||||
>
|
||||
<FolderPlusIcon className="w-4" />
|
||||
|
|
@ -556,7 +556,7 @@ const FolderContextMenuItems = ({
|
|||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 px-2 py-2 rounded-md hover:bg-ui-controls/60 text-sm text-base-fg"
|
||||
className="flex w-full items-center gap-2 rounded-[3px] px-2 py-2 hover:bg-ui-controls/60 text-sm text-base-fg"
|
||||
onClick={() => onRename(folderId)}
|
||||
>
|
||||
<PencilIcon className="w-4" />
|
||||
|
|
@ -564,7 +564,7 @@ const FolderContextMenuItems = ({
|
|||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 px-2 py-2 rounded-md hover:bg-ui-controls/60 text-sm text-red"
|
||||
className="flex w-full items-center gap-2 rounded-[3px] px-2 py-2 hover:bg-ui-controls/60 text-sm text-red"
|
||||
onClick={() => onDelete(folderId)}
|
||||
>
|
||||
<Trash2Icon className="w-4" />
|
||||
|
|
@ -2581,7 +2581,7 @@ export const GalleryModal = React.memo(
|
|||
}
|
||||
}}
|
||||
className={twMerge(
|
||||
"h-[780px] max-h-[90vh] w-full max-w-5xl rounded-2xl",
|
||||
"h-[780px] max-h-[90vh] w-full max-w-5xl",
|
||||
mode === "view" &&
|
||||
"h-[780px] min-h-[640px] min-w-[66rem] w-[66rem] max-w-none",
|
||||
)}
|
||||
|
|
@ -2597,18 +2597,18 @@ export const GalleryModal = React.memo(
|
|||
</Modal.DragHandle>
|
||||
)}
|
||||
<div className="relative flex h-full flex-col">
|
||||
<div className="border-b border-ui-panel-border p-4 py-3 bg-ui-panel rounded-t-xl">
|
||||
<div className="border-b border-ui-panel-border p-4 py-3 bg-ui-panel">
|
||||
<div className="flex flex-wrap justify-between items-center gap-y-2">
|
||||
<div className="flex items-center gap-3 flex-wrap gap-y-1">
|
||||
{(mode === "view" || mode === "select") && (
|
||||
<div className="flex items-center gap-0.5 rounded-lg bg-ui-controls/40 p-0.5 relative z-[51]">
|
||||
<div className="flex items-center gap-0.5 rounded-[3px] bg-ui-controls/40 p-0.5 relative z-[51]">
|
||||
{(["unsorted", "folders"] as const).map((t) => (
|
||||
<button
|
||||
key={t}
|
||||
type="button"
|
||||
onClick={() => switchGalleryTab(t)}
|
||||
className={twMerge(
|
||||
"rounded-md px-3 py-1 text-sm font-medium transition-colors",
|
||||
"rounded-[3px] px-3 py-1 text-sm font-medium transition-colors",
|
||||
galleryTab === t
|
||||
? "bg-ui-controls text-base-fg"
|
||||
: "text-base-fg/60 hover:text-base-fg",
|
||||
|
|
@ -2625,7 +2625,7 @@ export const GalleryModal = React.memo(
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => handleOpenNewFolderModal(null)}
|
||||
className="flex items-center gap-2 rounded-lg bg-ui-controls/60 px-3 py-1.5 text-sm font-medium text-base-fg hover:bg-ui-controls/90 transition-colors relative z-[51]"
|
||||
className="flex items-center gap-2 rounded-[3px] bg-ui-controls/60 px-3 py-1.5 text-sm font-medium text-base-fg hover:bg-ui-controls/90 transition-colors relative z-[51]"
|
||||
>
|
||||
<FolderPlusIcon className="text-xs" />
|
||||
New folder
|
||||
|
|
@ -2654,7 +2654,7 @@ export const GalleryModal = React.memo(
|
|||
type="button"
|
||||
onClick={handleCloseTagView}
|
||||
aria-label="Close tag view"
|
||||
className="flex h-7 w-7 items-center justify-center rounded-full hover:bg-ui-controls/60 text-base-fg/60 hover:text-base-fg transition-colors"
|
||||
className="flex h-7 w-7 items-center justify-center rounded-[3px] hover:bg-ui-controls/60 text-base-fg/60 hover:text-base-fg transition-colors"
|
||||
>
|
||||
<XIcon className="text-sm" />
|
||||
</button>
|
||||
|
|
@ -2668,7 +2668,7 @@ export const GalleryModal = React.memo(
|
|||
type="button"
|
||||
onClick={handleCloseTagView}
|
||||
aria-label="Close tags"
|
||||
className="flex h-7 w-7 items-center justify-center rounded-full hover:bg-ui-controls/60 text-base-fg/60 hover:text-base-fg transition-colors"
|
||||
className="flex h-7 w-7 items-center justify-center rounded-[3px] hover:bg-ui-controls/60 text-base-fg/60 hover:text-base-fg transition-colors"
|
||||
>
|
||||
<XIcon className="text-sm" />
|
||||
</button>
|
||||
|
|
@ -2709,12 +2709,12 @@ export const GalleryModal = React.memo(
|
|||
if (e.key === "Escape") setRenamingFolderId(null);
|
||||
}}
|
||||
onBlur={handleConfirmRename}
|
||||
className="text-lg font-semibold bg-transparent rounded px-1.5 py-0 text-base-fg outline-none min-w-[6rem] caret-primary"
|
||||
className="text-lg font-semibold bg-transparent px-1.5 py-0 text-base-fg outline-none min-w-[6rem] caret-primary"
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<h2
|
||||
className="text-lg font-semibold cursor-pointer hover:bg-ui-controls/30 rounded px-1.5 py-0 transition-colors truncate max-w-[20rem]"
|
||||
className="text-lg font-semibold cursor-pointer rounded-[3px] hover:bg-ui-controls/30 px-1.5 py-0 transition-colors truncate max-w-[20rem]"
|
||||
onClick={() => handleStartRename(activeFolderId!)}
|
||||
>
|
||||
{activeFolder.name}
|
||||
|
|
@ -2732,7 +2732,7 @@ export const GalleryModal = React.memo(
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setFolderMenuOpen((v) => !v)}
|
||||
className="flex h-7 w-7 items-center justify-center rounded-full hover:bg-ui-controls/60 text-base-fg/60 hover:text-base-fg transition-colors"
|
||||
className="flex h-7 w-7 items-center justify-center rounded-[3px] hover:bg-ui-controls/60 text-base-fg/60 hover:text-base-fg transition-colors"
|
||||
>
|
||||
<EllipsisIcon className="text-sm" />
|
||||
</button>
|
||||
|
|
@ -2916,7 +2916,7 @@ export const GalleryModal = React.memo(
|
|||
}
|
||||
}}
|
||||
className={twMerge(
|
||||
"flex items-center justify-between gap-2 rounded-md px-2.5 py-1.5 text-sm transition-colors",
|
||||
"flex items-center justify-between gap-2 rounded-[3px] px-2.5 py-1.5 text-sm transition-colors",
|
||||
isActive &&
|
||||
!activeFolderId &&
|
||||
!activeTagToken &&
|
||||
|
|
@ -2949,7 +2949,7 @@ export const GalleryModal = React.memo(
|
|||
type="button"
|
||||
onClick={() => handleOpenNewFolderModal(null)}
|
||||
aria-label="New folder"
|
||||
className="flex h-5 w-5 items-center justify-center rounded text-base-fg/50 hover:bg-ui-controls/60 hover:text-base-fg transition-colors"
|
||||
className="flex h-5 w-5 items-center justify-center rounded-[3px] text-base-fg/50 hover:bg-ui-controls/60 hover:text-base-fg transition-colors"
|
||||
>
|
||||
<PlusIcon className="text-[10px]" />
|
||||
</button>
|
||||
|
|
@ -2982,7 +2982,7 @@ export const GalleryModal = React.memo(
|
|||
: undefined
|
||||
}
|
||||
className={twMerge(
|
||||
"flex w-full flex-shrink-0 items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm transition-colors [&.folder-drag-over]:bg-primary/20 [&.folder-drag-over]:text-base-fg",
|
||||
"flex w-full flex-shrink-0 items-center gap-2.5 rounded-[3px] px-2.5 py-1.5 text-sm transition-colors [&.folder-drag-over]:bg-primary/20 [&.folder-drag-over]:text-base-fg",
|
||||
activeRootFolderId === folder.id
|
||||
? "bg-ui-controls/60 text-base-fg font-medium"
|
||||
: "text-base-fg/70 hover:bg-ui-controls/30 hover:text-base-fg",
|
||||
|
|
@ -3025,7 +3025,7 @@ export const GalleryModal = React.memo(
|
|||
handleOpenTagFromSidebar(tag.tag_token)
|
||||
}
|
||||
className={twMerge(
|
||||
"flex w-full flex-shrink-0 items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm transition-colors",
|
||||
"flex w-full flex-shrink-0 items-center gap-2.5 rounded-[3px] px-2.5 py-1.5 text-sm transition-colors",
|
||||
activeTagToken === tag.tag_token
|
||||
? "bg-ui-controls/60 text-base-fg font-medium"
|
||||
: "text-base-fg/70 hover:bg-ui-controls/30 hover:text-base-fg",
|
||||
|
|
@ -3042,7 +3042,7 @@ export const GalleryModal = React.memo(
|
|||
type="button"
|
||||
onClick={handleOpenTagBrowser}
|
||||
className={twMerge(
|
||||
"flex w-full flex-shrink-0 items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm transition-colors",
|
||||
"flex w-full flex-shrink-0 items-center gap-2.5 rounded-[3px] px-2.5 py-1.5 text-sm transition-colors",
|
||||
tagBrowserOpen && !activeTagToken
|
||||
? "bg-ui-controls/60 text-base-fg font-medium"
|
||||
: "text-base-fg/50 hover:bg-ui-controls/30 hover:text-base-fg",
|
||||
|
|
@ -3079,7 +3079,7 @@ export const GalleryModal = React.memo(
|
|||
</div>
|
||||
) : activeTagItems.length === 0 ? (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3">
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-violet-400/10">
|
||||
<div className="flex h-16 w-16 items-center justify-center bg-violet-400/10">
|
||||
<TagIcon className="text-violet-400 text-2xl" />
|
||||
</div>
|
||||
<div className="text-base-fg font-semibold">
|
||||
|
|
@ -3170,7 +3170,7 @@ export const GalleryModal = React.memo(
|
|||
/* ── All-tags word cloud ── */
|
||||
userTags.length === 0 ? (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3">
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-violet-400/10">
|
||||
<div className="flex h-16 w-16 items-center justify-center bg-violet-400/10">
|
||||
<TagIcon className="text-violet-400 text-2xl" />
|
||||
</div>
|
||||
<div className="text-base-fg font-semibold">
|
||||
|
|
@ -3191,7 +3191,7 @@ export const GalleryModal = React.memo(
|
|||
/* ── Folders tab, root: folder cards only (above) ── */
|
||||
currentSubfolders.length === 0 ? (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3">
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-primary/10">
|
||||
<div className="flex h-16 w-16 items-center justify-center bg-primary/10">
|
||||
<FolderIcon className="text-primary text-2xl" />
|
||||
</div>
|
||||
<div className="text-base-fg font-semibold">
|
||||
|
|
@ -3223,7 +3223,7 @@ export const GalleryModal = React.memo(
|
|||
) : activeFolderItems.length === 0 ? (
|
||||
currentSubfolders.length > 0 ? null : (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3">
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-primary/10">
|
||||
<div className="flex h-16 w-16 items-center justify-center bg-primary/10">
|
||||
<FolderIcon className="text-primary text-2xl" />
|
||||
</div>
|
||||
<div className="text-base-fg font-semibold">
|
||||
|
|
@ -3341,7 +3341,7 @@ export const GalleryModal = React.memo(
|
|||
</div>
|
||||
{onLoginClick ? (
|
||||
<button
|
||||
className="rounded-full bg-primary px-5 py-2 text-sm font-semibold text-white transition-colors hover:bg-primary/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
|
||||
className="rounded-[3px] bg-primary px-5 py-2 text-sm font-semibold text-white transition-colors hover:bg-primary/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
|
||||
onClick={() => onLoginClick()}
|
||||
>
|
||||
Log in
|
||||
|
|
@ -3523,7 +3523,7 @@ export const GalleryModal = React.memo(
|
|||
/>
|
||||
|
||||
{mode === "view" && bulkSelectionMode && (
|
||||
<div className="flex items-center justify-between border-t border-ui-panel-border bg-ui-background p-3 py-2 rounded-b-xl">
|
||||
<div className="flex items-center justify-between border-t border-ui-panel-border bg-ui-background p-3 py-2">
|
||||
<div className="flex items-center">
|
||||
{/* Thumbnail previews of selected items */}
|
||||
<div className="flex">
|
||||
|
|
@ -3548,7 +3548,7 @@ export const GalleryModal = React.memo(
|
|||
})}
|
||||
</div>
|
||||
{bulkSelectedItems.length > 4 && (
|
||||
<div className="h-8 w-8 rounded overflow-hidden border-2 border-ui-panel bg-black/20 flex-shrink-0 flex items-center justify-center">
|
||||
<div className="h-8 w-8 overflow-hidden border-2 border-ui-panel bg-black/20 flex-shrink-0 flex items-center justify-center">
|
||||
<span className="text-[11px] font-normal text-white/70">
|
||||
+{bulkSelectedItems.length - 4}
|
||||
</span>
|
||||
|
|
@ -3576,7 +3576,7 @@ export const GalleryModal = React.memo(
|
|||
className="fixed inset-0 z-[59]"
|
||||
onClick={() => setBulkFolderPopoverOpen(false)}
|
||||
/>
|
||||
<div className="absolute bottom-full mb-2 right-0 w-56 rounded-lg border border-ui-panel-border bg-ui-panel p-2 shadow-xl z-[60]">
|
||||
<div className="absolute bottom-full mb-2 right-0 w-56 rounded-[3px] border border-ui-panel-border bg-ui-panel p-2 shadow-xl z-[60]">
|
||||
{/* Folders */}
|
||||
<div className="text-[11px] font-semibold uppercase tracking-wider text-base-fg/40 px-2 py-1">
|
||||
Folders
|
||||
|
|
@ -3592,7 +3592,7 @@ export const GalleryModal = React.memo(
|
|||
<button
|
||||
key={folder.id}
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2.5 px-2 py-1.5 rounded-md hover:bg-ui-controls/50 text-sm text-base-fg transition-colors"
|
||||
className="flex w-full items-center gap-2.5 rounded-[3px] px-2 py-1.5 hover:bg-ui-controls/50 text-sm text-base-fg transition-colors"
|
||||
onClick={() => {
|
||||
const ids = Array.from(bulkSelectedIds);
|
||||
requestFolderDrop(ids, folder.id);
|
||||
|
|
@ -3607,7 +3607,7 @@ export const GalleryModal = React.memo(
|
|||
<div className="mx-1.5 my-1 border-t border-ui-panel-border" />
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2.5 px-2 py-1.5 rounded-md hover:bg-ui-controls/50 text-sm text-base-fg/70 transition-colors"
|
||||
className="flex w-full items-center gap-2.5 rounded-[3px] px-2 py-1.5 hover:bg-ui-controls/50 text-sm text-base-fg/70 transition-colors"
|
||||
onClick={() => {
|
||||
const ids = Array.from(bulkSelectedIds);
|
||||
setBulkFolderPopoverOpen(false);
|
||||
|
|
@ -3633,7 +3633,7 @@ export const GalleryModal = React.memo(
|
|||
)}
|
||||
<button
|
||||
onClick={clearBulkSelection}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-md bg-ui-controls/60 hover:bg-ui-controls/90 text-base-fg transition-colors"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-[3px] bg-ui-controls/60 hover:bg-ui-controls/90 text-base-fg transition-colors"
|
||||
aria-label="Clear selection"
|
||||
>
|
||||
<XIcon className="text-base" />
|
||||
|
|
@ -3643,7 +3643,7 @@ export const GalleryModal = React.memo(
|
|||
)}
|
||||
|
||||
{mode === "select" && (
|
||||
<div className="flex items-center justify-between border-t border-ui-panel-border bg-ui-panel p-4 rounded-b-xl">
|
||||
<div className="flex items-center justify-between border-t border-ui-panel-border bg-ui-panel p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-sm font-semibold text-base-fg/80">
|
||||
{selectedItemIds.length}/{maxSelections} selected
|
||||
|
|
@ -3756,7 +3756,7 @@ export const GalleryModal = React.memo(
|
|||
createPortal(
|
||||
<div
|
||||
ref={marqueeRef}
|
||||
className="pointer-events-none fixed z-[9999] rounded-sm border border-primary/60 bg-primary/10"
|
||||
className="pointer-events-none fixed z-[9999] border border-primary/60 bg-primary/10"
|
||||
style={{ display: "none" }}
|
||||
/>,
|
||||
document.body,
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export function CopyPromptButton({
|
|||
type="button"
|
||||
onClick={handleCopy}
|
||||
aria-label="Copy prompt"
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-white/40 transition-colors hover:bg-white/10 hover:text-white"
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-[3px] text-white/40 transition-colors hover:bg-white/10 hover:text-white"
|
||||
>
|
||||
<DynamicIcon icon={copied ? CheckIcon : CopyIcon} className="text-sm" />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export const FailedCard = memo(function FailedCard({
|
|||
const iconPath = modelId ? getCreatorIconPathForModelId(modelId) : null;
|
||||
|
||||
return (
|
||||
<div className="relative aspect-square w-full overflow-hidden rounded-lg bg-red-500/10">
|
||||
<div className="relative aspect-square w-full overflow-hidden rounded-[3px] bg-red-500/10">
|
||||
{refImageUrl && (
|
||||
<img
|
||||
src={refImageUrl}
|
||||
|
|
@ -42,9 +42,7 @@ export const FailedCard = memo(function FailedCard({
|
|||
/>
|
||||
)}
|
||||
<div className="relative flex h-full flex-col items-center justify-center gap-2 px-4 sm:px-6">
|
||||
<CircleAlertIcon
|
||||
|
||||
className="text-2xl text-red-400" />
|
||||
<CircleAlertIcon className="text-2xl text-red-400" />
|
||||
<span className="text-center text-xs font-medium text-red-400">
|
||||
{failureReason || "Generation failed"}
|
||||
</span>
|
||||
|
|
@ -58,7 +56,7 @@ export const FailedCard = memo(function FailedCard({
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => onDismiss(id)}
|
||||
className="flex items-center gap-1.5 rounded-md bg-white/5 px-3 py-1.5 text-xs text-white/50 transition-colors hover:bg-white/10 hover:text-white/70"
|
||||
className="flex items-center gap-1.5 rounded-[3px] bg-white/5 px-3 py-1.5 text-xs text-white/50 transition-colors hover:bg-white/10 hover:text-white/70"
|
||||
>
|
||||
<XIcon />
|
||||
Dismiss
|
||||
|
|
|
|||
|
|
@ -35,9 +35,9 @@ export const FailedRow = memo(function FailedRow({
|
|||
const iconPath = modelId ? getCreatorIconPathForModelId(modelId) : null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 rounded-lg px-2.5 py-2">
|
||||
<div className="flex items-center gap-3 px-2.5 py-2">
|
||||
{/* Error thumbnail — ref image (if any) faded behind the warning icon */}
|
||||
<div className="relative flex size-[100px] shrink-0 items-center justify-center overflow-hidden rounded-md bg-red-500/10 leading-none">
|
||||
<div className="relative flex size-[100px] shrink-0 items-center justify-center overflow-hidden bg-red-500/10 leading-none">
|
||||
{refImageUrl && (
|
||||
<img
|
||||
src={refImageUrl}
|
||||
|
|
@ -46,9 +46,7 @@ export const FailedRow = memo(function FailedRow({
|
|||
className="pointer-events-none absolute inset-0 h-full w-full object-cover opacity-10"
|
||||
/>
|
||||
)}
|
||||
<CircleAlertIcon
|
||||
|
||||
className="relative text-2xl text-red-400" />
|
||||
<CircleAlertIcon className="relative text-2xl text-red-400" />
|
||||
</div>
|
||||
|
||||
{/* Reason + prompt + model */}
|
||||
|
|
@ -87,7 +85,7 @@ export const FailedRow = memo(function FailedRow({
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => onDismiss(id)}
|
||||
className="flex shrink-0 items-center gap-1.5 rounded-md bg-white/5 px-3 py-1.5 text-xs text-white/50 transition-colors hover:bg-white/10 hover:text-white/70"
|
||||
className="flex shrink-0 items-center gap-1.5 rounded-[3px] bg-white/5 px-3 py-1.5 text-xs text-white/50 transition-colors hover:bg-white/10 hover:text-white/70"
|
||||
>
|
||||
<XIcon />
|
||||
Dismiss
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export function GalleryAutoplayToggle() {
|
|||
<div
|
||||
role="group"
|
||||
aria-label="Video preview playback"
|
||||
className="flex items-center gap-0.5 rounded-lg border border-white/[0.08] bg-white/[0.04] p-0.5"
|
||||
className="flex items-center gap-0.5 rounded-[3px] border border-white/[0.08] bg-white/[0.04] p-0.5"
|
||||
>
|
||||
{OPTIONS.map(({ autoplay, icon, label }) => {
|
||||
const active = autoplayVideos === autoplay;
|
||||
|
|
@ -35,7 +35,7 @@ export function GalleryAutoplayToggle() {
|
|||
aria-label={label}
|
||||
aria-pressed={active}
|
||||
onClick={() => setAutoplayVideos(autoplay)}
|
||||
className={`flex h-6 w-6 items-center justify-center rounded-md text-xs transition-colors ${
|
||||
className={`flex h-6 w-6 items-center justify-center rounded-[3px] text-xs transition-colors ${
|
||||
active
|
||||
? "bg-white/10 text-white"
|
||||
: "text-white/45 hover:text-white/80"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
import { memo, useCallback, useState, type ReactNode } from "react";
|
||||
import { BoxIcon, CheckIcon, EyeIcon, ImageIcon, MusicIcon, VideoIcon } from "lucide-react";
|
||||
import {
|
||||
BoxIcon,
|
||||
CheckIcon,
|
||||
EyeIcon,
|
||||
ImageIcon,
|
||||
MusicIcon,
|
||||
VideoIcon,
|
||||
} from "lucide-react";
|
||||
import { DynamicIcon } from "@storyteller/icons";
|
||||
import {
|
||||
getCreatorIconPathForModelId,
|
||||
|
|
@ -165,7 +172,7 @@ export const GalleryCard = memo(function GalleryCard({
|
|||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={`group relative block w-full rounded-lg bg-ui-controls/40 leading-none transition-shadow hover:ring-2 hover:ring-primary-400/60 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-400 cursor-pointer ${isSquare ? "aspect-square" : ""} ${selected ? "ring-2 ring-primary-400" : lastViewed ? "ring-2 ring-primary-400/50" : ""}`}
|
||||
className={`group relative block w-full rounded-[3px] bg-ui-controls/40 leading-none transition-shadow hover:ring-2 hover:ring-primary-400/60 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-400 cursor-pointer ${isSquare ? "aspect-square" : ""} ${selected ? "ring-2 ring-primary-400" : lastViewed ? "ring-2 ring-primary-400/50" : ""}`}
|
||||
style={outerStyle}
|
||||
onClick={handleCardClick}
|
||||
onKeyDown={handleCardKeyDown}
|
||||
|
|
@ -187,9 +194,7 @@ export const GalleryCard = memo(function GalleryCard({
|
|||
</p>
|
||||
<div className="flex min-h-0 flex-1 items-center justify-center">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-white/10 ring-1 ring-white/15">
|
||||
<MusicIcon
|
||||
|
||||
className="text-xl text-white/70" />
|
||||
<MusicIcon className="text-xl text-white/70" />
|
||||
</div>
|
||||
</div>
|
||||
{item.fullImage && (
|
||||
|
|
@ -216,13 +221,13 @@ export const GalleryCard = memo(function GalleryCard({
|
|||
{/* Selection checkbox chip (select mode only) */}
|
||||
{selectMode && item.fullImage && (
|
||||
<div
|
||||
className={`pointer-events-none absolute left-2 top-2 z-10 flex h-5 w-5 items-center justify-center rounded-md border transition-colors ${
|
||||
className={`pointer-events-none absolute left-2 top-2 z-10 flex h-5 w-5 items-center justify-center rounded-[3px] border transition-colors ${
|
||||
selected
|
||||
? "border-primary-400 bg-primary-400 text-white"
|
||||
: "border-white/60 bg-black/40 text-transparent"
|
||||
}`}
|
||||
>
|
||||
<CheckIcon className="text-[10px]" />
|
||||
<CheckIcon className="text-[10px]" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -230,21 +235,21 @@ export const GalleryCard = memo(function GalleryCard({
|
|||
in the lightbox). Top-right so it never collides with the selection
|
||||
checkbox chip at top-left. */}
|
||||
{lastViewed && (
|
||||
<div className="pointer-events-none absolute right-2 top-2 z-10 flex items-center gap-1 rounded-full bg-black/70 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-white/80">
|
||||
<div className="pointer-events-none absolute right-2 top-2 z-10 flex items-center gap-1 rounded-[3px] bg-black/70 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-white/80">
|
||||
<EyeIcon />
|
||||
Last viewed
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hover overlay with media type + model badges and quick actions */}
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 flex items-end justify-between gap-2 bg-gradient-to-t rounded-b-lg from-black/70 to-transparent px-2 pb-2 pt-6 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 flex items-end justify-between gap-2 bg-gradient-to-t rounded-b-[3px] from-black/70 to-transparent px-2 pb-2 pt-6 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<div className="pointer-events-auto flex min-w-0 flex-wrap items-center gap-1.5">
|
||||
<div className="flex items-center gap-1.5 rounded-lg bg-black/60 px-2.5 py-1 text-xs font-medium text-white/90">
|
||||
<div className="flex items-center gap-1.5 bg-black/60 px-2.5 py-1 text-xs font-medium text-white/90">
|
||||
<DynamicIcon icon={mediaIcon} className="text-[10px]" />
|
||||
{mediaLabel}
|
||||
</div>
|
||||
{modelDisplayName && modelIconPath && (
|
||||
<div className="flex items-center gap-1 rounded-lg bg-black/60 px-2 py-1 text-[10px] text-white/80">
|
||||
<div className="flex items-center gap-1 bg-black/60 px-2 py-1 text-[10px] text-white/80">
|
||||
<img
|
||||
src={modelIconPath}
|
||||
alt=""
|
||||
|
|
@ -256,7 +261,7 @@ export const GalleryCard = memo(function GalleryCard({
|
|||
</div>
|
||||
|
||||
{actionsSlot && !selectMode && (
|
||||
<div className="pointer-events-auto flex shrink-0 items-center gap-0.5 rounded-lg bg-black/60 p-1 backdrop-blur-sm">
|
||||
<div className="pointer-events-auto flex shrink-0 items-center gap-0.5 rounded-[3px] bg-black/60 p-1 backdrop-blur-sm">
|
||||
{actionsSlot}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
import { memo, useCallback, type ReactNode } from "react";
|
||||
import { BoxIcon, CheckIcon, EyeIcon, ImageIcon, MusicIcon, PlayIcon, VideoIcon } from "lucide-react";
|
||||
import {
|
||||
BoxIcon,
|
||||
CheckIcon,
|
||||
EyeIcon,
|
||||
ImageIcon,
|
||||
MusicIcon,
|
||||
PlayIcon,
|
||||
VideoIcon,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
getCreatorIconPathForModelId,
|
||||
getModelDisplayName,
|
||||
|
|
@ -54,7 +62,13 @@ export const GalleryRow = memo(function GalleryRow({
|
|||
: isAudio
|
||||
? MusicIcon
|
||||
: ImageIcon;
|
||||
const mediaLabel = isVideo ? "Video" : is3D ? "3D" : isAudio ? "Audio" : "Image";
|
||||
const mediaLabel = isVideo
|
||||
? "Video"
|
||||
: is3D
|
||||
? "3D"
|
||||
: isAudio
|
||||
? "Audio"
|
||||
: "Image";
|
||||
|
||||
const effectiveModelId = modelId ?? item.modelId;
|
||||
const modelDisplayName = effectiveModelId
|
||||
|
|
@ -90,23 +104,23 @@ export const GalleryRow = memo(function GalleryRow({
|
|||
tabIndex={0}
|
||||
onClick={handleRowClick}
|
||||
onKeyDown={handleRowKeyDown}
|
||||
className={`group flex cursor-pointer items-center gap-3 rounded-lg px-2.5 py-2 transition-colors hover:bg-white/[0.04] focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-400/60 ${selected ? "bg-primary-400/10" : ""}`}
|
||||
className={`group flex cursor-pointer items-center gap-3 rounded-[3px] px-2.5 py-2 transition-colors hover:bg-white/[0.04] focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-400/60 ${selected ? "bg-primary-400/10" : ""}`}
|
||||
>
|
||||
{/* Selection checkbox (select mode only) */}
|
||||
{selectMode && (
|
||||
<div
|
||||
className={`pointer-events-none flex h-5 w-5 shrink-0 items-center justify-center rounded-md border transition-colors ${
|
||||
className={`pointer-events-none flex h-5 w-5 shrink-0 items-center justify-center rounded-[3px] border transition-colors ${
|
||||
selected
|
||||
? "border-primary-400 bg-primary-400 text-white"
|
||||
: "border-white/60 bg-black/40 text-transparent"
|
||||
} ${item.fullImage ? "" : "invisible"}`}
|
||||
>
|
||||
<CheckIcon className="text-[10px]" />
|
||||
<CheckIcon className="text-[10px]" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Thumbnail */}
|
||||
<div className={`relative size-[100px] shrink-0 overflow-hidden rounded-md bg-ui-controls/40 leading-none ${lastViewed ? "ring-2 ring-primary-400/50" : ""}`}>
|
||||
<div className={`relative size-[100px] shrink-0 overflow-hidden bg-ui-controls/40 leading-none ${lastViewed ? "ring-2 ring-primary-400/50" : ""}`}>
|
||||
<GalleryThumbnail
|
||||
thumbnail={item.thumbnail}
|
||||
stillThumbnail={item.stillThumbnail}
|
||||
|
|
@ -119,9 +133,7 @@ export const GalleryRow = memo(function GalleryRow({
|
|||
{isVideo && item.thumbnail && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-black/55 backdrop-blur-sm">
|
||||
<PlayIcon
|
||||
|
||||
className="ml-0.5 text-[9px] text-white/90" />
|
||||
<PlayIcon className="ml-0.5 text-[9px] text-white/90" />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -138,7 +150,7 @@ export const GalleryRow = memo(function GalleryRow({
|
|||
{/* Title + model / media meta */}
|
||||
<div className="min-w-0 flex-1">
|
||||
{loading ? (
|
||||
<div className="relative h-4 w-2/3 max-w-[280px] overflow-hidden rounded bg-white/[0.06]">
|
||||
<div className="relative h-4 w-2/3 max-w-[280px] overflow-hidden bg-white/[0.06]">
|
||||
<div className="animate-shimmer absolute inset-0" />
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -155,7 +167,7 @@ export const GalleryRow = memo(function GalleryRow({
|
|||
</div>
|
||||
)}
|
||||
{isAudio && item.fullImage && (
|
||||
<div className="mt-1.5 max-w-md rounded-md bg-white/[0.04]">
|
||||
<div className="mt-1.5 max-w-md bg-white/[0.04]">
|
||||
<WaveformAudioPlayer
|
||||
src={item.fullImage}
|
||||
durationMillis={item.durationMillis}
|
||||
|
|
|
|||
|
|
@ -11,14 +11,14 @@ export function GallerySelectToggle() {
|
|||
const label = active ? "Exit selection" : "Select items";
|
||||
|
||||
return (
|
||||
<div className="flex items-center rounded-lg border border-white/[0.08] bg-white/[0.04] p-0.5">
|
||||
<div className="flex items-center rounded-[3px] border border-white/[0.08] bg-white/[0.04] p-0.5">
|
||||
<Tooltip content={label} position="bottom" delay={300}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={label}
|
||||
aria-pressed={active}
|
||||
onClick={() => setActive(!active)}
|
||||
className={`flex h-6 w-6 items-center justify-center rounded-md text-xs transition-colors ${
|
||||
className={`flex h-6 w-6 items-center justify-center rounded-[3px] text-xs transition-colors ${
|
||||
active
|
||||
? "bg-white/10 text-white"
|
||||
: "text-white/45 hover:text-white/80"
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export function GalleryViewToggle() {
|
|||
<div
|
||||
role="group"
|
||||
aria-label="Gallery layout"
|
||||
className="flex items-center gap-0.5 rounded-lg border border-white/[0.08] bg-white/[0.04] p-0.5"
|
||||
className="flex items-center gap-0.5 rounded-[3px] border border-white/[0.08] bg-white/[0.04] p-0.5"
|
||||
>
|
||||
{OPTIONS.map(({ mode, icon, label }) => {
|
||||
const active = viewMode === mode;
|
||||
|
|
@ -37,7 +37,7 @@ export function GalleryViewToggle() {
|
|||
aria-label={label}
|
||||
aria-pressed={active}
|
||||
onClick={() => setViewMode(mode)}
|
||||
className={`flex h-6 w-6 items-center justify-center rounded-md text-xs transition-colors ${
|
||||
className={`flex h-6 w-6 items-center justify-center rounded-[3px] text-xs transition-colors ${
|
||||
active
|
||||
? "bg-white/10 text-white"
|
||||
: "text-white/45 hover:text-white/80"
|
||||
|
|
|
|||
|
|
@ -36,18 +36,16 @@ export const PendingCard = memo(function PendingCard({
|
|||
const iconPath = getCreatorIconPathForModelId(modelId);
|
||||
|
||||
return (
|
||||
<div className="group relative aspect-square w-full overflow-hidden rounded-lg bg-white/[0.03]">
|
||||
<div className="group relative aspect-square w-full overflow-hidden rounded-[3px] bg-white/[0.03]">
|
||||
<div className="animate-shimmer h-full w-full" />
|
||||
{batchCount != null && batchCount > 1 && (
|
||||
<div className="absolute left-2 right-2 top-2 z-10 rounded-md bg-black/60 px-2.5 py-1.5 text-center text-[10px] leading-snug text-white/70 backdrop-blur-sm">
|
||||
<div className="absolute left-2 right-2 top-2 z-10 bg-black/60 px-2.5 py-1.5 text-center text-[10px] leading-snug text-white/70 backdrop-blur-sm">
|
||||
Generating {batchCount} {batchNoun(mediaClass)} · Results may appear
|
||||
one at a time
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-2">
|
||||
<LoaderCircleIcon
|
||||
|
||||
className="animate-spin text-2xl text-white/20" />
|
||||
<LoaderCircleIcon className="animate-spin text-2xl text-white/20" />
|
||||
{progressPercent != null && (
|
||||
<span className="text-xs tabular-nums text-white/40">
|
||||
{progressPercent}%
|
||||
|
|
|
|||
|
|
@ -38,9 +38,9 @@ export const PendingRow = memo(function PendingRow({
|
|||
const iconPath = getCreatorIconPathForModelId(modelId);
|
||||
|
||||
return (
|
||||
<div className="group flex items-center gap-3 rounded-lg px-2.5 py-2">
|
||||
<div className="group flex items-center gap-3 px-2.5 py-2">
|
||||
{/* Thumbnail placeholder */}
|
||||
<div className="relative size-[100px] shrink-0 overflow-hidden rounded-md bg-white/[0.03] leading-none">
|
||||
<div className="relative size-[100px] shrink-0 overflow-hidden bg-white/[0.03] leading-none">
|
||||
<div className="animate-shimmer h-full w-full" />
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<LoaderCircleIcon
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ function Gravatar(props: Props) {
|
|||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"relative aspect-square overflow-hidden rounded-full border border-white",
|
||||
"relative aspect-square overflow-hidden border border-white",
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -66,16 +66,16 @@ const NewsView = ({ onBack }: { onBack: () => void }) => {
|
|||
</button>
|
||||
|
||||
<div className="article-content">
|
||||
<h1 className="text-2xl font-bold mb-2">{selectedPost.title}</h1>
|
||||
<h1 className="font-display text-2xl font-bold mb-2">{selectedPost.title}</h1>
|
||||
{selectedPost.date && (
|
||||
<p className="text-xs text-white/50 mb-4">{selectedPost.date}</p>
|
||||
<p className="font-mono text-[11px] uppercase tracking-[0.12em] text-white/50 mb-4">{selectedPost.date}</p>
|
||||
)}
|
||||
|
||||
{selectedPost.thumbnail && (
|
||||
<img
|
||||
src={selectedPost.thumbnail}
|
||||
alt={selectedPost.title}
|
||||
className="w-full h-48 object-cover rounded-lg mb-4 border border-white/10"
|
||||
className="w-full h-48 object-cover rounded-none mb-4 border border-white/15"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
@ -86,17 +86,17 @@ const NewsView = ({ onBack }: { onBack: () => void }) => {
|
|||
</div>
|
||||
<style>{`
|
||||
.article-content h1 { font-size: 1.5rem; font-weight: 700; margin: 1rem 0 0.5rem; }
|
||||
.article-content h2 { font-size: 1.25rem; font-weight: 700; margin: 1rem 0 0.5rem; color: #93c5fd; }
|
||||
.article-content h2 { font-size: 1.25rem; font-weight: 700; margin: 1rem 0 0.5rem; color: #ffffff; }
|
||||
.article-content h3 { font-size: 1.1rem; font-weight: 600; margin: 0.75rem 0 0.5rem; }
|
||||
.article-content p { margin-bottom: 0.75rem; line-height: 1.6; }
|
||||
.article-content ul { list-style: disc; padding-left: 1.25rem; margin-bottom: 0.75rem; }
|
||||
.article-content li { margin-bottom: 0.25rem; }
|
||||
.article-content img { display: block; width: 100%; max-width: 100%; height: auto; border-radius: 0.5rem; border: 1px solid rgba(255,255,255,0.1); margin: 1rem 0; }
|
||||
.article-content a { color: #3b82f6; text-decoration: none; border-bottom: 1px solid transparent; transition: border-color 0.2s; }
|
||||
.article-content a:hover { border-bottom-color: #3b82f6; }
|
||||
.article-content img { display: block; width: 100%; max-width: 100%; height: auto; border-radius: 0; border: 1px solid rgba(255,255,255,0.15); margin: 1rem 0; }
|
||||
.article-content a { color: #74aaff; text-decoration: none; border-bottom: 1px solid transparent; transition: border-color 0.2s; }
|
||||
.article-content a:hover { border-bottom-color: #74aaff; }
|
||||
|
||||
/* Video embed styles */
|
||||
.article-content .video-embed { position: relative; width: 100%; margin: 1.5rem 0; border-radius: 0.5rem; overflow: hidden; border: 1px solid rgba(255,255,255,0.1); background: #000; }
|
||||
.article-content .video-embed { position: relative; width: 100%; margin: 1.5rem 0; border-radius: 0; overflow: hidden; border: 1px solid rgba(255,255,255,0.15); background: #000; }
|
||||
.article-content .youtube-embed { padding-bottom: 56.25%; /* 16:9 aspect ratio */ height: 0; }
|
||||
.article-content .youtube-embed iframe { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
|
||||
.article-content .video-embed video { display: block; width: 100%; height: auto; max-height: 50vh; }
|
||||
|
|
@ -111,18 +111,18 @@ const NewsView = ({ onBack }: { onBack: () => void }) => {
|
|||
<div
|
||||
key={item.slug}
|
||||
onClick={() => setSelectedSlug(item.slug)}
|
||||
className="bg-white/5 hover:bg-white/10 border border-white/10 p-4 rounded-lg cursor-pointer transition-all group"
|
||||
className="bg-white/5 hover:bg-white/[0.07] border border-white/15 hover:border-white/25 p-4 rounded-none cursor-pointer transition-colors group"
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<h3 className="text-lg font-semibold group-hover:text-blue-300 transition-colors flex items-center gap-2">
|
||||
<h3 className="text-lg font-semibold group-hover:text-white transition-colors flex items-center gap-2">
|
||||
{item.title}
|
||||
<ArrowRightIcon
|
||||
|
||||
size="xs"
|
||||
className="opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all text-blue-400" />
|
||||
className="opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all text-white" />
|
||||
</h3>
|
||||
{item.date && (
|
||||
<span className="text-xs text-white/40 font-mono whitespace-nowrap ml-2">
|
||||
<span className="font-mono text-[11px] uppercase tracking-[0.12em] text-white/40 whitespace-nowrap ml-2">
|
||||
{item.date}
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -372,7 +372,7 @@ export function HelpMenuButton({
|
|||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => viewTutorial(item)}
|
||||
className="group block overflow-hidden rounded-lg border border-white/10 bg-white/5 hover:bg-white/10 text-left"
|
||||
className="group block overflow-hidden rounded-none border border-white/15 bg-white/5 transition-colors hover:border-white/25 hover:bg-white/[0.07] text-left"
|
||||
>
|
||||
<div className="aspect-video w-full overflow-hidden">
|
||||
<img
|
||||
|
|
@ -402,12 +402,12 @@ export function HelpMenuButton({
|
|||
>
|
||||
Back
|
||||
</Button>
|
||||
<div className="text-lg font-bold text-base-fg">
|
||||
<div className="font-display text-lg font-bold text-base-fg">
|
||||
Tutorial: {selected.title}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="aspect-video w-full overflow-hidden rounded-lg border border-white/10 bg-black">
|
||||
<div className="aspect-video w-full overflow-hidden rounded-none border border-white/15 bg-black">
|
||||
<div ref={playerContainerRef} className="h-full w-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -39,10 +39,10 @@ export const InputVector = ({
|
|||
oldZ.current = z;
|
||||
|
||||
const inputCommonClasses =
|
||||
"relative h-6 rounded-r-lg bg-black/25 p-2 text-sm text-white transition-all duration-100 ease-in-out outline-none -outline-offset-2 text-end w-full hover:cursor-e-resize hover:bg-black/40";
|
||||
"relative h-6 rounded-r-[3px] bg-black/25 p-2 text-sm text-white transition-all duration-100 ease-in-out outline-none -outline-offset-2 text-end w-full hover:cursor-e-resize hover:bg-black/40";
|
||||
|
||||
const wrapperCommonClasses =
|
||||
"relative flex items-center before:inline-block before:h-6 before:bg-brand-primary before:text-white before:rounded-l-lg before:w-1.5 before:text-center before:justify-center before:items-center before:font-semibold before:flex before:align-middle before:leading-8 text-xs";
|
||||
"relative flex items-center before:inline-block before:h-6 before:bg-brand-primary before:text-white before:rounded-l-[3px] before:w-1.5 before:text-center before:justify-center before:items-center before:font-semibold before:flex before:align-middle before:leading-8 text-xs";
|
||||
|
||||
const lockedCommonClasses =
|
||||
"hover:cursor-not-allowed hover:bg-brand-secondary";
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export const NumberInput: React.FC<NumberInputProps> = ({
|
|||
};
|
||||
|
||||
const incrementButtonStyle =
|
||||
"flex h-[15px] w-6 items-center justify-center text-[10px] text-white/80 bg-ui-controls hover:bg-ui-controls-button outline-none focus:outline-none transition-all active:outline-none";
|
||||
"flex h-[15px] w-6 items-center justify-center text-[10px] text-white/80 bg-ui-controls hover:bg-white/10 outline-none focus:outline-none transition-colors active:outline-none";
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
|
|
@ -60,21 +60,21 @@ export const NumberInput: React.FC<NumberInputProps> = ({
|
|||
value={inputValue}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
inputClassName="w-[70px] h-[30px] rounded-md text-sm"
|
||||
inputClassName="w-[70px] h-[30px] rounded-[3px] text-sm"
|
||||
/>
|
||||
|
||||
<div className="absolute right-0 top-0 flex h-[30px] flex-col rounded-r-md border-l border-white/15 bg-gray-800">
|
||||
<div className="absolute right-0 top-0 flex h-[30px] flex-col rounded-r-[3px] border-l border-white/15 bg-ui-controls">
|
||||
<button
|
||||
className={twMerge(
|
||||
incrementButtonStyle,
|
||||
"rounded-tr-md border-b border-white/10",
|
||||
"rounded-tr-[3px] border-b border-white/15",
|
||||
)}
|
||||
onClick={handleIncrement}
|
||||
>
|
||||
<ChevronUpIcon />
|
||||
</button>
|
||||
<button
|
||||
className={twMerge(incrementButtonStyle, "rounded-br-md")}
|
||||
className={twMerge(incrementButtonStyle, "rounded-br-[3px]")}
|
||||
onClick={handleDecrement}
|
||||
>
|
||||
<ChevronDownIcon />
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export const Input = React.forwardRef(
|
|||
errorMessage,
|
||||
...rest
|
||||
}: InputProps,
|
||||
ref: React.ForwardedRef<HTMLInputElement>
|
||||
ref: React.ForwardedRef<HTMLInputElement>,
|
||||
) => {
|
||||
return (
|
||||
<div className={twMerge("flex flex-col", className)}>
|
||||
|
|
@ -45,13 +45,13 @@ export const Input = React.forwardRef(
|
|||
ref={ref}
|
||||
id={id ? id : label ? label : undefined}
|
||||
className={twMerge(
|
||||
"h-10 w-full rounded-lg px-3 py-2.5 outline-none",
|
||||
"bg-ui-panel text-base-fg placeholder-base-fg/50",
|
||||
"border border-ui-panel-border transition-all duration-150 ease-in-out hover:border-primary/60 focus:border-primary focus:!outline-none",
|
||||
"h-10 w-full rounded-[3px] px-3 py-2.5 outline-none",
|
||||
"bg-ui-panel text-base-fg placeholder-base-fg/40",
|
||||
"border border-ui-panel-border transition-colors duration-150 ease-in-out hover:border-white/40 focus:border-white focus:!outline-none",
|
||||
"disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:border-ui-panel-border",
|
||||
icon && "pl-10",
|
||||
isError && "outline-red focus:outline-red",
|
||||
inputClassName
|
||||
isError && "border-red-500 focus:border-red-500",
|
||||
inputClassName,
|
||||
)}
|
||||
onFocus={(e: React.FocusEvent<HTMLInputElement>) => {
|
||||
if (onFocus) {
|
||||
|
|
@ -66,12 +66,12 @@ export const Input = React.forwardRef(
|
|||
{...rest}
|
||||
/>
|
||||
{errorMessage && (
|
||||
<h6 className="absolute z-10 text-red">{errorMessage}</h6>
|
||||
<h6 className="absolute z-10 text-red-400">{errorMessage}</h6>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
Input.displayName = "Input";
|
||||
|
|
|
|||
|
|
@ -57,15 +57,15 @@ export function Cheatsheet({
|
|||
<div
|
||||
data-keybinds-cheatsheet
|
||||
className={twMerge(
|
||||
"max-h-[80%] w-[min(680px,90%)] overflow-auto rounded-xl border border-white/15 bg-black/70 p-6 text-white/90 shadow-2xl backdrop-blur-sm",
|
||||
"max-h-[80%] w-[min(680px,90%)] overflow-auto rounded-none border border-white/15 bg-ui-panel/95 p-6 text-white/90",
|
||||
pinned && "pointer-events-auto",
|
||||
)}
|
||||
>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<span className="text-xs uppercase tracking-widest text-white/50">
|
||||
<span className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60">
|
||||
Keyboard shortcuts
|
||||
</span>
|
||||
<span className="text-xs capitalize text-white/40">
|
||||
<span className="font-mono text-[11px] uppercase tracking-[0.12em] text-white/40">
|
||||
{selectedPreset} preset
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -75,7 +75,7 @@ export function Cheatsheet({
|
|||
if (!rows?.length) return null;
|
||||
return (
|
||||
<div key={group}>
|
||||
<div className="mb-2 text-xs font-semibold uppercase tracking-wider text-white/60">
|
||||
<div className="mb-2 font-mono text-[11px] font-medium uppercase tracking-[0.13em] text-white/45">
|
||||
{group}
|
||||
</div>
|
||||
<ul className="flex flex-col gap-1.5">
|
||||
|
|
@ -94,7 +94,7 @@ export function Cheatsheet({
|
|||
})}
|
||||
</div>
|
||||
{pinned && (
|
||||
<div className="mt-5 border-t border-white/10 pt-3 text-center text-xs text-white/40">
|
||||
<div className="mt-5 border-t border-white/15 pt-3 text-center font-mono text-[11px] uppercase tracking-[0.12em] text-white/40">
|
||||
Press Esc or click outside to close
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export function KeybindCaptureInput({
|
|||
onClick={() => setCapturing((v) => !v)}
|
||||
aria-label="Change keybinding"
|
||||
className={twMerge(
|
||||
"inline-flex min-h-8 min-w-[7rem] items-center justify-center rounded-md border px-2 py-1 text-sm transition-colors",
|
||||
"inline-flex min-h-8 min-w-[7rem] items-center justify-center rounded-[3px] border px-2 py-1 text-sm transition-colors",
|
||||
capturing
|
||||
? "border-primary/80 bg-primary/10 text-primary animate-pulse"
|
||||
: accent
|
||||
|
|
|
|||
|
|
@ -176,8 +176,8 @@ export function KeybindsSettings({
|
|||
Deliberately reset-free: destructive actions live with their scope
|
||||
(section headers) or in the danger row at the bottom, never beside
|
||||
the search field where a stray click can nuke every override. */}
|
||||
<div className="sticky top-0 z-10 -mt-2 flex flex-col gap-3 border-b border-white/[0.06] bg-ui-modal/80 py-3 backdrop-blur-xl">
|
||||
<div className="flex items-center gap-1.5 rounded-2xl bg-white/[0.04] p-1.5 ring-1 ring-white/[0.06]">
|
||||
<div className="sticky top-0 z-10 -mt-2 flex flex-col gap-3 border-b border-white/[0.06] bg-ui-modal py-3">
|
||||
<div className="flex items-center gap-1.5 rounded-none bg-white/[0.04] p-1.5 ring-1 ring-white/15">
|
||||
<ModeToggle mode={searchMode} onChange={switchMode} />
|
||||
{searchMode === "name" ? (
|
||||
<NameSearchField value={nameSearch} onChange={setNameSearch} />
|
||||
|
|
@ -273,7 +273,7 @@ export function KeybindsSettings({
|
|||
and gated behind a two-step confirm. Hidden when there is nothing to
|
||||
destroy. ─────────────────────────────────────────────────────────── */}
|
||||
{overriddenCount > 0 && (
|
||||
<div className="flex items-center justify-between gap-4 rounded-2xl bg-red/[0.04] p-4 ring-1 ring-red/20">
|
||||
<div className="flex items-center justify-between gap-4 rounded-none bg-red/[0.04] p-4 ring-1 ring-red/20">
|
||||
<div className="flex min-w-0 flex-col gap-0.5">
|
||||
<span className="text-[13px] font-medium text-base-fg/90">
|
||||
Reset all customizations
|
||||
|
|
@ -296,7 +296,7 @@ export function KeybindsSettings({
|
|||
)}
|
||||
|
||||
{/* ── Video editor lives in its own customizer ───────────────────────── */}
|
||||
<div className="flex items-start gap-3 rounded-2xl bg-white/[0.02] p-4 text-[13px] leading-relaxed text-base-fg/55 ring-1 ring-white/[0.06]">
|
||||
<div className="flex items-start gap-3 rounded-none bg-white/[0.02] p-4 text-[13px] leading-relaxed text-base-fg/55 ring-1 ring-white/15">
|
||||
<InfoIcon className="mt-0.5 h-4 w-4 shrink-0 text-base-fg/35" />
|
||||
<span>
|
||||
Video editor shortcuts are managed in the video editor itself
|
||||
|
|
@ -357,14 +357,14 @@ function PresetDropdown({
|
|||
aria-haspopup="listbox"
|
||||
aria-expanded={open}
|
||||
className={twMerge(
|
||||
"flex w-full items-center gap-3 rounded-2xl bg-white/[0.04] p-1.5 text-left ring-1 ring-white/[0.08] transition-all hover:bg-white/[0.06] active:scale-[0.995]",
|
||||
"flex w-full items-center gap-3 rounded-none bg-white/[0.04] p-1.5 text-left ring-1 ring-white/15 transition-all hover:bg-white/[0.06] active:scale-[0.995]",
|
||||
EASE,
|
||||
open && "ring-primary/40",
|
||||
)}
|
||||
>
|
||||
<div className="flex min-w-0 grow flex-col gap-0.5 px-3 py-1.5">
|
||||
<span className="text-sm font-medium text-base-fg">{def.label}</span>
|
||||
<span className="truncate text-[12px] text-base-fg/50">
|
||||
<span className="text-[12px] leading-relaxed text-base-fg/60">
|
||||
{def.description}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -381,7 +381,7 @@ function PresetDropdown({
|
|||
role="listbox"
|
||||
aria-label="Preset"
|
||||
className={twMerge(
|
||||
"absolute left-0 right-0 top-full z-20 mt-2 origin-top rounded-2xl bg-ui-modal shadow-[0_20px_50px_-16px_rgba(0,0,0,0.7)] ring-1 ring-white/10 transition-all duration-200 overflow-hidden",
|
||||
"absolute left-0 right-0 top-full z-20 mt-2 origin-top rounded-none bg-ui-modal shadow-[0_20px_50px_-16px_rgba(0,0,0,0.7)] ring-1 ring-white/10 transition-all duration-200 overflow-hidden",
|
||||
EASE,
|
||||
open
|
||||
? "scale-100 opacity-100"
|
||||
|
|
@ -457,7 +457,7 @@ function ModeToggle({
|
|||
onChange: (mode: SearchMode) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex shrink-0 items-center gap-0.5 rounded-[0.625rem] bg-ui-modal/60 p-0.5">
|
||||
<div className="flex shrink-0 items-center gap-0.5 rounded-[3px] bg-ui-modal/60 p-0.5">
|
||||
{(["name", "key"] as SearchMode[]).map((m) => (
|
||||
<button
|
||||
key={m}
|
||||
|
|
@ -465,7 +465,7 @@ function ModeToggle({
|
|||
onClick={() => onChange(m)}
|
||||
aria-pressed={mode === m}
|
||||
className={twMerge(
|
||||
"rounded-lg px-3 py-1.5 text-[12px] font-medium capitalize transition-all",
|
||||
"rounded-[3px] px-3 py-1.5 text-[12px] font-medium capitalize transition-all",
|
||||
EASE,
|
||||
mode === m
|
||||
? "bg-white/10 text-base-fg shadow-[inset_0_1px_0_rgba(255,255,255,0.06)]"
|
||||
|
|
@ -489,7 +489,7 @@ function NameSearchField({
|
|||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"group flex grow items-center rounded-[0.625rem] bg-ui-modal/60 ring-1 ring-transparent transition-all",
|
||||
"group flex grow items-center rounded-[3px] bg-ui-modal/60 ring-1 ring-transparent transition-all",
|
||||
EASE,
|
||||
"focus-within:bg-ui-modal/80 focus-within:ring-primary/50",
|
||||
)}
|
||||
|
|
@ -508,7 +508,7 @@ function NameSearchField({
|
|||
onClick={() => onChange("")}
|
||||
aria-label="Clear search"
|
||||
className={twMerge(
|
||||
"mr-1.5 grid h-7 w-7 shrink-0 place-items-center rounded-full text-base-fg/40 transition-all hover:bg-white/10 hover:text-base-fg active:scale-90",
|
||||
"mr-1.5 grid h-7 w-7 shrink-0 place-items-center rounded-[3px] text-base-fg/40 transition-all hover:bg-white/10 hover:text-base-fg active:scale-90",
|
||||
EASE,
|
||||
)}
|
||||
>
|
||||
|
|
@ -533,7 +533,7 @@ function KeySearchField({
|
|||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex grow items-center rounded-[0.625rem] bg-ui-modal/60 ring-1 transition-all",
|
||||
"flex grow items-center rounded-[3px] bg-ui-modal/60 ring-1 transition-all",
|
||||
EASE,
|
||||
listening ? "ring-primary/60" : "ring-transparent",
|
||||
)}
|
||||
|
|
@ -570,7 +570,7 @@ function KeySearchField({
|
|||
onClick={onClear}
|
||||
aria-label="Clear key filter"
|
||||
className={twMerge(
|
||||
"mr-1.5 grid h-7 w-7 shrink-0 place-items-center rounded-full text-base-fg/40 transition-all hover:bg-white/10 hover:text-base-fg active:scale-90",
|
||||
"mr-1.5 grid h-7 w-7 shrink-0 place-items-center rounded-[3px] text-base-fg/40 transition-all hover:bg-white/10 hover:text-base-fg active:scale-90",
|
||||
EASE,
|
||||
)}
|
||||
>
|
||||
|
|
@ -605,8 +605,8 @@ function CollapsibleSurface({
|
|||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-2xl bg-white/[0.04] ring-1 ring-white/[0.06]">
|
||||
<div className="overflow-hidden rounded-[0.625rem] bg-ui-modal/40">
|
||||
<div className="rounded-none bg-white/[0.04] ring-1 ring-white/15">
|
||||
<div className="overflow-hidden rounded-[3px] bg-ui-modal/40">
|
||||
{/* The toggle and the (destructive) section reset are sibling buttons —
|
||||
a nested button is invalid HTML and a mis-click on "reset" must
|
||||
never also collapse the section. */}
|
||||
|
|
@ -647,7 +647,7 @@ function CollapsibleSurface({
|
|||
onConfirm={onReset}
|
||||
/>
|
||||
)}
|
||||
<span className="shrink-0 rounded-full bg-white/[0.06] px-2 py-0.5 text-xs tabular-nums text-base-fg/55">
|
||||
<span className="shrink-0 rounded-[3px] bg-white/[0.06] px-2 py-0.5 text-xs tabular-nums text-base-fg/55">
|
||||
{count}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -738,7 +738,7 @@ function ConfirmResetButton({
|
|||
title={title}
|
||||
aria-label={armed ? `${title} — click again to confirm` : title}
|
||||
className={twMerge(
|
||||
"relative shrink-0 overflow-hidden whitespace-nowrap rounded-full font-medium transition-all active:scale-[0.97]",
|
||||
"relative shrink-0 overflow-hidden whitespace-nowrap rounded-[3px] font-medium transition-all active:scale-[0.97]",
|
||||
EASE,
|
||||
prominent
|
||||
? "h-9 px-4 text-[13px]"
|
||||
|
|
@ -746,7 +746,7 @@ function ConfirmResetButton({
|
|||
armed
|
||||
? ready
|
||||
? "bg-red/25 text-white shadow-[0_0_0_3px_rgba(0,0,0,0.15)]"
|
||||
: "cursor-wait bg-white/[0.06] text-base-fg/55 ring-1 ring-white/[0.08]"
|
||||
: "cursor-wait bg-white/[0.06] text-base-fg/55 ring-1 ring-white/15"
|
||||
: prominent
|
||||
? "bg-red/10 text-red ring-1 ring-red/30 hover:bg-red/[0.18]"
|
||||
: "text-base-fg/45 hover:bg-red/10 hover:text-red",
|
||||
|
|
@ -827,7 +827,7 @@ function ActionRow({
|
|||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"group/row flex items-center gap-3 rounded-xl px-3 py-2 transition-colors",
|
||||
"group/row flex items-center gap-3 rounded-[3px] px-3 py-2 transition-colors",
|
||||
EASE,
|
||||
"hover:bg-white/[0.03]",
|
||||
)}
|
||||
|
|
@ -859,7 +859,7 @@ function ActionRow({
|
|||
title="Revert to preset default"
|
||||
aria-label="Revert to preset default"
|
||||
className={twMerge(
|
||||
"grid h-7 w-7 shrink-0 place-items-center rounded-full text-base-fg/40 transition-all hover:bg-white/10 hover:text-base-fg active:scale-90",
|
||||
"grid h-7 w-7 shrink-0 place-items-center rounded-[3px] text-base-fg/40 transition-all hover:bg-white/10 hover:text-base-fg active:scale-90",
|
||||
EASE,
|
||||
)}
|
||||
>
|
||||
|
|
@ -887,7 +887,7 @@ function ConflictAlert({
|
|||
return (
|
||||
<div
|
||||
role="alert"
|
||||
className="flex flex-col gap-2.5 rounded-2xl bg-red/[0.08] p-3.5 text-[13px] ring-1 ring-red/30"
|
||||
className="flex flex-col gap-2.5 rounded-none bg-red/[0.08] p-3.5 text-[13px] ring-1 ring-red/30"
|
||||
>
|
||||
<span className="leading-relaxed text-base-fg/80">
|
||||
<KbdBindings bindings={[pending.binding]} /> is already used by{" "}
|
||||
|
|
@ -901,7 +901,7 @@ function ConflictAlert({
|
|||
type="button"
|
||||
onClick={onConfirm}
|
||||
className={twMerge(
|
||||
"rounded-full bg-red px-4 py-1.5 text-[13px] font-medium text-white transition-all hover:bg-red/85 active:scale-[0.97]",
|
||||
"rounded-[3px] bg-red px-4 py-1.5 text-[13px] font-medium text-white transition-all hover:bg-red/85 active:scale-[0.97]",
|
||||
EASE,
|
||||
)}
|
||||
>
|
||||
|
|
@ -911,7 +911,7 @@ function ConflictAlert({
|
|||
type="button"
|
||||
onClick={onCancel}
|
||||
className={twMerge(
|
||||
"rounded-full bg-white/[0.06] px-4 py-1.5 text-[13px] text-base-fg/80 transition-all hover:bg-white/10 active:scale-[0.97]",
|
||||
"rounded-[3px] bg-white/[0.06] px-4 py-1.5 text-[13px] text-base-fg/80 transition-all hover:bg-white/10 active:scale-[0.97]",
|
||||
EASE,
|
||||
)}
|
||||
>
|
||||
|
|
@ -932,7 +932,7 @@ function EmptyState({
|
|||
binding: Binding | null;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-3 rounded-2xl bg-white/[0.06] px-6 py-12 text-center ring-1 ring-white/[0.06]">
|
||||
<div className="flex flex-col items-center gap-3 rounded-none bg-white/[0.06] px-6 py-12 text-center ring-1 ring-white/15">
|
||||
{mode === "key" ? (
|
||||
<KeyIcon className="h-6 w-6 text-base-fg/25" />
|
||||
) : (
|
||||
|
|
@ -955,7 +955,7 @@ function EmptyState({
|
|||
|
||||
function Eyebrow({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<span className="text-xs font-medium tracking-wide text-base-fg/50">
|
||||
<span className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
|
@ -963,7 +963,7 @@ function Eyebrow({ children }: { children: React.ReactNode }) {
|
|||
|
||||
function GroupLabel({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="px-3 pb-1 pt-4 text-[11px] font-medium uppercase tracking-[0.13em] text-base-fg/45 first:pt-2">
|
||||
<div className="px-3 pb-1 pt-4 font-mono text-[11px] font-medium uppercase tracking-[0.13em] text-base-fg/45 first:pt-2">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -14,11 +14,14 @@ export const Label = ({
|
|||
...rest
|
||||
}: LabelProps) => (
|
||||
<label
|
||||
className={twMerge("text-base-fg mb-1.5 text-[15px] font-medium", className)}
|
||||
className={twMerge(
|
||||
"text-base-fg/60 mb-1.5 font-mono text-[10px] font-semibold uppercase tracking-[0.25em]",
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
{required && <span className="ml-0.5 text-[#ff6467]">*</span>}
|
||||
{required && <span className="ml-0.5 text-red-400">*</span>}
|
||||
</label>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Modal } from "@storyteller/ui-modal";
|
|||
import { Tooltip } from "@storyteller/ui-tooltip";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import dayjs from "dayjs";
|
||||
import { ArrowDownToLineIcon, BoxIcon, CheckIcon, ChevronLeftIcon, ChevronRightIcon, CopyIcon, GlobeIcon, LinkIcon, LogOutIcon, MusicIcon, PauseIcon, PencilIcon, PlayIcon, RotateCwIcon, SearchIcon, Trash2Icon, UserIcon, VideoIcon, WandSparklesIcon } from "lucide-react";
|
||||
import { ArrowDownToLineIcon, BoxIcon, CheckIcon, ChevronLeftIcon, ChevronRightIcon, CopyIcon, GlobeIcon, ImageIcon, InfoIcon, LinkIcon, LogOutIcon, MusicIcon, PauseIcon, PencilIcon, PlayIcon, RotateCwIcon, SearchIcon, Trash2Icon, UserIcon, VideoIcon, WandSparklesIcon } from "lucide-react";
|
||||
import { DynamicIcon } from "@storyteller/icons";
|
||||
import { MediaFileDelete } from "@storyteller/tauri-api";
|
||||
import { LoadingSpinner } from "@storyteller/ui-loading-spinner";
|
||||
|
|
@ -544,7 +544,7 @@ export function LightboxModal({
|
|||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
className="rounded-xl bg-ui-modal h-[760px] w-[1200px] max-w-screen min-w-[1000px] min-h-[600px] p-4"
|
||||
className="rounded-none border-white/15 bg-ui-modal h-[760px] w-[1200px] max-w-screen min-w-[1000px] min-h-[600px] p-4"
|
||||
draggable
|
||||
allowBackgroundInteraction={true}
|
||||
showClose={true}
|
||||
|
|
@ -555,15 +555,15 @@ export function LightboxModal({
|
|||
>
|
||||
{/* Invisible drag handle strip at the very top for moving */}
|
||||
<Modal.DragHandle>
|
||||
<div className="absolute left-0 top-0 z-20 h-12 w-full cursor-move rounded-t-xl" />
|
||||
<div className="absolute left-0 top-0 z-20 h-12 w-full cursor-move" />
|
||||
</Modal.DragHandle>
|
||||
|
||||
{/* content grid */}
|
||||
<div className="flex h-full gap-4">
|
||||
{/* image panel - flexible width */}
|
||||
<div className="group/nav relative flex h-full flex-1 items-center justify-center overflow-hidden rounded-l-xl bg-black/30">
|
||||
<div className="group/nav relative flex h-full flex-1 items-center justify-center overflow-hidden bg-black">
|
||||
{!selectedImageUrl ? (
|
||||
<div className="flex h-full w-full items-center justify-center bg-black/30">
|
||||
<div className="flex h-full w-full items-center justify-center bg-black">
|
||||
<span className="text-base-fg/60">Image not available</span>
|
||||
</div>
|
||||
) : mediaClass === "dimensional" ||
|
||||
|
|
@ -601,8 +601,8 @@ export function LightboxModal({
|
|||
</video>
|
||||
) : mediaClass === "audio" ? (
|
||||
<div className="flex h-full w-full items-center justify-center px-4 sm:px-8">
|
||||
<div className="w-full max-w-xl rounded-2xl border border-white/10 bg-gradient-to-br from-white/[0.08] via-white/[0.04] to-white/[0.02] px-4 py-10 sm:px-6">
|
||||
<div className="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-white/10 ring-1 ring-white/15">
|
||||
<div className="w-full max-w-xl border border-white/15 bg-ui-controls px-4 py-10 sm:px-6">
|
||||
<div className="mx-auto mb-6 flex h-16 w-16 items-center justify-center border border-white/15 bg-white/10">
|
||||
<MusicIcon
|
||||
|
||||
className="text-2xl text-white/70" />
|
||||
|
|
@ -625,7 +625,7 @@ export function LightboxModal({
|
|||
className="embla__slide flex-[0_0_100%]"
|
||||
key={`${url}-${idx}`}
|
||||
>
|
||||
<div className="relative flex h-full items-center justify-center overflow-hidden rounded-lg bg-black/20">
|
||||
<div className="relative flex h-full items-center justify-center overflow-hidden bg-black">
|
||||
<img
|
||||
data-lightbox-modal="true"
|
||||
src={addCorsParam(url) || url}
|
||||
|
|
@ -660,12 +660,12 @@ export function LightboxModal({
|
|||
</div>
|
||||
|
||||
{effectiveImageUrls.length > 1 && (
|
||||
<div className="mt-3 px-2">
|
||||
<div className="mt-3 px-2 pb-2">
|
||||
<div
|
||||
className="embla-thumbs overflow-hidden"
|
||||
ref={emblaThumbsRef}
|
||||
>
|
||||
<div className="embla-thumbs__container flex gap-2">
|
||||
<div className="embla-thumbs__container flex justify-center gap-2">
|
||||
{effectiveImageUrls.map((url, idx) => {
|
||||
const isSelected = idx === selectedIndex;
|
||||
return (
|
||||
|
|
@ -674,9 +674,9 @@ export function LightboxModal({
|
|||
type="button"
|
||||
onClick={() => onThumbClick(idx)}
|
||||
className={twMerge(
|
||||
"embla-thumbs__slide relative h-20 w-20 flex-[0_0_5rem] overflow-hidden rounded-md border-2 transition-all",
|
||||
"embla-thumbs__slide relative h-16 w-16 flex-[0_0_4rem] overflow-hidden border transition-all",
|
||||
isSelected
|
||||
? "border-brand-primary-400 opacity-100"
|
||||
? "border-white opacity-100"
|
||||
: "border-transparent opacity-60 hover:border-white/40 hover:opacity-100",
|
||||
)}
|
||||
>
|
||||
|
|
@ -701,7 +701,7 @@ export function LightboxModal({
|
|||
mediaClass !== "mesh" &&
|
||||
mediaClass !== "splat" &&
|
||||
mediaClass !== "audio" && (
|
||||
<div className="absolute inset-0 bg-ui-panel flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-black flex items-center justify-center">
|
||||
<LoadingSpinner className="h-12 w-12 text-base-fg" />
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -713,7 +713,7 @@ export function LightboxModal({
|
|||
e.stopPropagation();
|
||||
onNavigatePrev();
|
||||
}}
|
||||
className="absolute left-3 top-1/2 -translate-y-1/2 z-30 flex h-10 w-10 items-center justify-center rounded-full bg-black/50 text-white/70 opacity-0 transition-opacity duration-200 hover:bg-black/70 hover:text-white group-hover/nav:opacity-100 focus:outline-none"
|
||||
className="absolute left-3 top-1/2 -translate-y-1/2 z-30 flex h-10 w-10 items-center justify-center bg-black/50 text-white/70 opacity-0 transition-opacity duration-200 hover:bg-black/70 hover:text-white group-hover/nav:opacity-100 focus:outline-none"
|
||||
aria-label="Previous item"
|
||||
>
|
||||
<ChevronLeftIcon className="text-lg" />
|
||||
|
|
@ -725,7 +725,7 @@ export function LightboxModal({
|
|||
e.stopPropagation();
|
||||
onNavigateNext();
|
||||
}}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 z-30 flex h-10 w-10 items-center justify-center rounded-full bg-black/50 text-white/70 opacity-0 transition-opacity duration-200 hover:bg-black/70 hover:text-white group-hover/nav:opacity-100 focus:outline-none"
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 z-30 flex h-10 w-10 items-center justify-center bg-black/50 text-white/70 opacity-0 transition-opacity duration-200 hover:bg-black/70 hover:text-white group-hover/nav:opacity-100 focus:outline-none"
|
||||
aria-label="Next item"
|
||||
>
|
||||
<ChevronRightIcon className="text-lg" />
|
||||
|
|
@ -737,7 +737,7 @@ export function LightboxModal({
|
|||
<div className="flex h-full w-[280px] shrink-0 flex-col">
|
||||
<div className="flex-1 overflow-y-auto space-y-5 text-base-fg min-h-0 pb-2">
|
||||
{creator ? (
|
||||
<div className="sticky top-0 z-10 flex items-center gap-2.5 bg-ui-modal pb-3 pr-10 border-b border-white/5">
|
||||
<div className="sticky top-0 z-10 flex items-center gap-3 bg-ui-modal pb-3 pr-20 border-b border-white/10">
|
||||
{creator.core_info ? (
|
||||
<Gravatar
|
||||
size={36}
|
||||
|
|
@ -747,10 +747,10 @@ export function LightboxModal({
|
|||
backgroundIndex={
|
||||
creator.core_info.default_avatar.color_index
|
||||
}
|
||||
className="rounded-xl border-white/10"
|
||||
className="rounded-none border-white/15"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-9 w-9 shrink-0 flex items-center justify-center rounded-xl bg-white/10 text-white/50 border border-white/5">
|
||||
<div className="h-9 w-9 shrink-0 flex items-center justify-center bg-white/10 text-white/50 border border-white/15">
|
||||
<UserIcon />
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -758,27 +758,28 @@ export function LightboxModal({
|
|||
<span className="text-base-fg text-sm font-semibold leading-none truncate">
|
||||
{creator.display_name}
|
||||
</span>
|
||||
<span className="text-base-fg/60 text-xs font-medium">
|
||||
<span className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">
|
||||
Author
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : promptLoading ? (
|
||||
<div className="sticky top-0 z-10 flex items-center gap-3 bg-ui-modal pb-3 pr-10 border-b border-white/5 animate-pulse">
|
||||
<div className="h-9 w-9 shrink-0 rounded-xl bg-white/10" />
|
||||
<div className="sticky top-0 z-10 flex items-center gap-3 bg-ui-modal pb-3 pr-20 border-b border-white/10 animate-pulse">
|
||||
<div className="h-9 w-9 shrink-0 bg-white/10" />
|
||||
<div className="flex flex-col gap-1.5 min-w-0 flex-1">
|
||||
<div className="h-3.5 w-24 rounded bg-white/10" />
|
||||
<div className="h-3 w-12 rounded bg-white/10" />
|
||||
<div className="h-3.5 w-24 bg-white/10" />
|
||||
<div className="h-3 w-12 bg-white/10" />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{(hasPromptToken || promptLoading) && (
|
||||
<>
|
||||
{/* Prompt */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm font-medium text-base-fg/90">
|
||||
Prompt
|
||||
<div className="flex items-center gap-2 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">
|
||||
<PencilIcon />
|
||||
<span>Prompt</span>
|
||||
</div>
|
||||
{!promptLoading && prompt && (
|
||||
<button
|
||||
|
|
@ -814,16 +815,13 @@ export function LightboxModal({
|
|||
)}
|
||||
</div>
|
||||
<div
|
||||
className="text-sm text-base-fg break-words p-3 rounded-lg leading-relaxed"
|
||||
style={{
|
||||
background: "rgb(var(--st-controls-rgb) / 0.20)",
|
||||
}}
|
||||
className="text-sm text-base-fg/90 break-words px-4 py-3 bg-black/20 leading-relaxed border border-white/10"
|
||||
>
|
||||
{promptLoading ? (
|
||||
<div className="animate-pulse space-y-2">
|
||||
<div className="h-3 w-full rounded bg-white/10" />
|
||||
<div className="h-3 w-4/5 rounded bg-white/10" />
|
||||
<div className="h-3 w-3/5 rounded bg-white/10" />
|
||||
<div className="h-3 w-full bg-white/10" />
|
||||
<div className="h-3 w-4/5 bg-white/10" />
|
||||
<div className="h-3 w-3/5 bg-white/10" />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
|
|
@ -877,30 +875,25 @@ export function LightboxModal({
|
|||
/* Skeleton placeholders while fetching metadata */
|
||||
<>
|
||||
{/* Reference Images skeleton */}
|
||||
<div className="space-y-1.5 animate-pulse">
|
||||
<div className="h-3.5 w-28 rounded bg-white/10" />
|
||||
<div className="space-y-2 animate-pulse">
|
||||
<div className="h-3.5 w-28 bg-white/10" />
|
||||
<div className="flex gap-1.5">
|
||||
<div className="h-12 w-12 rounded-lg bg-white/10" />
|
||||
<div className="h-12 w-12 rounded-lg bg-white/10" />
|
||||
<div className="h-12 w-12 rounded-lg bg-white/10" />
|
||||
<div className="h-12 w-12 bg-white/10" />
|
||||
<div className="h-12 w-12 bg-white/10" />
|
||||
<div className="h-12 w-12 bg-white/10" />
|
||||
</div>
|
||||
</div>
|
||||
{/* Information skeleton */}
|
||||
<div className="space-y-1.5 animate-pulse">
|
||||
<div className="h-3.5 w-24 rounded bg-white/10" />
|
||||
<div
|
||||
className="flex flex-col rounded-lg border border-ui-panel-border overflow-hidden"
|
||||
style={{
|
||||
background: "rgb(var(--st-controls-rgb) / 0.20)",
|
||||
}}
|
||||
>
|
||||
<div className="space-y-2 animate-pulse">
|
||||
<div className="h-3.5 w-24 bg-white/10" />
|
||||
<div className="flex flex-col bg-black/20 border border-white/10 overflow-hidden">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center justify-between py-2.5 px-3 border-b border-white/5 last:border-0"
|
||||
className="flex items-center justify-between py-2.5 px-3 border-b border-white/10 last:border-0"
|
||||
>
|
||||
<div className="h-3.5 w-16 rounded bg-white/10" />
|
||||
<div className="h-3.5 w-24 rounded bg-white/10" />
|
||||
<div className="h-3.5 w-16 bg-white/10" />
|
||||
<div className="h-3.5 w-24 bg-white/10" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -909,9 +902,10 @@ export function LightboxModal({
|
|||
) : (
|
||||
<>
|
||||
{contextImages && contextImages.length > 0 && (
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-sm font-medium text-base-fg/90">
|
||||
Reference Media
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">
|
||||
<ImageIcon />
|
||||
<span>Reference Media</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-5 gap-1.5 w-fit">
|
||||
{contextImages.map((contextImage, index) => {
|
||||
|
|
@ -936,7 +930,7 @@ export function LightboxModal({
|
|||
<div className="flex flex-col gap-1.5 min-w-[100px]">
|
||||
{!isAudio && !isVideoRef && (
|
||||
<button
|
||||
className="text-xs text-left text-base-fg/80 hover:text-base-fg transition-colors py-1 px-1 rounded hover:bg-white/5"
|
||||
className="text-xs text-left text-base-fg/80 hover:text-base-fg transition-colors py-1 px-1 rounded-[3px] hover:bg-white/5"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setRefPreviewUrl(fullSize);
|
||||
|
|
@ -950,7 +944,7 @@ export function LightboxModal({
|
|||
)}
|
||||
{onNavigateToMedia && (
|
||||
<button
|
||||
className="text-xs text-left text-base-fg/80 hover:text-base-fg transition-colors py-1 px-1 rounded hover:bg-white/5"
|
||||
className="text-xs text-left text-base-fg/80 hover:text-base-fg transition-colors py-1 px-1 rounded-[3px] hover:bg-white/5"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onNavigateToMedia(
|
||||
|
|
@ -968,7 +962,7 @@ export function LightboxModal({
|
|||
}
|
||||
>
|
||||
<div
|
||||
className="glass relative aspect-square overflow-hidden rounded-lg w-12 border-2 border-white/30 hover:border-white/80 transition-all group cursor-pointer"
|
||||
className="relative aspect-square w-12 overflow-hidden border border-white/15 bg-white/5 transition-colors hover:border-white/40 group cursor-pointer"
|
||||
onClick={() => {
|
||||
if (isAudio) {
|
||||
handleAudioToggle(contextImage.media_token, contextImage.media_links.cdn_url);
|
||||
|
|
@ -1045,7 +1039,7 @@ export function LightboxModal({
|
|||
|
||||
{/* buttons with spacing */}
|
||||
{actionUrl && (
|
||||
<div className="mt-4 grid grid-cols-2 gap-1.5">
|
||||
<div className="mt-4 grid grid-cols-2 gap-2 border-t border-white/10 pt-4">
|
||||
{onRecreateClicked &&
|
||||
hasPromptToken &&
|
||||
promptData &&
|
||||
|
|
@ -1241,7 +1235,7 @@ export function LightboxModal({
|
|||
{selectedMediaToken && onDeleteClicked && (
|
||||
<Button
|
||||
icon={Trash2Icon}
|
||||
className="w-full py-1.5 text-[13px]"
|
||||
className="w-full border border-red-500/20 bg-red-500/10 py-1.5 text-[13px] text-red-500 hover:bg-red-500/20"
|
||||
variant="destructive"
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -1262,7 +1256,7 @@ export function LightboxModal({
|
|||
<Modal
|
||||
isOpen={true}
|
||||
onClose={() => setRefPreviewUrl(null)}
|
||||
className="rounded-xl bg-ui-modal w-auto h-auto max-w-[75vw] max-h-[75vh] p-4"
|
||||
className="rounded-none border-white/15 bg-ui-modal w-auto h-auto max-w-[75vw] max-h-[75vh] p-4"
|
||||
draggable
|
||||
allowBackgroundInteraction={true}
|
||||
showClose={true}
|
||||
|
|
@ -1272,9 +1266,9 @@ export function LightboxModal({
|
|||
expandable={false}
|
||||
>
|
||||
<Modal.DragHandle>
|
||||
<div className="absolute left-0 top-0 z-20 h-12 w-full cursor-move rounded-t-xl" />
|
||||
<div className="absolute left-0 top-0 z-20 h-12 w-full cursor-move" />
|
||||
</Modal.DragHandle>
|
||||
<div className="relative flex items-center justify-center overflow-hidden rounded-xl bg-black/30">
|
||||
<div className="relative flex items-center justify-center overflow-hidden bg-black">
|
||||
<img
|
||||
src={addCorsParam(refPreviewUrl) || refPreviewUrl}
|
||||
alt="Reference preview"
|
||||
|
|
@ -1323,12 +1317,12 @@ function InfoSection({
|
|||
if (!hasAnyInfo) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-sm font-medium text-base-fg/90">Information</div>
|
||||
<div
|
||||
className="flex flex-col rounded-lg border border-ui-panel-border overflow-hidden"
|
||||
style={{ background: "rgb(var(--st-controls-rgb) / 0.20)" }}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">
|
||||
<InfoIcon />
|
||||
<span>Information</span>
|
||||
</div>
|
||||
<div className="flex flex-col bg-black/20 border border-white/10 overflow-hidden">
|
||||
{modelType && (
|
||||
<InfoRow
|
||||
label="Model"
|
||||
|
|
@ -1386,9 +1380,11 @@ function InfoSection({
|
|||
|
||||
function InfoRow({ label, value }: { label: string; value: ReactNode }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between py-2 px-3 border-b border-white/5 last:border-0">
|
||||
<span className="text-sm text-base-fg/70 font-medium">{label}</span>
|
||||
<span className="text-sm text-base-fg flex items-center gap-2">
|
||||
<div className="flex items-start justify-between gap-4 px-3 py-2.5 border-b border-white/10 last:border-0">
|
||||
<span className="shrink-0 pt-0.5 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">
|
||||
{label}
|
||||
</span>
|
||||
<span className="min-w-0 text-right text-sm text-base-fg font-medium flex items-center justify-end gap-2">
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -134,14 +134,14 @@ export function TagChipInput({
|
|||
};
|
||||
|
||||
const chipClass = (value: string) =>
|
||||
`flex items-center gap-1 rounded-full bg-white/10 px-2.5 py-1 text-[11px] font-medium text-base-fg/80 transition-all ${
|
||||
`flex items-center gap-1 break-all text-left rounded-[3px] border border-white/15 bg-white/5 px-2.5 py-1 text-[11px] font-medium text-base-fg/80 transition-all ${
|
||||
flashValue === value ? "ring-2 ring-primary" : ""
|
||||
}`;
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div
|
||||
className={`flex flex-wrap items-center gap-1.5 rounded-xl bg-black/20 border border-white/5 px-3 py-2.5 ${
|
||||
className={`flex flex-wrap items-center gap-1.5 rounded-[3px] bg-ui-panel border border-ui-panel-border focus-within:border-primary/60 px-3 py-2.5 ${
|
||||
disabled ? "" : "cursor-text"
|
||||
}`}
|
||||
onClick={() => inputRef.current?.focus()}
|
||||
|
|
@ -183,6 +183,7 @@ export function TagChipInput({
|
|||
setFocused(false);
|
||||
if (draft.trim()) commit(draft);
|
||||
}}
|
||||
aria-label="Tags"
|
||||
placeholder={chips.length === 0 ? "Add tags (comma separated)" : "Add tag"}
|
||||
className="min-w-[7rem] flex-1 bg-transparent py-0.5 text-sm text-base-fg placeholder:text-base-fg/40 focus:outline-none"
|
||||
/>
|
||||
|
|
@ -191,7 +192,7 @@ export function TagChipInput({
|
|||
|
||||
{dropdownOpen && (
|
||||
<div
|
||||
className={`absolute left-0 right-0 z-30 max-h-48 overflow-y-auto rounded-lg border border-ui-panel-border bg-ui-panel p-1 shadow-xl ${
|
||||
className={`absolute left-0 right-0 z-30 max-h-48 overflow-y-auto rounded-none border border-ui-panel-border bg-ui-panel p-1 shadow-xl ${
|
||||
dropUp ? "bottom-full mb-1" : "top-full mt-1"
|
||||
}`}
|
||||
>
|
||||
|
|
@ -203,7 +204,7 @@ export function TagChipInput({
|
|||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => pickSuggestion(suggestion.value)}
|
||||
onMouseEnter={() => setHighlightIndex(index)}
|
||||
className={`flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-base-fg transition-colors ${
|
||||
className={`flex w-full items-center gap-2 rounded-[3px] px-2 py-1.5 text-sm text-base-fg transition-colors ${
|
||||
highlight === index ? "bg-ui-controls/60" : "hover:bg-ui-controls/40"
|
||||
}`}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ export function TagsSection({
|
|||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-xs font-medium text-base-fg/60">
|
||||
<div className="flex items-center gap-2 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">
|
||||
<TagIcon />
|
||||
<span>Tags</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export const ListDropdown = ({ list, onSelect }: ListDropdownProps) => {
|
|||
return (
|
||||
<Listbox value={selected} onChange={setSelected}>
|
||||
<div className="relative mt-1">
|
||||
<Listbox.Button className="relative h-10 w-full cursor-pointer rounded-md bg-brand-secondary py-2 pl-3 pr-10 text-left outline-none outline-offset-0 transition-all duration-150 ease-in-out focus:outline-brand-primary sm:text-sm">
|
||||
<Listbox.Button className="relative h-10 w-full cursor-pointer rounded-[3px] border border-ui-controls-border bg-ui-controls py-2 pl-3 pr-10 text-left text-base-fg outline-none outline-offset-0 transition-colors duration-150 ease-in-out hover:border-white/40 focus:!outline-none sm:text-sm">
|
||||
<span className="block truncate">{Object.keys(selected)[0]}</span>
|
||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<ChevronDownIcon aria-hidden="true" />
|
||||
|
|
@ -28,12 +28,17 @@ export const ListDropdown = ({ list, onSelect }: ListDropdownProps) => {
|
|||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-brand-secondary py-1 text-base shadow-lg focus:outline-none sm:text-sm">
|
||||
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-[3px] border border-ui-panel-border bg-ui-panel text-base focus:outline-none sm:text-sm">
|
||||
{list.map((item, itemIdx) => (
|
||||
<Listbox.Option
|
||||
key={itemIdx}
|
||||
className={({ active }) =>
|
||||
`relative cursor-pointer select-none py-2 pl-10 pr-4 text-white ${active ? "text-white" : "text-gray-400"
|
||||
className={({ active, selected }) =>
|
||||
`relative cursor-pointer select-none py-2 pl-10 pr-4 transition-colors duration-150 ${
|
||||
active
|
||||
? "bg-white text-black"
|
||||
: selected
|
||||
? "bg-white/10 text-base-fg"
|
||||
: "text-base-fg/90"
|
||||
}`
|
||||
}
|
||||
value={item}
|
||||
|
|
|
|||
|
|
@ -49,11 +49,11 @@ export const LoadingBar = ({
|
|||
propsInnerWrapperClassName,
|
||||
);
|
||||
const progressBackgroundClassName = twMerge(
|
||||
"w-full bg-brand-secondary-800 rounded-full h-2.5",
|
||||
"w-full bg-brand-secondary-800 rounded-none h-2.5",
|
||||
propsProgressBackgroundClassName,
|
||||
);
|
||||
const progressClassName = twMerge(
|
||||
"h-2.5 rounded-full transition-all duration-1000",
|
||||
"h-2.5 rounded-none transition-all duration-1000",
|
||||
getVariantClassNames(variant),
|
||||
propsProgressClassName,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -112,21 +112,21 @@ export function DesktopLoginBridge({ onSuccess, onStart, onActiveChange }: { onS
|
|||
};
|
||||
|
||||
const remaining = challenge ? Math.max(0, Math.ceil((Date.parse(challenge.expires_at) - now) / 1000)) : 0;
|
||||
return <section aria-label="Website login" className="mb-6 rounded-xl border border-white/15 p-4 text-center">
|
||||
return <section aria-label="Website login" className="mb-6 border border-white/15 bg-white/[0.03] p-4 text-center">
|
||||
{isActive && <div className="mb-4 text-left">
|
||||
<button type="button" className="inline-flex items-center gap-2 text-sm text-white/70 hover:text-white" onClick={back}>
|
||||
<button type="button" className="inline-flex items-center gap-2 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60 transition-colors hover:text-white" onClick={back}>
|
||||
<ArrowLeftIcon size={16} aria-hidden="true" /> Back
|
||||
</button>
|
||||
</div>}
|
||||
{challenge ? <>
|
||||
<p className="mb-3 text-sm text-white/70">Scan with your phone or approve in your browser.</p>
|
||||
<QRCodeSVG value={challenge.verification_url} size={192} marginSize={4} title="Scan to approve desktop login" className="mx-auto rounded-lg" />
|
||||
<p className="my-3 font-mono text-2xl tracking-widest">{challenge.confirmation_code.slice(0, 4)}-{challenge.confirmation_code.slice(4)}</p>
|
||||
<p className="mb-3 text-xs text-white/60">Verify this code on the website. Expires in {Math.floor(remaining / 60)}:{String(remaining % 60).padStart(2, "0")}</p>
|
||||
<button type="button" className="rounded-lg border border-white/30 px-4 py-2" onClick={() => OpenUrl(challenge.verification_url).catch(() => setMessage("Unable to open the browser. Scan the QR code instead."))}>Open website</button>
|
||||
</> : busy ? <p className="text-sm text-white/70" role="status">Preparing login…</p> : <div className="flex flex-col gap-2">
|
||||
<button type="button" className="rounded-lg bg-white px-4 py-3 font-medium text-black" onClick={() => start(true)}>Login with Website</button>
|
||||
<button type="button" className="rounded-lg border border-white/30 px-4 py-2" onClick={() => start(false)}>Scan to Login</button>
|
||||
<QRCodeSVG value={challenge.verification_url} size={192} marginSize={4} title="Scan to approve desktop login" className="mx-auto" />
|
||||
<p className="my-3 font-mono text-2xl tracking-[0.2em]">{challenge.confirmation_code.slice(0, 4)}-{challenge.confirmation_code.slice(4)}</p>
|
||||
<p className="mb-4 font-mono text-[11px] uppercase tracking-[0.12em] text-white/50">Verify this code on the website. Expires in {Math.floor(remaining / 60)}:{String(remaining % 60).padStart(2, "0")}</p>
|
||||
<button type="button" className="h-10 rounded-[3px] border border-white/15 bg-white/5 px-4 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white transition-colors hover:bg-white/10" onClick={() => OpenUrl(challenge.verification_url).catch(() => setMessage("Unable to open the browser. Scan the QR code instead."))}>Open website</button>
|
||||
</> : busy ? <p className="font-mono text-[11px] uppercase tracking-[0.12em] text-white/60" role="status">Preparing login…</p> : <div className="flex flex-col gap-2">
|
||||
<button type="button" className="h-10 rounded-[3px] bg-white px-4 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-black transition-colors hover:bg-white/90" onClick={() => start(true)}>Login with Website</button>
|
||||
<button type="button" className="h-10 rounded-[3px] border border-white/15 bg-white/5 px-4 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white transition-colors hover:bg-white/10" onClick={() => start(false)}>Scan to Login</button>
|
||||
<p className="text-xs text-white/50">Use Google or any account already signed in on the website.</p>
|
||||
</div>}
|
||||
{message && <p className="mt-3 text-sm text-white/70" role="status">{message}</p>}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
.panel {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
|
|
@ -8,60 +7,44 @@
|
|||
overflow: hidden;
|
||||
padding: 56px 32px;
|
||||
text-align: center;
|
||||
background: radial-gradient(ellipse at 50% 0%, rgb(94 234 212 / 9%), transparent 70%);
|
||||
animation: arrive 450ms ease-out both;
|
||||
}
|
||||
|
||||
.glow {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: -52px;
|
||||
left: calc(50% - 144px);
|
||||
width: 288px;
|
||||
height: 288px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgb(94 234 212 / 24%), rgb(129 140 248 / 12%) 45%, transparent 70%);
|
||||
filter: blur(24px);
|
||||
pointer-events: none;
|
||||
animation: glow 2400ms ease-in-out infinite;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: flex;
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 28px;
|
||||
border: 1px solid rgb(153 246 228 / 35%);
|
||||
border-radius: 50%;
|
||||
color: #b4f7de;
|
||||
background: linear-gradient(145deg, rgb(153 246 228 / 16%), rgb(94 234 212 / 4%));
|
||||
box-shadow: 0 0 0 8px rgb(94 234 212 / 4%), 0 0 44px rgb(94 234 212 / 18%), inset 0 1px 0 rgb(255 255 255 / 12%);
|
||||
border: 1px solid rgb(255 255 255 / 20%);
|
||||
color: #ffffff;
|
||||
background: rgb(255 255 255 / 5%);
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
color: rgb(255 255 255 / 80%);
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: rgb(255 255 255 / 60%);
|
||||
font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.12em;
|
||||
line-height: 1.5;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.username {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
margin-top: 10px;
|
||||
overflow-wrap: anywhere;
|
||||
color: #b4f7de;
|
||||
background: linear-gradient(100deg, #d4ffec, #99f6e4 55%, #c7d2fe);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
color: #ffffff;
|
||||
font-family: "Archivo", ui-sans-serif, system-ui, sans-serif;
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1.25;
|
||||
line-height: 1.1;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.caption {
|
||||
|
|
@ -72,15 +55,10 @@
|
|||
}
|
||||
|
||||
@keyframes arrive {
|
||||
from { opacity: 0; transform: translateY(8px) scale(0.98); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
|
||||
@keyframes glow {
|
||||
0%, 100% { opacity: 0.7; transform: scale(0.95); }
|
||||
50% { opacity: 1; transform: scale(1.06); }
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.panel, .glow { animation: none; }
|
||||
.panel { animation: none; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,8 @@ import styles from "./LoginSuccess.module.css";
|
|||
export function LoginSuccess({ username }: { username: string }) {
|
||||
return (
|
||||
<div className={styles.panel} role="status" aria-live="polite" aria-atomic="true">
|
||||
<div className={styles.glow} aria-hidden="true" />
|
||||
<div className={styles.badge} aria-hidden="true">
|
||||
<CheckIcon size={34} strokeWidth={2} />
|
||||
<CheckIcon size={28} strokeWidth={2} />
|
||||
</div>
|
||||
<h2 className={styles.heading}>
|
||||
Logged in as{" "}
|
||||
|
|
|
|||
|
|
@ -18,9 +18,10 @@ interface ArtCraftSignUpProps {
|
|||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const FIELD_LABEL = "text-xs font-semibold text-white/70 ml-1";
|
||||
const FIELD_LABEL =
|
||||
"block font-mono text-[11px] font-semibold leading-4 uppercase tracking-[0.12em] text-white/70";
|
||||
const FIELD_INPUT =
|
||||
"w-full bg-black/40 border border-white/10 focus:border-primary/50 rounded-xl px-4 py-3 text-white placeholder-white/20 outline-none transition-colors";
|
||||
"h-10 w-full rounded-[3px] border border-white/15 bg-white/5 px-4 py-2 text-sm font-normal leading-5 text-white placeholder-white/20 outline-none transition-colors focus:border-white/40";
|
||||
|
||||
export const ArtCraftSignUp = ({
|
||||
onSubmit,
|
||||
|
|
@ -74,12 +75,12 @@ export const ArtCraftSignUp = ({
|
|||
|
||||
return (
|
||||
<form
|
||||
className="flex w-full flex-col gap-2"
|
||||
className="flex w-full flex-col gap-4"
|
||||
onSubmit={handleSubmit}
|
||||
ref={formRef}
|
||||
>
|
||||
{localError && (
|
||||
<div className="flex items-center justify-center gap-2 rounded-xl border border-red-500/20 bg-red-500/10 px-4 py-3 text-center text-sm text-red-500">
|
||||
<div className="flex items-center justify-center gap-2 border border-red-500/20 bg-red-500/10 px-4 py-3 text-center text-sm text-red-500">
|
||||
<TriangleAlertIcon />
|
||||
{localError}
|
||||
</div>
|
||||
|
|
@ -87,7 +88,7 @@ export const ArtCraftSignUp = ({
|
|||
|
||||
{isSignUp ? (
|
||||
<>
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-2">
|
||||
<label className={FIELD_LABEL}>Username</label>
|
||||
<Input
|
||||
name="username"
|
||||
|
|
@ -97,7 +98,7 @@ export const ArtCraftSignUp = ({
|
|||
inputClassName={FIELD_INPUT}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-2">
|
||||
<label className={FIELD_LABEL}>Email</label>
|
||||
<Input
|
||||
name="email"
|
||||
|
|
@ -110,7 +111,7 @@ export const ArtCraftSignUp = ({
|
|||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-2">
|
||||
<label className={FIELD_LABEL}>Email or Username</label>
|
||||
<Input
|
||||
name="usernameOrEmail"
|
||||
|
|
@ -122,7 +123,7 @@ export const ArtCraftSignUp = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-2">
|
||||
<label className={FIELD_LABEL}>Password</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
|
|
@ -145,7 +146,7 @@ export const ArtCraftSignUp = ({
|
|||
</div>
|
||||
|
||||
{isSignUp && (
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-2">
|
||||
<label className={FIELD_LABEL}>Confirm Password</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
|
|
@ -168,14 +169,14 @@ export const ArtCraftSignUp = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-4">
|
||||
<div className="pt-2">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="h-10 w-full justify-center rounded-full border-none bg-primary font-bold text-white hover:bg-primary-600"
|
||||
className="h-10 w-full justify-center rounded-[3px] border-none bg-white font-mono text-xs font-semibold uppercase tracking-[0.12em] text-black shadow-none hover:bg-white/90"
|
||||
>
|
||||
{isLoading ? (
|
||||
<LoaderCircleIcon className="animate-spin" />
|
||||
<LoaderCircleIcon className="animate-spin" />
|
||||
) : isSignUp ? (
|
||||
"Sign up"
|
||||
) : (
|
||||
|
|
@ -184,7 +185,7 @@ export const ArtCraftSignUp = ({
|
|||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 text-center text-sm text-white/60">
|
||||
<div className="mt-4 text-center text-sm text-white/60">
|
||||
{isSignUp ? "Already have an account?" : "Don't have an account?"}{" "}
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@ import { useLoginModalStore } from "./useLoginModalStore";
|
|||
const DEFAULT_SHOWCASE_VIDEO =
|
||||
"https://frontend-cdn.fakeyou.com/videos/knight-video.mp4";
|
||||
const LOGIN_SUCCESS_DURATION_MS = 3000;
|
||||
const FRAME_CORNERS = [
|
||||
"top-0 left-0",
|
||||
"top-0 right-0",
|
||||
"bottom-0 left-0",
|
||||
"bottom-0 right-0",
|
||||
];
|
||||
|
||||
interface LoginModalProps {
|
||||
onClose?: () => void;
|
||||
|
|
@ -145,10 +151,10 @@ export function LoginModal({
|
|||
if (showSuccess) {
|
||||
return (
|
||||
<div className="flex flex-1 flex-col items-center justify-center px-8 py-16 text-center">
|
||||
<h2 className="mb-2 text-3xl font-bold text-white">
|
||||
<h2 className="mb-3 font-display text-3xl leading-[1.05] tracking-tight text-white sm:text-4xl">
|
||||
Thank you for signing in!
|
||||
</h2>
|
||||
<p className="mb-6 text-white/70">
|
||||
<p className="mb-8 text-sm leading-relaxed text-white/60">
|
||||
You're all set to start creating amazing content.
|
||||
</p>
|
||||
<Button
|
||||
|
|
@ -156,7 +162,7 @@ export function LoginModal({
|
|||
onClick={handleClose}
|
||||
icon={ArrowRightIcon}
|
||||
iconFlip={true}
|
||||
className="text-md"
|
||||
className="h-10 rounded-[3px] bg-white px-5 font-mono text-xs font-semibold uppercase tracking-[0.12em] text-black shadow-none hover:bg-white/90"
|
||||
>
|
||||
Get Started
|
||||
</Button>
|
||||
|
|
@ -167,16 +173,17 @@ export function LoginModal({
|
|||
// Discord
|
||||
return (
|
||||
<div className="flex flex-1 flex-col items-center justify-center px-8 py-16 text-center">
|
||||
<h2 className="mb-2 text-3xl font-bold text-white">
|
||||
Join Our Community
|
||||
<h2 className="mb-3 font-display text-3xl leading-[1.05] tracking-tight text-white sm:text-4xl">
|
||||
Join our <span className="font-serif-italic">community.</span>
|
||||
</h2>
|
||||
<p className="mb-6 max-w-md text-white/70">
|
||||
<p className="mb-8 max-w-md text-sm leading-relaxed text-white/60">
|
||||
Connect with other creators, share your work, and get the latest
|
||||
updates in our Discord community.
|
||||
</p>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="h-10 rounded-[3px] border border-white/15 bg-white/5 px-4 font-mono text-xs font-semibold uppercase tracking-[0.12em] text-white shadow-none hover:bg-white/10"
|
||||
onClick={() => {
|
||||
setShowDiscord(false);
|
||||
setShowSuccess(true);
|
||||
|
|
@ -188,7 +195,7 @@ export function LoginModal({
|
|||
variant="primary"
|
||||
onClick={handleDiscordJoin}
|
||||
icon={DiscordIcon}
|
||||
className="text-md bg-[#5865F2] hover:bg-[#6a76ff]"
|
||||
className="h-10 rounded-[3px] bg-[#5865F2] px-4 font-mono text-xs font-semibold uppercase tracking-[0.12em] text-white shadow-none hover:bg-[#6a76ff]"
|
||||
>
|
||||
Join Discord
|
||||
</Button>
|
||||
|
|
@ -222,59 +229,84 @@ export function LoginModal({
|
|||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<div
|
||||
className={`relative flex w-full overflow-hidden rounded-3xl border bg-[#1C1C20] text-white shadow-2xl ${loggedInUsername !== null ? "max-w-md border-emerald-200/20" : "max-w-5xl border-white/[4%] lg:min-h-[640px]"}`}
|
||||
className={`relative w-full ${loggedInUsername !== null ? "max-w-md" : "max-w-lg lg:max-w-5xl"}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{loggedInUsername !== null ? (
|
||||
<LoginSuccess username={loggedInUsername} />
|
||||
) : inOnboarding ? (
|
||||
renderOnboarding()
|
||||
) : (
|
||||
<>
|
||||
{/* ── Form pane ── (no dismiss control — login is required) */}
|
||||
<div className="relative flex w-full flex-col lg:w-1/2">
|
||||
<div className="flex flex-1 flex-col justify-center px-8 py-10 sm:px-10">
|
||||
<div className="mx-auto w-full max-w-sm">
|
||||
<div className="mb-8 text-center">
|
||||
<img
|
||||
src="/resources/logo/artcraft-icon.png"
|
||||
alt="ArtCraft"
|
||||
className="pointer-events-none mx-auto mb-6 h-12 w-auto select-none"
|
||||
draggable={false}
|
||||
/>
|
||||
<h1 className="mb-2 text-2xl font-semibold">
|
||||
{isSignUp ? "Create your account" : "Welcome back"}
|
||||
</h1>
|
||||
<p className="text-sm text-white/60">
|
||||
{isSignUp
|
||||
? "Sign up to start creating with ArtCraft"
|
||||
: "Log in to your account"}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className={`relative flex w-full overflow-hidden border border-white/20 bg-[#1e1f22] text-white ${loggedInUsername !== null ? "" : "lg:min-h-[560px]"}`}
|
||||
>
|
||||
{loggedInUsername !== null ? (
|
||||
<LoginSuccess username={loggedInUsername} />
|
||||
) : inOnboarding ? (
|
||||
renderOnboarding()
|
||||
) : (
|
||||
<>
|
||||
{/* ── Form pane ── (no dismiss control — login is required) */}
|
||||
<div className="relative flex w-full flex-col lg:w-1/2">
|
||||
<div className="flex flex-1 flex-col justify-center px-6 py-10 sm:px-10 sm:py-12">
|
||||
<div className="w-full">
|
||||
<div className="mb-8 text-left">
|
||||
<img
|
||||
src="/resources/logo/artcraft-icon.png"
|
||||
alt="ArtCraft"
|
||||
className="pointer-events-none mb-8 h-8 w-auto select-none"
|
||||
draggable={false}
|
||||
/>
|
||||
<h1 className="mb-3 text-balance font-display text-3xl leading-[1.05] tracking-tight sm:text-4xl">
|
||||
{isSignUp ? (
|
||||
"Create your account"
|
||||
) : (
|
||||
<>
|
||||
Welcome{" "}
|
||||
<span className="font-serif-italic">back.</span>
|
||||
</>
|
||||
)}
|
||||
</h1>
|
||||
<p className="text-sm leading-relaxed text-white/60">
|
||||
{isSignUp
|
||||
? "Sign up to start creating with ArtCraft"
|
||||
: "Log in to your creative workspace."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!isSignUp && <DesktopLoginBridge onActiveChange={setIsChallengeActive} onStart={() => { authGeneration.current += 1; }} onSuccess={handleLoginSuccess} />}
|
||||
{!isChallengeActive && <ArtCraftSignUp
|
||||
onSubmit={handleAuthSubmit}
|
||||
isSignUp={isSignUp}
|
||||
onToggleMode={() => setIsSignUp((prev) => !prev)}
|
||||
errorMessage={errorMessage}
|
||||
isLoading={isLoading}
|
||||
/>}
|
||||
{!isSignUp && <DesktopLoginBridge onActiveChange={setIsChallengeActive} onStart={() => { authGeneration.current += 1; }} onSuccess={handleLoginSuccess} />}
|
||||
{!isSignUp && !isChallengeActive && (
|
||||
<div className="mb-6 flex items-center gap-4 before:h-px before:flex-1 before:bg-white/15 after:h-px after:flex-1 after:bg-white/15">
|
||||
<span className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/40">
|
||||
or
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{!isChallengeActive && <ArtCraftSignUp
|
||||
onSubmit={handleAuthSubmit}
|
||||
isSignUp={isSignUp}
|
||||
onToggleMode={() => setIsSignUp((prev) => !prev)}
|
||||
errorMessage={errorMessage}
|
||||
isLoading={isLoading}
|
||||
/>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-6 pb-6 text-center font-mono text-[10px] uppercase tracking-[0.12em] text-white/25 sm:px-10">
|
||||
© {new Date().getFullYear()} ArtCraft. All rights
|
||||
reserved.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-8 pb-8 text-center text-xs text-white/20">
|
||||
© {new Date().getFullYear()} ArtCraft. All rights
|
||||
reserved.
|
||||
{/* ── Showcase pane (desktop only) ── */}
|
||||
<div className="relative hidden border-l border-white/15 lg:block lg:w-1/2">
|
||||
<LoginShowcase videoUrl={videoUrl} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Showcase pane (desktop only) ── */}
|
||||
<div className="relative hidden lg:block lg:w-1/2">
|
||||
<LoginShowcase videoUrl={videoUrl} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{FRAME_CORNERS.map((corner) => (
|
||||
<span
|
||||
key={corner}
|
||||
aria-hidden="true"
|
||||
className={`frame-corner-mark ${corner}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</TransitionChild>
|
||||
</div>
|
||||
|
|
@ -287,7 +319,7 @@ export function LoginModal({
|
|||
// with a legibility gradient + caption. Mirrors the webapp auth-showcase.
|
||||
function LoginShowcase({ videoUrl }: { videoUrl: string }) {
|
||||
return (
|
||||
<div className="absolute inset-2 overflow-hidden rounded-2xl bg-black">
|
||||
<div className="absolute inset-0 overflow-hidden bg-[#1e1f22]">
|
||||
<video
|
||||
src={videoUrl}
|
||||
autoPlay
|
||||
|
|
@ -304,7 +336,7 @@ function LoginShowcase({ videoUrl }: { videoUrl: string }) {
|
|||
/>
|
||||
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 p-8">
|
||||
<p className="mb-3 text-xs font-semibold uppercase tracking-widest text-primary-300">
|
||||
<p className="mb-3 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/70">
|
||||
One of the cheapest
|
||||
</p>
|
||||
<h2 className="text-2xl font-bold leading-tight">
|
||||
|
|
|
|||
|
|
@ -87,14 +87,14 @@ export const MenuIconSelector: React.FC<MenuIconSelectorProps> = ({
|
|||
>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative flex gap-1 rounded-full px-1 py-0.5 min-w-max bg-ui-panel border border-ui-panel-border"
|
||||
className="relative flex gap-1 rounded-[3px] px-1 py-0.5 min-w-max bg-ui-panel border border-ui-panel-border"
|
||||
style={{ minWidth: 0 }}
|
||||
onMouseLeave={() => setHoveredIndex(-1)}
|
||||
>
|
||||
{/* Active tab background */}
|
||||
{selectedIndex >= 0 && (
|
||||
<div
|
||||
className="absolute z-10 rounded-full bg-primary/25 border-2 border-primary shadow-md transition-all duration-150 ease-in-out -ml-[1px]"
|
||||
className="absolute z-10 rounded-[2px] bg-base-fg/[0.08] transition-all duration-150 ease-in-out after:absolute after:inset-x-1.5 after:-bottom-1 after:h-0.5 after:bg-base-fg"
|
||||
style={{
|
||||
left: activeStyle.left,
|
||||
width: activeStyle.width,
|
||||
|
|
@ -106,7 +106,7 @@ export const MenuIconSelector: React.FC<MenuIconSelectorProps> = ({
|
|||
{/* Hover background (only if hovering a different tab) */}
|
||||
{hoveredIndex !== -1 && hoveredIndex !== selectedIndex && (
|
||||
<div
|
||||
className="absolute z-20 rounded-full bg-primary/10 transition-all duration-150 ease-in-out pointer-events-none -ml-[1px]"
|
||||
className="absolute z-20 rounded-[2px] bg-base-fg/[0.08] transition-all duration-150 ease-in-out pointer-events-none"
|
||||
style={{
|
||||
left: hoverStyle.left,
|
||||
width: hoverStyle.width,
|
||||
|
|
@ -136,10 +136,7 @@ export const MenuIconSelector: React.FC<MenuIconSelectorProps> = ({
|
|||
closeOnClick={true}
|
||||
interactive={interactive}
|
||||
disabled={disabled}
|
||||
className={twMerge(
|
||||
"text-sm font-semibold",
|
||||
item.large && "text-md",
|
||||
)}
|
||||
className="font-sans text-xs font-medium normal-case tracking-normal"
|
||||
imageSrc={imageSrc || undefined}
|
||||
description={description || undefined}
|
||||
>
|
||||
|
|
@ -151,7 +148,10 @@ export const MenuIconSelector: React.FC<MenuIconSelectorProps> = ({
|
|||
onClick={() => !disabled && onMenuChange(item.id)}
|
||||
onMouseEnter={() => setHoveredIndex(idx)}
|
||||
className={twMerge(
|
||||
"relative z-30 flex flex-col items-center justify-center px-3 py-2 rounded-full transition-all duration-150 text-base-fg",
|
||||
"relative z-30 flex flex-col items-center justify-center px-3 py-2 rounded-[2px] transition-colors duration-150",
|
||||
idx === selectedIndex
|
||||
? "text-base-fg"
|
||||
: "text-base-fg/70 hover:text-base-fg",
|
||||
disabled ? "cursor-not-allowed opacity-60" : "",
|
||||
)}
|
||||
tabIndex={0}
|
||||
|
|
|
|||
|
|
@ -153,8 +153,7 @@ const AnimatedBackdrop = ({
|
|||
style={{
|
||||
opacity: styles.opacity,
|
||||
pointerEvents: "none",
|
||||
background:
|
||||
"radial-gradient(800px 400px at 10% -10%, rgba(45,129,255,0.10), transparent), radial-gradient(600px 320px at 110% 110%, rgba(28,182,190,0.10), transparent), rgba(0,0,0,0.60)",
|
||||
background: "rgba(0, 0, 0, 0.7)",
|
||||
}}
|
||||
/>
|
||||
</Dialog.Overlay>
|
||||
|
|
@ -194,19 +193,13 @@ const ExpandButton = ({ className, size = "md" }: ExpandButtonProps) => {
|
|||
aria-label={expanded ? "Restore modal size" : "Expand modal"}
|
||||
onClick={toggleExpanded}
|
||||
className={twMerge(
|
||||
"flex items-center justify-center rounded-full bg-black/40 text-white/60 transition-all hover:bg-black/70 hover:text-white",
|
||||
"flex items-center justify-center rounded-[3px] bg-black/40 text-white/60 transition-colors hover:bg-white hover:text-black",
|
||||
sizeClasses[size],
|
||||
"relative z-[70]",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<DynamicIcon
|
||||
icon={
|
||||
expanded
|
||||
? Minimize2Icon
|
||||
: Maximize2Icon
|
||||
}
|
||||
/>
|
||||
<DynamicIcon icon={expanded ? Minimize2Icon : Maximize2Icon} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
|
@ -1083,7 +1076,7 @@ export const Modal = ({
|
|||
>
|
||||
<animated.div
|
||||
className={twMerge(
|
||||
"w-full max-w-lg rounded-2xl relative border border-ui-panel-border bg-ui-modal text-left align-middle shadow-2xl z-[70]",
|
||||
"w-full max-w-lg rounded-none relative border border-ui-panel-border bg-ui-modal text-left align-middle z-[70]",
|
||||
childPadding && !expanded ? "p-4" : "",
|
||||
className,
|
||||
"!transition-none", // Always disable CSS transitions for spring animations
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
getCreatorIcon,
|
||||
getModelFamilyName,
|
||||
} from "@storyteller/model-list";
|
||||
import { ChevronUpIcon, CircleCheckIcon } from "lucide-react";
|
||||
import { ChevronUpIcon, CheckIcon } from "lucide-react";
|
||||
import { GenerationProvider } from "@storyteller/api-enums";
|
||||
import { Tooltip } from "@storyteller/ui-tooltip";
|
||||
import { defaultModelForPage } from "./defaultModelForPage";
|
||||
|
|
@ -107,7 +107,7 @@ function ProviderTooltipContent({
|
|||
onFinished?.();
|
||||
}}
|
||||
type="button"
|
||||
className={`group flex cursor-pointer items-center justify-between rounded-lg px-2 py-2 transition-all ${
|
||||
className={`group flex cursor-pointer items-center justify-between rounded-[3px] px-2 py-2 transition-all ${
|
||||
selectedProvider === p
|
||||
? "bg-ui-controls/70 border-l-4 border-primary"
|
||||
: "hover:bg-ui-controls/50"
|
||||
|
|
@ -118,8 +118,8 @@ function ProviderTooltipContent({
|
|||
{getProviderDisplayName(p)}
|
||||
</span>
|
||||
{selectedProvider === p && (
|
||||
<span className="text-primary text-xl font-bold bg-white rounded-full p-0 h-4 w-4 flex items-center justify-center">
|
||||
<CircleCheckIcon />
|
||||
<span className="ml-2 flex h-5 w-5 shrink-0 items-center justify-center text-white">
|
||||
<CheckIcon aria-hidden="true" className="h-4 w-4" strokeWidth={2.5} />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
|
@ -229,7 +229,7 @@ export function ClassyModelSelector({
|
|||
: undefined;
|
||||
const iconProvider = prov ?? allowedProviders[0];
|
||||
return iconProvider ? (
|
||||
<div className="mr-1 rounded-md p-1.5 bg-ui-controls/60 group-hover:bg-ui-controls/80 transition-colors">
|
||||
<div className="mr-1 p-1.5 bg-ui-controls/60 group-hover:bg-ui-controls/80 transition-colors">
|
||||
<span className="text-base-fg/70 group-hover:text-base-fg/90 text-lg">
|
||||
{getProviderIcon(iconProvider)}
|
||||
</span>
|
||||
|
|
@ -241,7 +241,7 @@ export function ClassyModelSelector({
|
|||
isSameModel(item.model as Model | undefined, selectedModel) &&
|
||||
selectedProvider &&
|
||||
hasMultipleProviders ? (
|
||||
<div className="mr-1 rounded-md p-1.5 bg-primary/60 group-hover:bg-primary/80 transition-colors">
|
||||
<div className="mr-1 p-1.5 bg-primary/60 group-hover:bg-primary/80 transition-colors">
|
||||
<span className="text-base-fg/70 group-hover:text-base-fg/90 text-lg">
|
||||
{getProviderIcon(selectedProvider)}
|
||||
</span>
|
||||
|
|
@ -313,7 +313,7 @@ export function ClassyModelSelector({
|
|||
mode="hoverSelect"
|
||||
maxListHeight={maxListHeight}
|
||||
{...popoverProps}
|
||||
buttonClassName="rounded-xl bg-ui-controls/90 hover:bg-ui-controls text-left shadow-sm px-3 py-1 gap-3 border border-ui-controls-border"
|
||||
buttonClassName="rounded-[3px] bg-ui-controls text-left shadow-sm px-3 py-1 gap-3 border border-ui-controls-border"
|
||||
renderTrigger={(selectedItem) => {
|
||||
const modelTitle =
|
||||
selectedItem?.label ?? selectedModel?.selectorName ?? "";
|
||||
|
|
@ -340,9 +340,7 @@ export function ClassyModelSelector({
|
|||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronUpIcon
|
||||
|
||||
className="text-base text-base-fg/70 self-center" />
|
||||
<ChevronUpIcon className="text-base text-base-fg/70 self-center" />
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -81,9 +81,11 @@ export const BoardGrid = ({
|
|||
const [viewportH, setViewportH] = useState(0);
|
||||
const [marquee, setMarquee] = useState<Marquee | null>(null);
|
||||
const [focusedId, setFocusedId] = useState<string | null>(null);
|
||||
const marqueeRef = useRef<{ active: boolean; moved: boolean; additive: boolean }>(
|
||||
{ active: false, moved: false, additive: false },
|
||||
);
|
||||
const marqueeRef = useRef<{
|
||||
active: boolean;
|
||||
moved: boolean;
|
||||
additive: boolean;
|
||||
}>({ active: false, moved: false, additive: false });
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const el = scrollRef.current;
|
||||
|
|
@ -103,7 +105,10 @@ export const BoardGrid = ({
|
|||
const groups = useMemo<SectionGroup[] | null>(() => {
|
||||
if (!sections || sections.length === 0) return null;
|
||||
const known = new Set(sections.map((s) => s.id));
|
||||
const bySection = new Map<string | null, Array<{ id: string; aspect: number }>>();
|
||||
const bySection = new Map<
|
||||
string | null,
|
||||
Array<{ id: string; aspect: number }>
|
||||
>();
|
||||
for (const it of items) {
|
||||
const key = it.sectionId && known.has(it.sectionId) ? it.sectionId : null;
|
||||
const entry = { id: it.id, aspect: it.aspect };
|
||||
|
|
@ -119,7 +124,10 @@ export const BoardGrid = ({
|
|||
}));
|
||||
const ungrouped = bySection.get(null) ?? [];
|
||||
return ungrouped.length > 0
|
||||
? [{ id: null, name: "Ungrouped", collapsed: false, items: ungrouped }, ...named]
|
||||
? [
|
||||
{ id: null, name: "Ungrouped", collapsed: false, items: ungrouped },
|
||||
...named,
|
||||
]
|
||||
: named;
|
||||
}, [items, sections]);
|
||||
|
||||
|
|
@ -313,7 +321,7 @@ export const BoardGrid = ({
|
|||
})}
|
||||
{marquee && marqueeRef.current.moved && (
|
||||
<div
|
||||
className="pointer-events-none absolute rounded-[4px] border border-primary bg-primary/10"
|
||||
className="pointer-events-none absolute rounded-[3px] border border-primary bg-primary/10"
|
||||
style={{
|
||||
left: Math.min(marquee.x0, marquee.x1),
|
||||
top: Math.min(marquee.y0, marquee.y1),
|
||||
|
|
|
|||
|
|
@ -488,20 +488,20 @@ export const Stage3DBody = ({
|
|||
className="absolute left-0 top-0 w-full"
|
||||
onClick={handleOverlayClick}
|
||||
>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-1 2xl:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)]">
|
||||
<div className="min-w-0 flex flex-col items-start gap-2">
|
||||
{!isRecord && <ControlsTopButtons />}
|
||||
{!isRecord && topBarStartSlot && (
|
||||
<div className="pl-3">{topBarStartSlot}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="col-span-2 row-start-2 flex flex-col items-center 2xl:col-span-1 2xl:col-start-2 2xl:row-start-1">
|
||||
<SceneModePill />
|
||||
{!isRecord && (
|
||||
<Controls3D showImageTo3DButton={showImageTo3DButton} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-start justify-end gap-2 pr-3 pt-3">
|
||||
<div className="col-start-2 row-start-1 flex min-w-0 flex-wrap items-start justify-end gap-2 pr-3 pt-3 2xl:col-start-3">
|
||||
{/* Camera cluster: which camera drives the viewport +
|
||||
the render-frame aspect ratio. */}
|
||||
{!isRecord && <CameraStatusPill />}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export const AnonHintChip = () => {
|
|||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
className="glass flex items-center gap-2 rounded-full border border-white/10 px-3 py-1.5 text-xs font-medium text-white/80 shadow-md transition-colors hover:bg-ui-controls/100 hover:text-white"
|
||||
className="flex shrink-0 items-center gap-2 whitespace-nowrap rounded-[3px] border border-ui-panel-border bg-ui-controls px-3 py-1.5 text-xs font-medium text-white/80 transition-colors hover:bg-white/10 hover:text-white"
|
||||
>
|
||||
<LogInIcon className="opacity-70" />
|
||||
Sign up to save
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { CameraAspectRatio } from "../../enums";
|
|||
import { setCameraAspect } from "../../actions";
|
||||
import { usePageSceneStore } from "../../PageSceneStore";
|
||||
import { EngineContext } from "../../contexts/EngineContext";
|
||||
import { TOOLBAR_BUTTON_CLASS_NAME } from "../toolbarStyles";
|
||||
|
||||
// Render-frame aspect ratio picker. Same popover pattern as the prompt
|
||||
// boxes' AspectRatioPicker, but driving the 3D editor's camera letterbox
|
||||
|
|
@ -52,7 +53,7 @@ export const AspectRatioMenu = () => {
|
|||
position="bottom"
|
||||
panelTitle="Aspect Ratio"
|
||||
showIconsInList
|
||||
buttonClassName="glass glass-no-hover h-[34px] rounded-full px-3 text-xs font-medium shadow-xl"
|
||||
buttonClassName={TOOLBAR_BUTTON_CLASS_NAME}
|
||||
triggerIcon={<AspectRatioIcon ratio={current.proportions} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ export const AnimationsModal = () => {
|
|||
<Modal
|
||||
isOpen={animationsModalVisible}
|
||||
onClose={handleClose}
|
||||
className="relative h-[640px] max-w-4xl"
|
||||
className="relative h-[640px] max-w-4xl bg-ui-controls"
|
||||
childPadding={false}
|
||||
showClose={false}
|
||||
backdropClassName="bg-transparent"
|
||||
|
|
@ -228,8 +228,8 @@ export const AnimationsModal = () => {
|
|||
</Modal.DragHandle>
|
||||
<div className="grid h-full grid-cols-12 gap-3">
|
||||
<div className="relative col-span-3 flex h-full flex-col p-3 pt-2 after:absolute after:right-0 after:top-0 after:h-full after:w-px after:bg-gray-200 after:bg-white/10">
|
||||
<div className="flex items-center justify-between gap-2.5 py-0.5">
|
||||
<h2 className="text-[18px] font-semibold opacity-80">
|
||||
<div className="flex h-10 items-center justify-between gap-2.5">
|
||||
<h2 className="font-display text-lg tracking-tight text-white">
|
||||
Animations
|
||||
</h2>
|
||||
</div>
|
||||
|
|
@ -240,9 +240,9 @@ export const AnimationsModal = () => {
|
|||
key={tab.id}
|
||||
variant={activeTab === tab.id ? "primary" : "secondary"}
|
||||
className={twMerge(
|
||||
"w-full justify-start rounded-xl border border-white/[2%] bg-white/[4%] px-3.5 py-2.5 text-left hover:bg-white/15",
|
||||
"w-full justify-start rounded-[3px] border border-transparent bg-transparent px-3.5 py-2.5 text-left hover:bg-white/15",
|
||||
activeTab === tab.id &&
|
||||
"border-primary bg-primary/10 hover:bg-primary/10",
|
||||
"border-white/15 bg-white/10 text-white hover:bg-white/10",
|
||||
)}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
>
|
||||
|
|
@ -259,7 +259,7 @@ export const AnimationsModal = () => {
|
|||
variant="secondary"
|
||||
icon={ArrowUpFromLineIcon}
|
||||
iconClassName="opacity-70"
|
||||
className="w-full justify-center rounded-xl border border-white/10 bg-white/[6%] px-3.5 py-2.5 hover:bg-white/15"
|
||||
className="w-full justify-center rounded-[3px] border border-white/10 bg-white/[6%] px-3.5 py-2.5 hover:bg-white/15"
|
||||
onClick={handleOpenUpload}
|
||||
>
|
||||
Upload animation
|
||||
|
|
@ -281,13 +281,13 @@ export const AnimationsModal = () => {
|
|||
ref={searchInputRef}
|
||||
placeholder="Search animations"
|
||||
className="relative z-[51] grow"
|
||||
inputClassName="pr-2.5"
|
||||
inputClassName="bg-white/5 pr-10 placeholder-white/40"
|
||||
icon={SearchIcon}
|
||||
value={searchTerm}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
setSearchTerm(e.target.value)
|
||||
}
|
||||
iconClassName="text-white/60"
|
||||
iconClassName="pointer-events-none left-3 top-1/2 h-4 w-4 -translate-y-1/2 p-0 text-white/50"
|
||||
/>
|
||||
{searchTerm && (
|
||||
<CloseButton
|
||||
|
|
|
|||
|
|
@ -56,12 +56,12 @@ const AllTabSection = ({
|
|||
<div className="mb-2 flex items-center justify-between">
|
||||
<h3 className="text-md ml-2 font-semibold opacity-90">{label}</h3>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="mr-3 flex items-center gap-1 px-2 py-1 text-xs"
|
||||
variant="ghost"
|
||||
className="mr-3 h-6 gap-1 px-1.5 py-0 text-[10px] tracking-[0.08em] leading-none"
|
||||
onClick={onViewAll}
|
||||
>
|
||||
View all
|
||||
<ChevronRightIcon className="text-xs opacity-70" />
|
||||
<ChevronRightIcon className="h-3 w-3 shrink-0 opacity-70" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-[170px]">
|
||||
|
|
@ -487,7 +487,7 @@ export const AssetModal = () => {
|
|||
<Modal
|
||||
isOpen={assetModalVisible}
|
||||
onClose={handleClose}
|
||||
className="relative h-[640px] max-w-4xl"
|
||||
className="relative h-[640px] max-w-4xl bg-ui-controls"
|
||||
childPadding={false}
|
||||
showClose={false}
|
||||
backdropClassName="bg-transparent"
|
||||
|
|
@ -507,8 +507,8 @@ export const AssetModal = () => {
|
|||
</Modal.DragHandle>
|
||||
<div className="grid h-full grid-cols-12 gap-3">
|
||||
<div className="relative col-span-3 flex h-full flex-col p-3 pt-2 after:absolute after:right-0 after:top-0 after:h-full after:w-px after:bg-gray-200 after:bg-white/10">
|
||||
<div className="flex items-center justify-between gap-2.5 py-0.5">
|
||||
<h2 className="text-[18px] font-semibold opacity-80">
|
||||
<div className="flex h-10 items-center justify-between gap-2.5">
|
||||
<h2 className="font-display text-lg tracking-tight text-white">
|
||||
ArtCraft Presets
|
||||
</h2>
|
||||
</div>
|
||||
|
|
@ -519,9 +519,9 @@ export const AssetModal = () => {
|
|||
key={tab.id}
|
||||
variant={activeAssetTab === tab.id ? "primary" : "secondary"}
|
||||
className={twMerge(
|
||||
"w-full justify-start rounded-xl border border-white/[2%] bg-white/[4%] px-3.5 py-2.5 text-left hover:bg-white/15",
|
||||
"w-full justify-start rounded-[3px] border border-transparent bg-transparent px-3.5 py-2.5 text-left hover:bg-white/15",
|
||||
activeAssetTab === tab.id &&
|
||||
"border-primary bg-primary/10 hover:bg-primary/10",
|
||||
"border-white/15 bg-white/10 text-white hover:bg-white/10",
|
||||
)}
|
||||
onClick={() => setActiveAssetTab(tab.id)}
|
||||
>
|
||||
|
|
@ -551,13 +551,13 @@ export const AssetModal = () => {
|
|||
ref={searchInputRef}
|
||||
placeholder="Search"
|
||||
className="relative z-[51] grow"
|
||||
inputClassName="pr-2.5"
|
||||
inputClassName="bg-white/5 pr-10 placeholder-white/40"
|
||||
icon={SearchIcon}
|
||||
value={searchTerm}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
setSearchTerm(e.target.value)
|
||||
}
|
||||
iconClassName="text-white/60"
|
||||
iconClassName="pointer-events-none left-3 top-1/2 h-4 w-4 -translate-y-1/2 p-0 text-white/50"
|
||||
/>
|
||||
{searchTerm && (
|
||||
<CloseButton
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ export const ItemElement = ({ item }: Props) => {
|
|||
)}
|
||||
|
||||
<div
|
||||
className="pointer-events-none relative aspect-[16/12] w-full select-none overflow-hidden rounded-xl border-[3px] border-white/5 bg-brand-secondary-600 object-cover object-center transition-all group-hover:border-brand-primary"
|
||||
className="pointer-events-none relative aspect-[16/12] w-full select-none overflow-hidden rounded-none border border-white/15 bg-brand-secondary-600 object-cover object-center transition-all group-hover:border-brand-primary"
|
||||
onPointerDown={(event) => dragAndDrop.onPointerDown(event, item, editor)}
|
||||
onClick={handleClick}
|
||||
style={{ cursor: "grab", pointerEvents: "auto" }}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue