mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-20 17:02:53 -04:00
Match desktop app styling to the webapp (#1963)
This commit is contained in:
@@ -31,7 +31,7 @@
|
|||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<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" />
|
rel="stylesheet" />
|
||||||
<!-- <link rel="stylesheet" href="/src/styles.css" /> -->
|
<!-- <link rel="stylesheet" href="/src/styles.css" /> -->
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
+5
-5
@@ -191,11 +191,11 @@ export function GlobalFileDropHandler() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isDragging && modalType === null && (
|
{isDragging && modalType === null && (
|
||||||
<div className="pointer-events-none fixed inset-0 z-[9999] flex items-center justify-center bg-black/40">
|
<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 rounded-2xl border-2 border-dashed border-white/60 bg-black/30 px-16 py-12 text-white backdrop-blur-sm">
|
<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-4xl opacity-80" />
|
<Maximize2Icon className="text-3xl opacity-60" />
|
||||||
<div className="text-xl font-semibold">Drop to Upload</div>
|
<div className="font-display text-2xl tracking-tight">Drop to Upload</div>
|
||||||
<div className="text-sm opacity-60">GLB, PNG, JPG, JPEG, SPZ</div>
|
<div className="hud-label text-white/50">GLB, PNG, JPG, JPEG, SPZ</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
useCostBreakdownModalStore,
|
useCostBreakdownModalStore,
|
||||||
} from "@storyteller/ui-pricing-modal";
|
} from "@storyteller/ui-pricing-modal";
|
||||||
import { HelpMenuButton } from "@storyteller/ui-help-menu";
|
import { HelpMenuButton } from "@storyteller/ui-help-menu";
|
||||||
|
import { CreateEmptyState } from "~/components/generation-feed/CreateEmptyState";
|
||||||
|
|
||||||
type Mode = "image" | "text";
|
type Mode = "image" | "text";
|
||||||
type Variant = "object" | "world";
|
type Variant = "object" | "world";
|
||||||
@@ -550,9 +551,9 @@ export const ImageTo3DExperience = ({
|
|||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className={twMerge(
|
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",
|
hasResults ? "aspect-square w-24" : "aspect-square w-48",
|
||||||
dragActive && "border-primary bg-primary/10",
|
dragActive && "border-white/60 bg-white/5",
|
||||||
)}
|
)}
|
||||||
onDragEnter={(event) => {
|
onDragEnter={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -582,11 +583,11 @@ export const ImageTo3DExperience = ({
|
|||||||
<PlusIcon
|
<PlusIcon
|
||||||
|
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"text-base-fg opacity-90 drop-shadow",
|
"text-base-fg opacity-80",
|
||||||
hasResults ? "text-2xl" : "text-4xl",
|
hasResults ? "text-2xl" : "text-4xl",
|
||||||
)} />
|
)} />
|
||||||
{!hasResults && (
|
{!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
|
Add Image
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -599,7 +600,7 @@ export const ImageTo3DExperience = ({
|
|||||||
{uploadedPreview ? (
|
{uploadedPreview ? (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
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",
|
hasResults ? "aspect-square w-24" : "aspect-square w-48",
|
||||||
)}
|
)}
|
||||||
onClick={() => !isUploading && fileInputRef.current?.click()}
|
onClick={() => !isUploading && fileInputRef.current?.click()}
|
||||||
@@ -614,13 +615,13 @@ export const ImageTo3DExperience = ({
|
|||||||
/>
|
/>
|
||||||
{isUploading && (
|
{isUploading && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isUploading && (
|
{!isUploading && (
|
||||||
<button
|
<button
|
||||||
type="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) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
setUploadedPreview(null);
|
setUploadedPreview(null);
|
||||||
@@ -646,7 +647,7 @@ export const ImageTo3DExperience = ({
|
|||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
id={promptInputId}
|
id={promptInputId}
|
||||||
rows={1}
|
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}
|
value={prompt}
|
||||||
placeholder="Describe any object you want to generate from scratch..."
|
placeholder="Describe any object you want to generate from scratch..."
|
||||||
onChange={(event) => setPrompt(event.target.value)}
|
onChange={(event) => setPrompt(event.target.value)}
|
||||||
@@ -692,8 +693,8 @@ export const ImageTo3DExperience = ({
|
|||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className={twMerge(
|
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",
|
"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-primary bg-primary/10",
|
dragActive && "border-white/60 bg-white/5",
|
||||||
)}
|
)}
|
||||||
onDragEnter={(e) => {
|
onDragEnter={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -731,8 +732,8 @@ export const ImageTo3DExperience = ({
|
|||||||
>
|
>
|
||||||
<PlusIcon
|
<PlusIcon
|
||||||
|
|
||||||
className="text-3xl text-base-fg opacity-90 drop-shadow" />
|
className="text-3xl text-base-fg opacity-80" />
|
||||||
<span className="mt-2 text-sm font-medium text-base-fg/50">
|
<span className="mt-2 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">
|
||||||
Add Images
|
Add Images
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -741,7 +742,7 @@ export const ImageTo3DExperience = ({
|
|||||||
<textarea
|
<textarea
|
||||||
ref={worldTextareaRef}
|
ref={worldTextareaRef}
|
||||||
rows={2}
|
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" }}
|
style={{ maxHeight: "5em" }}
|
||||||
value={worldPrompt}
|
value={worldPrompt}
|
||||||
placeholder="Describe your 3D world (optional)..."
|
placeholder="Describe your 3D world (optional)..."
|
||||||
@@ -755,7 +756,7 @@ export const ImageTo3DExperience = ({
|
|||||||
<div
|
<div
|
||||||
key={img.id}
|
key={img.id}
|
||||||
className={twMerge(
|
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",
|
hasResults ? "w-10" : "w-auto",
|
||||||
img.isUploading
|
img.isUploading
|
||||||
? "cursor-default"
|
? "cursor-default"
|
||||||
@@ -773,13 +774,13 @@ export const ImageTo3DExperience = ({
|
|||||||
/>
|
/>
|
||||||
{img.isUploading && (
|
{img.isUploading && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-black/20">
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!img.isUploading && (
|
{!img.isUploading && (
|
||||||
<button
|
<button
|
||||||
type="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) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
removeWorldImage(img.id);
|
removeWorldImage(img.id);
|
||||||
@@ -821,7 +822,7 @@ export const ImageTo3DExperience = ({
|
|||||||
<Button
|
<Button
|
||||||
variant="action"
|
variant="action"
|
||||||
className={twMerge(
|
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",
|
hasResults ? "w-10" : "w-14",
|
||||||
)}
|
)}
|
||||||
onClick={() => worldFileInputRef.current?.click()}
|
onClick={() => worldFileInputRef.current?.click()}
|
||||||
@@ -837,7 +838,7 @@ export const ImageTo3DExperience = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const imageCountRow = (
|
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>
|
<span>
|
||||||
{worldImages.length}/{MAX_WORLD_IMAGES} images
|
{worldImages.length}/{MAX_WORLD_IMAGES} images
|
||||||
</span>
|
</span>
|
||||||
@@ -866,7 +867,7 @@ export const ImageTo3DExperience = ({
|
|||||||
<textarea
|
<textarea
|
||||||
ref={worldTextareaRef}
|
ref={worldTextareaRef}
|
||||||
rows={1}
|
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}
|
value={worldPrompt}
|
||||||
placeholder="Describe world (optional)..."
|
placeholder="Describe world (optional)..."
|
||||||
onChange={(e) => setWorldPrompt(e.target.value)}
|
onChange={(e) => setWorldPrompt(e.target.value)}
|
||||||
@@ -888,7 +889,7 @@ export const ImageTo3DExperience = ({
|
|||||||
<textarea
|
<textarea
|
||||||
ref={worldTextareaRef}
|
ref={worldTextareaRef}
|
||||||
rows={2}
|
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" }}
|
style={{ maxHeight: "5em" }}
|
||||||
value={worldPrompt}
|
value={worldPrompt}
|
||||||
placeholder="Describe your 3D world (optional)..."
|
placeholder="Describe your 3D world (optional)..."
|
||||||
@@ -908,7 +909,7 @@ export const ImageTo3DExperience = ({
|
|||||||
results.find((r) => r.id === selectedResultId) || results[0];
|
results.find((r) => r.id === selectedResultId) || results[0];
|
||||||
|
|
||||||
return (
|
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 && (
|
{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%)]" />
|
<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">
|
<div className="relative z-10 h-full w-full p-8">
|
||||||
{!hasResults && (
|
{!hasResults && (
|
||||||
<div className="pointer-events-none absolute left-0 top-[calc(50%-280px)] w-full text-center">
|
<div className="pointer-events-none absolute left-0 top-[calc(50%-280px)] flex w-full justify-center">
|
||||||
<h1 className="mb-3 text-7xl font-bold tracking-tight">{title}</h1>
|
<CreateEmptyState
|
||||||
<p className="text-xl text-base-fg/70">{subtitle}</p>
|
title={title}
|
||||||
|
subtitle={subtitle}
|
||||||
|
className="py-8 sm:py-8"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -945,7 +949,7 @@ export const ImageTo3DExperience = ({
|
|||||||
style={{ height: `calc(100vh - ${bottomOffsetPx + 80}px)` }}
|
style={{ height: `calc(100vh - ${bottomOffsetPx + 80}px)` }}
|
||||||
>
|
>
|
||||||
{/* Left: Viewer */}
|
{/* 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
|
<Viewer3D
|
||||||
key={activeResult?.id}
|
key={activeResult?.id}
|
||||||
modelUrl={activeResult?.modelUrl}
|
modelUrl={activeResult?.modelUrl}
|
||||||
@@ -1003,13 +1007,15 @@ export const ImageTo3DExperience = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right: History List */}
|
{/* Right: History List */}
|
||||||
<div className="glass flex h-full flex-col overflow-hidden rounded-xl border border-ui-panel-border">
|
<div className="flex h-full flex-col overflow-hidden border border-ui-panel-border bg-ui-panel">
|
||||||
<div className="flex items-center justify-between p-4">
|
<div className="flex items-center justify-between border-b border-ui-panel-border px-4 py-3">
|
||||||
<h3 className="font-semibold text-base-fg/80">History</h3>
|
<h3 className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">
|
||||||
|
History
|
||||||
|
</h3>
|
||||||
{results.length > 0 && (
|
{results.length > 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={resetResults}
|
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
|
Clear Session
|
||||||
</button>
|
</button>
|
||||||
@@ -1025,13 +1031,13 @@ export const ImageTo3DExperience = ({
|
|||||||
key={result.id}
|
key={result.id}
|
||||||
onClick={() => setSelectedResultId(result.id)}
|
onClick={() => setSelectedResultId(result.id)}
|
||||||
className={twMerge(
|
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
|
isSelected
|
||||||
? "border-primary/50 bg-primary/10"
|
? "border-white bg-white/10"
|
||||||
: "border-transparent bg-ui-controls/20",
|
: "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 ? (
|
{result.previewUrl ? (
|
||||||
<img
|
<img
|
||||||
src={result.previewUrl}
|
src={result.previewUrl}
|
||||||
@@ -1050,7 +1056,7 @@ export const ImageTo3DExperience = ({
|
|||||||
)}
|
)}
|
||||||
{isPending && (
|
{isPending && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-black/30">
|
<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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1064,7 +1070,7 @@ export const ImageTo3DExperience = ({
|
|||||||
Generating…
|
Generating…
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-base-fg/45">
|
<span className="font-mono tabular-nums text-base-fg/45">
|
||||||
{formatTime(result.timestamp)}
|
{formatTime(result.timestamp)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -1094,7 +1100,7 @@ export const ImageTo3DExperience = ({
|
|||||||
<div ref={promptContentRef}>
|
<div ref={promptContentRef}>
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"glass w-full rounded-xl shadow-2xl ring-1 ring-white/10",
|
"glass w-full",
|
||||||
hasResults ? "p-3" : "p-5",
|
hasResults ? "p-3" : "p-5",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -1139,7 +1145,7 @@ export const ImageTo3DExperience = ({
|
|||||||
activeTab={activeMode}
|
activeTab={activeMode}
|
||||||
onTabChange={(tabId) => setActiveMode(tabId as Mode)}
|
onTabChange={(tabId) => setActiveMode(tabId as Mode)}
|
||||||
className="w-fit"
|
className="w-fit"
|
||||||
indicatorClassName="bg-primary/25"
|
indicatorClassName="bg-white/15"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -1196,12 +1202,12 @@ export const ImageTo3DExperience = ({
|
|||||||
|
|
||||||
{previewImage && (
|
{previewImage && (
|
||||||
<div
|
<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)}
|
onClick={() => setPreviewImage(null)}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="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)}
|
onClick={() => setPreviewImage(null)}
|
||||||
>
|
>
|
||||||
<XIcon className="h-4 w-4" />
|
<XIcon className="h-4 w-4" />
|
||||||
@@ -1209,7 +1215,7 @@ export const ImageTo3DExperience = ({
|
|||||||
<img
|
<img
|
||||||
src={previewImage}
|
src={previewImage}
|
||||||
alt="Preview"
|
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()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+21
-20
@@ -1,15 +1,16 @@
|
|||||||
import { useEffect, useState, type ReactNode } from "react";
|
import { useEffect, useState, type ReactNode } from "react";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { CreateEmptyState } from "./CreateEmptyState";
|
||||||
|
|
||||||
// Shared page chrome for the desktop Generate Image / Generate Video pages,
|
// Shared page chrome for the desktop Generate Image / Generate Video pages,
|
||||||
// mirroring the webapp's CreateMediaPageShell: a centered hero over the
|
// mirroring the webapp's CreateMediaPageShell: a centered empty-state panel,
|
||||||
// decorative background, and a scrollable generation feed (padded past the
|
// and a scrollable generation feed (padded past the fixed promptbox) once
|
||||||
// fixed promptbox) once there is anything to show.
|
// there is anything to show.
|
||||||
//
|
//
|
||||||
// The hero + background behave like a splash screen: they stay up while the
|
// The empty state (and optional background) behave like a splash screen:
|
||||||
// feed is still loading (pages remount on tab switches, so this covers the
|
// they stay up while the feed is still loading (pages remount on tab
|
||||||
// reload) and fade out once content arrives. With a genuinely empty library
|
// switches, so this covers the reload) and fade out once content arrives.
|
||||||
// they simply stay — they ARE the empty state.
|
// With a genuinely empty library they simply stay.
|
||||||
|
|
||||||
const SPLASH_FADE_MS = 700;
|
const SPLASH_FADE_MS = 700;
|
||||||
|
|
||||||
@@ -17,7 +18,8 @@ interface DesktopCreatePageShellProps {
|
|||||||
hasContent: boolean;
|
hasContent: boolean;
|
||||||
emptyStateTitle: string;
|
emptyStateTitle: string;
|
||||||
emptyStateSubtitle: 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;
|
background?: ReactNode;
|
||||||
/** Height of the fixed promptbox area, used to pad the feed's bottom. */
|
/** Height of the fixed promptbox area, used to pad the feed's bottom. */
|
||||||
bottomOffset: number;
|
bottomOffset: number;
|
||||||
@@ -55,11 +57,11 @@ export function DesktopCreatePageShell({
|
|||||||
}, [hasContent, splashState]);
|
}, [hasContent, splashState]);
|
||||||
|
|
||||||
return (
|
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">
|
<div className="relative h-full w-full">
|
||||||
{hasContent && (
|
{hasContent && (
|
||||||
<div
|
<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 }}
|
style={{ paddingBottom: bottomOffset + 24 }}
|
||||||
>
|
>
|
||||||
{listContent}
|
{listContent}
|
||||||
@@ -73,15 +75,14 @@ export function DesktopCreatePageShell({
|
|||||||
splashState === "fading" ? "opacity-0" : "opacity-100",
|
splashState === "fading" ? "opacity-0" : "opacity-100",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center pb-52">
|
<div
|
||||||
<div className="relative z-20 flex flex-col items-center justify-center text-center drop-shadow-xl">
|
className="relative z-20 flex h-full w-full items-center justify-center px-6"
|
||||||
<h1 className="text-7xl font-bold text-base-fg">
|
style={{ paddingBottom: bottomOffset }}
|
||||||
{emptyStateTitle}
|
>
|
||||||
</h1>
|
<CreateEmptyState
|
||||||
<span className="pt-2 text-xl text-base-fg opacity-80">
|
title={emptyStateTitle}
|
||||||
{emptyStateSubtitle}
|
subtitle={emptyStateSubtitle}
|
||||||
</span>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{background}
|
{background}
|
||||||
</div>
|
</div>
|
||||||
@@ -92,7 +93,7 @@ export function DesktopCreatePageShell({
|
|||||||
{hasContent && (
|
{hasContent && (
|
||||||
<div
|
<div
|
||||||
aria-hidden
|
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"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -180,7 +180,7 @@ function SelectionDownloadBar({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
disabled={isDownloading || selectedItems.length === 0}
|
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
|
<DynamicIcon
|
||||||
icon={isDownloading ? LoaderCircleIcon : ArrowDownToLineIcon}
|
icon={isDownloading ? LoaderCircleIcon : ArrowDownToLineIcon}
|
||||||
@@ -226,8 +226,8 @@ function ItemActions({
|
|||||||
|
|
||||||
const buttonClass =
|
const buttonClass =
|
||||||
variant === "card"
|
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-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 rounded-md text-white/70 transition-colors hover:bg-white/10 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(
|
const handleRecreate = useCallback(
|
||||||
async (e: React.MouseEvent) => {
|
async (e: React.MouseEvent) => {
|
||||||
@@ -305,7 +305,7 @@ function ItemActions({
|
|||||||
aria-label="Make Video"
|
aria-label="Make Video"
|
||||||
className={buttonClass}
|
className={buttonClass}
|
||||||
>
|
>
|
||||||
<VideoIcon className="text-sm" />
|
<VideoIcon className="text-sm" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -106,8 +106,8 @@ export const UploadEntryCard = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
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",
|
"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-primary/80 bg-primary/5",
|
isDragActive && "border-white bg-white/10 hover:border-white",
|
||||||
disabled && "pointer-events-none opacity-60",
|
disabled && "pointer-events-none opacity-60",
|
||||||
)}
|
)}
|
||||||
onDragEnter={handleDragEnter}
|
onDragEnter={handleDragEnter}
|
||||||
@@ -124,35 +124,28 @@ export const UploadEntryCard = ({
|
|||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col items-center gap-6">
|
<div className="flex flex-col items-center gap-4">
|
||||||
<div className="relative">
|
<div
|
||||||
<div
|
className={twMerge(
|
||||||
className={twMerge(
|
"flex h-14 w-14 items-center justify-center border",
|
||||||
"relative flex h-32 w-32 items-center justify-center rounded-2xl border-2 shadow-xl backdrop-blur-sm",
|
accentBackgroundClass,
|
||||||
accentBackgroundClass,
|
accentBorderClass,
|
||||||
accentBorderClass,
|
)}
|
||||||
)}
|
>
|
||||||
>
|
<DynamicIcon icon={icon} className="text-xl text-white" />
|
||||||
<DynamicIcon
|
|
||||||
icon={icon}
|
|
||||||
className="text-5xl text-white drop-shadow-lg"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
<div>
|
||||||
<h3 className="text-4xl font-bold tracking-tight text-base-fg">
|
<h3 className="text-lg font-semibold text-base-fg">{title}</h3>
|
||||||
{title}
|
<p className="mx-auto mt-1 max-w-md text-sm text-base-fg/60">
|
||||||
</h3>
|
|
||||||
<p className="mx-auto max-w-md text-base leading-relaxed text-base-fg/70">
|
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex flex-wrap justify-center gap-4">
|
<div className="mt-2 flex flex-wrap justify-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
icon={primaryIcon}
|
icon={primaryIcon}
|
||||||
onClick={handlePrimaryClick}
|
onClick={handlePrimaryClick}
|
||||||
className="px-8 py-3 text-base font-semibold shadow-lg"
|
className="px-3 py-2"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{primaryLabel}
|
{primaryLabel}
|
||||||
@@ -162,7 +155,7 @@ export const UploadEntryCard = ({
|
|||||||
variant="action"
|
variant="action"
|
||||||
icon={secondaryIcon}
|
icon={secondaryIcon}
|
||||||
onClick={onSecondaryClick}
|
onClick={onSecondaryClick}
|
||||||
className="border-2 px-8 py-3 text-base font-semibold"
|
className="px-3 py-2"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{secondaryLabel}
|
{secondaryLabel}
|
||||||
@@ -173,7 +166,7 @@ export const UploadEntryCard = ({
|
|||||||
variant="action"
|
variant="action"
|
||||||
icon={tertiaryIcon}
|
icon={tertiaryIcon}
|
||||||
onClick={onTertiaryClick}
|
onClick={onTertiaryClick}
|
||||||
className="border-2 px-8 py-3 text-base font-semibold"
|
className="px-3 py-2"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{tertiaryLabel}
|
{tertiaryLabel}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const ConfirmationModal = ({
|
|||||||
onClose,
|
onClose,
|
||||||
onOk,
|
onOk,
|
||||||
okText = "OK",
|
okText = "OK",
|
||||||
okColor = "bg-brand-success",
|
okColor = "",
|
||||||
onCancel,
|
onCancel,
|
||||||
cancelText = "Cancel",
|
cancelText = "Cancel",
|
||||||
canHide,
|
canHide,
|
||||||
@@ -52,7 +52,7 @@ export const ConfirmationModal = ({
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-black/40" />
|
<div className="fixed inset-0 bg-black/80" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
<div className="fixed inset-0 flex w-screen items-center justify-center p-4">
|
<div className="fixed inset-0 flex w-screen items-center justify-center p-4">
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
@@ -64,15 +64,15 @@ export const ConfirmationModal = ({
|
|||||||
leaveFrom="opacity-100 scale-100"
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo="opacity-0 scale-95"
|
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
|
<Dialog.Title
|
||||||
as="h4"
|
as="h4"
|
||||||
className="mb-4 text-xl font-bold text-white"
|
className="mb-4 font-display text-xl tracking-tight text-white"
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
|
|
||||||
<div className="mt-2">{text}</div>
|
<div className="mt-2 text-sm leading-relaxed text-white/70">{text}</div>
|
||||||
{canHide && (
|
{canHide && (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<Checkbox id="hide-dialog" label="Do not show this again" />
|
<Checkbox id="hide-dialog" label="Do not show this again" />
|
||||||
@@ -84,7 +84,7 @@ export const ConfirmationModal = ({
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
className="rounded-lg px-3 py-2"
|
className="px-3 py-2"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
>
|
>
|
||||||
{cancelText}
|
{cancelText}
|
||||||
@@ -94,7 +94,7 @@ export const ConfirmationModal = ({
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleOk}
|
onClick={handleOk}
|
||||||
className={[okColor, "rounded-lg px-3 py-2"].join(" ")}
|
className={[okColor, "px-3 py-2"].join(" ").trim()}
|
||||||
>
|
>
|
||||||
{okText}
|
{okText}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ export const CostModal = ({ credits = 1, onClose }: CostModalProps) => {
|
|||||||
transition={{ duration: 0.1, ease: "easeOut" }}
|
transition={{ duration: 0.1, ease: "easeOut" }}
|
||||||
drag
|
drag
|
||||||
dragMomentum={false}
|
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 cursor-move select-none items-center justify-between border-b border-white/15 px-4 py-3">
|
||||||
<div className="flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-base-fg">
|
<div className="flex items-center gap-2 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg">
|
||||||
<CoinsIcon className="text-yellow-400" />
|
<CoinsIcon className="text-primary" />
|
||||||
Cost Breakdown
|
Cost Breakdown
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -53,10 +53,10 @@ export const CostModal = ({ credits = 1, onClose }: CostModalProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4 bg-ui-panel p-4">
|
<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">
|
<div className="mb-1 flex items-center justify-between">
|
||||||
<span className="text-sm text-base-fg/80">Total Cost</span>
|
<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
|
{credits} Credits
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,7 +66,7 @@ export const CostModal = ({ credits = 1, onClose }: CostModalProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<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
|
Currency
|
||||||
</label>
|
</label>
|
||||||
<Select
|
<Select
|
||||||
@@ -77,7 +77,7 @@ export const CostModal = ({ credits = 1, onClose }: CostModalProps) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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
|
1 Credit = $0.01 USD
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -84,7 +84,7 @@ export const UploadFilesMedia = ({
|
|||||||
<h6 className="z-10 text-red">{assetFile.error}</h6>
|
<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 && (
|
{!assetFile.value && (
|
||||||
<h6 className="absolute left-0 top-1/2 -mt-5 w-full text-center">
|
<h6 className="absolute left-0 top-1/2 -mt-5 w-full text-center">
|
||||||
File Preview
|
File Preview
|
||||||
|
|||||||
+1
-1
@@ -85,7 +85,7 @@ export const UploadImageMedia = ({
|
|||||||
<H6 className="z-10 text-red"> {assetFile.error} </H6>
|
<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 && (
|
{!assetFile.value && (
|
||||||
<H6 className="absolute left-0 top-1/2 -mt-5 w-full text-center">
|
<H6 className="absolute left-0 top-1/2 -mt-5 w-full text-center">
|
||||||
File Preview
|
File Preview
|
||||||
|
|||||||
+1
-1
@@ -61,7 +61,7 @@ export function UploadImageMediaModal({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LoadingDots className="mb-1 bg-transparent" />
|
<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:
|
case UploaderStates.success:
|
||||||
|
|||||||
+1
-1
@@ -59,7 +59,7 @@ export function UploadModalMedia({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LoadingDots className="mb-1 bg-transparent" />
|
<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:
|
case UploaderStates.success:
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ export function ErrorDialog() {
|
|||||||
<Modal
|
<Modal
|
||||||
title={errorDialogTitle}
|
title={errorDialogTitle}
|
||||||
titleIcon={TriangleAlertIcon}
|
titleIcon={TriangleAlertIcon}
|
||||||
titleIconClassName="text-brand-primary"
|
titleIconClassName="text-red-400"
|
||||||
isOpen={showErrorDialog}
|
isOpen={showErrorDialog}
|
||||||
onClose={() => setShowErrorDialog(false)}
|
onClose={() => setShowErrorDialog(false)}
|
||||||
showClose={false}
|
showClose={false}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{errorDialogMessage}
|
{errorDialogMessage}
|
||||||
<div className="flex justify-end">
|
<div className="mt-4 flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowErrorDialog(false)}
|
onClick={() => setShowErrorDialog(false)}
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import { twMerge } from "tailwind-merge";
|
|||||||
import {
|
import {
|
||||||
useGenerateApps,
|
useGenerateApps,
|
||||||
useEditApps,
|
useEditApps,
|
||||||
|
getAppCardPalette,
|
||||||
getBadgeStyles,
|
getBadgeStyles,
|
||||||
goToApp,
|
goToApp,
|
||||||
|
type FullAppItem,
|
||||||
} from "~/config/appMenu";
|
} from "~/config/appMenu";
|
||||||
|
|
||||||
export const AppsQuickMenu = () => {
|
export const AppsQuickMenu = () => {
|
||||||
@@ -12,101 +14,68 @@ export const AppsQuickMenu = () => {
|
|||||||
const editApps = useEditApps();
|
const editApps = useEditApps();
|
||||||
return (
|
return (
|
||||||
<div className="grid w-[680px] grid-cols-2 gap-3">
|
<div className="grid w-[680px] grid-cols-2 gap-3">
|
||||||
<div>
|
<AppsQuickMenuSection title="Create" apps={generateApps} />
|
||||||
<h3 className="mb-2 px-2 text-xs font-semibold opacity-50">Create</h3>
|
<AppsQuickMenuSection title="Edit" apps={editApps} />
|
||||||
<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>
|
|
||||||
</div>
|
</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 { useSignals } from "@preact/signals-react/runtime";
|
||||||
import { AUTH_STATUS } from "~/enums";
|
import { AUTH_STATUS } from "~/enums";
|
||||||
import { authentication } from "~/signals";
|
import { authentication } from "~/signals";
|
||||||
import { Button } from "@storyteller/ui-button";
|
|
||||||
// import ProfileDropdown from "./ProfileDropdown";
|
// import ProfileDropdown from "./ProfileDropdown";
|
||||||
|
|
||||||
export const AuthButtons = ({
|
export const AuthButtons = ({
|
||||||
@@ -19,15 +18,14 @@ export const AuthButtons = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-white/20">|</span>
|
<button
|
||||||
<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"
|
||||||
className="h-[38px]"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
loginSignUpPressed();
|
loginSignUpPressed();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Login / Sign Up
|
Login / Sign Up
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -56,20 +56,20 @@ export default function ProfileDropdown() {
|
|||||||
>
|
>
|
||||||
<Menu.Items
|
<Menu.Items
|
||||||
static
|
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}>
|
<Menu.Item key={0}>
|
||||||
{({ active }) => (
|
{({ active }) => (
|
||||||
<a
|
<a
|
||||||
className={twMerge(
|
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",
|
"group flex w-full items-center gap-2 px-4 py-2 text-start text-sm text-white/70 transition-colors",
|
||||||
active && "bg-action-500/80",
|
active && "bg-white/10",
|
||||||
)}
|
)}
|
||||||
href={profileUrl}
|
href={profileUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
<UserIcon />
|
<UserIcon className="text-[11px] text-white/50" />
|
||||||
My Profile
|
My Profile
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
@@ -79,12 +79,15 @@ export default function ProfileDropdown() {
|
|||||||
{({ active }) => (
|
{({ active }) => (
|
||||||
<button
|
<button
|
||||||
className={twMerge(
|
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",
|
"group flex w-full items-center gap-2 px-4 py-2 text-start text-sm text-white/70 transition-colors",
|
||||||
active && "bg-action-500/80",
|
active && "bg-white/10",
|
||||||
)}
|
)}
|
||||||
onClick={() => option.onClick()}
|
onClick={() => option.onClick()}
|
||||||
>
|
>
|
||||||
<DynamicIcon icon={option.icon} />
|
<DynamicIcon
|
||||||
|
icon={option.icon}
|
||||||
|
className="text-[11px] text-white/50"
|
||||||
|
/>
|
||||||
{option.label}
|
{option.label}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import { useSignals } from "@preact/signals-react/runtime";
|
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 { scene, signalScene, authentication } from "~/signals";
|
||||||
import { usePageSceneStore } from "@storyteller/ui-pagescene";
|
import { usePageSceneStore } from "@storyteller/ui-pagescene";
|
||||||
import { Input } from "@storyteller/ui-input";
|
import { Input } from "@storyteller/ui-input";
|
||||||
import { MediaFilesApi } from "~/Classes/ApiManager/MediaFilesApi";
|
import { MediaFilesApi } from "~/Classes/ApiManager/MediaFilesApi";
|
||||||
import { getCreatorIcon, ModelCreator } from "@storyteller/model-list";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
pageName: string;
|
pageName: string;
|
||||||
@@ -95,27 +94,29 @@ export const SceneTitleInput = ({ pageName }: Props) => {
|
|||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
>
|
>
|
||||||
{!showInput && (
|
{!showInput && (
|
||||||
<div className="flex items-center gap-2.5" data-tauri-drag-region>
|
<div className="flex items-center gap-1.5" data-tauri-drag-region>
|
||||||
{getCreatorIcon(
|
<span
|
||||||
ModelCreator.ArtCraft,
|
className="hud-label text-nowrap text-base-fg/70"
|
||||||
"h-5 w-5 icon-auto-contrast opacity-60",
|
data-tauri-drag-region
|
||||||
)}
|
>
|
||||||
<span className="text-nowrap opacity-60">{pageName}</span>
|
{pageName}
|
||||||
<span className="opacity-60">/</span>
|
</span>
|
||||||
|
<ChevronRightIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
className="h-3 w-3 shrink-0 text-base-fg/50"
|
||||||
|
/>
|
||||||
|
|
||||||
{isSceneOwner ? (
|
{isSceneOwner ? (
|
||||||
<button
|
<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}
|
onClick={handleShowInput}
|
||||||
data-tauri-drag-region="false"
|
data-tauri-drag-region="false"
|
||||||
>
|
>
|
||||||
{scene.value.title || ""}
|
<span className="truncate">{scene.value.title || ""}</span>
|
||||||
<PencilIcon
|
<PencilIcon className="ml-2 shrink-0 text-sm opacity-50" />
|
||||||
|
|
||||||
className="ml-2 text-sm opacity-50" />
|
|
||||||
</button>
|
</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 || ""}
|
{scene.value.title || ""}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -128,8 +129,8 @@ export const SceneTitleInput = ({ pageName }: Props) => {
|
|||||||
disabled={scene.value.ownerToken !== userInfo.value?.user_token}
|
disabled={scene.value.ownerToken !== userInfo.value?.user_token}
|
||||||
className="w-[420px]"
|
className="w-[420px]"
|
||||||
inputClassName={twMerge(
|
inputClassName={twMerge(
|
||||||
"text-center h-8 focus:outline-brand-primary",
|
"text-center h-[34px] text-sm font-semibold focus:outline-white/60",
|
||||||
isSaving && "outline-brand-secondary",
|
isSaving && "outline-white/30",
|
||||||
)}
|
)}
|
||||||
isError={!isValid}
|
isError={!isValid}
|
||||||
value={scene.value.title || ""}
|
value={scene.value.title || ""}
|
||||||
@@ -152,7 +153,9 @@ export const SceneTitleInput = ({ pageName }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isSaving && <LoaderCircleIcon className="animate-spin" />}
|
{isSaving && (
|
||||||
|
<LoaderCircleIcon className="shrink-0 animate-spin text-sm opacity-70" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import { coverImageCache } from "~/pages/PageImageTo3DObject/ImageTo3DStore";
|
|||||||
import { useCreditsState } from "@storyteller/credits";
|
import { useCreditsState } from "@storyteller/credits";
|
||||||
import { getMetaForTask, cleanupOldEntries } from "./taskEnqueueMeta";
|
import { getMetaForTask, cleanupOldEntries } from "./taskEnqueueMeta";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
type InProgressTask = {
|
type InProgressTask = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -159,7 +160,7 @@ const CopyPromptButton = ({ prompt }: { prompt: string }) => {
|
|||||||
delay={300}
|
delay={300}
|
||||||
>
|
>
|
||||||
<button
|
<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"
|
aria-label="Copy prompt"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -195,7 +196,7 @@ const InProgressCard = ({
|
|||||||
const hasRefImages = task.refImageUrls && task.refImageUrls.length > 0;
|
const hasRefImages = task.refImageUrls && task.refImageUrls.length > 0;
|
||||||
|
|
||||||
const thumbnailContent = hasRefImages ? (
|
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
|
<img
|
||||||
src={task.refImageUrls![0]}
|
src={task.refImageUrls![0]}
|
||||||
alt="Reference"
|
alt="Reference"
|
||||||
@@ -212,13 +213,13 @@ const InProgressCard = ({
|
|||||||
size="lg" />
|
size="lg" />
|
||||||
</div>
|
</div>
|
||||||
{task.refImageUrls!.length > 1 && (
|
{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}
|
+{task.refImageUrls!.length - 1}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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
|
<LoaderCircleIcon
|
||||||
|
|
||||||
className="animate-spin text-base-fg/60"
|
className="animate-spin text-base-fg/60"
|
||||||
@@ -227,7 +228,7 @@ const InProgressCard = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
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">
|
<div className="flex items-center gap-2.5">
|
||||||
{thumbnailContent}
|
{thumbnailContent}
|
||||||
<div className="min-w-0 flex-1">
|
<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."
|
content="Seedance 2.0 is in Early Alpha. Generations may be slow and may experience outages."
|
||||||
position="top"
|
position="top"
|
||||||
strategy="fixed"
|
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}
|
zIndex={50}
|
||||||
delay={100}
|
delay={100}
|
||||||
>
|
>
|
||||||
@@ -249,7 +250,7 @@ const InProgressCard = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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}%
|
{progressPercent}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -259,9 +260,9 @@ const InProgressCard = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="mt-1.5 flex items-center gap-2">
|
<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
|
<div
|
||||||
className="h-1.5 rounded bg-brand-primary-400"
|
className="h-1.5 bg-white"
|
||||||
style={{
|
style={{
|
||||||
width: `${Math.max(0, Math.min(100, task.progress))}%`,
|
width: `${Math.max(0, Math.min(100, task.progress))}%`,
|
||||||
}}
|
}}
|
||||||
@@ -269,7 +270,7 @@ const InProgressCard = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{timeLabel && (
|
{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" />}
|
{task.prompt && <PromptLine prompt={task.prompt} className="mt-0" />}
|
||||||
</div>
|
</div>
|
||||||
@@ -277,7 +278,7 @@ const InProgressCard = ({
|
|||||||
{task.prompt && <CopyPromptButton prompt={task.prompt} />}
|
{task.prompt && <CopyPromptButton prompt={task.prompt} />}
|
||||||
{onDismiss && (
|
{onDismiss && (
|
||||||
<button
|
<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"
|
aria-label="Dismiss"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -304,12 +305,12 @@ const CompletedCard = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<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}
|
onClick={onClick}
|
||||||
role={onClick ? "button" : undefined}
|
role={onClick ? "button" : undefined}
|
||||||
tabIndex={onClick ? 0 : -1}
|
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 ? (
|
{task.thumbnailUrl ? (
|
||||||
<img
|
<img
|
||||||
src={task.thumbnailUrl}
|
src={task.thumbnailUrl}
|
||||||
@@ -342,7 +343,7 @@ const CompletedCard = ({
|
|||||||
)}
|
)}
|
||||||
{task.completedAt && (
|
{task.completedAt && (
|
||||||
<div className="text-xs text-base-fg opacity-60">
|
<div className="text-xs text-base-fg opacity-60">
|
||||||
{task.completedAt.toISOString()}
|
{dayjs(task.completedAt).format("MMM D, h:mm A")}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{task.prompt && <PromptLine prompt={task.prompt} />}
|
{task.prompt && <PromptLine prompt={task.prompt} />}
|
||||||
@@ -351,7 +352,7 @@ const CompletedCard = ({
|
|||||||
{task.prompt && <CopyPromptButton prompt={task.prompt} />}
|
{task.prompt && <CopyPromptButton prompt={task.prompt} />}
|
||||||
{onDismiss && (
|
{onDismiss && (
|
||||||
<button
|
<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"
|
aria-label="Dismiss"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -398,7 +399,7 @@ const FailedCard = ({
|
|||||||
const hasRefImages = task.refImageUrls && task.refImageUrls.length > 0;
|
const hasRefImages = task.refImageUrls && task.refImageUrls.length > 0;
|
||||||
|
|
||||||
const thumbnailContent = hasRefImages ? (
|
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
|
<img
|
||||||
src={task.refImageUrls![0]}
|
src={task.refImageUrls![0]}
|
||||||
alt="Reference"
|
alt="Reference"
|
||||||
@@ -415,13 +416,13 @@ const FailedCard = ({
|
|||||||
size="lg" />
|
size="lg" />
|
||||||
</div>
|
</div>
|
||||||
{task.refImageUrls!.length > 1 && (
|
{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}
|
+{task.refImageUrls!.length - 1}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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
|
<CircleAlertIcon
|
||||||
|
|
||||||
className="text-red-400"
|
className="text-red-400"
|
||||||
@@ -430,7 +431,7 @@ const FailedCard = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
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">
|
<div className="flex items-center gap-2.5">
|
||||||
{thumbnailContent}
|
{thumbnailContent}
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
@@ -445,7 +446,7 @@ const FailedCard = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="mt-1 flex min-w-0 items-center gap-1.5 overflow-hidden">
|
<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}
|
{statusLabel}
|
||||||
</span>
|
</span>
|
||||||
{task.failureReason && (
|
{task.failureReason && (
|
||||||
@@ -478,13 +479,18 @@ const FailedCard = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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} />}
|
{task.prompt && <PromptLine prompt={task.prompt} />}
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto flex shrink-0 items-center gap-1">
|
<div className="ml-auto flex shrink-0 items-center gap-1">
|
||||||
{task.prompt && <CopyPromptButton prompt={task.prompt} />}
|
{task.prompt && <CopyPromptButton prompt={task.prompt} />}
|
||||||
{onDismiss && (
|
{onDismiss && (
|
||||||
<button
|
<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"
|
aria-label="Dismiss"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -1002,23 +1008,21 @@ export const TaskQueue = () => {
|
|||||||
<Tooltip content="Task Queue" position="bottom" closeOnClick={true}>
|
<Tooltip content="Task Queue" position="bottom" closeOnClick={true}>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{badgeCount > 0 && (
|
{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}
|
{badgeCount}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<PopoverMenu
|
<PopoverMenu
|
||||||
mode="default"
|
mode="default"
|
||||||
buttonClassName="h-[34px] w-[34px] !p-0 relative"
|
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-ui-panel mt-2.5"
|
panelClassName="w-[400px] p-2 bg-[#101014] border border-white/15 mt-2 rounded-[3px]"
|
||||||
position="bottom"
|
position="bottom"
|
||||||
align="end"
|
align="end"
|
||||||
triggerIcon={
|
triggerIcon={
|
||||||
inProgressCount > 0 ? (
|
inProgressCount > 0 ? (
|
||||||
<LoaderCircleIcon
|
<LoaderCircleIcon className="animate-spin text-[11px]" />
|
||||||
|
|
||||||
className="animate-spin" />
|
|
||||||
) : (
|
) : (
|
||||||
<ListChecksIcon />
|
<ListChecksIcon className="text-[11px]" />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onOpenChange={handleOpenChange}
|
onOpenChange={handleOpenChange}
|
||||||
@@ -1037,7 +1041,7 @@ export const TaskQueue = () => {
|
|||||||
<div>
|
<div>
|
||||||
{inProgress.length > 0 && (
|
{inProgress.length > 0 && (
|
||||||
<div className="mb-4">
|
<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
|
In Progress
|
||||||
</div>
|
</div>
|
||||||
{inProgress.map((t) => (
|
{inProgress.map((t) => (
|
||||||
@@ -1056,7 +1060,7 @@ export const TaskQueue = () => {
|
|||||||
{failed.length > 0 && (
|
{failed.length > 0 && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="mb-1 flex items-center justify-between px-1">
|
<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
|
Failed
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -1081,7 +1085,7 @@ export const TaskQueue = () => {
|
|||||||
{completed.length > 0 && (
|
{completed.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-1 flex items-center justify-between px-1">
|
<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
|
Completed
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -1159,7 +1163,7 @@ export const TaskQueue = () => {
|
|||||||
showClose={false}
|
showClose={false}
|
||||||
>
|
>
|
||||||
<div className="flex h-full flex-col">
|
<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">
|
<div className="flex items-center justify-between p-3">
|
||||||
<h2 className="text-lg font-semibold">Task Queue</h2>
|
<h2 className="text-lg font-semibold">Task Queue</h2>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -1207,7 +1211,7 @@ export const TaskQueue = () => {
|
|||||||
<div>
|
<div>
|
||||||
{inProgress.length > 0 && (
|
{inProgress.length > 0 && (
|
||||||
<div className="mb-4">
|
<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
|
In Progress
|
||||||
</div>
|
</div>
|
||||||
{inProgress.map((t) => (
|
{inProgress.map((t) => (
|
||||||
@@ -1223,7 +1227,7 @@ export const TaskQueue = () => {
|
|||||||
)}
|
)}
|
||||||
{failed.length > 0 && (
|
{failed.length > 0 && (
|
||||||
<div className="mb-4">
|
<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
|
Failed
|
||||||
</div>
|
</div>
|
||||||
{failed.map((t) => (
|
{failed.map((t) => (
|
||||||
@@ -1237,7 +1241,7 @@ export const TaskQueue = () => {
|
|||||||
)}
|
)}
|
||||||
{completed.length > 0 && (
|
{completed.length > 0 && (
|
||||||
<div>
|
<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
|
Completed
|
||||||
</div>
|
</div>
|
||||||
{completed.map((t) => (
|
{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 { DynamicIcon } from "@storyteller/icons";
|
||||||
import { signal } from "@preact/signals-react";
|
import { signal } from "@preact/signals-react";
|
||||||
import { useSignals } from "@preact/signals-react/runtime";
|
import { useSignals } from "@preact/signals-react/runtime";
|
||||||
import { getCreatorIcon, ModelCreator } from "@storyteller/model-list";
|
|
||||||
import { useCreditsState, type CreditsIconStatus } from "@storyteller/credits";
|
import { useCreditsState, type CreditsIconStatus } from "@storyteller/credits";
|
||||||
import { gtagEvent } from "@storyteller/google-analytics";
|
import { gtagEvent } from "@storyteller/google-analytics";
|
||||||
import { ProviderBillingModal } from "@storyteller/provider-billing-modal";
|
import { ProviderBillingModal } from "@storyteller/provider-billing-modal";
|
||||||
@@ -42,7 +41,7 @@ import {
|
|||||||
} from "@storyteller/ui-generation-list";
|
} from "@storyteller/ui-generation-list";
|
||||||
import { SettingsModal } from "@storyteller/ui-settings-modal";
|
import { SettingsModal } from "@storyteller/ui-settings-modal";
|
||||||
import { Tooltip } from "@storyteller/ui-tooltip";
|
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 { APP_DESCRIPTORS, goToApp } from "~/config/appMenu";
|
||||||
import {
|
import {
|
||||||
applyMakeVideoFromImage,
|
applyMakeVideoFromImage,
|
||||||
@@ -167,10 +166,10 @@ const CreditsCoinWithStatus = ({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className="relative inline-flex">
|
<span className="relative inline-flex">
|
||||||
<CoinsIcon className="text-primary" />
|
<CoinsIcon className="text-[11px] text-primary" />
|
||||||
{showBadge && (
|
{showBadge && (
|
||||||
<span
|
<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]" />
|
<DynamicIcon icon={badgeIconDef} className="text-[7px]" />
|
||||||
</span>
|
</span>
|
||||||
@@ -347,40 +346,50 @@ export const TopBar = ({ pageName }: Props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPageTitle = (): string => {
|
const getPageCrumbs = (): string[] => {
|
||||||
switch (tabStore.activeTabId) {
|
switch (tabStore.activeTabId) {
|
||||||
case "2D":
|
case "2D":
|
||||||
return "Canvas";
|
return ["Studio", "Canvas"];
|
||||||
case "3D":
|
case "3D":
|
||||||
return "3D Editor";
|
return ["Studio", "3D Editor"];
|
||||||
case "IMAGE":
|
case "IMAGE":
|
||||||
return "Create Image";
|
return ["Create", "Image"];
|
||||||
case "VIDEO":
|
case "VIDEO":
|
||||||
return "Create Video";
|
return ["Create", "Video"];
|
||||||
case "AUDIO":
|
case "AUDIO":
|
||||||
return "Create Audio";
|
return ["Create", "Audio"];
|
||||||
case "EDIT":
|
case "EDIT":
|
||||||
return "Edit Image";
|
return ["Studio", "Edit Image"];
|
||||||
case "VIDEO_FRAME_EXTRACTOR":
|
case "VIDEO_FRAME_EXTRACTOR":
|
||||||
return "Video Frame Extractor";
|
return ["Studio", "Frame Extractor"];
|
||||||
case "VIDEO_WATERMARK_REMOVAL":
|
case "VIDEO_WATERMARK_REMOVAL":
|
||||||
return "Video Watermark Remover";
|
return ["Studio", "Video Watermark Remover"];
|
||||||
case "IMAGE_WATERMARK_REMOVAL":
|
case "IMAGE_WATERMARK_REMOVAL":
|
||||||
return "Image Watermark Remover";
|
return ["Studio", "Image Watermark Remover"];
|
||||||
case "IMAGE_TO_3D_OBJECT":
|
case "IMAGE_TO_3D_OBJECT":
|
||||||
return "Image to 3D Object";
|
return ["Create", "3D Object"];
|
||||||
case "IMAGE_TO_3D_WORLD":
|
case "IMAGE_TO_3D_WORLD":
|
||||||
return "Image to 3D World";
|
return ["Create", "3D World"];
|
||||||
case "APPS":
|
case "REMOVE_BACKGROUND":
|
||||||
return "ArtCraft";
|
return ["Studio", "Remove Background"];
|
||||||
|
case "ANGLES":
|
||||||
|
return ["Create", "Angles"];
|
||||||
|
case "STORYBOARD":
|
||||||
|
return ["Create", "Storyboard"];
|
||||||
case "BACKGROUND_CHANGE":
|
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:
|
default:
|
||||||
return "Artcraft";
|
return ["ArtCraft"];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const pageTitle = getPageTitle();
|
const pageCrumbs = getPageCrumbs();
|
||||||
|
|
||||||
const { toggleModal: toggleSubscriptionModal } = usePricingModalStore();
|
const { toggleModal: toggleSubscriptionModal } = usePricingModalStore();
|
||||||
const { toggleModal: toggleCreditsModal } = useCreditsModalStore();
|
const { toggleModal: toggleCreditsModal } = useCreditsModalStore();
|
||||||
@@ -418,7 +427,7 @@ export const TopBar = ({ pageName }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header
|
<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
|
data-tauri-drag-region
|
||||||
>
|
>
|
||||||
<nav
|
<nav
|
||||||
@@ -479,27 +488,47 @@ export const TopBar = ({ pageName }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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
|
data-tauri-drag-region
|
||||||
>
|
>
|
||||||
{tabStore.activeTabId === "3D" ? (
|
{tabStore.activeTabId === "3D" ? (
|
||||||
<SceneTitleInput pageName={pageName} />
|
<SceneTitleInput pageName={pageName} />
|
||||||
) : (
|
) : (
|
||||||
<h1
|
<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
|
data-tauri-drag-region
|
||||||
>
|
>
|
||||||
{getCreatorIcon(
|
{pageCrumbs.map((crumb, i) => {
|
||||||
ModelCreator.ArtCraft,
|
const isLast = i === pageCrumbs.length - 1;
|
||||||
"h-5 w-5 icon-auto-contrast opacity-50",
|
return (
|
||||||
)}
|
<Fragment key={`${crumb}-${i}`}>
|
||||||
{pageTitle}
|
{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>
|
</h1>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2" data-tauri-drag-region>
|
<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 === "IMAGE" ||
|
||||||
tabStore.activeTabId === "VIDEO") && <GallerySelectToggle />}
|
tabStore.activeTabId === "VIDEO") && <GallerySelectToggle />}
|
||||||
{tabStore.activeTabId === "VIDEO" && <GalleryAutoplayToggle />}
|
{tabStore.activeTabId === "VIDEO" && <GalleryAutoplayToggle />}
|
||||||
@@ -508,26 +537,26 @@ export const TopBar = ({ pageName }: Props) => {
|
|||||||
tabStore.activeTabId === "AUDIO") && <GalleryViewToggle />}
|
tabStore.activeTabId === "AUDIO") && <GalleryViewToggle />}
|
||||||
<PopoverMenu
|
<PopoverMenu
|
||||||
position="bottom"
|
position="bottom"
|
||||||
align="center"
|
align="end"
|
||||||
triggerIcon={
|
triggerIcon={
|
||||||
<CreditsCoinWithStatus iconStatus={creditsIconStatus} />
|
<CreditsCoinWithStatus iconStatus={creditsIconStatus} />
|
||||||
}
|
}
|
||||||
triggerLabel={
|
triggerLabel={
|
||||||
<span className="whitespace-nowrap text-sm font-medium">
|
<span className="whitespace-nowrap text-sm font-medium">
|
||||||
{sumTotalCredits} Credits
|
{sumTotalCredits.toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
buttonClassName="h-[30px] px-2 ps-1.5 bg-transparent hover:bg-ui-controls/30 border-0 shadow-none"
|
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-3 bg-ui-panel border border-ui-panel-border text-base-fg"
|
panelClassName="mt-2 bg-[#101014] border border-white/15 text-white rounded-[3px]"
|
||||||
>
|
>
|
||||||
{(close) => (
|
{(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">
|
<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
|
Your credit balance
|
||||||
</span>
|
</span>
|
||||||
<button
|
<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={() => {
|
onClick={() => {
|
||||||
close();
|
close();
|
||||||
toggleCreditsModal();
|
toggleCreditsModal();
|
||||||
@@ -536,15 +565,13 @@ export const TopBar = ({ pageName }: Props) => {
|
|||||||
Buy credits
|
Buy credits
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-4xl font-bold text-base-fg">
|
<div className="flex items-center gap-2 text-3xl font-semibold tracking-tight text-white">
|
||||||
<CoinsIcon
|
<CoinsIcon className="text-xl text-primary" />
|
||||||
|
{sumTotalCredits.toLocaleString()}
|
||||||
className="text-2xl text-primary" />
|
|
||||||
{sumTotalCredits}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<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={() => {
|
onClick={() => {
|
||||||
close();
|
close();
|
||||||
useCostBreakdownModalStore.getState().openModal();
|
useCostBreakdownModalStore.getState().openModal();
|
||||||
@@ -556,7 +583,7 @@ export const TopBar = ({ pageName }: Props) => {
|
|||||||
|
|
||||||
<div className="mt-3 flex gap-2">
|
<div className="mt-3 flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="action"
|
variant="secondary"
|
||||||
className="h-9 grow"
|
className="h-9 grow"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
close();
|
close();
|
||||||
@@ -586,21 +613,35 @@ export const TopBar = ({ pageName }: Props) => {
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
icon={GemIcon}
|
icon={GemIcon}
|
||||||
onClick={toggleSubscriptionModal}
|
onClick={toggleSubscriptionModal}
|
||||||
className="transition-all duration-300"
|
className="h-8 px-3"
|
||||||
>
|
>
|
||||||
Upgrade
|
Upgrade
|
||||||
</Button>
|
</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}>
|
<Tooltip content="Settings" position="bottom" delay={300}>
|
||||||
<Button
|
<Button
|
||||||
aria-label="Settings"
|
aria-label="Settings"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
icon={SettingsIcon}
|
icon={SettingsIcon}
|
||||||
iconClassName="h-5 w-5 shrink-0"
|
iconClassName="h-4 w-4 shrink-0"
|
||||||
className="h-[34px] w-[34px] p-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={() => {
|
onClick={() => {
|
||||||
setSettingsSection("general");
|
setSettingsSection("general");
|
||||||
setIsSettingsModalOpen(true);
|
setIsSettingsModalOpen(true);
|
||||||
@@ -608,19 +649,6 @@ export const TopBar = ({ pageName }: Props) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</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>
|
||||||
|
|
||||||
<div className="no-drag">
|
<div className="no-drag">
|
||||||
@@ -631,14 +659,14 @@ export const TopBar = ({ pageName }: Props) => {
|
|||||||
<div className="no-drag flex items-center">
|
<div className="no-drag flex items-center">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
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}
|
onClick={minimize}
|
||||||
>
|
>
|
||||||
<MinusIcon className="text-xs" />
|
<MinusIcon className="text-xs" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
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}
|
onClick={toggleMaximize}
|
||||||
>
|
>
|
||||||
<DynamicIcon
|
<DynamicIcon
|
||||||
@@ -648,10 +676,10 @@ export const TopBar = ({ pageName }: Props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
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}
|
onClick={close}
|
||||||
>
|
>
|
||||||
<XIcon className="text-lg" />
|
<XIcon className="text-lg" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -18,8 +18,11 @@ export const UploadImagesButton = ({ className }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
icon={UploadIcon}
|
icon={UploadIcon}
|
||||||
iconClassName="h-5 w-5 shrink-0"
|
iconClassName="h-4 w-4 shrink-0"
|
||||||
className={twMerge("h-[38px] w-[38px] p-0", className)}
|
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)}
|
onClick={() => setIsOpen(true)}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -83,98 +83,122 @@ export interface FullAppItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Per-app card palette for the Apps page (webapp-home-style cards). Derived
|
// 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
|
// from each app's Tailwind color family so the icon tile, its border, and the
|
||||||
// hover gradient all share a hue. Keyed by app id; falls back to a neutral
|
// hover state all share a hue. Keyed by app id; falls back to a neutral tone
|
||||||
// slate when an id is missing.
|
// when an id is missing.
|
||||||
interface AppCardPalette {
|
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;
|
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;
|
iconBg: string;
|
||||||
/** Icon glyph color, e.g. "text-blue-300". */
|
/** Icon glyph color, e.g. "text-blue-300". */
|
||||||
iconColor: string;
|
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> = {
|
const APP_CARD_PALETTES: Record<string, AppCardPalette> = {
|
||||||
"text-to-image": {
|
"text-to-image": {
|
||||||
accent: "from-blue-500/20 to-blue-500/0",
|
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",
|
iconColor: "text-blue-300",
|
||||||
|
hoverStyle: "hover:border-blue-400/60 hover:bg-blue-500/10",
|
||||||
},
|
},
|
||||||
"image-to-video": {
|
"image-to-video": {
|
||||||
accent: "from-amber-500/20 to-amber-500/0",
|
accent: "from-purple-500/20 to-purple-500/0",
|
||||||
iconBg: "bg-amber-500/20 border-amber-400/30",
|
iconBg: "bg-purple-500/20 border-purple-400/40",
|
||||||
iconColor: "text-amber-300",
|
iconColor: "text-purple-300",
|
||||||
|
hoverStyle: "hover:border-purple-400/60 hover:bg-purple-500/10",
|
||||||
},
|
},
|
||||||
"create-audio": {
|
"create-audio": {
|
||||||
accent: "from-pink-500/20 to-pink-500/0",
|
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",
|
iconColor: "text-pink-300",
|
||||||
|
hoverStyle: "hover:border-pink-400/60 hover:bg-pink-500/10",
|
||||||
},
|
},
|
||||||
"image-to-3d-object": {
|
"image-to-3d-object": {
|
||||||
accent: "from-emerald-500/20 to-emerald-500/0",
|
accent: "from-cyan-500/20 to-cyan-500/0",
|
||||||
iconBg: "bg-emerald-500/20 border-emerald-400/30",
|
iconBg: "bg-cyan-500/20 border-cyan-400/40",
|
||||||
iconColor: "text-emerald-300",
|
iconColor: "text-cyan-300",
|
||||||
|
hoverStyle: "hover:border-cyan-400/60 hover:bg-cyan-500/10",
|
||||||
},
|
},
|
||||||
"image-to-3d-world": {
|
"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",
|
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",
|
iconColor: "text-sky-300",
|
||||||
},
|
hoverStyle: "hover:border-sky-400/60 hover:bg-sky-500/10",
|
||||||
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",
|
|
||||||
},
|
},
|
||||||
"2d-canvas": {
|
"2d-canvas": {
|
||||||
accent: "from-sky-500/20 to-sky-500/0",
|
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",
|
iconColor: "text-sky-300",
|
||||||
},
|
hoverStyle: "hover:border-sky-400/60 hover:bg-sky-500/10",
|
||||||
"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",
|
|
||||||
},
|
},
|
||||||
"remove-background": {
|
"remove-background": {
|
||||||
accent: "from-violet-500/20 to-violet-500/0",
|
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",
|
iconColor: "text-violet-300",
|
||||||
|
hoverStyle: "hover:border-violet-400/60 hover:bg-violet-500/10",
|
||||||
},
|
},
|
||||||
"video-frame-extractor": {
|
angles: {
|
||||||
accent: "from-rose-500/20 to-rose-500/0",
|
accent: "from-lime-500/20 to-lime-500/0",
|
||||||
iconBg: "bg-rose-500/20 border-rose-400/30",
|
iconBg: "bg-lime-500/20 border-lime-400/40",
|
||||||
iconColor: "text-rose-300",
|
iconColor: "text-lime-300",
|
||||||
|
hoverStyle: "hover:border-lime-400/60 hover:bg-lime-500/10",
|
||||||
},
|
},
|
||||||
"background-change": {
|
storyboard: {
|
||||||
accent: "from-orange-500/20 to-orange-500/0",
|
accent: "from-fuchsia-500/20 to-fuchsia-500/0",
|
||||||
iconBg: "bg-orange-500/20 border-orange-400/30",
|
iconBg: "bg-fuchsia-500/20 border-fuchsia-400/40",
|
||||||
iconColor: "text-orange-300",
|
iconColor: "text-fuchsia-300",
|
||||||
},
|
hoverStyle: "hover:border-fuchsia-400/60 hover:bg-fuchsia-500/10",
|
||||||
"video-editor": {
|
|
||||||
accent: "from-teal-500/20 to-teal-500/0",
|
|
||||||
iconBg: "bg-teal-500/20 border-teal-400/30",
|
|
||||||
iconColor: "text-teal-300",
|
|
||||||
},
|
},
|
||||||
"video-watermark-removal": {
|
"video-watermark-removal": {
|
||||||
accent: "from-cyan-500/20 to-cyan-500/0",
|
accent: "from-yellow-500/20 to-yellow-500/0",
|
||||||
iconBg: "bg-cyan-500/20 border-cyan-400/30",
|
iconBg: "bg-yellow-500/20 border-yellow-400/40",
|
||||||
iconColor: "text-cyan-300",
|
iconColor: "text-yellow-300",
|
||||||
|
hoverStyle: "hover:border-yellow-400/60 hover:bg-yellow-500/10",
|
||||||
},
|
},
|
||||||
"image-watermark-removal": {
|
"image-watermark-removal": {
|
||||||
accent: "from-indigo-500/20 to-indigo-500/0",
|
accent: "from-red-500/20 to-red-500/0",
|
||||||
iconBg: "bg-indigo-500/20 border-indigo-400/30",
|
iconBg: "bg-red-500/20 border-red-400/40",
|
||||||
iconColor: "text-indigo-300",
|
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",
|
accent: "from-white/10 to-white/0",
|
||||||
iconBg: "bg-ui-controls border-ui-controls-border",
|
iconBg: "bg-ui-controls border-ui-controls-border",
|
||||||
iconColor: "text-base-fg",
|
iconColor: "text-base-fg",
|
||||||
|
hoverStyle: "hover:border-white/30 hover:bg-white/10",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAppCardPalette = (id: string): AppCardPalette =>
|
export const getAppCardPalette = (id: string): AppCardPalette =>
|
||||||
@@ -387,13 +412,13 @@ export const useEditApps = (): FullAppItem[] => {
|
|||||||
export const getBadgeStyles = (badge?: string) => {
|
export const getBadgeStyles = (badge?: string) => {
|
||||||
switch (badge) {
|
switch (badge) {
|
||||||
case "NEW":
|
case "NEW":
|
||||||
return "bg-teal-600 text-white";
|
return "border-purple-400/40 text-purple-300";
|
||||||
case "BEST":
|
case "BEST":
|
||||||
return "bg-primary text-white";
|
return "border-primary/30 text-ui-accent-ink";
|
||||||
case "SOON":
|
case "SOON":
|
||||||
return "bg-gray-600 text-white";
|
return "border-white/20 text-base-fg/50";
|
||||||
case "BETA":
|
case "BETA":
|
||||||
return "bg-amber-500 text-white";
|
return "border-amber-400/40 text-amber-300";
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -481,7 +481,7 @@ export const Angles = () => {
|
|||||||
/* ──── Upload screen ──── */
|
/* ──── Upload screen ──── */
|
||||||
<div className="flex flex-1 items-center justify-center p-8 lg:p-16">
|
<div className="flex flex-1 items-center justify-center p-8 lg:p-16">
|
||||||
<div className="w-full max-w-5xl">
|
<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
|
<UploadEntryCard
|
||||||
icon={CrosshairIcon}
|
icon={CrosshairIcon}
|
||||||
title="Angles"
|
title="Angles"
|
||||||
@@ -497,7 +497,7 @@ export const Angles = () => {
|
|||||||
disabled={isLoadingImage}
|
disabled={isLoadingImage}
|
||||||
/>
|
/>
|
||||||
{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" />
|
<LoadingSpinner className="h-12 w-12" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -518,7 +518,7 @@ export const Angles = () => {
|
|||||||
{/* Full-bleed image display */}
|
{/* Full-bleed image display */}
|
||||||
<div className="flex h-full w-full items-center justify-center px-16 pb-56 pt-16">
|
<div className="flex h-full w-full items-center justify-center px-16 pb-56 pt-16">
|
||||||
<div
|
<div
|
||||||
className="relative overflow-hidden rounded-xl shadow-lg"
|
className="relative overflow-hidden"
|
||||||
style={imageContainerStyle}
|
style={imageContainerStyle}
|
||||||
>
|
>
|
||||||
{activeAngle ? (
|
{activeAngle ? (
|
||||||
@@ -583,12 +583,12 @@ export const Angles = () => {
|
|||||||
|
|
||||||
{/* ── Floating bottom angle controls ── */}
|
{/* ── 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="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 */}
|
{/* Orbit sphere */}
|
||||||
<div className="relative shrink-0 px-5 py-5">
|
<div className="relative shrink-0 px-5 py-5">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleTiltStep(1)}
|
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
|
<ChevronUpIcon
|
||||||
|
|
||||||
@@ -596,7 +596,7 @@ export const Angles = () => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleTiltStep(-1)}
|
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
|
<ChevronDownIcon
|
||||||
|
|
||||||
@@ -604,7 +604,7 @@ export const Angles = () => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleRotationStep(-1)}
|
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
|
<ChevronLeftIcon
|
||||||
|
|
||||||
@@ -612,7 +612,7 @@ export const Angles = () => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleRotationStep(1)}
|
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
|
<ChevronRightIcon
|
||||||
|
|
||||||
@@ -632,7 +632,7 @@ export const Angles = () => {
|
|||||||
{/* Sliders group */}
|
{/* Sliders group */}
|
||||||
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
<div className="flex min-w-0 flex-1 flex-col gap-2">
|
||||||
<div className="flex items-center gap-3">
|
<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
|
Rotation
|
||||||
</span>
|
</span>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
@@ -645,13 +645,13 @@ export const Angles = () => {
|
|||||||
suffix="°"
|
suffix="°"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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)}°
|
{rotationToDisplay(angleConfig.rotation)}°
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<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
|
Tilt
|
||||||
</span>
|
</span>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
@@ -664,13 +664,13 @@ export const Angles = () => {
|
|||||||
suffix="°"
|
suffix="°"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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}°
|
{angleConfig.tilt}°
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<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
|
Zoom
|
||||||
</span>
|
</span>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
@@ -682,7 +682,7 @@ export const Angles = () => {
|
|||||||
onChange={handleZoomSlider}
|
onChange={handleZoomSlider}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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}
|
{angleConfig.zoom}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,28 +23,35 @@ export const AppsIndexPage = () => {
|
|||||||
// flush against the right edge; horizontal padding lives on the inner
|
// flush against the right edge; horizontal padding lives on the inner
|
||||||
// wrapper instead.
|
// wrapper instead.
|
||||||
<div className="fixed inset-0 overflow-y-auto bg-ui-background pt-[56px] text-base-fg">
|
<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">
|
<main className="mx-auto w-full max-w-6xl px-5 pb-20 pt-8 sm:px-8 sm:pt-12">
|
||||||
<h1 className="mx-auto text-center text-4xl font-semibold tracking-tight sm:text-6xl">
|
<p className="hud-label mb-4 text-ui-accent-ink">
|
||||||
What will you <span className="text-primary">craft</span> today?
|
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>
|
</h1>
|
||||||
<p className="mx-auto mt-3 max-w-xl text-center text-lg text-base-fg/55">
|
<p className="mt-4 max-w-xl text-sm leading-relaxed text-base-fg/70 sm:text-base">
|
||||||
Every tool in your toolbox - pick one and start creating.
|
Start with an idea. Choose a tool to bring it to life.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-12 space-y-10">
|
{categories.map((category, index) => (
|
||||||
{categories.map((category) => (
|
<section
|
||||||
<section key={category.title}>
|
key={category.title}
|
||||||
<h2 className="mb-4 text-sm font-semibold text-base-fg/85">
|
className={twMerge(
|
||||||
{category.title}
|
"border-t border-ui-border pt-6",
|
||||||
</h2>
|
index === 0 ? "mt-8 sm:mt-10" : "mt-10",
|
||||||
<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} />
|
<h2 className="hud-label mb-4 text-base-fg/70">
|
||||||
))}
|
{category.title}
|
||||||
</div>
|
</h2>
|
||||||
</section>
|
<div className="grid auto-rows-fr gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||||
))}
|
{category.apps.map((app) => (
|
||||||
</div>
|
<AppCard key={app.id} app={app} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -59,27 +66,14 @@ function AppCard({ app }: { app: FullAppItem }) {
|
|||||||
onClick={() => goToApp(app.action)}
|
onClick={() => goToApp(app.action)}
|
||||||
disabled={!enabled}
|
disabled={!enabled}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"group relative block overflow-hidden rounded-2xl bg-ui-controls/50 p-5 text-left transition-colors",
|
"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
|
enabled ? twMerge("cursor-pointer", palette.hoverStyle) : "cursor-default opacity-60",
|
||||||
? "cursor-pointer hover:bg-ui-controls"
|
|
||||||
: "cursor-default opacity-60",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Hover accent — gradient wash matching the app's hue. */}
|
<div className="relative flex w-full items-start gap-4">
|
||||||
{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
|
<div
|
||||||
className={twMerge(
|
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.iconBg,
|
||||||
palette.iconColor,
|
palette.iconColor,
|
||||||
)}
|
)}
|
||||||
@@ -88,15 +82,17 @@ function AppCard({ app }: { app: FullAppItem }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<div className="flex min-w-0 items-center gap-2">
|
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||||
<h3 className="truncate text-base font-semibold text-base-fg">
|
<h3 className="text-base font-bold uppercase tracking-tight text-ui-ink">
|
||||||
{app.label}
|
{app.label}
|
||||||
</h3>
|
</h3>
|
||||||
{app.badge && (
|
{app.badge && (
|
||||||
<span
|
<span
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"shrink-0 rounded-full px-1.5 py-0.5 text-[10px] font-bold uppercase leading-none tracking-wider",
|
"shrink-0 border px-1.5 py-1 font-mono text-[9px] font-medium leading-none tracking-wider",
|
||||||
getBadgeStyles(app.badge),
|
app.badge === "SOON"
|
||||||
|
? getBadgeStyles(app.badge)
|
||||||
|
: "border-primary/30 text-ui-accent-ink",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{app.badge}
|
{app.badge}
|
||||||
@@ -104,12 +100,12 @@ function AppCard({ app }: { app: FullAppItem }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{enabled && (
|
{enabled && (
|
||||||
<ArrowRightIcon
|
<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" />
|
||||||
className="text-sm text-base-fg/40 transition-all group-hover:translate-x-0.5 group-hover:text-base-fg/70" />
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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}
|
{app.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
FilterMediaClasses,
|
FilterMediaClasses,
|
||||||
} from "@storyteller/api";
|
} from "@storyteller/api";
|
||||||
import { useOmniGenAudioModels, useAudioCostEstimate } from "@storyteller/omni-gen";
|
import { useOmniGenAudioModels, useAudioCostEstimate } from "@storyteller/omni-gen";
|
||||||
import BackgroundGallery from "../PageImage/BackgroundGallery";
|
|
||||||
import {
|
import {
|
||||||
galleryModalLightboxImage,
|
galleryModalLightboxImage,
|
||||||
galleryModalLightboxMediaId,
|
galleryModalLightboxMediaId,
|
||||||
@@ -149,7 +148,6 @@ const CreateAudio = () => {
|
|||||||
hasContent={hasContent}
|
hasContent={hasContent}
|
||||||
emptyStateTitle="Create Audio"
|
emptyStateTitle="Create Audio"
|
||||||
emptyStateSubtitle="Describe a song, a sound, or a sample"
|
emptyStateSubtitle="Describe a song, a sound, or a sample"
|
||||||
background={<BackgroundGallery />}
|
|
||||||
bottomOffset={promptHeight + 40}
|
bottomOffset={promptHeight + 40}
|
||||||
listContent={
|
listContent={
|
||||||
<DesktopGenerationGallery
|
<DesktopGenerationGallery
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { SparklesIcon } from "lucide-react";
|
|
||||||
import { toast } from "@storyteller/ui-toaster";
|
import { toast } from "@storyteller/ui-toaster";
|
||||||
import { UploadImageMedia, UploadVideoMedia } from "@storyteller/api";
|
import { UploadImageMedia, UploadVideoMedia } from "@storyteller/api";
|
||||||
import {
|
import {
|
||||||
PromptBoxVFX,
|
PromptBoxVFX,
|
||||||
SubTabStrip,
|
SubTabStrip,
|
||||||
TruchetPattern,
|
|
||||||
VFXResultCard,
|
VFXResultCard,
|
||||||
VFXShowcaseView,
|
VFXShowcaseView,
|
||||||
newIdempotencyToken,
|
newIdempotencyToken,
|
||||||
@@ -14,6 +12,7 @@ import {
|
|||||||
VFX_NOT_AVAILABLE_ERROR,
|
VFX_NOT_AVAILABLE_ERROR,
|
||||||
VFX_SHOWCASE,
|
VFX_SHOWCASE,
|
||||||
} from "@storyteller/ui-vfx";
|
} from "@storyteller/ui-vfx";
|
||||||
|
import { CreateEmptyState } from "~/components/generation-feed/CreateEmptyState";
|
||||||
|
|
||||||
export const PageBackgroundChange = () => {
|
export const PageBackgroundChange = () => {
|
||||||
const subTab = useVFXStore((s) => s.subTab);
|
const subTab = useVFXStore((s) => s.subTab);
|
||||||
@@ -113,34 +112,17 @@ export const PageBackgroundChange = () => {
|
|||||||
className="relative flex flex-1 items-center justify-center px-6"
|
className="relative flex flex-1 items-center justify-center px-6"
|
||||||
style={{ paddingBottom: Math.max(promptBoxHeight + 36, 240) }}
|
style={{ paddingBottom: Math.max(promptBoxHeight + 36, 240) }}
|
||||||
>
|
>
|
||||||
<div
|
<CreateEmptyState
|
||||||
aria-hidden
|
title="No background changes yet"
|
||||||
className="pointer-events-none absolute inset-0 z-0"
|
subtitle="Upload a source video and a reference image, then optionally add a prompt."
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className="flex-1 overflow-y-auto"
|
className="flex-1 overflow-y-auto"
|
||||||
style={{ paddingBottom: Math.max(promptBoxHeight + 36, 240) }}
|
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) => (
|
{history.map((r) => (
|
||||||
<VFXResultCard
|
<VFXResultCard
|
||||||
key={r.id}
|
key={r.id}
|
||||||
@@ -155,6 +137,7 @@ export const PageBackgroundChange = () => {
|
|||||||
failureReason: r.failureReason,
|
failureReason: r.failureReason,
|
||||||
}}
|
}}
|
||||||
onDismiss={() => dismissResult(r.id)}
|
onDismiss={() => dismissResult(r.id)}
|
||||||
|
className="w-[min(960px,calc(100vw-32px))]"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -163,7 +146,7 @@ export const PageBackgroundChange = () => {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
aria-hidden
|
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
|
<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;
|
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 { JobContextType } from "@storyteller/common";
|
||||||
import { PromptBoxImage } from "@storyteller/ui-promptbox";
|
import { PromptBoxImage } from "@storyteller/ui-promptbox";
|
||||||
import { UploadImageMedia, FilterMediaClasses } from "@storyteller/api";
|
import { UploadImageMedia, FilterMediaClasses } from "@storyteller/api";
|
||||||
import BackgroundGallery from "./BackgroundGallery";
|
|
||||||
import {
|
import {
|
||||||
useTextToImagePageModelList,
|
useTextToImagePageModelList,
|
||||||
ModelPage,
|
ModelPage,
|
||||||
@@ -158,7 +157,6 @@ const TextToImage = ({ imageMediaId, imageUrl }: TextToImageProps) => {
|
|||||||
hasContent={hasContent}
|
hasContent={hasContent}
|
||||||
emptyStateTitle="Create Image"
|
emptyStateTitle="Create Image"
|
||||||
emptyStateSubtitle="Add a prompt, then generate"
|
emptyStateSubtitle="Add a prompt, then generate"
|
||||||
background={<BackgroundGallery />}
|
|
||||||
bottomOffset={promptHeight + 40}
|
bottomOffset={promptHeight + 40}
|
||||||
listContent={
|
listContent={
|
||||||
<DesktopGenerationGallery
|
<DesktopGenerationGallery
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ export const ImageTo3DObject = () => {
|
|||||||
title="Generate 3D Object"
|
title="Generate 3D Object"
|
||||||
subtitle="Transform your image into a 3D object with textures."
|
subtitle="Transform your image into a 3D object with textures."
|
||||||
variant="object"
|
variant="object"
|
||||||
backgroundImage="/resources/images/floating-cubes.webp"
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ export const ImageTo3DWorld = () => {
|
|||||||
title="Image to 3D World"
|
title="Image to 3D World"
|
||||||
subtitle="Generate 3D worlds from an image with World Labs"
|
subtitle="Generate 3D worlds from an image with World Labs"
|
||||||
variant="world"
|
variant="world"
|
||||||
backgroundImage="/resources/images/room-of-items.webp"
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+10
-10
@@ -104,7 +104,7 @@ export const ImageWatermarkRemover = () => {
|
|||||||
|
|
||||||
return (
|
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">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<main
|
<main
|
||||||
className={
|
className={
|
||||||
@@ -115,7 +115,7 @@ export const ImageWatermarkRemover = () => {
|
|||||||
>
|
>
|
||||||
{!imageUrl ? (
|
{!imageUrl ? (
|
||||||
<div className="w-full max-w-5xl">
|
<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
|
<UploadEntryCard
|
||||||
icon={DropletIcon}
|
icon={DropletIcon}
|
||||||
title="Remove Image Watermark"
|
title="Remove Image Watermark"
|
||||||
@@ -133,7 +133,7 @@ export const ImageWatermarkRemover = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex w-full max-w-5xl flex-col gap-5">
|
<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">
|
<div className="relative w-full bg-black">
|
||||||
<Button
|
<Button
|
||||||
icon={RotateCwIcon}
|
icon={RotateCwIcon}
|
||||||
@@ -142,7 +142,7 @@ export const ImageWatermarkRemover = () => {
|
|||||||
setImageUrl("");
|
setImageUrl("");
|
||||||
setImageDimensions(null);
|
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
|
Switch Image
|
||||||
</Button>
|
</Button>
|
||||||
@@ -151,7 +151,7 @@ export const ImageWatermarkRemover = () => {
|
|||||||
ref={imageRef}
|
ref={imageRef}
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt="Selected pic"
|
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) => {
|
onLoad={(e) => {
|
||||||
const img = e.currentTarget;
|
const img = e.currentTarget;
|
||||||
setImageDimensions({
|
setImageDimensions({
|
||||||
@@ -177,12 +177,12 @@ export const ImageWatermarkRemover = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</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>
|
<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
|
<ImagesIcon
|
||||||
|
|
||||||
className="text-primary" />
|
className="text-ui-accent-ink" />
|
||||||
Image Information
|
Image Information
|
||||||
</div>
|
</div>
|
||||||
{imageDimensions ? (
|
{imageDimensions ? (
|
||||||
@@ -191,7 +191,7 @@ export const ImageWatermarkRemover = () => {
|
|||||||
<span className="font-medium text-base-fg/70">
|
<span className="font-medium text-base-fg/70">
|
||||||
Resolution
|
Resolution
|
||||||
</span>
|
</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}
|
{imageDimensions.width} × {imageDimensions.height}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -199,7 +199,7 @@ export const ImageWatermarkRemover = () => {
|
|||||||
<span className="font-medium text-base-fg/70">
|
<span className="font-medium text-base-fg/70">
|
||||||
Aspect Ratio
|
Aspect Ratio
|
||||||
</span>
|
</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
|
imageDimensions.width / imageDimensions.height
|
||||||
).toFixed(2)}
|
).toFixed(2)}
|
||||||
|
|||||||
@@ -543,12 +543,12 @@ export const RemoveBackground = () => {
|
|||||||
|
|
||||||
return (
|
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">
|
<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">
|
<main className="flex h-full w-full flex-col items-center justify-center">
|
||||||
{showUploadScreen ? (
|
{showUploadScreen ? (
|
||||||
<div className="w-full max-w-5xl">
|
<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
|
<UploadEntryCard
|
||||||
icon={WandSparklesIcon}
|
icon={WandSparklesIcon}
|
||||||
title="Remove Background"
|
title="Remove Background"
|
||||||
@@ -564,7 +564,7 @@ export const RemoveBackground = () => {
|
|||||||
disabled={isLoadingImage}
|
disabled={isLoadingImage}
|
||||||
/>
|
/>
|
||||||
{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" />
|
<LoadingSpinner className="h-12 w-12" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -580,9 +580,9 @@ export const RemoveBackground = () => {
|
|||||||
onMouseDown={handleCompareMouseDown}
|
onMouseDown={handleCompareMouseDown}
|
||||||
disabled={!activeImage || isProcessing}
|
disabled={!activeImage || isProcessing}
|
||||||
className={twMerge(
|
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
|
isHoldingCompare
|
||||||
? "border-primary bg-primary/20"
|
? "border-white bg-white/10"
|
||||||
: "border-ui-controls-border",
|
: "border-ui-controls-border",
|
||||||
(!activeImage || isProcessing) &&
|
(!activeImage || isProcessing) &&
|
||||||
"cursor-not-allowed opacity-50",
|
"cursor-not-allowed opacity-50",
|
||||||
@@ -598,7 +598,7 @@ export const RemoveBackground = () => {
|
|||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
disabled={!activeImage || isProcessing}
|
disabled={!activeImage || isProcessing}
|
||||||
className={twMerge(
|
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) &&
|
(!activeImage || isProcessing) &&
|
||||||
"cursor-not-allowed opacity-50",
|
"cursor-not-allowed opacity-50",
|
||||||
)}
|
)}
|
||||||
@@ -610,11 +610,11 @@ export const RemoveBackground = () => {
|
|||||||
|
|
||||||
<div className="flex flex-1 items-center justify-center">
|
<div className="flex flex-1 items-center justify-center">
|
||||||
<div
|
<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}
|
style={imageContainerStyle}
|
||||||
>
|
>
|
||||||
{isProcessing && (
|
{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 && (
|
{currentOriginalUrl && (
|
||||||
<img
|
<img
|
||||||
src={currentOriginalUrl}
|
src={currentOriginalUrl}
|
||||||
@@ -624,12 +624,12 @@ export const RemoveBackground = () => {
|
|||||||
)}
|
)}
|
||||||
<div className="relative z-10 flex flex-col items-center gap-4">
|
<div className="relative z-10 flex flex-col items-center gap-4">
|
||||||
<div className="relative">
|
<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
|
<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>
|
</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...
|
Removing Background...
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -679,7 +679,7 @@ export const RemoveBackground = () => {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
ref={progressBarRef}
|
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={{
|
style={{
|
||||||
left: `${revealProgressRef.current}%`,
|
left: `${revealProgressRef.current}%`,
|
||||||
transform: "translateX(-50%)",
|
transform: "translateX(-50%)",
|
||||||
@@ -695,7 +695,7 @@ export const RemoveBackground = () => {
|
|||||||
</div>
|
</div>
|
||||||
</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
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
@@ -711,7 +711,7 @@ export const RemoveBackground = () => {
|
|||||||
position="top"
|
position="top"
|
||||||
showIconsInList
|
showIconsInList
|
||||||
buttonClassName={twMerge(
|
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",
|
isProcessing && "cursor-not-allowed opacity-50",
|
||||||
)}
|
)}
|
||||||
triggerIcon={
|
triggerIcon={
|
||||||
@@ -724,10 +724,10 @@ export const RemoveBackground = () => {
|
|||||||
key={img.id}
|
key={img.id}
|
||||||
onClick={() => handleThumbnailClick(img)}
|
onClick={() => handleThumbnailClick(img)}
|
||||||
className={twMerge(
|
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
|
img.id === activeImageId
|
||||||
? "border-primary ring-2 ring-primary/30"
|
? "border-white"
|
||||||
: "border-transparent hover:border-primary/50",
|
: "border-white/15 hover:border-white/30",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -40,11 +40,11 @@ const formatTime = (s: number): string => {
|
|||||||
const EmptyState = ({ onAdd }: { onAdd: () => void }) => (
|
const EmptyState = ({ onAdd }: { onAdd: () => void }) => (
|
||||||
<div className="flex h-[calc(100vh-56px)] w-full items-center justify-center bg-ui-background">
|
<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 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" />
|
<ImagesIcon className="text-4xl text-base-fg/40" />
|
||||||
</div>
|
</div>
|
||||||
<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>
|
<p className="mt-1 text-sm text-base-fg/50">Add your first shot to get started</p>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="primary" icon={PlusIcon} onClick={onAdd}>
|
<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">
|
<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-1 overflow-y-auto p-4">
|
||||||
<div className="flex flex-col gap-3">
|
<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}
|
Shot {board.shotNumber}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={board.title}
|
value={board.title}
|
||||||
onChange={(e) => onUpdate({ title: e.target.value })}
|
onChange={(e) => onUpdate({ title: e.target.value })}
|
||||||
placeholder="Shot title"
|
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>
|
||||||
|
|
||||||
<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
|
<textarea
|
||||||
value={board.dialogue}
|
value={board.dialogue}
|
||||||
onChange={(e) => onUpdate({ dialogue: e.target.value })}
|
onChange={(e) => onUpdate({ dialogue: e.target.value })}
|
||||||
placeholder="Character dialogue..."
|
placeholder="Character dialogue..."
|
||||||
rows={3}
|
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>
|
||||||
|
|
||||||
<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
|
<textarea
|
||||||
value={board.action}
|
value={board.action}
|
||||||
onChange={(e) => onUpdate({ action: e.target.value })}
|
onChange={(e) => onUpdate({ action: e.target.value })}
|
||||||
placeholder="On-screen action..."
|
placeholder="On-screen action..."
|
||||||
rows={3}
|
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>
|
||||||
|
|
||||||
<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
|
<textarea
|
||||||
value={board.notes}
|
value={board.notes}
|
||||||
onChange={(e) => onUpdate({ notes: e.target.value })}
|
onChange={(e) => onUpdate({ notes: e.target.value })}
|
||||||
placeholder="Director notes..."
|
placeholder="Director notes..."
|
||||||
rows={3}
|
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>
|
||||||
|
|
||||||
<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
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min={0}
|
min={0}
|
||||||
step={0.5}
|
step={0.5}
|
||||||
value={board.duration}
|
value={board.duration}
|
||||||
onChange={(e) => onUpdate({ duration: parseFloat(e.target.value) || 0 })}
|
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>
|
||||||
</div>
|
</div>
|
||||||
@@ -170,7 +170,7 @@ const MainPreview = ({
|
|||||||
style={{ pointerEvents: "none" }}
|
style={{ pointerEvents: "none" }}
|
||||||
/>
|
/>
|
||||||
{board.title && (
|
{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>
|
<p className="text-sm font-medium text-white">{board.title}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -178,7 +178,7 @@ const MainPreview = ({
|
|||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={onUploadClick}
|
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" />
|
<UploadIcon className="text-3xl" />
|
||||||
<span className="text-sm">Click to upload an image</span>
|
<span className="text-sm">Click to upload an image</span>
|
||||||
@@ -332,9 +332,9 @@ const SketchCanvas = ({ initialImageDataUrl, onSave, onExit }: SketchCanvasProps
|
|||||||
onClick={() => setTool(t)}
|
onClick={() => setTool(t)}
|
||||||
title={label}
|
title={label}
|
||||||
className={twMerge(
|
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
|
tool === t
|
||||||
? "bg-primary text-white"
|
? "bg-white text-black"
|
||||||
: "text-base-fg/60 hover:bg-white/10 hover:text-base-fg",
|
: "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" />
|
<div className="mx-1 h-5 w-px bg-ui-panel-border" />
|
||||||
|
|
||||||
{/* Color picker */}
|
{/* 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
|
<span
|
||||||
className="h-4 w-4 rounded-sm border border-white/20"
|
className="h-4 w-4 rounded-sm border border-white/20"
|
||||||
style={{ background: color }}
|
style={{ background: color }}
|
||||||
@@ -367,16 +367,16 @@ const SketchCanvas = ({ initialImageDataUrl, onSave, onExit }: SketchCanvasProps
|
|||||||
|
|
||||||
{/* Brush size */}
|
{/* Brush size */}
|
||||||
<div className="flex items-center gap-1.5">
|
<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
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min={1}
|
min={1}
|
||||||
max={32}
|
max={32}
|
||||||
value={brushSize}
|
value={brushSize}
|
||||||
onChange={(e) => setBrushSize(Number(e.target.value))}
|
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>
|
||||||
|
|
||||||
<div className="mx-1 h-5 w-px bg-ui-panel-border" />
|
<div className="mx-1 h-5 w-px bg-ui-panel-border" />
|
||||||
@@ -385,7 +385,7 @@ const SketchCanvas = ({ initialImageDataUrl, onSave, onExit }: SketchCanvasProps
|
|||||||
onClick={handleUndo}
|
onClick={handleUndo}
|
||||||
disabled={undoStack.length === 0}
|
disabled={undoStack.length === 0}
|
||||||
title="Undo"
|
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" />
|
<RotateCcwIcon className="text-sm" />
|
||||||
</button>
|
</button>
|
||||||
@@ -393,7 +393,7 @@ const SketchCanvas = ({ initialImageDataUrl, onSave, onExit }: SketchCanvasProps
|
|||||||
<button
|
<button
|
||||||
onClick={handleClear}
|
onClick={handleClear}
|
||||||
title="Clear canvas"
|
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" />
|
<XIcon className="text-sm" />
|
||||||
</button>
|
</button>
|
||||||
@@ -401,7 +401,7 @@ const SketchCanvas = ({ initialImageDataUrl, onSave, onExit }: SketchCanvasProps
|
|||||||
<button
|
<button
|
||||||
onClick={onExit}
|
onClick={onExit}
|
||||||
title="Done sketching"
|
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" />
|
<CheckIcon className="text-xs" />
|
||||||
Done
|
Done
|
||||||
@@ -421,7 +421,7 @@ const SketchCanvas = ({ initialImageDataUrl, onSave, onExit }: SketchCanvasProps
|
|||||||
touchAction: "none",
|
touchAction: "none",
|
||||||
display: "block",
|
display: "block",
|
||||||
}}
|
}}
|
||||||
className="rounded shadow-xl"
|
className="border border-white/15"
|
||||||
onPointerDown={handlePointerDown}
|
onPointerDown={handlePointerDown}
|
||||||
onPointerMove={handlePointerMove}
|
onPointerMove={handlePointerMove}
|
||||||
onPointerUp={handlePointerUp}
|
onPointerUp={handlePointerUp}
|
||||||
@@ -467,11 +467,11 @@ const ThumbnailItem = ({
|
|||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
className={twMerge(
|
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
|
isSelected
|
||||||
? "border-primary ring-2 ring-primary/30"
|
? "border-white"
|
||||||
: "border-transparent hover:border-white/20",
|
: "border-white/15 hover:border-white/30",
|
||||||
isDragOver && "border-primary/60 opacity-60",
|
isDragOver && "border-white/60 opacity-60",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{board.imageDataUrl ? (
|
{board.imageDataUrl ? (
|
||||||
@@ -482,7 +482,7 @@ const ThumbnailItem = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="absolute bottom-0 left-0 right-0 bg-black/60 px-1.5 py-0.5">
|
<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>
|
||||||
</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">
|
<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
|
<button
|
||||||
onClick={onAdd}
|
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"
|
title="Add shot"
|
||||||
>
|
>
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
@@ -705,12 +705,12 @@ const Timeline = ({
|
|||||||
const playheadLeft = currentTimeSeconds * PIXELS_PER_SECOND;
|
const playheadLeft = currentTimeSeconds * PIXELS_PER_SECOND;
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* Header row */}
|
||||||
<div className="flex items-center justify-between border-b border-ui-panel-border/40 px-3 py-1">
|
<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="font-mono text-xs tabular-nums text-base-fg/50">{formatTime(currentTimeSeconds)}</span>
|
||||||
<span className="text-xs text-base-fg/25">Timeline</span>
|
<span className="hud-label 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(totalDuration)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Lane */}
|
{/* Lane */}
|
||||||
@@ -729,7 +729,7 @@ const Timeline = ({
|
|||||||
style={{ left: i * PIXELS_PER_SECOND }}
|
style={{ left: i * PIXELS_PER_SECOND }}
|
||||||
>
|
>
|
||||||
<div className="h-2 w-px bg-white/10" />
|
<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>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -743,9 +743,9 @@ const Timeline = ({
|
|||||||
<div
|
<div
|
||||||
key={board.id}
|
key={board.id}
|
||||||
className={twMerge(
|
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
|
isSelected
|
||||||
? "border-primary bg-primary/25"
|
? "border-white bg-white/15"
|
||||||
: "border-white/15 bg-white/5 hover:bg-white/10",
|
: "border-white/15 bg-white/5 hover:bg-white/10",
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
@@ -776,13 +776,13 @@ const Timeline = ({
|
|||||||
"{board.dialogue}"
|
"{board.dialogue}"
|
||||||
</span>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Resize handle */}
|
{/* Resize handle */}
|
||||||
<div
|
<div
|
||||||
data-resize="true"
|
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" }}
|
style={{ cursor: "ew-resize" }}
|
||||||
onPointerDown={(e) => handleResizePointerDown(e, board.id, board.duration)}
|
onPointerDown={(e) => handleResizePointerDown(e, board.id, board.duration)}
|
||||||
title="Drag to resize duration"
|
title="Drag to resize duration"
|
||||||
@@ -804,10 +804,10 @@ const Timeline = ({
|
|||||||
|
|
||||||
{/* Playhead */}
|
{/* Playhead */}
|
||||||
<div
|
<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 }}
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -845,14 +845,14 @@ const PlaybackControls = ({
|
|||||||
onNextScene,
|
onNextScene,
|
||||||
}: PlaybackControlsProps) => {
|
}: PlaybackControlsProps) => {
|
||||||
const transportBtnClass =
|
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 (
|
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 */}
|
{/* Left stats */}
|
||||||
<div className="flex w-32 flex-col">
|
<div className="flex w-32 flex-col">
|
||||||
<span className="font-mono text-xs text-base-fg/60">{formatTime(currentTimeSeconds)}</span>
|
<span className="font-mono text-xs tabular-nums text-base-fg/60">{formatTime(currentTimeSeconds)}</span>
|
||||||
<span className="text-[10px] text-base-fg/30">
|
<span className="font-mono text-[10px] uppercase tracking-[0.12em] text-base-fg/30">
|
||||||
{currentBoardNumber !== null ? `Shot ${currentBoardNumber}` : "—"}
|
{currentBoardNumber !== null ? `Shot ${currentBoardNumber}` : "—"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -876,7 +876,7 @@ const PlaybackControls = ({
|
|||||||
|
|
||||||
{/* Play / Pause */}
|
{/* Play / Pause */}
|
||||||
<button
|
<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}
|
onClick={isPlaying ? onPause : onPlay}
|
||||||
title={isPlaying ? "Pause" : "Play"}
|
title={isPlaying ? "Pause" : "Play"}
|
||||||
>
|
>
|
||||||
@@ -901,8 +901,8 @@ const PlaybackControls = ({
|
|||||||
|
|
||||||
{/* Right stats */}
|
{/* Right stats */}
|
||||||
<div className="flex w-32 flex-col items-end">
|
<div className="flex w-32 flex-col items-end">
|
||||||
<span className="font-mono text-xs text-base-fg/60">{formatTime(totalDuration)}</span>
|
<span className="font-mono text-xs tabular-nums 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-[10px] uppercase tracking-[0.12em] text-base-fg/30">{totalBoards} shot{totalBoards !== 1 ? "s" : ""}</span>
|
||||||
</div>
|
</div>
|
||||||
</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,
|
UploadAudioMedia,
|
||||||
FilterMediaClasses,
|
FilterMediaClasses,
|
||||||
} from "@storyteller/api";
|
} from "@storyteller/api";
|
||||||
import BackgroundGallery from "./BackgroundGallery";
|
|
||||||
import {
|
import {
|
||||||
ClassyModelSelector,
|
ClassyModelSelector,
|
||||||
useImageToVideoPageModelList,
|
useImageToVideoPageModelList,
|
||||||
@@ -186,7 +185,6 @@ const ImageToVideo = ({ imageMediaId, imageUrl }: ImageToVideoProps) => {
|
|||||||
hasContent={hasContent}
|
hasContent={hasContent}
|
||||||
emptyStateTitle="Create Video"
|
emptyStateTitle="Create Video"
|
||||||
emptyStateSubtitle="Choose an image, add a prompt, then generate"
|
emptyStateSubtitle="Choose an image, add a prompt, then generate"
|
||||||
background={<BackgroundGallery />}
|
|
||||||
bottomOffset={promptHeight + 40}
|
bottomOffset={promptHeight + 40}
|
||||||
listContent={
|
listContent={
|
||||||
<DesktopGenerationGallery
|
<DesktopGenerationGallery
|
||||||
|
|||||||
+38
-38
@@ -457,7 +457,7 @@ export const VideoFrameExtractor = () => {
|
|||||||
|
|
||||||
return (
|
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">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<main
|
<main
|
||||||
className={
|
className={
|
||||||
@@ -468,7 +468,7 @@ export const VideoFrameExtractor = () => {
|
|||||||
>
|
>
|
||||||
{!videoUrl ? (
|
{!videoUrl ? (
|
||||||
<div className="w-full max-w-5xl">
|
<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
|
<UploadEntryCard
|
||||||
icon={ImagesIcon}
|
icon={ImagesIcon}
|
||||||
title="Extract Video Frames"
|
title="Extract Video Frames"
|
||||||
@@ -486,7 +486,7 @@ export const VideoFrameExtractor = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex w-full max-w-5xl flex-col gap-5">
|
<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">
|
<div className="relative aspect-video w-full bg-black">
|
||||||
<Button
|
<Button
|
||||||
icon={RotateCwIcon}
|
icon={RotateCwIcon}
|
||||||
@@ -500,7 +500,7 @@ export const VideoFrameExtractor = () => {
|
|||||||
setSavedFrames(new Set());
|
setSavedFrames(new Set());
|
||||||
setConvertingFrames(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
|
Switch Video
|
||||||
</Button>
|
</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 className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 to-transparent p-4">
|
||||||
<div
|
<div
|
||||||
ref={progressBarRef}
|
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}
|
onClick={handleProgressBarClick}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="absolute h-full overflow-hidden rounded-full bg-primary"
|
className="absolute h-full overflow-hidden bg-white"
|
||||||
style={{
|
style={{
|
||||||
width: `${duration ? (currentTime / duration) * 100 : 0}%`,
|
width: `${duration ? (currentTime / duration) * 100 : 0}%`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
<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={{
|
style={{
|
||||||
left: `${duration ? (startTime / duration) * 100 : 0}%`,
|
left: `${duration ? (startTime / duration) * 100 : 0}%`,
|
||||||
}}
|
}}
|
||||||
@@ -570,7 +570,7 @@ export const VideoFrameExtractor = () => {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => seekToFrame("first")}
|
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"
|
title="First Frame"
|
||||||
>
|
>
|
||||||
<SkipBackIcon
|
<SkipBackIcon
|
||||||
@@ -579,7 +579,7 @@ export const VideoFrameExtractor = () => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={togglePlayPause}
|
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
|
<DynamicIcon
|
||||||
icon={isPlaying ? PauseIcon : PlayIcon}
|
icon={isPlaying ? PauseIcon : PlayIcon}
|
||||||
@@ -588,7 +588,7 @@ export const VideoFrameExtractor = () => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => seekToFrame("last")}
|
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"
|
title="Last Frame"
|
||||||
>
|
>
|
||||||
<SkipForwardIcon
|
<SkipForwardIcon
|
||||||
@@ -598,7 +598,7 @@ export const VideoFrameExtractor = () => {
|
|||||||
<div className="mx-1 h-6 w-px bg-white/20" />
|
<div className="mx-1 h-6 w-px bg-white/20" />
|
||||||
<button
|
<button
|
||||||
onClick={toggleMute}
|
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
|
<DynamicIcon
|
||||||
icon={isMuted ? VolumeXIcon : Volume2Icon}
|
icon={isMuted ? VolumeXIcon : Volume2Icon}
|
||||||
@@ -617,7 +617,7 @@ export const VideoFrameExtractor = () => {
|
|||||||
className="w-20 accent-white"
|
className="w-20 accent-white"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="font-mono text-sm text-white">
|
<div className="font-mono text-sm tabular-nums text-white">
|
||||||
{formatTime(currentTime)} / {formatTime(duration)}
|
{formatTime(currentTime)} / {formatTime(duration)}
|
||||||
</div>
|
</div>
|
||||||
</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="grid w-full grid-cols-1 gap-5 md:grid-cols-2">
|
||||||
<div className="flex flex-col gap-5">
|
<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>
|
||||||
<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
|
Start Time
|
||||||
</div>
|
</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)}
|
{formatTimePrecise(startTime)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 flex items-center gap-2 text-xs text-base-fg/60">
|
<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>
|
<span>Adjust via the yellow marker on the video</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
<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
|
Video Information
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3 text-sm">
|
<div className="space-y-3 text-sm">
|
||||||
@@ -665,7 +665,7 @@ export const VideoFrameExtractor = () => {
|
|||||||
<span className="font-medium text-base-fg/70">
|
<span className="font-medium text-base-fg/70">
|
||||||
Duration
|
Duration
|
||||||
</span>
|
</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)}
|
{formatTime(duration)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -674,7 +674,7 @@ export const VideoFrameExtractor = () => {
|
|||||||
<span className="font-medium text-base-fg/70">
|
<span className="font-medium text-base-fg/70">
|
||||||
Resolution
|
Resolution
|
||||||
</span>
|
</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.videoWidth} ×{" "}
|
||||||
{videoRef.current.videoHeight}
|
{videoRef.current.videoHeight}
|
||||||
</span>
|
</span>
|
||||||
@@ -685,12 +685,12 @@ export const VideoFrameExtractor = () => {
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
<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
|
<SparklesIcon
|
||||||
|
|
||||||
className="text-primary" />
|
className="text-ui-accent-ink" />
|
||||||
Extraction Settings
|
Extraction Settings
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
@@ -698,7 +698,7 @@ export const VideoFrameExtractor = () => {
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="numFrames"
|
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
|
Number of Frames
|
||||||
</label>
|
</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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="frameDistance"
|
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)
|
Distance (ms)
|
||||||
</label>
|
</label>
|
||||||
@@ -738,11 +738,11 @@ export const VideoFrameExtractor = () => {
|
|||||||
Math.max(1, parseInt(e.target.value) || 1),
|
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>
|
||||||
|
|
||||||
<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,
|
Extracting multiple frames helps capture a sharp,
|
||||||
non-blurred frame.
|
non-blurred frame.
|
||||||
</div>
|
</div>
|
||||||
@@ -754,26 +754,26 @@ export const VideoFrameExtractor = () => {
|
|||||||
{extractedFrames.length > 0 && (
|
{extractedFrames.length > 0 && (
|
||||||
<div
|
<div
|
||||||
ref={extractedFramesRef}
|
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>
|
||||||
<div className="mb-6 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<h3 className="flex items-center gap-2 text-xl font-bold uppercase tracking-wider text-base-fg">
|
<h3 className="flex items-center gap-2 text-sm font-semibold text-base-fg">
|
||||||
<ImagesIcon
|
<ImagesIcon
|
||||||
|
|
||||||
className="text-primary" />
|
className="text-ui-accent-ink" />
|
||||||
Extracted Frames
|
Extracted Frames
|
||||||
</h3>
|
</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}{" "}
|
||||||
{extractedFrames.length === 1 ? "Frame" : "Frames"}
|
{extractedFrames.length === 1 ? "Frame" : "Frames"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-5">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
{extractedFrames.map((frame, index) => (
|
{extractedFrames.map((frame, index) => (
|
||||||
<div
|
<div
|
||||||
key={frame.id}
|
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">
|
<div className="aspect-video overflow-hidden bg-black">
|
||||||
<img
|
<img
|
||||||
@@ -782,12 +782,12 @@ export const VideoFrameExtractor = () => {
|
|||||||
className="h-full w-full object-contain"
|
className="h-full w-full object-contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<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}
|
#{index + 1}
|
||||||
</span>
|
</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)}
|
{formatTimePrecise(frame.timestamp)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+16
-16
@@ -203,7 +203,7 @@ export const VideoWatermarkRemover = () => {
|
|||||||
|
|
||||||
return (
|
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">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<main
|
<main
|
||||||
className={
|
className={
|
||||||
@@ -214,7 +214,7 @@ export const VideoWatermarkRemover = () => {
|
|||||||
>
|
>
|
||||||
{!videoUrl ? (
|
{!videoUrl ? (
|
||||||
<div className="w-full max-w-5xl">
|
<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
|
<UploadEntryCard
|
||||||
icon={DropletIcon}
|
icon={DropletIcon}
|
||||||
title="Remove Video Watermark"
|
title="Remove Video Watermark"
|
||||||
@@ -232,7 +232,7 @@ export const VideoWatermarkRemover = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex w-full max-w-5xl flex-col gap-5">
|
<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">
|
<div className="relative aspect-video w-full bg-black">
|
||||||
<Button
|
<Button
|
||||||
icon={RotateCwIcon}
|
icon={RotateCwIcon}
|
||||||
@@ -241,7 +241,7 @@ export const VideoWatermarkRemover = () => {
|
|||||||
setVideoUrl("");
|
setVideoUrl("");
|
||||||
setCurrentTime(0);
|
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
|
Switch Video
|
||||||
</Button>
|
</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 className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 to-transparent p-4">
|
||||||
<div
|
<div
|
||||||
ref={progressBarRef}
|
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}
|
onClick={handleProgressBarClick}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="absolute h-full overflow-hidden rounded-full bg-primary"
|
className="absolute h-full overflow-hidden bg-white"
|
||||||
style={{
|
style={{
|
||||||
width: `${duration ? (currentTime / duration) * 100 : 0}%`,
|
width: `${duration ? (currentTime / duration) * 100 : 0}%`,
|
||||||
}}
|
}}
|
||||||
@@ -270,7 +270,7 @@ export const VideoWatermarkRemover = () => {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => seekToFrame("first")}
|
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"
|
title="First Frame"
|
||||||
>
|
>
|
||||||
<SkipBackIcon
|
<SkipBackIcon
|
||||||
@@ -279,7 +279,7 @@ export const VideoWatermarkRemover = () => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={togglePlayPause}
|
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
|
<DynamicIcon
|
||||||
icon={isPlaying ? PauseIcon : PlayIcon}
|
icon={isPlaying ? PauseIcon : PlayIcon}
|
||||||
@@ -288,7 +288,7 @@ export const VideoWatermarkRemover = () => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => seekToFrame("last")}
|
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"
|
title="Last Frame"
|
||||||
>
|
>
|
||||||
<SkipForwardIcon
|
<SkipForwardIcon
|
||||||
@@ -298,7 +298,7 @@ export const VideoWatermarkRemover = () => {
|
|||||||
<div className="mx-1 h-6 w-px bg-white/20" />
|
<div className="mx-1 h-6 w-px bg-white/20" />
|
||||||
<button
|
<button
|
||||||
onClick={toggleMute}
|
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
|
<DynamicIcon
|
||||||
icon={isMuted ? VolumeXIcon : Volume2Icon}
|
icon={isMuted ? VolumeXIcon : Volume2Icon}
|
||||||
@@ -317,7 +317,7 @@ export const VideoWatermarkRemover = () => {
|
|||||||
className="w-20 accent-white"
|
className="w-20 accent-white"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="font-mono text-sm text-white">
|
<div className="font-mono text-sm tabular-nums text-white">
|
||||||
{formatTime(currentTime)} / {formatTime(duration)}
|
{formatTime(currentTime)} / {formatTime(duration)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -338,12 +338,12 @@ export const VideoWatermarkRemover = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</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>
|
<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
|
<VideoIcon
|
||||||
|
|
||||||
className="text-primary" />
|
className="text-ui-accent-ink" />
|
||||||
Video Information
|
Video Information
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3 text-sm">
|
<div className="space-y-3 text-sm">
|
||||||
@@ -351,7 +351,7 @@ export const VideoWatermarkRemover = () => {
|
|||||||
<span className="font-medium text-base-fg/70">
|
<span className="font-medium text-base-fg/70">
|
||||||
Duration
|
Duration
|
||||||
</span>
|
</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)}
|
{formatTime(duration)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -360,7 +360,7 @@ export const VideoWatermarkRemover = () => {
|
|||||||
<span className="font-medium text-base-fg/70">
|
<span className="font-medium text-base-fg/70">
|
||||||
Resolution
|
Resolution
|
||||||
</span>
|
</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.videoWidth} ×{" "}
|
||||||
{videoRef.current.videoHeight}
|
{videoRef.current.videoHeight}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export const links: LinksFunction = () => [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
rel: "stylesheet",
|
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-width: thin;
|
||||||
scrollbar-color: #363636 #171717;
|
scrollbar-color: #2c2c2f #0b0b0c;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Works on Chrome, Edge, and Safari */
|
/* Works on Chrome, Edge, and Safari */
|
||||||
@@ -9,13 +9,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar-track {
|
*::-webkit-scrollbar-track {
|
||||||
background: #171717;
|
background: #0b0b0c;
|
||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar-thumb {
|
*::-webkit-scrollbar-thumb {
|
||||||
background-color: #363636;
|
background-color: #2c2c2f;
|
||||||
border-radius: 4px;
|
border: 2px solid #0b0b0c;
|
||||||
border: 2px solid #171717;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light theme: the scrollbar track/thumb must be light, not the dark defaults. */
|
/* Light theme: the scrollbar track/thumb must be light, not the dark defaults. */
|
||||||
@@ -46,18 +45,37 @@ input[type="number"] {
|
|||||||
appearance: textfield;
|
appearance: textfield;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Display face: Archivo, matching artcraft-webapp. */
|
||||||
h1,
|
h1,
|
||||||
h2,
|
h2,
|
||||||
h3,
|
h3,
|
||||||
h4,
|
h4,
|
||||||
h5,
|
h5,
|
||||||
h6 {
|
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 {
|
body {
|
||||||
font-family:
|
font-family:
|
||||||
"Source Sans 3",
|
|
||||||
system-ui,
|
system-ui,
|
||||||
-apple-system,
|
-apple-system,
|
||||||
BlinkMacSystemFont,
|
BlinkMacSystemFont,
|
||||||
@@ -70,22 +88,61 @@ body {
|
|||||||
background-color: var(--st-bg);
|
background-color: var(--st-bg);
|
||||||
color-scheme: var(--st-color-scheme, dark);
|
color-scheme: var(--st-color-scheme, dark);
|
||||||
user-select: none;
|
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 ===================== */
|
/* ===================== THEME VARIABLES ===================== */
|
||||||
|
/* The default (the "gray" theme id) is the brutalist system shared with
|
||||||
|
artcraft-webapp: near-black surfaces, white/15 hairline borders. */
|
||||||
:root {
|
:root {
|
||||||
--st-bg: #121212;
|
--st-bg: #121316;
|
||||||
/* gray theme default */
|
--st-panel: #101014;
|
||||||
--st-panel: #1f1f1f;
|
--st-panel-border: rgba(255, 255, 255, 0.15);
|
||||||
--st-panel-border: #3f3f3f;
|
--st-modal: #101014;
|
||||||
--st-modal: #1f1f1f;
|
|
||||||
/* default modal surface (can be gradient per theme) */
|
/* default modal surface (can be gradient per theme) */
|
||||||
--st-controls: #3e3e41;
|
--st-controls: #1e1f22;
|
||||||
--st-controls-rgb: 62 62 65;
|
--st-controls-rgb: 30 31 34;
|
||||||
--st-controls-border: #3f3f3f;
|
--st-controls-border: rgba(255, 255, 255, 0.15);
|
||||||
--st-fg: #ffffff;
|
--st-sunken: #060607;
|
||||||
--st-fg-rgb: 255 255 255;
|
--st-fg: #f2f1ee;
|
||||||
--st-divider: #515168;
|
--st-fg-rgb: 242 241 238;
|
||||||
|
--st-divider: rgba(255, 255, 255, 0.15);
|
||||||
--st-gallery-vignette-alpha: 0.6;
|
--st-gallery-vignette-alpha: 0.6;
|
||||||
/* Photo tint (default neutral) */
|
/* Photo tint (default neutral) */
|
||||||
--st-photo-tint-rgb: 0 0 0;
|
--st-photo-tint-rgb: 0 0 0;
|
||||||
@@ -95,28 +152,45 @@ body {
|
|||||||
--st-badge-alpha: 0.4;
|
--st-badge-alpha: 0.4;
|
||||||
|
|
||||||
/* shadcn design tokens consumed by @storyteller/ui-video-editor.
|
/* shadcn design tokens consumed by @storyteller/ui-video-editor.
|
||||||
Dark-theme palette — the editor only runs in dark mode today.
|
Same values as artcraft-webapp's styles.css. */
|
||||||
--primary matches the webapp's brand blue (#2d81ff) so the
|
--background: 225 10% 8%;
|
||||||
selection ring is consistent across hosts. */
|
|
||||||
--background: 0 0% 9%;
|
|
||||||
--foreground: 0 0% 98%;
|
--foreground: 0 0% 98%;
|
||||||
--card: 0 0% 12%;
|
--card: 240 11% 7%;
|
||||||
--card-foreground: 0 0% 98%;
|
--card-foreground: 0 0% 98%;
|
||||||
--popover: 0 0% 12%;
|
--popover: 240 11% 7%;
|
||||||
--popover-foreground: 0 0% 98%;
|
--popover-foreground: 0 0% 98%;
|
||||||
--primary: 216 100% 59%;
|
--primary: 216 100% 59%;
|
||||||
--primary-foreground: 0 0% 98%;
|
--primary-foreground: 0 0% 98%;
|
||||||
--secondary: 0 0% 24%;
|
--secondary: 240 4% 9%;
|
||||||
--secondary-foreground: 0 0% 98%;
|
--secondary-foreground: 0 0% 98%;
|
||||||
--muted: 0 0% 18%;
|
--muted: 240 4% 9%;
|
||||||
--muted-foreground: 0 0% 64%;
|
--muted-foreground: 0 0% 64%;
|
||||||
--accent: 0 0% 16%;
|
--accent: 240 4% 9%;
|
||||||
--accent-foreground: 0 0% 98%;
|
--accent-foreground: 0 0% 98%;
|
||||||
--destructive: 0 70% 50%;
|
--destructive: 0 70% 50%;
|
||||||
--destructive-foreground: 0 0% 98%;
|
--destructive-foreground: 0 0% 98%;
|
||||||
--border: 0 0% 18%;
|
--border: 0 0% 100% / 0.15;
|
||||||
--input: 0 0% 18%;
|
--input: 0 0% 100% / 0.15;
|
||||||
--ring: 217 91% 60%;
|
--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 */
|
/* Light theme */
|
||||||
@@ -130,6 +204,7 @@ body {
|
|||||||
--st-controls-rgb: 238 238 239;
|
--st-controls-rgb: 238 238 239;
|
||||||
--st-controls-border: #dcdcdf;
|
--st-controls-border: #dcdcdf;
|
||||||
/* requested light button border */
|
/* requested light button border */
|
||||||
|
--st-sunken: #ececef;
|
||||||
--st-fg: #111827;
|
--st-fg: #111827;
|
||||||
--st-fg-rgb: 17 24 39;
|
--st-fg-rgb: 17 24 39;
|
||||||
--st-divider: #d1d5db;
|
--st-divider: #d1d5db;
|
||||||
@@ -141,18 +216,19 @@ body {
|
|||||||
--st-badge-alpha: 0.1;
|
--st-badge-alpha: 0.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Gray (current) */
|
/* Default dark (brutalist, matches artcraft-webapp) */
|
||||||
.theme-gray {
|
.theme-gray {
|
||||||
--st-bg: #121212;
|
--st-bg: #121316;
|
||||||
--st-panel: #1f1f1f;
|
--st-panel: #101014;
|
||||||
--st-panel-border: #272727;
|
--st-panel-border: rgba(255, 255, 255, 0.15);
|
||||||
--st-modal: #1f1f1f;
|
--st-modal: #101014;
|
||||||
--st-controls: #3e3e41;
|
--st-controls: #1e1f22;
|
||||||
--st-controls-rgb: 62 62 65;
|
--st-controls-rgb: 30 31 34;
|
||||||
--st-controls-border: #464646;
|
--st-controls-border: rgba(255, 255, 255, 0.15);
|
||||||
--st-fg: #ffffff;
|
--st-sunken: #060607;
|
||||||
--st-fg-rgb: 255 255 255;
|
--st-fg: #f2f1ee;
|
||||||
--st-divider: #272727;
|
--st-fg-rgb: 242 241 238;
|
||||||
|
--st-divider: rgba(255, 255, 255, 0.15);
|
||||||
--st-color-scheme: dark;
|
--st-color-scheme: dark;
|
||||||
--st-gallery-vignette-alpha: 0.3;
|
--st-gallery-vignette-alpha: 0.3;
|
||||||
--st-photo-tint-rgb: 0 0 0;
|
--st-photo-tint-rgb: 0 0 0;
|
||||||
@@ -165,11 +241,12 @@ body {
|
|||||||
.theme-black {
|
.theme-black {
|
||||||
--st-bg: #000000;
|
--st-bg: #000000;
|
||||||
--st-panel: #0a0a0a;
|
--st-panel: #0a0a0a;
|
||||||
--st-panel-border: #242424;
|
--st-panel-border: rgba(255, 255, 255, 0.15);
|
||||||
--st-modal: #0a0a0a;
|
--st-modal: #0a0a0a;
|
||||||
--st-controls: #111111;
|
--st-controls: #111111;
|
||||||
--st-controls-rgb: 17 17 17;
|
--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: #ffffff;
|
||||||
--st-fg-rgb: 255 255 255;
|
--st-fg-rgb: 255 255 255;
|
||||||
--st-divider: #303030;
|
--st-divider: #303030;
|
||||||
@@ -193,7 +270,8 @@ body {
|
|||||||
radial-gradient(520px 280px at 110% 120%, #1cb6be33, transparent), #0f1220;
|
radial-gradient(520px 280px at 110% 120%, #1cb6be33, transparent), #0f1220;
|
||||||
--st-controls: #1a1f35;
|
--st-controls: #1a1f35;
|
||||||
--st-controls-rgb: 26 31 53;
|
--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: #f8fafc;
|
||||||
--st-fg-rgb: 248 250 252;
|
--st-fg-rgb: 248 250 252;
|
||||||
--st-divider: #2a3152;
|
--st-divider: #2a3152;
|
||||||
@@ -217,7 +295,8 @@ body {
|
|||||||
radial-gradient(700px 360px at 115% 120%, #fb923c88, transparent), #1a121a;
|
radial-gradient(700px 360px at 115% 120%, #fb923c88, transparent), #1a121a;
|
||||||
--st-controls: #271927;
|
--st-controls: #271927;
|
||||||
--st-controls-rgb: 39 25 39;
|
--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: #f8fafc;
|
||||||
--st-fg-rgb: 248 250 252;
|
--st-fg-rgb: 248 250 252;
|
||||||
--st-divider: #4a2e3a;
|
--st-divider: #4a2e3a;
|
||||||
@@ -238,14 +317,10 @@ body {
|
|||||||
background: var(--st-panel);
|
background: var(--st-panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Panel surface. Brutalist system: flat, no gradient (class name kept
|
||||||
|
for lib compatibility). */
|
||||||
.bg-ui-panel-gradient {
|
.bg-ui-panel-gradient {
|
||||||
background:
|
background: var(--st-panel);
|
||||||
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-color: var(--st-panel);
|
background-color: var(--st-panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,7 +406,6 @@ img {
|
|||||||
|
|
||||||
.promptbox-scrollbar::-webkit-scrollbar-thumb {
|
.promptbox-scrollbar::-webkit-scrollbar-thumb {
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
border-radius: 9999px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.promptbox-scrollbar::-webkit-scrollbar-thumb:hover {
|
.promptbox-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
@@ -730,3 +804,60 @@ img {
|
|||||||
.promptbox-drop-chip {
|
.promptbox-drop-chip {
|
||||||
animation: promptbox-drop-chip-in 0.25s cubic-bezier(0.22, 1, 0.36, 1) both;
|
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 components;
|
||||||
@tailwind utilities;
|
@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 {
|
@layer components {
|
||||||
.filter-tab {
|
.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 {
|
.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 {
|
.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 {
|
.glass {
|
||||||
@apply relative border border-white/10 shadow-lg backdrop-blur-xl transition-colors duration-300;
|
@apply relative rounded-[3px] border border-white/15 backdrop-blur-sm transition-colors duration-300;
|
||||||
@apply bg-[#303032]/90 !important;
|
@apply bg-[#1E1F22]/95 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light theme: the glass surface must be a light panel, not the dark
|
/* 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. */
|
hard-coded fill above. Tint + border swapped to read on a light bg. */
|
||||||
.theme-light .glass {
|
.theme-light .glass {
|
||||||
@apply border-black/10 !important;
|
@apply border-black/15 !important;
|
||||||
background-color: rgb(248 248 248 / 0.92) !important;
|
background-color: rgb(248 248 248 / 0.95) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,36 @@ export default {
|
|||||||
extend: {
|
extend: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
"custom-font": ["Fira Sans", "sans-serif"],
|
"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: {
|
keyframes: {
|
||||||
fadeIn: {
|
fadeIn: {
|
||||||
@@ -225,12 +255,20 @@ export default {
|
|||||||
wrapper: "#39394d",
|
wrapper: "#39394d",
|
||||||
},
|
},
|
||||||
ui: {
|
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)",
|
background: "var(--st-bg)",
|
||||||
panel: "var(--st-panel)",
|
panel: "var(--st-panel)",
|
||||||
"panel-border": "var(--st-panel-border)",
|
"panel-border": "var(--st-panel-border)",
|
||||||
|
border: "var(--st-panel-border)",
|
||||||
controls: "rgb(var(--st-controls-rgb) / <alpha-value>)",
|
controls: "rgb(var(--st-controls-rgb) / <alpha-value>)",
|
||||||
"controls-button": "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)",
|
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>)",
|
"base-fg": "rgb(var(--st-fg-rgb) / <alpha-value>)",
|
||||||
media: {
|
media: {
|
||||||
@@ -288,6 +326,9 @@ export default {
|
|||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
safelist: [
|
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-selected",
|
||||||
"bg-character-unselected",
|
"bg-character-unselected",
|
||||||
"bg-character-clip",
|
"bg-character-clip",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const Badge = ({ label, className, icon }: BadgeProps) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
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
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export const ButtonDropdown = ({
|
|||||||
<Menu.Items
|
<Menu.Items
|
||||||
static
|
static
|
||||||
className={twMerge(
|
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",
|
align === "left" ? "left-0" : "right-0",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -109,19 +109,19 @@ export const ButtonDropdown = ({
|
|||||||
{options.map((option, index) => (
|
{options.map((option, index) => (
|
||||||
<Fragment key={index}>
|
<Fragment key={index}>
|
||||||
{option.divider && (
|
{option.divider && (
|
||||||
<div className="my-1.5 border-t border-white/5" />
|
<div className="my-1.5 border-t border-white/15" />
|
||||||
)}
|
)}
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
{({ active }) => (
|
{({ active }) => (
|
||||||
<button
|
<button
|
||||||
disabled={option.disabled}
|
disabled={option.disabled}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"duration-50 bg-brand-secondary font-medium text-white transition-all",
|
"rounded-[3px] bg-transparent font-medium text-base-fg transition-colors duration-150",
|
||||||
active ? "bg-brand-secondary-800" : "",
|
active ? "bg-white/10" : "",
|
||||||
option.disabled
|
option.disabled
|
||||||
? "pointer-events-none opacity-40"
|
? "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,
|
option.className,
|
||||||
)}
|
)}
|
||||||
onClick={() => handleOptionClick(index)}
|
onClick={() => handleOptionClick(index)}
|
||||||
@@ -130,11 +130,11 @@ export const ButtonDropdown = ({
|
|||||||
{option.icon && (
|
{option.icon && (
|
||||||
<DynamicIcon
|
<DynamicIcon
|
||||||
icon={option.icon}
|
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="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}
|
{option.description && option.description}
|
||||||
</div>
|
</div>
|
||||||
{showSelected && (
|
{showSelected && (
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ interface ButtonIconSelectProps {
|
|||||||
options: Option[];
|
options: Option[];
|
||||||
onOptionChange?: (value: string) => void;
|
onOptionChange?: (value: string) => void;
|
||||||
selectedOption?: string;
|
selectedOption?: string;
|
||||||
|
buttonClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ButtonIconSelect({
|
export function ButtonIconSelect({
|
||||||
options,
|
options,
|
||||||
onOptionChange,
|
onOptionChange,
|
||||||
selectedOption,
|
selectedOption,
|
||||||
|
buttonClassName,
|
||||||
}: ButtonIconSelectProps) {
|
}: ButtonIconSelectProps) {
|
||||||
const [internalSelectedOption, setInternalSelectedOption] = useState<string>(
|
const [internalSelectedOption, setInternalSelectedOption] = useState<string>(
|
||||||
selectedOption || options[0].value
|
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",
|
text ? "h-auto w-auto gap-2 px-3 py-1.5" : "w-9",
|
||||||
internalSelectedOption === value
|
internalSelectedOption === value
|
||||||
? "border-brand-primary bg-brand-primary/20"
|
? "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)}
|
onClick={() => handleOptionChange(value)}
|
||||||
>
|
>
|
||||||
@@ -72,7 +75,8 @@ export function ButtonIconSelect({
|
|||||||
text ? "h-auto w-auto gap-2 px-3 py-1.5" : "w-9",
|
text ? "h-auto w-auto gap-2 px-3 py-1.5" : "w-9",
|
||||||
internalSelectedOption === value
|
internalSelectedOption === value
|
||||||
? "border-brand-primary bg-brand-primary/20"
|
? "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)}
|
onClick={() => handleOptionChange(value)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ export const ButtonIcon = ({
|
|||||||
...rest
|
...rest
|
||||||
}: ButtonIconProps) => {
|
}: ButtonIconProps) => {
|
||||||
const className = twMerge(
|
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
|
bgFill
|
||||||
? "bg-ui-controls-button hover:bg-ui-controls-button/[0.75]"
|
? "bg-ui-controls-button hover:bg-ui-controls-button/[0.75]"
|
||||||
: "bg-transparent hover:bg-ui-panel/[0.4]",
|
: "bg-transparent hover:bg-ui-panel/[0.4]",
|
||||||
disabled && "opacity-50 hover:bg-transparent",
|
disabled && "opacity-50 hover:bg-transparent",
|
||||||
propsClassName
|
propsClassName,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ export const GenerateButton = ({
|
|||||||
disabled && "opacity-50",
|
disabled && "opacity-50",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<CoinsIcon className="text-xs text-white" />
|
<CoinsIcon className="text-xs" />
|
||||||
<span className="text-[13px] font-bold text-white/90">
|
<span className="text-[13px] font-bold">
|
||||||
{credits}
|
{credits}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
import { DynamicIcon } from "@storyteller/icons";
|
import { DynamicIcon } from "@storyteller/icons";
|
||||||
import { Button } from "./button";
|
import { Button, NEUTRAL_BUTTON_HOVER_CLASSES } from "./button";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
interface ToggleButtonProps {
|
interface ToggleButtonProps {
|
||||||
@@ -26,14 +26,15 @@ export const ToggleButton = ({
|
|||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
// 34px matches the sibling toolbar controls (GenerateButton, the
|
// Same box and type as the PopoverMenu triggers beside it (34px,
|
||||||
// PopoverMenu triggers); the 2px border is inside the box.
|
// text-sm sans, flat control surface + hairline border), overriding
|
||||||
"flex h-[34px] items-center justify-center rounded-lg border-2 border-transparent text-sm text-white backdrop-blur-lg transition-all",
|
// the Button base's mono uppercase label.
|
||||||
hasLabel ? "px-3" : "w-[34px]",
|
"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
|
isActive
|
||||||
? "border-white/20 bg-brand-primary/40 hover:border-white/30 hover:bg-brand-primary/40"
|
? "border-white/40 bg-white/10 hover:border-white/40 hover:bg-white/15"
|
||||||
: "bg-[#5F5F68]/60 hover:bg-[#5F5F68]/90",
|
: NEUTRAL_BUTTON_HOVER_CLASSES,
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
@@ -46,7 +47,7 @@ export const ToggleButton = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{label && (
|
{label && (
|
||||||
<span className="text-sm font-medium text-white/90">{label}</span>
|
<span className="whitespace-nowrap">{label}</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import { DynamicIcon } from "@storyteller/icons";
|
|||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import { ButtonHTMLAttributes, AnchorHTMLAttributes } from "react";
|
import { ButtonHTMLAttributes, AnchorHTMLAttributes } from "react";
|
||||||
|
|
||||||
|
export const NEUTRAL_BUTTON_HOVER_CLASSES =
|
||||||
|
"hover:bg-white/[0.07] hover:border-white/25";
|
||||||
|
|
||||||
type AnchorProps = Omit<
|
type AnchorProps = Omit<
|
||||||
AnchorHTMLAttributes<HTMLAnchorElement>,
|
AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||||
keyof ButtonHTMLAttributes<HTMLButtonElement>
|
keyof ButtonHTMLAttributes<HTMLButtonElement>
|
||||||
@@ -39,20 +42,22 @@ export const Button = ({
|
|||||||
function getVariantClassNames(variant: string) {
|
function getVariantClassNames(variant: string) {
|
||||||
switch (variant) {
|
switch (variant) {
|
||||||
case "secondary": {
|
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": {
|
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": {
|
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": {
|
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":
|
case "primary":
|
||||||
default: {
|
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(
|
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),
|
getVariantClassNames(propsVariant),
|
||||||
|
(propsVariant === "secondary" || propsVariant === "action" || propsVariant === "ghost") &&
|
||||||
|
NEUTRAL_BUTTON_HOVER_CLASSES,
|
||||||
propsClassName,
|
propsClassName,
|
||||||
disabledClass,
|
disabledClass,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -115,25 +115,27 @@ export const CameraSettingsModal = ({
|
|||||||
showClose={false}
|
showClose={false}
|
||||||
>
|
>
|
||||||
<div className="grid h-full grid-cols-12 gap-3">
|
<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">
|
<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}>
|
<Tooltip content="Add camera" position="top" delay={200}>
|
||||||
<button
|
<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}
|
onClick={onAddCamera}
|
||||||
>
|
>
|
||||||
<PlusIcon className="text-xl" />
|
<PlusIcon className="text-xl" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<hr className="my-2 w-full border-white/10" />
|
<hr className="my-2 w-full border-white/15" />
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{cameras.map((camera) => (
|
{cameras.map((camera) => (
|
||||||
<button
|
<button
|
||||||
key={camera.id}
|
key={camera.id}
|
||||||
className={`h-9 w-full rounded-lg p-2 text-left transition-colors duration-100 hover:bg-[#63636B]/40 ${
|
className={`h-9 w-full rounded-[3px] border-l-2 p-2 text-left transition-colors duration-100 ${
|
||||||
camera.id === selectedCameraId ? "bg-[#63636B]/40" : ""
|
camera.id === selectedCameraId
|
||||||
|
? "border-white bg-white/10"
|
||||||
|
: "border-transparent hover:bg-white/5"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleCameraSelect(camera)}
|
onClick={() => handleCameraSelect(camera)}
|
||||||
>
|
>
|
||||||
@@ -151,7 +153,7 @@ export const CameraSettingsModal = ({
|
|||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between gap-2.5 py-0.5 opacity-100">
|
<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"}
|
{selectedCamera?.label || "Camera"}
|
||||||
</h2>
|
</h2>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@@ -164,10 +166,10 @@ export const CameraSettingsModal = ({
|
|||||||
delay={200}
|
delay={200}
|
||||||
>
|
>
|
||||||
<button
|
<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"
|
selectedCamera?.id === "main"
|
||||||
? "cursor-not-allowed text-white/30"
|
? "cursor-not-allowed text-white/30"
|
||||||
: "text-white/60 hover:text-white/100"
|
: "text-white/60 hover:bg-white/10 hover:text-white"
|
||||||
}`}
|
}`}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
selectedCamera &&
|
selectedCamera &&
|
||||||
@@ -180,10 +182,10 @@ export const CameraSettingsModal = ({
|
|||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</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-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label htmlFor="camera-name" className="text-sm opacity-70">
|
<Label htmlFor="camera-name">
|
||||||
Name
|
Name
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -199,7 +201,7 @@ export const CameraSettingsModal = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="focal-length" className="text-sm opacity-70">
|
<Label htmlFor="focal-length">
|
||||||
Focal Length
|
Focal Length
|
||||||
</Label>
|
</Label>
|
||||||
<div
|
<div
|
||||||
@@ -220,7 +222,7 @@ export const CameraSettingsModal = ({
|
|||||||
showIncrement={true}
|
showIncrement={true}
|
||||||
className="w-full"
|
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
|
{selectedCamera?.focalLength || 35}mm
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -97,12 +97,12 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
|
|||||||
<span
|
<span
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"relative inline-block shrink-0 rounded-[4px] border transition-colors",
|
"relative inline-block shrink-0 rounded-[3px] border transition-colors",
|
||||||
boxDims,
|
boxDims,
|
||||||
isChecked
|
isChecked
|
||||||
? "border-primary bg-primary text-white"
|
? "border-white bg-white text-black"
|
||||||
: "border-ui-controls-border bg-ui-controls hover:border-primary/60",
|
: "border-ui-controls-border bg-transparent hover:border-white/60",
|
||||||
"peer-focus-visible:ring-2 peer-focus-visible:ring-primary/40",
|
"peer-focus-visible:ring-1 peer-focus-visible:ring-white/60",
|
||||||
checkboxClassName,
|
checkboxClassName,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ export const CloseButton = ({
|
|||||||
<button
|
<button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={twMerge(
|
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],
|
sizeClasses[size],
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ export const DragAndDropZone = ({
|
|||||||
}) => {
|
}) => {
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex cursor-pointer items-center gap-3.5 rounded-lg border border-dashed border-[#363636] bg-brand-secondary p-3">
|
<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" />
|
<FileUpIcon className="text-4xl text-white/60" />
|
||||||
<div className="flex flex-col gap-0">
|
<div className="flex flex-col gap-0">
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
<u>Upload a file</u> or drop it here
|
<u>Upload a file</u> or drop it here
|
||||||
</p>
|
</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
|
{fileTypes.join(", ").toString()} supported
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,7 +37,7 @@ export const DragAndDropZone = ({
|
|||||||
: `${Math.floor(file.size / 1024)} KB`;
|
: `${Math.floor(file.size / 1024)} KB`;
|
||||||
|
|
||||||
return (
|
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" />
|
<DynamicIcon icon={fileIcon} className="text-4xl" />
|
||||||
<div className="flex grow flex-col gap-0">
|
<div className="flex grow flex-col gap-0">
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
@@ -55,7 +55,7 @@ export const DragAndDropZone = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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" />
|
<DynamicIcon icon={fileIcon} className="text-4xl" />
|
||||||
<div className="flex grow flex-col gap-0">
|
<div className="flex grow flex-col gap-0">
|
||||||
<p className="font-medium">{files.length} files selected</p>
|
<p className="font-medium">{files.length} files selected</p>
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ export function DragGhost({
|
|||||||
zIndex: 10000,
|
zIndex: 10000,
|
||||||
pointerEvents: "none",
|
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
|
<img
|
||||||
src={state.thumbnail}
|
src={state.thumbnail}
|
||||||
@@ -143,7 +143,7 @@ export function DragGhost({
|
|||||||
/>
|
/>
|
||||||
{state.indicator && (
|
{state.indicator && (
|
||||||
<div
|
<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"
|
state.indicator === "ok" ? "bg-emerald-500" : "bg-red-500"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const FolderColorRow: React.FC<FolderColorRowProps> = ({
|
|||||||
onClick={() => onSetColor(c)}
|
onClick={() => onSetColor(c)}
|
||||||
style={{ backgroundColor: c }}
|
style={{ backgroundColor: c }}
|
||||||
className={twMerge(
|
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 &&
|
colorCode === c &&
|
||||||
"ring-2 ring-white ring-offset-1 ring-offset-ui-panel",
|
"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"
|
aria-label="Clear color"
|
||||||
onClick={() => onSetColor(null)}
|
onClick={() => onSetColor(null)}
|
||||||
className={twMerge(
|
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",
|
!colorCode && "ring-2 ring-white ring-offset-1 ring-offset-ui-panel",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<XIcon className="text-[10px]" />
|
<XIcon className="text-[10px]" />
|
||||||
</button>
|
</button>
|
||||||
{/* Custom hex (native picker) */}
|
{/* Custom hex (native picker) */}
|
||||||
<label
|
<label
|
||||||
title="Custom color"
|
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={{
|
style={{
|
||||||
background:
|
background:
|
||||||
"conic-gradient(red, orange, yellow, lime, cyan, blue, magenta, red)",
|
"conic-gradient(red, orange, yellow, lime, cyan, blue, magenta, red)",
|
||||||
@@ -55,7 +55,9 @@ export const FolderColorRow: React.FC<FolderColorRowProps> = ({
|
|||||||
<input
|
<input
|
||||||
type="color"
|
type="color"
|
||||||
value={
|
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)}
|
onChange={(e) => onSetColor(e.target.value)}
|
||||||
className="absolute -inset-1 h-[150%] w-[150%] cursor-pointer opacity-0"
|
className="absolute -inset-1 h-[150%] w-[150%] cursor-pointer opacity-0"
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const FolderNameDialog: React.FC<FolderNameDialogProps> = ({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
title={title}
|
title={title}
|
||||||
accessibleTitle={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">
|
<div className="space-y-3">
|
||||||
{subtitle && (
|
{subtitle && (
|
||||||
@@ -57,26 +57,27 @@ export const FolderNameDialog: React.FC<FolderNameDialogProps> = ({
|
|||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="text"
|
type="text"
|
||||||
|
aria-label="Folder name"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => setValue(e.target.value)}
|
onChange={(e) => setValue(e.target.value)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") submit();
|
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
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="action"
|
variant="action"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="px-3 py-1 text-sm"
|
className="px-3 py-1.5 text-[11px]"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={submit}
|
onClick={submit}
|
||||||
disabled={!value.trim()}
|
disabled={!value.trim()}
|
||||||
className="px-3 py-1 text-sm"
|
className="px-3 py-1.5 text-[11px]"
|
||||||
>
|
>
|
||||||
{confirmLabel}
|
{confirmLabel}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
import React, {
|
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||||
useEffect,
|
|
||||||
useLayoutEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { CheckIcon, EllipsisIcon, MusicIcon, UploadIcon } from "lucide-react";
|
import { CheckIcon, EllipsisIcon, MusicIcon, UploadIcon } from "lucide-react";
|
||||||
import { LoadingSpinner } from "@storyteller/ui-loading-spinner";
|
import { LoadingSpinner } from "@storyteller/ui-loading-spinner";
|
||||||
@@ -248,7 +243,7 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
|
|||||||
type="button"
|
type="button"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className={twMerge(
|
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
|
selected || bulkSelected
|
||||||
? "border-primary"
|
? "border-primary"
|
||||||
: disabled
|
: disabled
|
||||||
@@ -272,10 +267,8 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
|
|||||||
{isAudio ? (
|
{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 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 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">
|
<div className="flex h-11 w-11 items-center justify-center bg-white/10 ring-1 ring-white/15">
|
||||||
<MusicIcon
|
<MusicIcon className="text-lg text-white/70" />
|
||||||
|
|
||||||
className="text-lg text-white/70" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="line-clamp-2 shrink-0 px-2.5 pb-2.5 text-left text-xs leading-snug text-white/75">
|
<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 && (
|
{selected && (
|
||||||
<div className="absolute left-2 top-2 flex h-6 w-6 items-center justify-center rounded-full bg-primary">
|
<div className="absolute left-2 top-2 flex h-6 w-6 items-center justify-center rounded-[3px] bg-primary">
|
||||||
<CheckIcon className="text-sm" />
|
<CheckIcon className="text-sm" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{disabled && (
|
{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
|
Added
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -356,10 +349,8 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
|
|||||||
position="bottom"
|
position="bottom"
|
||||||
align="end"
|
align="end"
|
||||||
mode="default"
|
mode="default"
|
||||||
triggerIcon={
|
triggerIcon={<EllipsisIcon className="text-base-fg" />}
|
||||||
<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"
|
||||||
}
|
|
||||||
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"
|
|
||||||
panelClassName="w-max min-w-44 p-1"
|
panelClassName="w-max min-w-44 p-1"
|
||||||
closeOnUnhover
|
closeOnUnhover
|
||||||
>
|
>
|
||||||
@@ -394,7 +385,7 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
ref={ctxPanelRef}
|
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 }}
|
style={{ left: ctxMenu.x, top: ctxMenu.y }}
|
||||||
>
|
>
|
||||||
<GalleryItemMenuItems
|
<GalleryItemMenuItems
|
||||||
@@ -418,7 +409,7 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
|
|||||||
{mode !== "select" && onBulkSelectToggle && (
|
{mode !== "select" && onBulkSelectToggle && (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
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
|
bulkSelected
|
||||||
? "bg-primary border-primary"
|
? "bg-primary border-primary"
|
||||||
: "border-white/60 bg-black/40 hover:border-white",
|
: "border-white/60 bg-black/40 hover:border-white",
|
||||||
@@ -432,16 +423,12 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
|
|||||||
onBulkSelectToggle?.();
|
onBulkSelectToggle?.();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{bulkSelected && (
|
{bulkSelected && <CheckIcon className="text-[10px] text-white" />}
|
||||||
<CheckIcon
|
|
||||||
|
|
||||||
className="text-[10px] text-white" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Media class badge on hover — bottom-left */}
|
{/* Media class badge on hover — bottom-left */}
|
||||||
{!disableTooltipAndBadge && item.mediaClass && (
|
{!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 === "dimensional" ||
|
||||||
item.mediaClass === "mesh" ||
|
item.mediaClass === "mesh" ||
|
||||||
item.mediaClass === "splat"
|
item.mediaClass === "splat"
|
||||||
@@ -451,8 +438,8 @@ export const GalleryDraggableItem: React.FC<GalleryDraggableItemProps> = ({
|
|||||||
)}
|
)}
|
||||||
{/* Upload badge — bottom-right (always visible, even in select mode) */}
|
{/* Upload badge — bottom-right (always visible, even in select mode) */}
|
||||||
{item.isUpload && (
|
{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">
|
<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]" />
|
<UploadIcon className="text-[10px]" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Conditionally wrap with Tooltip — hidden when selecting or in bulk mode */}
|
{/* 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}
|
style={colorCode ? { borderColor: colorCode } : undefined}
|
||||||
className={twMerge(
|
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,
|
// A dragged item hovering the folder makes it lift and glow — clear,
|
||||||
// tactile "drop here" feedback rather than a flat color swap.
|
// 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",
|
"[&.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();
|
e.stopPropagation();
|
||||||
openMenuAt(e.currentTarget as HTMLElement);
|
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" />
|
<EllipsisIcon className="text-sm" />
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import React, { useState } from "react";
|
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 { twMerge } from "tailwind-merge";
|
||||||
import { GalleryItem } from "./gallery-modal";
|
import { GalleryItem } from "./gallery-modal";
|
||||||
import { GalleryFolder } from "./GalleryDraggableItem";
|
import { GalleryFolder } from "./GalleryDraggableItem";
|
||||||
@@ -28,7 +37,7 @@ export interface GalleryItemMenuItemsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ROW =
|
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
|
* The gallery-tile menu body (Open / Edit / Add to Folder ▸ / Remove from
|
||||||
@@ -60,7 +69,7 @@ export const GalleryItemMenuItems: React.FC<GalleryItemMenuItemsProps> = ({
|
|||||||
onOpen();
|
onOpen();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<EyeIcon className="text-base-fg w-4" />
|
<EyeIcon className="text-base-fg w-4" />
|
||||||
<span>
|
<span>
|
||||||
{item.mediaClass === "video"
|
{item.mediaClass === "video"
|
||||||
? "View video"
|
? "View video"
|
||||||
@@ -85,7 +94,7 @@ export const GalleryItemMenuItems: React.FC<GalleryItemMenuItemsProps> = ({
|
|||||||
close();
|
close();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PencilIcon className="text-base-fg w-4" />
|
<PencilIcon className="text-base-fg w-4" />
|
||||||
<span>Edit image</span>
|
<span>Edit image</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -103,28 +112,28 @@ export const GalleryItemMenuItems: React.FC<GalleryItemMenuItemsProps> = ({
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="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) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setFolderSubmenuOpen((v) => !v);
|
setFolderSubmenuOpen((v) => !v);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<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>
|
<span>Add to Folder</span>
|
||||||
</div>
|
</div>
|
||||||
<ChevronRightIcon
|
<ChevronRightIcon
|
||||||
|
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"text-[10px] text-base-fg/50 transition-transform",
|
"text-[10px] text-base-fg/50 transition-transform",
|
||||||
COARSE_POINTER && folderSubmenuOpen && "rotate-90",
|
COARSE_POINTER && folderSubmenuOpen && "rotate-90",
|
||||||
)} />
|
)}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
{folderSubmenuOpen &&
|
{folderSubmenuOpen &&
|
||||||
(COARSE_POINTER ? (
|
(COARSE_POINTER ? (
|
||||||
// Touch: expand inline (accordion) — a fly-out would overflow
|
// Touch: expand inline (accordion) — a fly-out would overflow
|
||||||
// the screen edge on narrow viewports.
|
// 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
|
<FolderList
|
||||||
item={item}
|
item={item}
|
||||||
folders={folders}
|
folders={folders}
|
||||||
@@ -138,7 +147,7 @@ export const GalleryItemMenuItems: React.FC<GalleryItemMenuItemsProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="absolute left-full top-0 -ml-1 pl-2 z-50">
|
<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
|
<FolderList
|
||||||
item={item}
|
item={item}
|
||||||
folders={folders}
|
folders={folders}
|
||||||
@@ -165,7 +174,7 @@ export const GalleryItemMenuItems: React.FC<GalleryItemMenuItemsProps> = ({
|
|||||||
close();
|
close();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FolderMinusIcon className="text-base-fg w-4" />
|
<FolderMinusIcon className="text-base-fg w-4" />
|
||||||
<span>Remove from folder</span>
|
<span>Remove from folder</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -173,14 +182,14 @@ export const GalleryItemMenuItems: React.FC<GalleryItemMenuItemsProps> = ({
|
|||||||
{onDelete && (
|
{onDelete && (
|
||||||
<button
|
<button
|
||||||
type="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) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
close();
|
close();
|
||||||
onDelete();
|
onDelete();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2Icon className="text-red w-4" />
|
<Trash2Icon className="text-red w-4" />
|
||||||
<span className="text-red">Delete</span>
|
<span className="text-red">Delete</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -209,7 +218,7 @@ const FolderList = ({
|
|||||||
<button
|
<button
|
||||||
key={folder.id}
|
key={folder.id}
|
||||||
type="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 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) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onAddToFolder?.([item.id], folder.id);
|
onAddToFolder?.([item.id], folder.id);
|
||||||
@@ -217,9 +226,9 @@ const FolderList = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FolderIcon
|
<FolderIcon
|
||||||
|
|
||||||
className={folder.colorCode ? "text-xs" : "text-primary text-xs"}
|
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>
|
<span className="truncate">{folder.name}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -228,14 +237,14 @@ const FolderList = ({
|
|||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
type="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) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
closeAll();
|
closeAll();
|
||||||
onCreateFolderFromMenu?.();
|
onCreateFolderFromMenu?.();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlusIcon className="text-xs w-4" />
|
<PlusIcon className="text-xs w-4" />
|
||||||
<span>New Folder</span>
|
<span>New Folder</span>
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ const SkeletonGrid = ({ columns }: { columns: number }) => {
|
|||||||
<div className="space-y-6 p-4 animate-in fade-in duration-200">
|
<div className="space-y-6 p-4 animate-in fade-in duration-200">
|
||||||
{/* Fake date heading */}
|
{/* Fake date heading */}
|
||||||
<div>
|
<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
|
<div
|
||||||
className={twMerge("grid", gapClass)}
|
className={twMerge("grid", gapClass)}
|
||||||
style={{
|
style={{
|
||||||
@@ -143,7 +143,7 @@ const SkeletonGrid = ({ columns }: { columns: number }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Array.from({ length: SKELETON_COUNT }).map((_, i) => (
|
{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
|
<div
|
||||||
className="h-full w-full bg-white/[0.06]"
|
className="h-full w-full bg-white/[0.06]"
|
||||||
style={{
|
style={{
|
||||||
@@ -477,7 +477,7 @@ const BulkThumb = ({
|
|||||||
const showImage = !!thumbnail && !failed;
|
const showImage = !!thumbnail && !failed;
|
||||||
|
|
||||||
return (
|
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 ? (
|
{showImage ? (
|
||||||
<img
|
<img
|
||||||
src={thumbnail}
|
src={thumbnail}
|
||||||
@@ -523,14 +523,14 @@ const FolderContextMenuItems = ({
|
|||||||
}) => (
|
}) => (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="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)}
|
onClick={() => onToggleStar(folderId, !hasStar)}
|
||||||
>
|
>
|
||||||
<StarIcon
|
<StarIcon
|
||||||
@@ -548,7 +548,7 @@ const FolderContextMenuItems = ({
|
|||||||
<div className="mx-1.5 my-1 border-t border-ui-panel-border" />
|
<div className="mx-1.5 my-1 border-t border-ui-panel-border" />
|
||||||
<button
|
<button
|
||||||
type="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)}
|
onClick={() => onNewSubfolder(folderId)}
|
||||||
>
|
>
|
||||||
<FolderPlusIcon className="w-4" />
|
<FolderPlusIcon className="w-4" />
|
||||||
@@ -556,7 +556,7 @@ const FolderContextMenuItems = ({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="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)}
|
onClick={() => onRename(folderId)}
|
||||||
>
|
>
|
||||||
<PencilIcon className="w-4" />
|
<PencilIcon className="w-4" />
|
||||||
@@ -564,7 +564,7 @@ const FolderContextMenuItems = ({
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="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)}
|
onClick={() => onDelete(folderId)}
|
||||||
>
|
>
|
||||||
<Trash2Icon className="w-4" />
|
<Trash2Icon className="w-4" />
|
||||||
@@ -2581,7 +2581,7 @@ export const GalleryModal = React.memo(
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={twMerge(
|
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" &&
|
mode === "view" &&
|
||||||
"h-[780px] min-h-[640px] min-w-[66rem] w-[66rem] max-w-none",
|
"h-[780px] min-h-[640px] min-w-[66rem] w-[66rem] max-w-none",
|
||||||
)}
|
)}
|
||||||
@@ -2597,18 +2597,18 @@ export const GalleryModal = React.memo(
|
|||||||
</Modal.DragHandle>
|
</Modal.DragHandle>
|
||||||
)}
|
)}
|
||||||
<div className="relative flex h-full flex-col">
|
<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 flex-wrap justify-between items-center gap-y-2">
|
||||||
<div className="flex items-center gap-3 flex-wrap gap-y-1">
|
<div className="flex items-center gap-3 flex-wrap gap-y-1">
|
||||||
{(mode === "view" || mode === "select") && (
|
{(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) => (
|
{(["unsorted", "folders"] as const).map((t) => (
|
||||||
<button
|
<button
|
||||||
key={t}
|
key={t}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => switchGalleryTab(t)}
|
onClick={() => switchGalleryTab(t)}
|
||||||
className={twMerge(
|
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
|
galleryTab === t
|
||||||
? "bg-ui-controls text-base-fg"
|
? "bg-ui-controls text-base-fg"
|
||||||
: "text-base-fg/60 hover:text-base-fg",
|
: "text-base-fg/60 hover:text-base-fg",
|
||||||
@@ -2625,7 +2625,7 @@ export const GalleryModal = React.memo(
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleOpenNewFolderModal(null)}
|
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" />
|
<FolderPlusIcon className="text-xs" />
|
||||||
New folder
|
New folder
|
||||||
@@ -2654,7 +2654,7 @@ export const GalleryModal = React.memo(
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleCloseTagView}
|
onClick={handleCloseTagView}
|
||||||
aria-label="Close tag view"
|
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" />
|
<XIcon className="text-sm" />
|
||||||
</button>
|
</button>
|
||||||
@@ -2668,7 +2668,7 @@ export const GalleryModal = React.memo(
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleCloseTagView}
|
onClick={handleCloseTagView}
|
||||||
aria-label="Close tags"
|
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" />
|
<XIcon className="text-sm" />
|
||||||
</button>
|
</button>
|
||||||
@@ -2709,12 +2709,12 @@ export const GalleryModal = React.memo(
|
|||||||
if (e.key === "Escape") setRenamingFolderId(null);
|
if (e.key === "Escape") setRenamingFolderId(null);
|
||||||
}}
|
}}
|
||||||
onBlur={handleConfirmRename}
|
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
|
autoFocus
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<h2
|
<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!)}
|
onClick={() => handleStartRename(activeFolderId!)}
|
||||||
>
|
>
|
||||||
{activeFolder.name}
|
{activeFolder.name}
|
||||||
@@ -2732,7 +2732,7 @@ export const GalleryModal = React.memo(
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setFolderMenuOpen((v) => !v)}
|
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" />
|
<EllipsisIcon className="text-sm" />
|
||||||
</button>
|
</button>
|
||||||
@@ -2916,7 +2916,7 @@ export const GalleryModal = React.memo(
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={twMerge(
|
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 &&
|
isActive &&
|
||||||
!activeFolderId &&
|
!activeFolderId &&
|
||||||
!activeTagToken &&
|
!activeTagToken &&
|
||||||
@@ -2949,7 +2949,7 @@ export const GalleryModal = React.memo(
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleOpenNewFolderModal(null)}
|
onClick={() => handleOpenNewFolderModal(null)}
|
||||||
aria-label="New folder"
|
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]" />
|
<PlusIcon className="text-[10px]" />
|
||||||
</button>
|
</button>
|
||||||
@@ -2982,7 +2982,7 @@ export const GalleryModal = React.memo(
|
|||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
className={twMerge(
|
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
|
activeRootFolderId === folder.id
|
||||||
? "bg-ui-controls/60 text-base-fg font-medium"
|
? "bg-ui-controls/60 text-base-fg font-medium"
|
||||||
: "text-base-fg/70 hover:bg-ui-controls/30 hover:text-base-fg",
|
: "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)
|
handleOpenTagFromSidebar(tag.tag_token)
|
||||||
}
|
}
|
||||||
className={twMerge(
|
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
|
activeTagToken === tag.tag_token
|
||||||
? "bg-ui-controls/60 text-base-fg font-medium"
|
? "bg-ui-controls/60 text-base-fg font-medium"
|
||||||
: "text-base-fg/70 hover:bg-ui-controls/30 hover:text-base-fg",
|
: "text-base-fg/70 hover:bg-ui-controls/30 hover:text-base-fg",
|
||||||
@@ -3042,7 +3042,7 @@ export const GalleryModal = React.memo(
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleOpenTagBrowser}
|
onClick={handleOpenTagBrowser}
|
||||||
className={twMerge(
|
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
|
tagBrowserOpen && !activeTagToken
|
||||||
? "bg-ui-controls/60 text-base-fg font-medium"
|
? "bg-ui-controls/60 text-base-fg font-medium"
|
||||||
: "text-base-fg/50 hover:bg-ui-controls/30 hover:text-base-fg",
|
: "text-base-fg/50 hover:bg-ui-controls/30 hover:text-base-fg",
|
||||||
@@ -3079,7 +3079,7 @@ export const GalleryModal = React.memo(
|
|||||||
</div>
|
</div>
|
||||||
) : activeTagItems.length === 0 ? (
|
) : activeTagItems.length === 0 ? (
|
||||||
<div className="flex h-full flex-col items-center justify-center gap-3">
|
<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" />
|
<TagIcon className="text-violet-400 text-2xl" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-base-fg font-semibold">
|
<div className="text-base-fg font-semibold">
|
||||||
@@ -3170,7 +3170,7 @@ export const GalleryModal = React.memo(
|
|||||||
/* ── All-tags word cloud ── */
|
/* ── All-tags word cloud ── */
|
||||||
userTags.length === 0 ? (
|
userTags.length === 0 ? (
|
||||||
<div className="flex h-full flex-col items-center justify-center gap-3">
|
<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" />
|
<TagIcon className="text-violet-400 text-2xl" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-base-fg font-semibold">
|
<div className="text-base-fg font-semibold">
|
||||||
@@ -3191,7 +3191,7 @@ export const GalleryModal = React.memo(
|
|||||||
/* ── Folders tab, root: folder cards only (above) ── */
|
/* ── Folders tab, root: folder cards only (above) ── */
|
||||||
currentSubfolders.length === 0 ? (
|
currentSubfolders.length === 0 ? (
|
||||||
<div className="flex h-full flex-col items-center justify-center gap-3">
|
<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" />
|
<FolderIcon className="text-primary text-2xl" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-base-fg font-semibold">
|
<div className="text-base-fg font-semibold">
|
||||||
@@ -3223,7 +3223,7 @@ export const GalleryModal = React.memo(
|
|||||||
) : activeFolderItems.length === 0 ? (
|
) : activeFolderItems.length === 0 ? (
|
||||||
currentSubfolders.length > 0 ? null : (
|
currentSubfolders.length > 0 ? null : (
|
||||||
<div className="flex h-full flex-col items-center justify-center gap-3">
|
<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" />
|
<FolderIcon className="text-primary text-2xl" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-base-fg font-semibold">
|
<div className="text-base-fg font-semibold">
|
||||||
@@ -3341,7 +3341,7 @@ export const GalleryModal = React.memo(
|
|||||||
</div>
|
</div>
|
||||||
{onLoginClick ? (
|
{onLoginClick ? (
|
||||||
<button
|
<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()}
|
onClick={() => onLoginClick()}
|
||||||
>
|
>
|
||||||
Log in
|
Log in
|
||||||
@@ -3523,7 +3523,7 @@ export const GalleryModal = React.memo(
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{mode === "view" && bulkSelectionMode && (
|
{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">
|
<div className="flex items-center">
|
||||||
{/* Thumbnail previews of selected items */}
|
{/* Thumbnail previews of selected items */}
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
@@ -3548,7 +3548,7 @@ export const GalleryModal = React.memo(
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{bulkSelectedItems.length > 4 && (
|
{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">
|
<span className="text-[11px] font-normal text-white/70">
|
||||||
+{bulkSelectedItems.length - 4}
|
+{bulkSelectedItems.length - 4}
|
||||||
</span>
|
</span>
|
||||||
@@ -3576,7 +3576,7 @@ export const GalleryModal = React.memo(
|
|||||||
className="fixed inset-0 z-[59]"
|
className="fixed inset-0 z-[59]"
|
||||||
onClick={() => setBulkFolderPopoverOpen(false)}
|
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 */}
|
{/* Folders */}
|
||||||
<div className="text-[11px] font-semibold uppercase tracking-wider text-base-fg/40 px-2 py-1">
|
<div className="text-[11px] font-semibold uppercase tracking-wider text-base-fg/40 px-2 py-1">
|
||||||
Folders
|
Folders
|
||||||
@@ -3592,7 +3592,7 @@ export const GalleryModal = React.memo(
|
|||||||
<button
|
<button
|
||||||
key={folder.id}
|
key={folder.id}
|
||||||
type="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 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={() => {
|
onClick={() => {
|
||||||
const ids = Array.from(bulkSelectedIds);
|
const ids = Array.from(bulkSelectedIds);
|
||||||
requestFolderDrop(ids, folder.id);
|
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" />
|
<div className="mx-1.5 my-1 border-t border-ui-panel-border" />
|
||||||
<button
|
<button
|
||||||
type="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={() => {
|
onClick={() => {
|
||||||
const ids = Array.from(bulkSelectedIds);
|
const ids = Array.from(bulkSelectedIds);
|
||||||
setBulkFolderPopoverOpen(false);
|
setBulkFolderPopoverOpen(false);
|
||||||
@@ -3633,7 +3633,7 @@ export const GalleryModal = React.memo(
|
|||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={clearBulkSelection}
|
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"
|
aria-label="Clear selection"
|
||||||
>
|
>
|
||||||
<XIcon className="text-base" />
|
<XIcon className="text-base" />
|
||||||
@@ -3643,7 +3643,7 @@ export const GalleryModal = React.memo(
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{mode === "select" && (
|
{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="flex items-center gap-3">
|
||||||
<div className="text-sm font-semibold text-base-fg/80">
|
<div className="text-sm font-semibold text-base-fg/80">
|
||||||
{selectedItemIds.length}/{maxSelections} selected
|
{selectedItemIds.length}/{maxSelections} selected
|
||||||
@@ -3756,7 +3756,7 @@ export const GalleryModal = React.memo(
|
|||||||
createPortal(
|
createPortal(
|
||||||
<div
|
<div
|
||||||
ref={marqueeRef}
|
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" }}
|
style={{ display: "none" }}
|
||||||
/>,
|
/>,
|
||||||
document.body,
|
document.body,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function CopyPromptButton({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
aria-label="Copy prompt"
|
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" />
|
<DynamicIcon icon={copied ? CheckIcon : CopyIcon} className="text-sm" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const FailedCard = memo(function FailedCard({
|
|||||||
const iconPath = modelId ? getCreatorIconPathForModelId(modelId) : null;
|
const iconPath = modelId ? getCreatorIconPathForModelId(modelId) : null;
|
||||||
|
|
||||||
return (
|
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 && (
|
{refImageUrl && (
|
||||||
<img
|
<img
|
||||||
src={refImageUrl}
|
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">
|
<div className="relative flex h-full flex-col items-center justify-center gap-2 px-4 sm:px-6">
|
||||||
<CircleAlertIcon
|
<CircleAlertIcon className="text-2xl text-red-400" />
|
||||||
|
|
||||||
className="text-2xl text-red-400" />
|
|
||||||
<span className="text-center text-xs font-medium text-red-400">
|
<span className="text-center text-xs font-medium text-red-400">
|
||||||
{failureReason || "Generation failed"}
|
{failureReason || "Generation failed"}
|
||||||
</span>
|
</span>
|
||||||
@@ -58,7 +56,7 @@ export const FailedCard = memo(function FailedCard({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onDismiss(id)}
|
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 />
|
<XIcon />
|
||||||
Dismiss
|
Dismiss
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ export const FailedRow = memo(function FailedRow({
|
|||||||
const iconPath = modelId ? getCreatorIconPathForModelId(modelId) : null;
|
const iconPath = modelId ? getCreatorIconPathForModelId(modelId) : null;
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* 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 && (
|
{refImageUrl && (
|
||||||
<img
|
<img
|
||||||
src={refImageUrl}
|
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"
|
className="pointer-events-none absolute inset-0 h-full w-full object-cover opacity-10"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<CircleAlertIcon
|
<CircleAlertIcon className="relative text-2xl text-red-400" />
|
||||||
|
|
||||||
className="relative text-2xl text-red-400" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Reason + prompt + model */}
|
{/* Reason + prompt + model */}
|
||||||
@@ -87,7 +85,7 @@ export const FailedRow = memo(function FailedRow({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onDismiss(id)}
|
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 />
|
<XIcon />
|
||||||
Dismiss
|
Dismiss
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function GalleryAutoplayToggle() {
|
|||||||
<div
|
<div
|
||||||
role="group"
|
role="group"
|
||||||
aria-label="Video preview playback"
|
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 }) => {
|
{OPTIONS.map(({ autoplay, icon, label }) => {
|
||||||
const active = autoplayVideos === autoplay;
|
const active = autoplayVideos === autoplay;
|
||||||
@@ -35,7 +35,7 @@ export function GalleryAutoplayToggle() {
|
|||||||
aria-label={label}
|
aria-label={label}
|
||||||
aria-pressed={active}
|
aria-pressed={active}
|
||||||
onClick={() => setAutoplayVideos(autoplay)}
|
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
|
active
|
||||||
? "bg-white/10 text-white"
|
? "bg-white/10 text-white"
|
||||||
: "text-white/45 hover:text-white/80"
|
: "text-white/45 hover:text-white/80"
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { memo, useCallback, useState, type ReactNode } from "react";
|
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 { DynamicIcon } from "@storyteller/icons";
|
||||||
import {
|
import {
|
||||||
getCreatorIconPathForModelId,
|
getCreatorIconPathForModelId,
|
||||||
@@ -165,7 +172,7 @@ export const GalleryCard = memo(function GalleryCard({
|
|||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
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}
|
style={outerStyle}
|
||||||
onClick={handleCardClick}
|
onClick={handleCardClick}
|
||||||
onKeyDown={handleCardKeyDown}
|
onKeyDown={handleCardKeyDown}
|
||||||
@@ -187,9 +194,7 @@ export const GalleryCard = memo(function GalleryCard({
|
|||||||
</p>
|
</p>
|
||||||
<div className="flex min-h-0 flex-1 items-center justify-center">
|
<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">
|
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-white/10 ring-1 ring-white/15">
|
||||||
<MusicIcon
|
<MusicIcon className="text-xl text-white/70" />
|
||||||
|
|
||||||
className="text-xl text-white/70" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{item.fullImage && (
|
{item.fullImage && (
|
||||||
@@ -216,13 +221,13 @@ export const GalleryCard = memo(function GalleryCard({
|
|||||||
{/* Selection checkbox chip (select mode only) */}
|
{/* Selection checkbox chip (select mode only) */}
|
||||||
{selectMode && item.fullImage && (
|
{selectMode && item.fullImage && (
|
||||||
<div
|
<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
|
selected
|
||||||
? "border-primary-400 bg-primary-400 text-white"
|
? "border-primary-400 bg-primary-400 text-white"
|
||||||
: "border-white/60 bg-black/40 text-transparent"
|
: "border-white/60 bg-black/40 text-transparent"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<CheckIcon className="text-[10px]" />
|
<CheckIcon className="text-[10px]" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -230,21 +235,21 @@ export const GalleryCard = memo(function GalleryCard({
|
|||||||
in the lightbox). Top-right so it never collides with the selection
|
in the lightbox). Top-right so it never collides with the selection
|
||||||
checkbox chip at top-left. */}
|
checkbox chip at top-left. */}
|
||||||
{lastViewed && (
|
{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 />
|
<EyeIcon />
|
||||||
Last viewed
|
Last viewed
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Hover overlay with media type + model badges and quick actions */}
|
{/* 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="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]" />
|
<DynamicIcon icon={mediaIcon} className="text-[10px]" />
|
||||||
{mediaLabel}
|
{mediaLabel}
|
||||||
</div>
|
</div>
|
||||||
{modelDisplayName && modelIconPath && (
|
{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
|
<img
|
||||||
src={modelIconPath}
|
src={modelIconPath}
|
||||||
alt=""
|
alt=""
|
||||||
@@ -256,7 +261,7 @@ export const GalleryCard = memo(function GalleryCard({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{actionsSlot && !selectMode && (
|
{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}
|
{actionsSlot}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
import { memo, useCallback, type ReactNode } from "react";
|
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 {
|
import {
|
||||||
getCreatorIconPathForModelId,
|
getCreatorIconPathForModelId,
|
||||||
getModelDisplayName,
|
getModelDisplayName,
|
||||||
@@ -54,7 +62,13 @@ export const GalleryRow = memo(function GalleryRow({
|
|||||||
: isAudio
|
: isAudio
|
||||||
? MusicIcon
|
? MusicIcon
|
||||||
: ImageIcon;
|
: 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 effectiveModelId = modelId ?? item.modelId;
|
||||||
const modelDisplayName = effectiveModelId
|
const modelDisplayName = effectiveModelId
|
||||||
@@ -90,23 +104,23 @@ export const GalleryRow = memo(function GalleryRow({
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={handleRowClick}
|
onClick={handleRowClick}
|
||||||
onKeyDown={handleRowKeyDown}
|
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) */}
|
{/* Selection checkbox (select mode only) */}
|
||||||
{selectMode && (
|
{selectMode && (
|
||||||
<div
|
<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
|
selected
|
||||||
? "border-primary-400 bg-primary-400 text-white"
|
? "border-primary-400 bg-primary-400 text-white"
|
||||||
: "border-white/60 bg-black/40 text-transparent"
|
: "border-white/60 bg-black/40 text-transparent"
|
||||||
} ${item.fullImage ? "" : "invisible"}`}
|
} ${item.fullImage ? "" : "invisible"}`}
|
||||||
>
|
>
|
||||||
<CheckIcon className="text-[10px]" />
|
<CheckIcon className="text-[10px]" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Thumbnail */}
|
{/* 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
|
<GalleryThumbnail
|
||||||
thumbnail={item.thumbnail}
|
thumbnail={item.thumbnail}
|
||||||
stillThumbnail={item.stillThumbnail}
|
stillThumbnail={item.stillThumbnail}
|
||||||
@@ -119,9 +133,7 @@ export const GalleryRow = memo(function GalleryRow({
|
|||||||
{isVideo && item.thumbnail && (
|
{isVideo && item.thumbnail && (
|
||||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
<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">
|
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-black/55 backdrop-blur-sm">
|
||||||
<PlayIcon
|
<PlayIcon className="ml-0.5 text-[9px] text-white/90" />
|
||||||
|
|
||||||
className="ml-0.5 text-[9px] text-white/90" />
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -138,7 +150,7 @@ export const GalleryRow = memo(function GalleryRow({
|
|||||||
{/* Title + model / media meta */}
|
{/* Title + model / media meta */}
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
{loading ? (
|
{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 className="animate-shimmer absolute inset-0" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -155,7 +167,7 @@ export const GalleryRow = memo(function GalleryRow({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isAudio && item.fullImage && (
|
{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
|
<WaveformAudioPlayer
|
||||||
src={item.fullImage}
|
src={item.fullImage}
|
||||||
durationMillis={item.durationMillis}
|
durationMillis={item.durationMillis}
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ export function GallerySelectToggle() {
|
|||||||
const label = active ? "Exit selection" : "Select items";
|
const label = active ? "Exit selection" : "Select items";
|
||||||
|
|
||||||
return (
|
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}>
|
<Tooltip content={label} position="bottom" delay={300}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
aria-pressed={active}
|
aria-pressed={active}
|
||||||
onClick={() => setActive(!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
|
active
|
||||||
? "bg-white/10 text-white"
|
? "bg-white/10 text-white"
|
||||||
: "text-white/45 hover:text-white/80"
|
: "text-white/45 hover:text-white/80"
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function GalleryViewToggle() {
|
|||||||
<div
|
<div
|
||||||
role="group"
|
role="group"
|
||||||
aria-label="Gallery layout"
|
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 }) => {
|
{OPTIONS.map(({ mode, icon, label }) => {
|
||||||
const active = viewMode === mode;
|
const active = viewMode === mode;
|
||||||
@@ -37,7 +37,7 @@ export function GalleryViewToggle() {
|
|||||||
aria-label={label}
|
aria-label={label}
|
||||||
aria-pressed={active}
|
aria-pressed={active}
|
||||||
onClick={() => setViewMode(mode)}
|
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
|
active
|
||||||
? "bg-white/10 text-white"
|
? "bg-white/10 text-white"
|
||||||
: "text-white/45 hover:text-white/80"
|
: "text-white/45 hover:text-white/80"
|
||||||
|
|||||||
@@ -36,18 +36,16 @@ export const PendingCard = memo(function PendingCard({
|
|||||||
const iconPath = getCreatorIconPathForModelId(modelId);
|
const iconPath = getCreatorIconPathForModelId(modelId);
|
||||||
|
|
||||||
return (
|
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" />
|
<div className="animate-shimmer h-full w-full" />
|
||||||
{batchCount != null && batchCount > 1 && (
|
{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
|
Generating {batchCount} {batchNoun(mediaClass)} · Results may appear
|
||||||
one at a time
|
one at a time
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="absolute inset-0 flex flex-col items-center justify-center gap-2">
|
<div className="absolute inset-0 flex flex-col items-center justify-center gap-2">
|
||||||
<LoaderCircleIcon
|
<LoaderCircleIcon className="animate-spin text-2xl text-white/20" />
|
||||||
|
|
||||||
className="animate-spin text-2xl text-white/20" />
|
|
||||||
{progressPercent != null && (
|
{progressPercent != null && (
|
||||||
<span className="text-xs tabular-nums text-white/40">
|
<span className="text-xs tabular-nums text-white/40">
|
||||||
{progressPercent}%
|
{progressPercent}%
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ export const PendingRow = memo(function PendingRow({
|
|||||||
const iconPath = getCreatorIconPathForModelId(modelId);
|
const iconPath = getCreatorIconPathForModelId(modelId);
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* 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="animate-shimmer h-full w-full" />
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
<LoaderCircleIcon
|
<LoaderCircleIcon
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ function Gravatar(props: Props) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"relative aspect-square overflow-hidden rounded-full border border-white",
|
"relative aspect-square overflow-hidden border border-white",
|
||||||
props.className
|
props.className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -66,16 +66,16 @@ const NewsView = ({ onBack }: { onBack: () => void }) => {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="article-content">
|
<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 && (
|
{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 && (
|
{selectedPost.thumbnail && (
|
||||||
<img
|
<img
|
||||||
src={selectedPost.thumbnail}
|
src={selectedPost.thumbnail}
|
||||||
alt={selectedPost.title}
|
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>
|
</div>
|
||||||
<style>{`
|
<style>{`
|
||||||
.article-content h1 { font-size: 1.5rem; font-weight: 700; margin: 1rem 0 0.5rem; }
|
.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 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 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 ul { list-style: disc; padding-left: 1.25rem; margin-bottom: 0.75rem; }
|
||||||
.article-content li { margin-bottom: 0.25rem; }
|
.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 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: #3b82f6; text-decoration: none; border-bottom: 1px solid transparent; transition: border-color 0.2s; }
|
.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: #3b82f6; }
|
.article-content a:hover { border-bottom-color: #74aaff; }
|
||||||
|
|
||||||
/* Video embed styles */
|
/* 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 { 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 .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; }
|
.article-content .video-embed video { display: block; width: 100%; height: auto; max-height: 50vh; }
|
||||||
@@ -111,18 +111,18 @@ const NewsView = ({ onBack }: { onBack: () => void }) => {
|
|||||||
<div
|
<div
|
||||||
key={item.slug}
|
key={item.slug}
|
||||||
onClick={() => setSelectedSlug(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">
|
<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}
|
{item.title}
|
||||||
<ArrowRightIcon
|
<ArrowRightIcon
|
||||||
|
|
||||||
size="xs"
|
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>
|
</h3>
|
||||||
{item.date && (
|
{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}
|
{item.date}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -372,7 +372,7 @@ export function HelpMenuButton({
|
|||||||
key={item.id}
|
key={item.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => viewTutorial(item)}
|
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">
|
<div className="aspect-video w-full overflow-hidden">
|
||||||
<img
|
<img
|
||||||
@@ -402,12 +402,12 @@ export function HelpMenuButton({
|
|||||||
>
|
>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<div className="text-lg font-bold text-base-fg">
|
<div className="font-display text-lg font-bold text-base-fg">
|
||||||
Tutorial: {selected.title}
|
Tutorial: {selected.title}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<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 ref={playerContainerRef} className="h-full w-full" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,10 +39,10 @@ export const InputVector = ({
|
|||||||
oldZ.current = z;
|
oldZ.current = z;
|
||||||
|
|
||||||
const inputCommonClasses =
|
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 =
|
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 =
|
const lockedCommonClasses =
|
||||||
"hover:cursor-not-allowed hover:bg-brand-secondary";
|
"hover:cursor-not-allowed hover:bg-brand-secondary";
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export const NumberInput: React.FC<NumberInputProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const incrementButtonStyle =
|
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 (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -60,21 +60,21 @@ export const NumberInput: React.FC<NumberInputProps> = ({
|
|||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onBlur={handleBlur}
|
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
|
<button
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
incrementButtonStyle,
|
incrementButtonStyle,
|
||||||
"rounded-tr-md border-b border-white/10",
|
"rounded-tr-[3px] border-b border-white/15",
|
||||||
)}
|
)}
|
||||||
onClick={handleIncrement}
|
onClick={handleIncrement}
|
||||||
>
|
>
|
||||||
<ChevronUpIcon />
|
<ChevronUpIcon />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={twMerge(incrementButtonStyle, "rounded-br-md")}
|
className={twMerge(incrementButtonStyle, "rounded-br-[3px]")}
|
||||||
onClick={handleDecrement}
|
onClick={handleDecrement}
|
||||||
>
|
>
|
||||||
<ChevronDownIcon />
|
<ChevronDownIcon />
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const Input = React.forwardRef(
|
|||||||
errorMessage,
|
errorMessage,
|
||||||
...rest
|
...rest
|
||||||
}: InputProps,
|
}: InputProps,
|
||||||
ref: React.ForwardedRef<HTMLInputElement>
|
ref: React.ForwardedRef<HTMLInputElement>,
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
<div className={twMerge("flex flex-col", className)}>
|
<div className={twMerge("flex flex-col", className)}>
|
||||||
@@ -45,13 +45,13 @@ export const Input = React.forwardRef(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
id={id ? id : label ? label : undefined}
|
id={id ? id : label ? label : undefined}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"h-10 w-full rounded-lg px-3 py-2.5 outline-none",
|
"h-10 w-full rounded-[3px] px-3 py-2.5 outline-none",
|
||||||
"bg-ui-panel text-base-fg placeholder-base-fg/50",
|
"bg-ui-panel text-base-fg placeholder-base-fg/40",
|
||||||
"border border-ui-panel-border transition-all duration-150 ease-in-out hover:border-primary/60 focus:border-primary focus:!outline-none",
|
"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",
|
"disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:border-ui-panel-border",
|
||||||
icon && "pl-10",
|
icon && "pl-10",
|
||||||
isError && "outline-red focus:outline-red",
|
isError && "border-red-500 focus:border-red-500",
|
||||||
inputClassName
|
inputClassName,
|
||||||
)}
|
)}
|
||||||
onFocus={(e: React.FocusEvent<HTMLInputElement>) => {
|
onFocus={(e: React.FocusEvent<HTMLInputElement>) => {
|
||||||
if (onFocus) {
|
if (onFocus) {
|
||||||
@@ -66,12 +66,12 @@ export const Input = React.forwardRef(
|
|||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<h6 className="absolute z-10 text-red">{errorMessage}</h6>
|
<h6 className="absolute z-10 text-red-400">{errorMessage}</h6>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
Input.displayName = "Input";
|
Input.displayName = "Input";
|
||||||
|
|||||||
@@ -57,15 +57,15 @@ export function Cheatsheet({
|
|||||||
<div
|
<div
|
||||||
data-keybinds-cheatsheet
|
data-keybinds-cheatsheet
|
||||||
className={twMerge(
|
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",
|
pinned && "pointer-events-auto",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="mb-3 flex items-center justify-between">
|
<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
|
Keyboard shortcuts
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs capitalize text-white/40">
|
<span className="font-mono text-[11px] uppercase tracking-[0.12em] text-white/40">
|
||||||
{selectedPreset} preset
|
{selectedPreset} preset
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,7 +75,7 @@ export function Cheatsheet({
|
|||||||
if (!rows?.length) return null;
|
if (!rows?.length) return null;
|
||||||
return (
|
return (
|
||||||
<div key={group}>
|
<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}
|
{group}
|
||||||
</div>
|
</div>
|
||||||
<ul className="flex flex-col gap-1.5">
|
<ul className="flex flex-col gap-1.5">
|
||||||
@@ -94,7 +94,7 @@ export function Cheatsheet({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{pinned && (
|
{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
|
Press Esc or click outside to close
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export function KeybindCaptureInput({
|
|||||||
onClick={() => setCapturing((v) => !v)}
|
onClick={() => setCapturing((v) => !v)}
|
||||||
aria-label="Change keybinding"
|
aria-label="Change keybinding"
|
||||||
className={twMerge(
|
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
|
capturing
|
||||||
? "border-primary/80 bg-primary/10 text-primary animate-pulse"
|
? "border-primary/80 bg-primary/10 text-primary animate-pulse"
|
||||||
: accent
|
: accent
|
||||||
|
|||||||
@@ -176,8 +176,8 @@ export function KeybindsSettings({
|
|||||||
Deliberately reset-free: destructive actions live with their scope
|
Deliberately reset-free: destructive actions live with their scope
|
||||||
(section headers) or in the danger row at the bottom, never beside
|
(section headers) or in the danger row at the bottom, never beside
|
||||||
the search field where a stray click can nuke every override. */}
|
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="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-2xl bg-white/[0.04] p-1.5 ring-1 ring-white/[0.06]">
|
<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} />
|
<ModeToggle mode={searchMode} onChange={switchMode} />
|
||||||
{searchMode === "name" ? (
|
{searchMode === "name" ? (
|
||||||
<NameSearchField value={nameSearch} onChange={setNameSearch} />
|
<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
|
and gated behind a two-step confirm. Hidden when there is nothing to
|
||||||
destroy. ─────────────────────────────────────────────────────────── */}
|
destroy. ─────────────────────────────────────────────────────────── */}
|
||||||
{overriddenCount > 0 && (
|
{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">
|
<div className="flex min-w-0 flex-col gap-0.5">
|
||||||
<span className="text-[13px] font-medium text-base-fg/90">
|
<span className="text-[13px] font-medium text-base-fg/90">
|
||||||
Reset all customizations
|
Reset all customizations
|
||||||
@@ -296,7 +296,7 @@ export function KeybindsSettings({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── Video editor lives in its own customizer ───────────────────────── */}
|
{/* ── 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" />
|
<InfoIcon className="mt-0.5 h-4 w-4 shrink-0 text-base-fg/35" />
|
||||||
<span>
|
<span>
|
||||||
Video editor shortcuts are managed in the video editor itself
|
Video editor shortcuts are managed in the video editor itself
|
||||||
@@ -357,14 +357,14 @@ function PresetDropdown({
|
|||||||
aria-haspopup="listbox"
|
aria-haspopup="listbox"
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
className={twMerge(
|
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,
|
EASE,
|
||||||
open && "ring-primary/40",
|
open && "ring-primary/40",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex min-w-0 grow flex-col gap-0.5 px-3 py-1.5">
|
<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="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}
|
{def.description}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -381,7 +381,7 @@ function PresetDropdown({
|
|||||||
role="listbox"
|
role="listbox"
|
||||||
aria-label="Preset"
|
aria-label="Preset"
|
||||||
className={twMerge(
|
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,
|
EASE,
|
||||||
open
|
open
|
||||||
? "scale-100 opacity-100"
|
? "scale-100 opacity-100"
|
||||||
@@ -457,7 +457,7 @@ function ModeToggle({
|
|||||||
onChange: (mode: SearchMode) => void;
|
onChange: (mode: SearchMode) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
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) => (
|
{(["name", "key"] as SearchMode[]).map((m) => (
|
||||||
<button
|
<button
|
||||||
key={m}
|
key={m}
|
||||||
@@ -465,7 +465,7 @@ function ModeToggle({
|
|||||||
onClick={() => onChange(m)}
|
onClick={() => onChange(m)}
|
||||||
aria-pressed={mode === m}
|
aria-pressed={mode === m}
|
||||||
className={twMerge(
|
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,
|
EASE,
|
||||||
mode === m
|
mode === m
|
||||||
? "bg-white/10 text-base-fg shadow-[inset_0_1px_0_rgba(255,255,255,0.06)]"
|
? "bg-white/10 text-base-fg shadow-[inset_0_1px_0_rgba(255,255,255,0.06)]"
|
||||||
@@ -489,7 +489,7 @@ function NameSearchField({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
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,
|
EASE,
|
||||||
"focus-within:bg-ui-modal/80 focus-within:ring-primary/50",
|
"focus-within:bg-ui-modal/80 focus-within:ring-primary/50",
|
||||||
)}
|
)}
|
||||||
@@ -508,7 +508,7 @@ function NameSearchField({
|
|||||||
onClick={() => onChange("")}
|
onClick={() => onChange("")}
|
||||||
aria-label="Clear search"
|
aria-label="Clear search"
|
||||||
className={twMerge(
|
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,
|
EASE,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -533,7 +533,7 @@ function KeySearchField({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
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,
|
EASE,
|
||||||
listening ? "ring-primary/60" : "ring-transparent",
|
listening ? "ring-primary/60" : "ring-transparent",
|
||||||
)}
|
)}
|
||||||
@@ -570,7 +570,7 @@ function KeySearchField({
|
|||||||
onClick={onClear}
|
onClick={onClear}
|
||||||
aria-label="Clear key filter"
|
aria-label="Clear key filter"
|
||||||
className={twMerge(
|
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,
|
EASE,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -605,8 +605,8 @@ function CollapsibleSurface({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-2xl bg-white/[0.04] ring-1 ring-white/[0.06]">
|
<div className="rounded-none bg-white/[0.04] ring-1 ring-white/15">
|
||||||
<div className="overflow-hidden rounded-[0.625rem] bg-ui-modal/40">
|
<div className="overflow-hidden rounded-[3px] bg-ui-modal/40">
|
||||||
{/* The toggle and the (destructive) section reset are sibling buttons —
|
{/* The toggle and the (destructive) section reset are sibling buttons —
|
||||||
a nested button is invalid HTML and a mis-click on "reset" must
|
a nested button is invalid HTML and a mis-click on "reset" must
|
||||||
never also collapse the section. */}
|
never also collapse the section. */}
|
||||||
@@ -647,7 +647,7 @@ function CollapsibleSurface({
|
|||||||
onConfirm={onReset}
|
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}
|
{count}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -738,7 +738,7 @@ function ConfirmResetButton({
|
|||||||
title={title}
|
title={title}
|
||||||
aria-label={armed ? `${title} — click again to confirm` : title}
|
aria-label={armed ? `${title} — click again to confirm` : title}
|
||||||
className={twMerge(
|
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,
|
EASE,
|
||||||
prominent
|
prominent
|
||||||
? "h-9 px-4 text-[13px]"
|
? "h-9 px-4 text-[13px]"
|
||||||
@@ -746,7 +746,7 @@ function ConfirmResetButton({
|
|||||||
armed
|
armed
|
||||||
? ready
|
? ready
|
||||||
? "bg-red/25 text-white shadow-[0_0_0_3px_rgba(0,0,0,0.15)]"
|
? "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
|
: prominent
|
||||||
? "bg-red/10 text-red ring-1 ring-red/30 hover:bg-red/[0.18]"
|
? "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",
|
: "text-base-fg/45 hover:bg-red/10 hover:text-red",
|
||||||
@@ -827,7 +827,7 @@ function ActionRow({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
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,
|
EASE,
|
||||||
"hover:bg-white/[0.03]",
|
"hover:bg-white/[0.03]",
|
||||||
)}
|
)}
|
||||||
@@ -859,7 +859,7 @@ function ActionRow({
|
|||||||
title="Revert to preset default"
|
title="Revert to preset default"
|
||||||
aria-label="Revert to preset default"
|
aria-label="Revert to preset default"
|
||||||
className={twMerge(
|
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,
|
EASE,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -887,7 +887,7 @@ function ConflictAlert({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="alert"
|
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">
|
<span className="leading-relaxed text-base-fg/80">
|
||||||
<KbdBindings bindings={[pending.binding]} /> is already used by{" "}
|
<KbdBindings bindings={[pending.binding]} /> is already used by{" "}
|
||||||
@@ -901,7 +901,7 @@ function ConflictAlert({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onConfirm}
|
onClick={onConfirm}
|
||||||
className={twMerge(
|
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,
|
EASE,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -911,7 +911,7 @@ function ConflictAlert({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
className={twMerge(
|
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,
|
EASE,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -932,7 +932,7 @@ function EmptyState({
|
|||||||
binding: Binding | null;
|
binding: Binding | null;
|
||||||
}) {
|
}) {
|
||||||
return (
|
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" ? (
|
{mode === "key" ? (
|
||||||
<KeyIcon className="h-6 w-6 text-base-fg/25" />
|
<KeyIcon className="h-6 w-6 text-base-fg/25" />
|
||||||
) : (
|
) : (
|
||||||
@@ -955,7 +955,7 @@ function EmptyState({
|
|||||||
|
|
||||||
function Eyebrow({ children }: { children: React.ReactNode }) {
|
function Eyebrow({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
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}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@@ -963,7 +963,7 @@ function Eyebrow({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
function GroupLabel({ children }: { children: React.ReactNode }) {
|
function GroupLabel({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
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}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,11 +14,14 @@ export const Label = ({
|
|||||||
...rest
|
...rest
|
||||||
}: LabelProps) => (
|
}: LabelProps) => (
|
||||||
<label
|
<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}
|
{...rest}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{required && <span className="ml-0.5 text-[#ff6467]">*</span>}
|
{required && <span className="ml-0.5 text-red-400">*</span>}
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Modal } from "@storyteller/ui-modal";
|
|||||||
import { Tooltip } from "@storyteller/ui-tooltip";
|
import { Tooltip } from "@storyteller/ui-tooltip";
|
||||||
import { Button } from "@storyteller/ui-button";
|
import { Button } from "@storyteller/ui-button";
|
||||||
import dayjs from "dayjs";
|
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 { DynamicIcon } from "@storyteller/icons";
|
||||||
import { MediaFileDelete } from "@storyteller/tauri-api";
|
import { MediaFileDelete } from "@storyteller/tauri-api";
|
||||||
import { LoadingSpinner } from "@storyteller/ui-loading-spinner";
|
import { LoadingSpinner } from "@storyteller/ui-loading-spinner";
|
||||||
@@ -544,7 +544,7 @@ export function LightboxModal({
|
|||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={onClose}
|
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
|
draggable
|
||||||
allowBackgroundInteraction={true}
|
allowBackgroundInteraction={true}
|
||||||
showClose={true}
|
showClose={true}
|
||||||
@@ -555,15 +555,15 @@ export function LightboxModal({
|
|||||||
>
|
>
|
||||||
{/* Invisible drag handle strip at the very top for moving */}
|
{/* Invisible drag handle strip at the very top for moving */}
|
||||||
<Modal.DragHandle>
|
<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>
|
</Modal.DragHandle>
|
||||||
|
|
||||||
{/* content grid */}
|
{/* content grid */}
|
||||||
<div className="flex h-full gap-4">
|
<div className="flex h-full gap-4">
|
||||||
{/* image panel - flexible width */}
|
{/* 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 ? (
|
{!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>
|
<span className="text-base-fg/60">Image not available</span>
|
||||||
</div>
|
</div>
|
||||||
) : mediaClass === "dimensional" ||
|
) : mediaClass === "dimensional" ||
|
||||||
@@ -601,8 +601,8 @@ export function LightboxModal({
|
|||||||
</video>
|
</video>
|
||||||
) : mediaClass === "audio" ? (
|
) : mediaClass === "audio" ? (
|
||||||
<div className="flex h-full w-full items-center justify-center px-4 sm:px-8">
|
<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="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 rounded-full bg-white/10 ring-1 ring-white/15">
|
<div className="mx-auto mb-6 flex h-16 w-16 items-center justify-center border border-white/15 bg-white/10">
|
||||||
<MusicIcon
|
<MusicIcon
|
||||||
|
|
||||||
className="text-2xl text-white/70" />
|
className="text-2xl text-white/70" />
|
||||||
@@ -625,7 +625,7 @@ export function LightboxModal({
|
|||||||
className="embla__slide flex-[0_0_100%]"
|
className="embla__slide flex-[0_0_100%]"
|
||||||
key={`${url}-${idx}`}
|
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
|
<img
|
||||||
data-lightbox-modal="true"
|
data-lightbox-modal="true"
|
||||||
src={addCorsParam(url) || url}
|
src={addCorsParam(url) || url}
|
||||||
@@ -660,12 +660,12 @@ export function LightboxModal({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{effectiveImageUrls.length > 1 && (
|
{effectiveImageUrls.length > 1 && (
|
||||||
<div className="mt-3 px-2">
|
<div className="mt-3 px-2 pb-2">
|
||||||
<div
|
<div
|
||||||
className="embla-thumbs overflow-hidden"
|
className="embla-thumbs overflow-hidden"
|
||||||
ref={emblaThumbsRef}
|
ref={emblaThumbsRef}
|
||||||
>
|
>
|
||||||
<div className="embla-thumbs__container flex gap-2">
|
<div className="embla-thumbs__container flex justify-center gap-2">
|
||||||
{effectiveImageUrls.map((url, idx) => {
|
{effectiveImageUrls.map((url, idx) => {
|
||||||
const isSelected = idx === selectedIndex;
|
const isSelected = idx === selectedIndex;
|
||||||
return (
|
return (
|
||||||
@@ -674,9 +674,9 @@ export function LightboxModal({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => onThumbClick(idx)}
|
onClick={() => onThumbClick(idx)}
|
||||||
className={twMerge(
|
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
|
isSelected
|
||||||
? "border-brand-primary-400 opacity-100"
|
? "border-white opacity-100"
|
||||||
: "border-transparent opacity-60 hover:border-white/40 hover:opacity-100",
|
: "border-transparent opacity-60 hover:border-white/40 hover:opacity-100",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -701,7 +701,7 @@ export function LightboxModal({
|
|||||||
mediaClass !== "mesh" &&
|
mediaClass !== "mesh" &&
|
||||||
mediaClass !== "splat" &&
|
mediaClass !== "splat" &&
|
||||||
mediaClass !== "audio" && (
|
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" />
|
<LoadingSpinner className="h-12 w-12 text-base-fg" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -713,7 +713,7 @@ export function LightboxModal({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onNavigatePrev();
|
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"
|
aria-label="Previous item"
|
||||||
>
|
>
|
||||||
<ChevronLeftIcon className="text-lg" />
|
<ChevronLeftIcon className="text-lg" />
|
||||||
@@ -725,7 +725,7 @@ export function LightboxModal({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onNavigateNext();
|
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"
|
aria-label="Next item"
|
||||||
>
|
>
|
||||||
<ChevronRightIcon className="text-lg" />
|
<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 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">
|
<div className="flex-1 overflow-y-auto space-y-5 text-base-fg min-h-0 pb-2">
|
||||||
{creator ? (
|
{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 ? (
|
{creator.core_info ? (
|
||||||
<Gravatar
|
<Gravatar
|
||||||
size={36}
|
size={36}
|
||||||
@@ -747,10 +747,10 @@ export function LightboxModal({
|
|||||||
backgroundIndex={
|
backgroundIndex={
|
||||||
creator.core_info.default_avatar.color_index
|
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 />
|
<UserIcon />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -758,27 +758,28 @@ export function LightboxModal({
|
|||||||
<span className="text-base-fg text-sm font-semibold leading-none truncate">
|
<span className="text-base-fg text-sm font-semibold leading-none truncate">
|
||||||
{creator.display_name}
|
{creator.display_name}
|
||||||
</span>
|
</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
|
Author
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : promptLoading ? (
|
) : 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="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 rounded-xl bg-white/10" />
|
<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="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.5 w-24 bg-white/10" />
|
||||||
<div className="h-3 w-12 rounded bg-white/10" />
|
<div className="h-3 w-12 bg-white/10" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{(hasPromptToken || promptLoading) && (
|
{(hasPromptToken || promptLoading) && (
|
||||||
<>
|
<>
|
||||||
{/* Prompt */}
|
{/* Prompt */}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="text-sm font-medium text-base-fg/90">
|
<div className="flex items-center gap-2 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">
|
||||||
Prompt
|
<PencilIcon />
|
||||||
|
<span>Prompt</span>
|
||||||
</div>
|
</div>
|
||||||
{!promptLoading && prompt && (
|
{!promptLoading && prompt && (
|
||||||
<button
|
<button
|
||||||
@@ -814,16 +815,13 @@ export function LightboxModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="text-sm text-base-fg break-words p-3 rounded-lg leading-relaxed"
|
className="text-sm text-base-fg/90 break-words px-4 py-3 bg-black/20 leading-relaxed border border-white/10"
|
||||||
style={{
|
|
||||||
background: "rgb(var(--st-controls-rgb) / 0.20)",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{promptLoading ? (
|
{promptLoading ? (
|
||||||
<div className="animate-pulse space-y-2">
|
<div className="animate-pulse space-y-2">
|
||||||
<div className="h-3 w-full rounded bg-white/10" />
|
<div className="h-3 w-full bg-white/10" />
|
||||||
<div className="h-3 w-4/5 rounded bg-white/10" />
|
<div className="h-3 w-4/5 bg-white/10" />
|
||||||
<div className="h-3 w-3/5 rounded bg-white/10" />
|
<div className="h-3 w-3/5 bg-white/10" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
@@ -877,30 +875,25 @@ export function LightboxModal({
|
|||||||
/* Skeleton placeholders while fetching metadata */
|
/* Skeleton placeholders while fetching metadata */
|
||||||
<>
|
<>
|
||||||
{/* Reference Images skeleton */}
|
{/* Reference Images skeleton */}
|
||||||
<div className="space-y-1.5 animate-pulse">
|
<div className="space-y-2 animate-pulse">
|
||||||
<div className="h-3.5 w-28 rounded bg-white/10" />
|
<div className="h-3.5 w-28 bg-white/10" />
|
||||||
<div className="flex gap-1.5">
|
<div className="flex gap-1.5">
|
||||||
<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 rounded-lg bg-white/10" />
|
<div className="h-12 w-12 bg-white/10" />
|
||||||
<div className="h-12 w-12 rounded-lg bg-white/10" />
|
<div className="h-12 w-12 bg-white/10" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Information skeleton */}
|
{/* Information skeleton */}
|
||||||
<div className="space-y-1.5 animate-pulse">
|
<div className="space-y-2 animate-pulse">
|
||||||
<div className="h-3.5 w-24 rounded bg-white/10" />
|
<div className="h-3.5 w-24 bg-white/10" />
|
||||||
<div
|
<div className="flex flex-col bg-black/20 border border-white/10 overflow-hidden">
|
||||||
className="flex flex-col rounded-lg border border-ui-panel-border overflow-hidden"
|
|
||||||
style={{
|
|
||||||
background: "rgb(var(--st-controls-rgb) / 0.20)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Array.from({ length: 5 }).map((_, i) => (
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
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-16 bg-white/10" />
|
||||||
<div className="h-3.5 w-24 rounded bg-white/10" />
|
<div className="h-3.5 w-24 bg-white/10" />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -909,9 +902,10 @@ export function LightboxModal({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{contextImages && contextImages.length > 0 && (
|
{contextImages && contextImages.length > 0 && (
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-2">
|
||||||
<div className="text-sm font-medium text-base-fg/90">
|
<div className="flex items-center gap-2 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">
|
||||||
Reference Media
|
<ImageIcon />
|
||||||
|
<span>Reference Media</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-5 gap-1.5 w-fit">
|
<div className="grid grid-cols-5 gap-1.5 w-fit">
|
||||||
{contextImages.map((contextImage, index) => {
|
{contextImages.map((contextImage, index) => {
|
||||||
@@ -936,7 +930,7 @@ export function LightboxModal({
|
|||||||
<div className="flex flex-col gap-1.5 min-w-[100px]">
|
<div className="flex flex-col gap-1.5 min-w-[100px]">
|
||||||
{!isAudio && !isVideoRef && (
|
{!isAudio && !isVideoRef && (
|
||||||
<button
|
<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) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setRefPreviewUrl(fullSize);
|
setRefPreviewUrl(fullSize);
|
||||||
@@ -950,7 +944,7 @@ export function LightboxModal({
|
|||||||
)}
|
)}
|
||||||
{onNavigateToMedia && (
|
{onNavigateToMedia && (
|
||||||
<button
|
<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) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onNavigateToMedia(
|
onNavigateToMedia(
|
||||||
@@ -968,7 +962,7 @@ export function LightboxModal({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<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={() => {
|
onClick={() => {
|
||||||
if (isAudio) {
|
if (isAudio) {
|
||||||
handleAudioToggle(contextImage.media_token, contextImage.media_links.cdn_url);
|
handleAudioToggle(contextImage.media_token, contextImage.media_links.cdn_url);
|
||||||
@@ -1045,7 +1039,7 @@ export function LightboxModal({
|
|||||||
|
|
||||||
{/* buttons with spacing */}
|
{/* buttons with spacing */}
|
||||||
{actionUrl && (
|
{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 &&
|
{onRecreateClicked &&
|
||||||
hasPromptToken &&
|
hasPromptToken &&
|
||||||
promptData &&
|
promptData &&
|
||||||
@@ -1241,7 +1235,7 @@ export function LightboxModal({
|
|||||||
{selectedMediaToken && onDeleteClicked && (
|
{selectedMediaToken && onDeleteClicked && (
|
||||||
<Button
|
<Button
|
||||||
icon={Trash2Icon}
|
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"
|
variant="destructive"
|
||||||
onClick={async (e) => {
|
onClick={async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -1262,7 +1256,7 @@ export function LightboxModal({
|
|||||||
<Modal
|
<Modal
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
onClose={() => setRefPreviewUrl(null)}
|
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
|
draggable
|
||||||
allowBackgroundInteraction={true}
|
allowBackgroundInteraction={true}
|
||||||
showClose={true}
|
showClose={true}
|
||||||
@@ -1272,9 +1266,9 @@ export function LightboxModal({
|
|||||||
expandable={false}
|
expandable={false}
|
||||||
>
|
>
|
||||||
<Modal.DragHandle>
|
<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>
|
</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
|
<img
|
||||||
src={addCorsParam(refPreviewUrl) || refPreviewUrl}
|
src={addCorsParam(refPreviewUrl) || refPreviewUrl}
|
||||||
alt="Reference preview"
|
alt="Reference preview"
|
||||||
@@ -1323,12 +1317,12 @@ function InfoSection({
|
|||||||
if (!hasAnyInfo) return null;
|
if (!hasAnyInfo) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-2">
|
||||||
<div className="text-sm font-medium text-base-fg/90">Information</div>
|
<div className="flex items-center gap-2 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">
|
||||||
<div
|
<InfoIcon />
|
||||||
className="flex flex-col rounded-lg border border-ui-panel-border overflow-hidden"
|
<span>Information</span>
|
||||||
style={{ background: "rgb(var(--st-controls-rgb) / 0.20)" }}
|
</div>
|
||||||
>
|
<div className="flex flex-col bg-black/20 border border-white/10 overflow-hidden">
|
||||||
{modelType && (
|
{modelType && (
|
||||||
<InfoRow
|
<InfoRow
|
||||||
label="Model"
|
label="Model"
|
||||||
@@ -1386,9 +1380,11 @@ function InfoSection({
|
|||||||
|
|
||||||
function InfoRow({ label, value }: { label: string; value: ReactNode }) {
|
function InfoRow({ label, value }: { label: string; value: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between py-2 px-3 border-b border-white/5 last:border-0">
|
<div className="flex items-start justify-between gap-4 px-3 py-2.5 border-b border-white/10 last:border-0">
|
||||||
<span className="text-sm text-base-fg/70 font-medium">{label}</span>
|
<span className="shrink-0 pt-0.5 font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-base-fg/60">
|
||||||
<span className="text-sm text-base-fg flex items-center gap-2">
|
{label}
|
||||||
|
</span>
|
||||||
|
<span className="min-w-0 text-right text-sm text-base-fg font-medium flex items-center justify-end gap-2">
|
||||||
{value}
|
{value}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -134,14 +134,14 @@ export function TagChipInput({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const chipClass = (value: string) =>
|
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" : ""
|
flashValue === value ? "ring-2 ring-primary" : ""
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div
|
<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"
|
disabled ? "" : "cursor-text"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => inputRef.current?.focus()}
|
onClick={() => inputRef.current?.focus()}
|
||||||
@@ -183,6 +183,7 @@ export function TagChipInput({
|
|||||||
setFocused(false);
|
setFocused(false);
|
||||||
if (draft.trim()) commit(draft);
|
if (draft.trim()) commit(draft);
|
||||||
}}
|
}}
|
||||||
|
aria-label="Tags"
|
||||||
placeholder={chips.length === 0 ? "Add tags (comma separated)" : "Add tag"}
|
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"
|
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 && (
|
{dropdownOpen && (
|
||||||
<div
|
<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"
|
dropUp ? "bottom-full mb-1" : "top-full mt-1"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -203,7 +204,7 @@ export function TagChipInput({
|
|||||||
onMouseDown={(e) => e.preventDefault()}
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
onClick={() => pickSuggestion(suggestion.value)}
|
onClick={() => pickSuggestion(suggestion.value)}
|
||||||
onMouseEnter={() => setHighlightIndex(index)}
|
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"
|
highlight === index ? "bg-ui-controls/60" : "hover:bg-ui-controls/40"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ export function TagsSection({
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<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 />
|
<TagIcon />
|
||||||
<span>Tags</span>
|
<span>Tags</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const ListDropdown = ({ list, onSelect }: ListDropdownProps) => {
|
|||||||
return (
|
return (
|
||||||
<Listbox value={selected} onChange={setSelected}>
|
<Listbox value={selected} onChange={setSelected}>
|
||||||
<div className="relative mt-1">
|
<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="block truncate">{Object.keys(selected)[0]}</span>
|
||||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
<ChevronDownIcon aria-hidden="true" />
|
<ChevronDownIcon aria-hidden="true" />
|
||||||
@@ -28,12 +28,17 @@ export const ListDropdown = ({ list, onSelect }: ListDropdownProps) => {
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
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) => (
|
{list.map((item, itemIdx) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key={itemIdx}
|
key={itemIdx}
|
||||||
className={({ active }) =>
|
className={({ active, selected }) =>
|
||||||
`relative cursor-pointer select-none py-2 pl-10 pr-4 text-white ${active ? "text-white" : "text-gray-400"
|
`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}
|
value={item}
|
||||||
|
|||||||
@@ -49,11 +49,11 @@ export const LoadingBar = ({
|
|||||||
propsInnerWrapperClassName,
|
propsInnerWrapperClassName,
|
||||||
);
|
);
|
||||||
const progressBackgroundClassName = twMerge(
|
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,
|
propsProgressBackgroundClassName,
|
||||||
);
|
);
|
||||||
const progressClassName = twMerge(
|
const progressClassName = twMerge(
|
||||||
"h-2.5 rounded-full transition-all duration-1000",
|
"h-2.5 rounded-none transition-all duration-1000",
|
||||||
getVariantClassNames(variant),
|
getVariantClassNames(variant),
|
||||||
propsProgressClassName,
|
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;
|
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">
|
{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
|
<ArrowLeftIcon size={16} aria-hidden="true" /> Back
|
||||||
</button>
|
</button>
|
||||||
</div>}
|
</div>}
|
||||||
{challenge ? <>
|
{challenge ? <>
|
||||||
<p className="mb-3 text-sm text-white/70">Scan with your phone or approve in your browser.</p>
|
<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" />
|
<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-widest">{challenge.confirmation_code.slice(0, 4)}-{challenge.confirmation_code.slice(4)}</p>
|
<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-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>
|
<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="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>
|
<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="text-sm text-white/70" role="status">Preparing login…</p> : <div className="flex flex-col gap-2">
|
</> : 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="rounded-lg bg-white px-4 py-3 font-medium text-black" onClick={() => start(true)}>Login with Website</button>
|
<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="rounded-lg border border-white/30 px-4 py-2" onClick={() => start(false)}>Scan to Login</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>
|
<p className="text-xs text-white/50">Use Google or any account already signed in on the website.</p>
|
||||||
</div>}
|
</div>}
|
||||||
{message && <p className="mt-3 text-sm text-white/70" role="status">{message}</p>}
|
{message && <p className="mt-3 text-sm text-white/70" role="status">{message}</p>}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
.panel {
|
.panel {
|
||||||
position: relative;
|
position: relative;
|
||||||
isolation: isolate;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -8,60 +7,44 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 56px 32px;
|
padding: 56px 32px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background: radial-gradient(ellipse at 50% 0%, rgb(94 234 212 / 9%), transparent 70%);
|
|
||||||
animation: arrive 450ms ease-out both;
|
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 {
|
.badge {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 76px;
|
width: 64px;
|
||||||
height: 76px;
|
height: 64px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-bottom: 28px;
|
margin-bottom: 28px;
|
||||||
border: 1px solid rgb(153 246 228 / 35%);
|
border: 1px solid rgb(255 255 255 / 20%);
|
||||||
border-radius: 50%;
|
color: #ffffff;
|
||||||
color: #b4f7de;
|
background: rgb(255 255 255 / 5%);
|
||||||
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%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.heading {
|
.heading {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
color: rgb(255 255 255 / 80%);
|
color: rgb(255 255 255 / 60%);
|
||||||
font-size: 18px;
|
font-family: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
font-weight: 500;
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.username {
|
.username {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 4px;
|
margin-top: 10px;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
color: #b4f7de;
|
color: #ffffff;
|
||||||
background: linear-gradient(100deg, #d4ffec, #99f6e4 55%, #c7d2fe);
|
font-family: "Archivo", ui-sans-serif, system-ui, sans-serif;
|
||||||
background-clip: text;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
letter-spacing: -0.025em;
|
letter-spacing: -0.025em;
|
||||||
line-height: 1.25;
|
line-height: 1.1;
|
||||||
|
text-transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.caption {
|
.caption {
|
||||||
@@ -72,15 +55,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes arrive {
|
@keyframes arrive {
|
||||||
from { opacity: 0; transform: translateY(8px) scale(0.98); }
|
from { opacity: 0; transform: translateY(8px); }
|
||||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes glow {
|
|
||||||
0%, 100% { opacity: 0.7; transform: scale(0.95); }
|
|
||||||
50% { opacity: 1; transform: scale(1.06); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@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 }) {
|
export function LoginSuccess({ username }: { username: string }) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.panel} role="status" aria-live="polite" aria-atomic="true">
|
<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">
|
<div className={styles.badge} aria-hidden="true">
|
||||||
<CheckIcon size={34} strokeWidth={2} />
|
<CheckIcon size={28} strokeWidth={2} />
|
||||||
</div>
|
</div>
|
||||||
<h2 className={styles.heading}>
|
<h2 className={styles.heading}>
|
||||||
Logged in as{" "}
|
Logged in as{" "}
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ interface ArtCraftSignUpProps {
|
|||||||
isLoading?: boolean;
|
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 =
|
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 = ({
|
export const ArtCraftSignUp = ({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
@@ -74,12 +75,12 @@ export const ArtCraftSignUp = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className="flex w-full flex-col gap-2"
|
className="flex w-full flex-col gap-4"
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
ref={formRef}
|
ref={formRef}
|
||||||
>
|
>
|
||||||
{localError && (
|
{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 />
|
<TriangleAlertIcon />
|
||||||
{localError}
|
{localError}
|
||||||
</div>
|
</div>
|
||||||
@@ -87,7 +88,7 @@ export const ArtCraftSignUp = ({
|
|||||||
|
|
||||||
{isSignUp ? (
|
{isSignUp ? (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-1">
|
<div className="space-y-2">
|
||||||
<label className={FIELD_LABEL}>Username</label>
|
<label className={FIELD_LABEL}>Username</label>
|
||||||
<Input
|
<Input
|
||||||
name="username"
|
name="username"
|
||||||
@@ -97,7 +98,7 @@ export const ArtCraftSignUp = ({
|
|||||||
inputClassName={FIELD_INPUT}
|
inputClassName={FIELD_INPUT}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-2">
|
||||||
<label className={FIELD_LABEL}>Email</label>
|
<label className={FIELD_LABEL}>Email</label>
|
||||||
<Input
|
<Input
|
||||||
name="email"
|
name="email"
|
||||||
@@ -110,7 +111,7 @@ export const ArtCraftSignUp = ({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-1">
|
<div className="space-y-2">
|
||||||
<label className={FIELD_LABEL}>Email or Username</label>
|
<label className={FIELD_LABEL}>Email or Username</label>
|
||||||
<Input
|
<Input
|
||||||
name="usernameOrEmail"
|
name="usernameOrEmail"
|
||||||
@@ -122,7 +123,7 @@ export const ArtCraftSignUp = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-2">
|
||||||
<label className={FIELD_LABEL}>Password</label>
|
<label className={FIELD_LABEL}>Password</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
@@ -145,7 +146,7 @@ export const ArtCraftSignUp = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isSignUp && (
|
{isSignUp && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-2">
|
||||||
<label className={FIELD_LABEL}>Confirm Password</label>
|
<label className={FIELD_LABEL}>Confirm Password</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
@@ -168,14 +169,14 @@ export const ArtCraftSignUp = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="pt-4">
|
<div className="pt-2">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isLoading}
|
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 ? (
|
{isLoading ? (
|
||||||
<LoaderCircleIcon className="animate-spin" />
|
<LoaderCircleIcon className="animate-spin" />
|
||||||
) : isSignUp ? (
|
) : isSignUp ? (
|
||||||
"Sign up"
|
"Sign up"
|
||||||
) : (
|
) : (
|
||||||
@@ -184,7 +185,7 @@ export const ArtCraftSignUp = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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?"}{" "}
|
{isSignUp ? "Already have an account?" : "Don't have an account?"}{" "}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ import { useLoginModalStore } from "./useLoginModalStore";
|
|||||||
const DEFAULT_SHOWCASE_VIDEO =
|
const DEFAULT_SHOWCASE_VIDEO =
|
||||||
"https://frontend-cdn.fakeyou.com/videos/knight-video.mp4";
|
"https://frontend-cdn.fakeyou.com/videos/knight-video.mp4";
|
||||||
const LOGIN_SUCCESS_DURATION_MS = 3000;
|
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 {
|
interface LoginModalProps {
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
@@ -145,10 +151,10 @@ export function LoginModal({
|
|||||||
if (showSuccess) {
|
if (showSuccess) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 flex-col items-center justify-center px-8 py-16 text-center">
|
<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!
|
Thank you for signing in!
|
||||||
</h2>
|
</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.
|
You're all set to start creating amazing content.
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
@@ -156,7 +162,7 @@ export function LoginModal({
|
|||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
icon={ArrowRightIcon}
|
icon={ArrowRightIcon}
|
||||||
iconFlip={true}
|
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
|
Get Started
|
||||||
</Button>
|
</Button>
|
||||||
@@ -167,16 +173,17 @@ export function LoginModal({
|
|||||||
// Discord
|
// Discord
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 flex-col items-center justify-center px-8 py-16 text-center">
|
<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">
|
||||||
Join Our Community
|
Join our <span className="font-serif-italic">community.</span>
|
||||||
</h2>
|
</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
|
Connect with other creators, share your work, and get the latest
|
||||||
updates in our Discord community.
|
updates in our Discord community.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-3">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
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={() => {
|
onClick={() => {
|
||||||
setShowDiscord(false);
|
setShowDiscord(false);
|
||||||
setShowSuccess(true);
|
setShowSuccess(true);
|
||||||
@@ -188,7 +195,7 @@ export function LoginModal({
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={handleDiscordJoin}
|
onClick={handleDiscordJoin}
|
||||||
icon={DiscordIcon}
|
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
|
Join Discord
|
||||||
</Button>
|
</Button>
|
||||||
@@ -222,59 +229,84 @@ export function LoginModal({
|
|||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<div
|
<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()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{loggedInUsername !== null ? (
|
<div
|
||||||
<LoginSuccess username={loggedInUsername} />
|
className={`relative flex w-full overflow-hidden border border-white/20 bg-[#1e1f22] text-white ${loggedInUsername !== null ? "" : "lg:min-h-[560px]"}`}
|
||||||
) : inOnboarding ? (
|
>
|
||||||
renderOnboarding()
|
{loggedInUsername !== null ? (
|
||||||
) : (
|
<LoginSuccess username={loggedInUsername} />
|
||||||
<>
|
) : inOnboarding ? (
|
||||||
{/* ── Form pane ── (no dismiss control — login is required) */}
|
renderOnboarding()
|
||||||
<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">
|
{/* ── Form pane ── (no dismiss control — login is required) */}
|
||||||
<div className="mb-8 text-center">
|
<div className="relative flex w-full flex-col lg:w-1/2">
|
||||||
<img
|
<div className="flex flex-1 flex-col justify-center px-6 py-10 sm:px-10 sm:py-12">
|
||||||
src="/resources/logo/artcraft-icon.png"
|
<div className="w-full">
|
||||||
alt="ArtCraft"
|
<div className="mb-8 text-left">
|
||||||
className="pointer-events-none mx-auto mb-6 h-12 w-auto select-none"
|
<img
|
||||||
draggable={false}
|
src="/resources/logo/artcraft-icon.png"
|
||||||
/>
|
alt="ArtCraft"
|
||||||
<h1 className="mb-2 text-2xl font-semibold">
|
className="pointer-events-none mb-8 h-8 w-auto select-none"
|
||||||
{isSignUp ? "Create your account" : "Welcome back"}
|
draggable={false}
|
||||||
</h1>
|
/>
|
||||||
<p className="text-sm text-white/60">
|
<h1 className="mb-3 text-balance font-display text-3xl leading-[1.05] tracking-tight sm:text-4xl">
|
||||||
{isSignUp
|
{isSignUp ? (
|
||||||
? "Sign up to start creating with ArtCraft"
|
"Create your account"
|
||||||
: "Log in to your account"}
|
) : (
|
||||||
</p>
|
<>
|
||||||
</div>
|
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} />}
|
{!isSignUp && <DesktopLoginBridge onActiveChange={setIsChallengeActive} onStart={() => { authGeneration.current += 1; }} onSuccess={handleLoginSuccess} />}
|
||||||
{!isChallengeActive && <ArtCraftSignUp
|
{!isSignUp && !isChallengeActive && (
|
||||||
onSubmit={handleAuthSubmit}
|
<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">
|
||||||
isSignUp={isSignUp}
|
<span className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/40">
|
||||||
onToggleMode={() => setIsSignUp((prev) => !prev)}
|
or
|
||||||
errorMessage={errorMessage}
|
</span>
|
||||||
isLoading={isLoading}
|
</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>
|
</div>
|
||||||
|
|
||||||
<div className="px-8 pb-8 text-center text-xs text-white/20">
|
{/* ── Showcase pane (desktop only) ── */}
|
||||||
© {new Date().getFullYear()} ArtCraft. All rights
|
<div className="relative hidden border-l border-white/15 lg:block lg:w-1/2">
|
||||||
reserved.
|
<LoginShowcase videoUrl={videoUrl} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
|
)}
|
||||||
{/* ── Showcase pane (desktop only) ── */}
|
</div>
|
||||||
<div className="relative hidden lg:block lg:w-1/2">
|
{FRAME_CORNERS.map((corner) => (
|
||||||
<LoginShowcase videoUrl={videoUrl} />
|
<span
|
||||||
</div>
|
key={corner}
|
||||||
</>
|
aria-hidden="true"
|
||||||
)}
|
className={`frame-corner-mark ${corner}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</TransitionChild>
|
</TransitionChild>
|
||||||
</div>
|
</div>
|
||||||
@@ -287,7 +319,7 @@ export function LoginModal({
|
|||||||
// with a legibility gradient + caption. Mirrors the webapp auth-showcase.
|
// with a legibility gradient + caption. Mirrors the webapp auth-showcase.
|
||||||
function LoginShowcase({ videoUrl }: { videoUrl: string }) {
|
function LoginShowcase({ videoUrl }: { videoUrl: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-2 overflow-hidden rounded-2xl bg-black">
|
<div className="absolute inset-0 overflow-hidden bg-[#1e1f22]">
|
||||||
<video
|
<video
|
||||||
src={videoUrl}
|
src={videoUrl}
|
||||||
autoPlay
|
autoPlay
|
||||||
@@ -304,7 +336,7 @@ function LoginShowcase({ videoUrl }: { videoUrl: string }) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 p-8">
|
<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
|
One of the cheapest
|
||||||
</p>
|
</p>
|
||||||
<h2 className="text-2xl font-bold leading-tight">
|
<h2 className="text-2xl font-bold leading-tight">
|
||||||
|
|||||||
@@ -87,14 +87,14 @@ export const MenuIconSelector: React.FC<MenuIconSelectorProps> = ({
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
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 }}
|
style={{ minWidth: 0 }}
|
||||||
onMouseLeave={() => setHoveredIndex(-1)}
|
onMouseLeave={() => setHoveredIndex(-1)}
|
||||||
>
|
>
|
||||||
{/* Active tab background */}
|
{/* Active tab background */}
|
||||||
{selectedIndex >= 0 && (
|
{selectedIndex >= 0 && (
|
||||||
<div
|
<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={{
|
style={{
|
||||||
left: activeStyle.left,
|
left: activeStyle.left,
|
||||||
width: activeStyle.width,
|
width: activeStyle.width,
|
||||||
@@ -106,7 +106,7 @@ export const MenuIconSelector: React.FC<MenuIconSelectorProps> = ({
|
|||||||
{/* Hover background (only if hovering a different tab) */}
|
{/* Hover background (only if hovering a different tab) */}
|
||||||
{hoveredIndex !== -1 && hoveredIndex !== selectedIndex && (
|
{hoveredIndex !== -1 && hoveredIndex !== selectedIndex && (
|
||||||
<div
|
<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={{
|
style={{
|
||||||
left: hoverStyle.left,
|
left: hoverStyle.left,
|
||||||
width: hoverStyle.width,
|
width: hoverStyle.width,
|
||||||
@@ -136,10 +136,7 @@ export const MenuIconSelector: React.FC<MenuIconSelectorProps> = ({
|
|||||||
closeOnClick={true}
|
closeOnClick={true}
|
||||||
interactive={interactive}
|
interactive={interactive}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={twMerge(
|
className="font-sans text-xs font-medium normal-case tracking-normal"
|
||||||
"text-sm font-semibold",
|
|
||||||
item.large && "text-md",
|
|
||||||
)}
|
|
||||||
imageSrc={imageSrc || undefined}
|
imageSrc={imageSrc || undefined}
|
||||||
description={description || undefined}
|
description={description || undefined}
|
||||||
>
|
>
|
||||||
@@ -151,7 +148,10 @@ export const MenuIconSelector: React.FC<MenuIconSelectorProps> = ({
|
|||||||
onClick={() => !disabled && onMenuChange(item.id)}
|
onClick={() => !disabled && onMenuChange(item.id)}
|
||||||
onMouseEnter={() => setHoveredIndex(idx)}
|
onMouseEnter={() => setHoveredIndex(idx)}
|
||||||
className={twMerge(
|
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" : "",
|
disabled ? "cursor-not-allowed opacity-60" : "",
|
||||||
)}
|
)}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
|||||||
@@ -153,8 +153,7 @@ const AnimatedBackdrop = ({
|
|||||||
style={{
|
style={{
|
||||||
opacity: styles.opacity,
|
opacity: styles.opacity,
|
||||||
pointerEvents: "none",
|
pointerEvents: "none",
|
||||||
background:
|
background: "rgba(0, 0, 0, 0.7)",
|
||||||
"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)",
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Dialog.Overlay>
|
</Dialog.Overlay>
|
||||||
@@ -194,19 +193,13 @@ const ExpandButton = ({ className, size = "md" }: ExpandButtonProps) => {
|
|||||||
aria-label={expanded ? "Restore modal size" : "Expand modal"}
|
aria-label={expanded ? "Restore modal size" : "Expand modal"}
|
||||||
onClick={toggleExpanded}
|
onClick={toggleExpanded}
|
||||||
className={twMerge(
|
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],
|
sizeClasses[size],
|
||||||
"relative z-[70]",
|
"relative z-[70]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<DynamicIcon
|
<DynamicIcon icon={expanded ? Minimize2Icon : Maximize2Icon} />
|
||||||
icon={
|
|
||||||
expanded
|
|
||||||
? Minimize2Icon
|
|
||||||
: Maximize2Icon
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1083,7 +1076,7 @@ export const Modal = ({
|
|||||||
>
|
>
|
||||||
<animated.div
|
<animated.div
|
||||||
className={twMerge(
|
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" : "",
|
childPadding && !expanded ? "p-4" : "",
|
||||||
className,
|
className,
|
||||||
"!transition-none", // Always disable CSS transitions for spring animations
|
"!transition-none", // Always disable CSS transitions for spring animations
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
getCreatorIcon,
|
getCreatorIcon,
|
||||||
getModelFamilyName,
|
getModelFamilyName,
|
||||||
} from "@storyteller/model-list";
|
} from "@storyteller/model-list";
|
||||||
import { ChevronUpIcon, CircleCheckIcon } from "lucide-react";
|
import { ChevronUpIcon, CheckIcon } from "lucide-react";
|
||||||
import { GenerationProvider } from "@storyteller/api-enums";
|
import { GenerationProvider } from "@storyteller/api-enums";
|
||||||
import { Tooltip } from "@storyteller/ui-tooltip";
|
import { Tooltip } from "@storyteller/ui-tooltip";
|
||||||
import { defaultModelForPage } from "./defaultModelForPage";
|
import { defaultModelForPage } from "./defaultModelForPage";
|
||||||
@@ -107,7 +107,7 @@ function ProviderTooltipContent({
|
|||||||
onFinished?.();
|
onFinished?.();
|
||||||
}}
|
}}
|
||||||
type="button"
|
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
|
selectedProvider === p
|
||||||
? "bg-ui-controls/70 border-l-4 border-primary"
|
? "bg-ui-controls/70 border-l-4 border-primary"
|
||||||
: "hover:bg-ui-controls/50"
|
: "hover:bg-ui-controls/50"
|
||||||
@@ -118,8 +118,8 @@ function ProviderTooltipContent({
|
|||||||
{getProviderDisplayName(p)}
|
{getProviderDisplayName(p)}
|
||||||
</span>
|
</span>
|
||||||
{selectedProvider === p && (
|
{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">
|
<span className="ml-2 flex h-5 w-5 shrink-0 items-center justify-center text-white">
|
||||||
<CircleCheckIcon />
|
<CheckIcon aria-hidden="true" className="h-4 w-4" strokeWidth={2.5} />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
@@ -229,7 +229,7 @@ export function ClassyModelSelector({
|
|||||||
: undefined;
|
: undefined;
|
||||||
const iconProvider = prov ?? allowedProviders[0];
|
const iconProvider = prov ?? allowedProviders[0];
|
||||||
return iconProvider ? (
|
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">
|
<span className="text-base-fg/70 group-hover:text-base-fg/90 text-lg">
|
||||||
{getProviderIcon(iconProvider)}
|
{getProviderIcon(iconProvider)}
|
||||||
</span>
|
</span>
|
||||||
@@ -241,7 +241,7 @@ export function ClassyModelSelector({
|
|||||||
isSameModel(item.model as Model | undefined, selectedModel) &&
|
isSameModel(item.model as Model | undefined, selectedModel) &&
|
||||||
selectedProvider &&
|
selectedProvider &&
|
||||||
hasMultipleProviders ? (
|
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">
|
<span className="text-base-fg/70 group-hover:text-base-fg/90 text-lg">
|
||||||
{getProviderIcon(selectedProvider)}
|
{getProviderIcon(selectedProvider)}
|
||||||
</span>
|
</span>
|
||||||
@@ -313,7 +313,7 @@ export function ClassyModelSelector({
|
|||||||
mode="hoverSelect"
|
mode="hoverSelect"
|
||||||
maxListHeight={maxListHeight}
|
maxListHeight={maxListHeight}
|
||||||
{...popoverProps}
|
{...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) => {
|
renderTrigger={(selectedItem) => {
|
||||||
const modelTitle =
|
const modelTitle =
|
||||||
selectedItem?.label ?? selectedModel?.selectorName ?? "";
|
selectedItem?.label ?? selectedModel?.selectorName ?? "";
|
||||||
@@ -340,9 +340,7 @@ export function ClassyModelSelector({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ChevronUpIcon
|
<ChevronUpIcon className="text-base text-base-fg/70 self-center" />
|
||||||
|
|
||||||
className="text-base text-base-fg/70 self-center" />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -81,9 +81,11 @@ export const BoardGrid = ({
|
|||||||
const [viewportH, setViewportH] = useState(0);
|
const [viewportH, setViewportH] = useState(0);
|
||||||
const [marquee, setMarquee] = useState<Marquee | null>(null);
|
const [marquee, setMarquee] = useState<Marquee | null>(null);
|
||||||
const [focusedId, setFocusedId] = useState<string | null>(null);
|
const [focusedId, setFocusedId] = useState<string | null>(null);
|
||||||
const marqueeRef = useRef<{ active: boolean; moved: boolean; additive: boolean }>(
|
const marqueeRef = useRef<{
|
||||||
{ active: false, moved: false, additive: false },
|
active: boolean;
|
||||||
);
|
moved: boolean;
|
||||||
|
additive: boolean;
|
||||||
|
}>({ active: false, moved: false, additive: false });
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const el = scrollRef.current;
|
const el = scrollRef.current;
|
||||||
@@ -103,7 +105,10 @@ export const BoardGrid = ({
|
|||||||
const groups = useMemo<SectionGroup[] | null>(() => {
|
const groups = useMemo<SectionGroup[] | null>(() => {
|
||||||
if (!sections || sections.length === 0) return null;
|
if (!sections || sections.length === 0) return null;
|
||||||
const known = new Set(sections.map((s) => s.id));
|
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) {
|
for (const it of items) {
|
||||||
const key = it.sectionId && known.has(it.sectionId) ? it.sectionId : null;
|
const key = it.sectionId && known.has(it.sectionId) ? it.sectionId : null;
|
||||||
const entry = { id: it.id, aspect: it.aspect };
|
const entry = { id: it.id, aspect: it.aspect };
|
||||||
@@ -119,7 +124,10 @@ export const BoardGrid = ({
|
|||||||
}));
|
}));
|
||||||
const ungrouped = bySection.get(null) ?? [];
|
const ungrouped = bySection.get(null) ?? [];
|
||||||
return ungrouped.length > 0
|
return ungrouped.length > 0
|
||||||
? [{ id: null, name: "Ungrouped", collapsed: false, items: ungrouped }, ...named]
|
? [
|
||||||
|
{ id: null, name: "Ungrouped", collapsed: false, items: ungrouped },
|
||||||
|
...named,
|
||||||
|
]
|
||||||
: named;
|
: named;
|
||||||
}, [items, sections]);
|
}, [items, sections]);
|
||||||
|
|
||||||
@@ -313,7 +321,7 @@ export const BoardGrid = ({
|
|||||||
})}
|
})}
|
||||||
{marquee && marqueeRef.current.moved && (
|
{marquee && marqueeRef.current.moved && (
|
||||||
<div
|
<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={{
|
style={{
|
||||||
left: Math.min(marquee.x0, marquee.x1),
|
left: Math.min(marquee.x0, marquee.x1),
|
||||||
top: Math.min(marquee.y0, marquee.y1),
|
top: Math.min(marquee.y0, marquee.y1),
|
||||||
|
|||||||
@@ -488,20 +488,20 @@ export const Stage3DBody = ({
|
|||||||
className="absolute left-0 top-0 w-full"
|
className="absolute left-0 top-0 w-full"
|
||||||
onClick={handleOverlayClick}
|
onClick={handleOverlayClick}
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-2 gap-x-4 gap-y-1 2xl:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)]">
|
||||||
<div className="flex flex-col items-start gap-2">
|
<div className="min-w-0 flex flex-col items-start gap-2">
|
||||||
{!isRecord && <ControlsTopButtons />}
|
{!isRecord && <ControlsTopButtons />}
|
||||||
{!isRecord && topBarStartSlot && (
|
{!isRecord && topBarStartSlot && (
|
||||||
<div className="pl-3">{topBarStartSlot}</div>
|
<div className="pl-3">{topBarStartSlot}</div>
|
||||||
)}
|
)}
|
||||||
</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 />
|
<SceneModePill />
|
||||||
{!isRecord && (
|
{!isRecord && (
|
||||||
<Controls3D showImageTo3DButton={showImageTo3DButton} />
|
<Controls3D showImageTo3DButton={showImageTo3DButton} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</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 +
|
{/* Camera cluster: which camera drives the viewport +
|
||||||
the render-frame aspect ratio. */}
|
the render-frame aspect ratio. */}
|
||||||
{!isRecord && <CameraStatusPill />}
|
{!isRecord && <CameraStatusPill />}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export const AnonHintChip = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleClick}
|
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" />
|
<LogInIcon className="opacity-70" />
|
||||||
Sign up to save
|
Sign up to save
|
||||||
|
|||||||
+2
-1
@@ -6,6 +6,7 @@ import { CameraAspectRatio } from "../../enums";
|
|||||||
import { setCameraAspect } from "../../actions";
|
import { setCameraAspect } from "../../actions";
|
||||||
import { usePageSceneStore } from "../../PageSceneStore";
|
import { usePageSceneStore } from "../../PageSceneStore";
|
||||||
import { EngineContext } from "../../contexts/EngineContext";
|
import { EngineContext } from "../../contexts/EngineContext";
|
||||||
|
import { TOOLBAR_BUTTON_CLASS_NAME } from "../toolbarStyles";
|
||||||
|
|
||||||
// Render-frame aspect ratio picker. Same popover pattern as the prompt
|
// Render-frame aspect ratio picker. Same popover pattern as the prompt
|
||||||
// boxes' AspectRatioPicker, but driving the 3D editor's camera letterbox
|
// boxes' AspectRatioPicker, but driving the 3D editor's camera letterbox
|
||||||
@@ -52,7 +53,7 @@ export const AspectRatioMenu = () => {
|
|||||||
position="bottom"
|
position="bottom"
|
||||||
panelTitle="Aspect Ratio"
|
panelTitle="Aspect Ratio"
|
||||||
showIconsInList
|
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} />}
|
triggerIcon={<AspectRatioIcon ratio={current.proportions} />}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ export const AnimationsModal = () => {
|
|||||||
<Modal
|
<Modal
|
||||||
isOpen={animationsModalVisible}
|
isOpen={animationsModalVisible}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
className="relative h-[640px] max-w-4xl"
|
className="relative h-[640px] max-w-4xl bg-ui-controls"
|
||||||
childPadding={false}
|
childPadding={false}
|
||||||
showClose={false}
|
showClose={false}
|
||||||
backdropClassName="bg-transparent"
|
backdropClassName="bg-transparent"
|
||||||
@@ -228,8 +228,8 @@ export const AnimationsModal = () => {
|
|||||||
</Modal.DragHandle>
|
</Modal.DragHandle>
|
||||||
<div className="grid h-full grid-cols-12 gap-3">
|
<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="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">
|
<div className="flex h-10 items-center justify-between gap-2.5">
|
||||||
<h2 className="text-[18px] font-semibold opacity-80">
|
<h2 className="font-display text-lg tracking-tight text-white">
|
||||||
Animations
|
Animations
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -240,9 +240,9 @@ export const AnimationsModal = () => {
|
|||||||
key={tab.id}
|
key={tab.id}
|
||||||
variant={activeTab === tab.id ? "primary" : "secondary"}
|
variant={activeTab === tab.id ? "primary" : "secondary"}
|
||||||
className={twMerge(
|
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 &&
|
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)}
|
onClick={() => setActiveTab(tab.id)}
|
||||||
>
|
>
|
||||||
@@ -259,7 +259,7 @@ export const AnimationsModal = () => {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
icon={ArrowUpFromLineIcon}
|
icon={ArrowUpFromLineIcon}
|
||||||
iconClassName="opacity-70"
|
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}
|
onClick={handleOpenUpload}
|
||||||
>
|
>
|
||||||
Upload animation
|
Upload animation
|
||||||
@@ -281,13 +281,13 @@ export const AnimationsModal = () => {
|
|||||||
ref={searchInputRef}
|
ref={searchInputRef}
|
||||||
placeholder="Search animations"
|
placeholder="Search animations"
|
||||||
className="relative z-[51] grow"
|
className="relative z-[51] grow"
|
||||||
inputClassName="pr-2.5"
|
inputClassName="bg-white/5 pr-10 placeholder-white/40"
|
||||||
icon={SearchIcon}
|
icon={SearchIcon}
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||||
setSearchTerm(e.target.value)
|
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 && (
|
{searchTerm && (
|
||||||
<CloseButton
|
<CloseButton
|
||||||
|
|||||||
@@ -56,12 +56,12 @@ const AllTabSection = ({
|
|||||||
<div className="mb-2 flex items-center justify-between">
|
<div className="mb-2 flex items-center justify-between">
|
||||||
<h3 className="text-md ml-2 font-semibold opacity-90">{label}</h3>
|
<h3 className="text-md ml-2 font-semibold opacity-90">{label}</h3>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="ghost"
|
||||||
className="mr-3 flex items-center gap-1 px-2 py-1 text-xs"
|
className="mr-3 h-6 gap-1 px-1.5 py-0 text-[10px] tracking-[0.08em] leading-none"
|
||||||
onClick={onViewAll}
|
onClick={onViewAll}
|
||||||
>
|
>
|
||||||
View all
|
View all
|
||||||
<ChevronRightIcon className="text-xs opacity-70" />
|
<ChevronRightIcon className="h-3 w-3 shrink-0 opacity-70" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[170px]">
|
<div className="h-[170px]">
|
||||||
@@ -487,7 +487,7 @@ export const AssetModal = () => {
|
|||||||
<Modal
|
<Modal
|
||||||
isOpen={assetModalVisible}
|
isOpen={assetModalVisible}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
className="relative h-[640px] max-w-4xl"
|
className="relative h-[640px] max-w-4xl bg-ui-controls"
|
||||||
childPadding={false}
|
childPadding={false}
|
||||||
showClose={false}
|
showClose={false}
|
||||||
backdropClassName="bg-transparent"
|
backdropClassName="bg-transparent"
|
||||||
@@ -507,8 +507,8 @@ export const AssetModal = () => {
|
|||||||
</Modal.DragHandle>
|
</Modal.DragHandle>
|
||||||
<div className="grid h-full grid-cols-12 gap-3">
|
<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="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">
|
<div className="flex h-10 items-center justify-between gap-2.5">
|
||||||
<h2 className="text-[18px] font-semibold opacity-80">
|
<h2 className="font-display text-lg tracking-tight text-white">
|
||||||
ArtCraft Presets
|
ArtCraft Presets
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -519,9 +519,9 @@ export const AssetModal = () => {
|
|||||||
key={tab.id}
|
key={tab.id}
|
||||||
variant={activeAssetTab === tab.id ? "primary" : "secondary"}
|
variant={activeAssetTab === tab.id ? "primary" : "secondary"}
|
||||||
className={twMerge(
|
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 &&
|
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)}
|
onClick={() => setActiveAssetTab(tab.id)}
|
||||||
>
|
>
|
||||||
@@ -551,13 +551,13 @@ export const AssetModal = () => {
|
|||||||
ref={searchInputRef}
|
ref={searchInputRef}
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
className="relative z-[51] grow"
|
className="relative z-[51] grow"
|
||||||
inputClassName="pr-2.5"
|
inputClassName="bg-white/5 pr-10 placeholder-white/40"
|
||||||
icon={SearchIcon}
|
icon={SearchIcon}
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||||
setSearchTerm(e.target.value)
|
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 && (
|
{searchTerm && (
|
||||||
<CloseButton
|
<CloseButton
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export const ItemElement = ({ item }: Props) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<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)}
|
onPointerDown={(event) => dragAndDrop.onPointerDown(event, item, editor)}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
style={{ cursor: "grab", pointerEvents: "auto" }}
|
style={{ cursor: "grab", pointerEvents: "auto" }}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user