Spaces:
Running
Running
adding light mode
Browse files- app/api/merge/route.ts +27 -18
- app/api/process/route.ts +2 -3
- app/editor/nodes.tsx +54 -12
- app/editor/page.tsx +76 -16
- app/globals.css +47 -1
app/api/merge/route.ts
CHANGED
|
@@ -60,25 +60,34 @@ export async function POST(req: NextRequest) {
|
|
| 60 |
let prompt = body.prompt;
|
| 61 |
|
| 62 |
if (!prompt) {
|
| 63 |
-
prompt = `MERGE TASK:
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
1.
|
| 67 |
-
2.
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
-
|
| 76 |
-
-
|
| 77 |
-
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
} else {
|
| 80 |
-
// Even with custom prompt, append
|
| 81 |
-
const enforcement = `\n\nIMPORTANT:
|
| 82 |
prompt = `${prompt}${enforcement}`;
|
| 83 |
}
|
| 84 |
|
|
|
|
| 60 |
let prompt = body.prompt;
|
| 61 |
|
| 62 |
if (!prompt) {
|
| 63 |
+
prompt = `MERGE TASK: Create a natural, cohesive group photo combining ALL subjects from ${imgs.length} provided images.
|
| 64 |
+
|
| 65 |
+
CRITICAL REQUIREMENTS:
|
| 66 |
+
1. Extract ALL people/subjects from EACH image exactly as they appear
|
| 67 |
+
2. Place them together in a SINGLE UNIFIED SCENE with:
|
| 68 |
+
- Consistent lighting direction and color temperature
|
| 69 |
+
- Matching shadows and ambient lighting
|
| 70 |
+
- Proper scale relationships (realistic relative sizes)
|
| 71 |
+
- Natural spacing as if they were photographed together
|
| 72 |
+
- Shared environment/background that looks cohesive
|
| 73 |
+
|
| 74 |
+
3. Composition guidelines:
|
| 75 |
+
- Arrange subjects at similar depth (not one far behind another)
|
| 76 |
+
- Use natural group photo positioning (slight overlap is ok)
|
| 77 |
+
- Ensure all faces are clearly visible
|
| 78 |
+
- Create visual balance in the composition
|
| 79 |
+
- Apply consistent color grading across all subjects
|
| 80 |
+
|
| 81 |
+
4. Environmental unity:
|
| 82 |
+
- Use a single, coherent background for all subjects
|
| 83 |
+
- Match the perspective as if taken with one camera
|
| 84 |
+
- Ensure ground plane continuity (all standing on same level)
|
| 85 |
+
- Apply consistent atmospheric effects (if any)
|
| 86 |
+
|
| 87 |
+
The result should look like all subjects were photographed together in the same place at the same time, NOT like separate images placed side by side.`;
|
| 88 |
} else {
|
| 89 |
+
// Even with custom prompt, append cohesion requirements
|
| 90 |
+
const enforcement = `\n\nIMPORTANT: Create a COHESIVE group photo where all subjects appear to be in the same scene with consistent lighting, scale, and environment. The result should look naturally photographed together, not composited.`;
|
| 91 |
prompt = `${prompt}${enforcement}`;
|
| 92 |
}
|
| 93 |
|
app/api/process/route.ts
CHANGED
|
@@ -107,7 +107,7 @@ export async function POST(req: NextRequest) {
|
|
| 107 |
// Style blending
|
| 108 |
if (params.styleImage) {
|
| 109 |
const strength = params.blendStrength || 50;
|
| 110 |
-
prompts.push(`Apply artistic style
|
| 111 |
const styleRef = await toInlineDataFromAny(params.styleImage);
|
| 112 |
if (styleRef) referenceParts.push({ inlineData: styleRef });
|
| 113 |
}
|
|
@@ -119,7 +119,7 @@ export async function POST(req: NextRequest) {
|
|
| 119 |
|
| 120 |
// Camera settings
|
| 121 |
if (params.focalLength || params.aperture || params.shutterSpeed || params.whiteBalance || params.angle ||
|
| 122 |
-
params.iso || params.filmStyle || params.lighting || params.bokeh || params.composition
|
| 123 |
const cameraSettings: string[] = [];
|
| 124 |
if (params.focalLength) {
|
| 125 |
if (params.focalLength === "8mm fisheye") {
|
|
@@ -137,7 +137,6 @@ export async function POST(req: NextRequest) {
|
|
| 137 |
if (params.lighting) cameraSettings.push(`Lighting: ${params.lighting}`);
|
| 138 |
if (params.bokeh) cameraSettings.push(`Bokeh effect: ${params.bokeh}`);
|
| 139 |
if (params.composition) cameraSettings.push(`Composition: ${params.composition}`);
|
| 140 |
-
if (params.aspectRatio) cameraSettings.push(`Aspect Ratio: ${params.aspectRatio}`);
|
| 141 |
|
| 142 |
if (cameraSettings.length > 0) {
|
| 143 |
prompts.push(`Apply professional photography settings: ${cameraSettings.join(", ")}`);
|
|
|
|
| 107 |
// Style blending
|
| 108 |
if (params.styleImage) {
|
| 109 |
const strength = params.blendStrength || 50;
|
| 110 |
+
prompts.push(`Apply artistic style blending using the provided style reference image (attached below) at ${strength}% strength.`);
|
| 111 |
const styleRef = await toInlineDataFromAny(params.styleImage);
|
| 112 |
if (styleRef) referenceParts.push({ inlineData: styleRef });
|
| 113 |
}
|
|
|
|
| 119 |
|
| 120 |
// Camera settings
|
| 121 |
if (params.focalLength || params.aperture || params.shutterSpeed || params.whiteBalance || params.angle ||
|
| 122 |
+
params.iso || params.filmStyle || params.lighting || params.bokeh || params.composition) {
|
| 123 |
const cameraSettings: string[] = [];
|
| 124 |
if (params.focalLength) {
|
| 125 |
if (params.focalLength === "8mm fisheye") {
|
|
|
|
| 137 |
if (params.lighting) cameraSettings.push(`Lighting: ${params.lighting}`);
|
| 138 |
if (params.bokeh) cameraSettings.push(`Bokeh effect: ${params.bokeh}`);
|
| 139 |
if (params.composition) cameraSettings.push(`Composition: ${params.composition}`);
|
|
|
|
| 140 |
|
| 141 |
if (cameraSettings.length > 0) {
|
| 142 |
prompts.push(`Apply professional photography settings: ${cameraSettings.join(", ")}`);
|
app/editor/nodes.tsx
CHANGED
|
@@ -180,8 +180,15 @@ export function BackgroundNodeView({
|
|
| 180 |
<Button
|
| 181 |
variant="ghost"
|
| 182 |
size="icon"
|
| 183 |
-
className="text-destructive"
|
| 184 |
-
onClick={() =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
title="Delete node"
|
| 186 |
aria-label="Delete node"
|
| 187 |
>
|
|
@@ -358,8 +365,15 @@ export function ClothesNodeView({ node, onDelete, onUpdate, onStartConnection, o
|
|
| 358 |
<Button
|
| 359 |
variant="ghost"
|
| 360 |
size="icon"
|
| 361 |
-
className="text-destructive"
|
| 362 |
-
onClick={() =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
title="Delete node"
|
| 364 |
aria-label="Delete node"
|
| 365 |
>
|
|
@@ -487,8 +501,15 @@ export function AgeNodeView({ node, onDelete, onUpdate, onStartConnection, onEnd
|
|
| 487 |
<Button
|
| 488 |
variant="ghost"
|
| 489 |
size="icon"
|
| 490 |
-
className="text-destructive"
|
| 491 |
-
onClick={() =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
title="Delete node"
|
| 493 |
aria-label="Delete node"
|
| 494 |
>
|
|
@@ -576,8 +597,15 @@ export function CameraNodeView({ node, onDelete, onUpdate, onStartConnection, on
|
|
| 576 |
<Button
|
| 577 |
variant="ghost"
|
| 578 |
size="icon"
|
| 579 |
-
className="text-destructive"
|
| 580 |
-
onClick={() =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 581 |
title="Delete node"
|
| 582 |
aria-label="Delete node"
|
| 583 |
>
|
|
@@ -771,8 +799,15 @@ export function FaceNodeView({ node, onDelete, onUpdate, onStartConnection, onEn
|
|
| 771 |
<Button
|
| 772 |
variant="ghost"
|
| 773 |
size="icon"
|
| 774 |
-
className="text-destructive"
|
| 775 |
-
onClick={() =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 776 |
title="Delete node"
|
| 777 |
aria-label="Delete node"
|
| 778 |
>
|
|
@@ -943,8 +978,15 @@ export function BlendNodeView({ node, onDelete, onUpdate, onStartConnection, onE
|
|
| 943 |
<Button
|
| 944 |
variant="ghost"
|
| 945 |
size="icon"
|
| 946 |
-
className="text-destructive"
|
| 947 |
-
onClick={() =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 948 |
title="Delete node"
|
| 949 |
aria-label="Delete node"
|
| 950 |
>
|
|
|
|
| 180 |
<Button
|
| 181 |
variant="ghost"
|
| 182 |
size="icon"
|
| 183 |
+
className="text-destructive hover:bg-destructive/20"
|
| 184 |
+
onClick={(e) => {
|
| 185 |
+
e.stopPropagation();
|
| 186 |
+
e.preventDefault();
|
| 187 |
+
if (confirm('Delete this node?')) {
|
| 188 |
+
onDelete(node.id);
|
| 189 |
+
}
|
| 190 |
+
}}
|
| 191 |
+
onPointerDown={(e) => e.stopPropagation()}
|
| 192 |
title="Delete node"
|
| 193 |
aria-label="Delete node"
|
| 194 |
>
|
|
|
|
| 365 |
<Button
|
| 366 |
variant="ghost"
|
| 367 |
size="icon"
|
| 368 |
+
className="text-destructive hover:bg-destructive/20"
|
| 369 |
+
onClick={(e) => {
|
| 370 |
+
e.stopPropagation();
|
| 371 |
+
e.preventDefault();
|
| 372 |
+
if (confirm('Delete this node?')) {
|
| 373 |
+
onDelete(node.id);
|
| 374 |
+
}
|
| 375 |
+
}}
|
| 376 |
+
onPointerDown={(e) => e.stopPropagation()}
|
| 377 |
title="Delete node"
|
| 378 |
aria-label="Delete node"
|
| 379 |
>
|
|
|
|
| 501 |
<Button
|
| 502 |
variant="ghost"
|
| 503 |
size="icon"
|
| 504 |
+
className="text-destructive hover:bg-destructive/20"
|
| 505 |
+
onClick={(e) => {
|
| 506 |
+
e.stopPropagation();
|
| 507 |
+
e.preventDefault();
|
| 508 |
+
if (confirm('Delete this node?')) {
|
| 509 |
+
onDelete(node.id);
|
| 510 |
+
}
|
| 511 |
+
}}
|
| 512 |
+
onPointerDown={(e) => e.stopPropagation()}
|
| 513 |
title="Delete node"
|
| 514 |
aria-label="Delete node"
|
| 515 |
>
|
|
|
|
| 597 |
<Button
|
| 598 |
variant="ghost"
|
| 599 |
size="icon"
|
| 600 |
+
className="text-destructive hover:bg-destructive/20"
|
| 601 |
+
onClick={(e) => {
|
| 602 |
+
e.stopPropagation();
|
| 603 |
+
e.preventDefault();
|
| 604 |
+
if (confirm('Delete this node?')) {
|
| 605 |
+
onDelete(node.id);
|
| 606 |
+
}
|
| 607 |
+
}}
|
| 608 |
+
onPointerDown={(e) => e.stopPropagation()}
|
| 609 |
title="Delete node"
|
| 610 |
aria-label="Delete node"
|
| 611 |
>
|
|
|
|
| 799 |
<Button
|
| 800 |
variant="ghost"
|
| 801 |
size="icon"
|
| 802 |
+
className="text-destructive hover:bg-destructive/20"
|
| 803 |
+
onClick={(e) => {
|
| 804 |
+
e.stopPropagation();
|
| 805 |
+
e.preventDefault();
|
| 806 |
+
if (confirm('Delete this node?')) {
|
| 807 |
+
onDelete(node.id);
|
| 808 |
+
}
|
| 809 |
+
}}
|
| 810 |
+
onPointerDown={(e) => e.stopPropagation()}
|
| 811 |
title="Delete node"
|
| 812 |
aria-label="Delete node"
|
| 813 |
>
|
|
|
|
| 978 |
<Button
|
| 979 |
variant="ghost"
|
| 980 |
size="icon"
|
| 981 |
+
className="text-destructive hover:bg-destructive/20"
|
| 982 |
+
onClick={(e) => {
|
| 983 |
+
e.stopPropagation();
|
| 984 |
+
e.preventDefault();
|
| 985 |
+
if (confirm('Delete this node?')) {
|
| 986 |
+
onDelete(node.id);
|
| 987 |
+
}
|
| 988 |
+
}}
|
| 989 |
+
onPointerDown={(e) => e.stopPropagation()}
|
| 990 |
title="Delete node"
|
| 991 |
aria-label="Delete node"
|
| 992 |
>
|
app/editor/page.tsx
CHANGED
|
@@ -25,25 +25,34 @@ function generateMergePrompt(characterData: { image: string; label: string }[]):
|
|
| 25 |
|
| 26 |
const labels = characterData.map((d, i) => `Image ${i + 1} (${d.label})`).join(", ");
|
| 27 |
|
| 28 |
-
return `MERGE TASK:
|
| 29 |
|
| 30 |
Images provided:
|
| 31 |
${characterData.map((d, i) => `- Image ${i + 1}: ${d.label}`).join("\n")}
|
| 32 |
|
| 33 |
-
|
| 34 |
-
1.
|
| 35 |
-
2.
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
-
|
| 44 |
-
-
|
| 45 |
-
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
}
|
| 48 |
|
| 49 |
// Types
|
|
@@ -583,6 +592,18 @@ export default function EditorPage() {
|
|
| 583 |
} as CharacterNode,
|
| 584 |
]);
|
| 585 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 586 |
// Viewport state
|
| 587 |
const [scale, setScale] = useState(1);
|
| 588 |
const [tx, setTx] = useState(0);
|
|
@@ -694,6 +715,9 @@ export default function EditorPage() {
|
|
| 694 |
}
|
| 695 |
setDraggingFrom(null);
|
| 696 |
setDragPos(null);
|
|
|
|
|
|
|
|
|
|
| 697 |
}
|
| 698 |
};
|
| 699 |
|
|
@@ -1051,6 +1075,9 @@ export default function EditorPage() {
|
|
| 1051 |
// Connection drag handlers
|
| 1052 |
const handleStartConnection = (characterId: string) => {
|
| 1053 |
setDraggingFrom(characterId);
|
|
|
|
|
|
|
|
|
|
| 1054 |
};
|
| 1055 |
|
| 1056 |
const handleEndConnection = (mergeId: string) => {
|
|
@@ -1058,6 +1085,9 @@ export default function EditorPage() {
|
|
| 1058 |
connectToMerge(mergeId, draggingFrom);
|
| 1059 |
setDraggingFrom(null);
|
| 1060 |
setDragPos(null);
|
|
|
|
|
|
|
|
|
|
| 1061 |
}
|
| 1062 |
};
|
| 1063 |
|
|
@@ -1073,6 +1103,9 @@ export default function EditorPage() {
|
|
| 1073 |
if (draggingFrom) {
|
| 1074 |
setDraggingFrom(null);
|
| 1075 |
setDragPos(null);
|
|
|
|
|
|
|
|
|
|
| 1076 |
}
|
| 1077 |
};
|
| 1078 |
const disconnectFromMerge = (mergeId: string, characterId: string) => {
|
|
@@ -1376,7 +1409,34 @@ export default function EditorPage() {
|
|
| 1376 |
return (
|
| 1377 |
<div className="min-h-[100svh] bg-background text-foreground">
|
| 1378 |
<header className="flex items-center justify-between px-6 py-4 border-b border-border/60 bg-card/70 backdrop-blur">
|
| 1379 |
-
<h1 className="text-lg font-semibold tracking-wide"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1380 |
</header>
|
| 1381 |
|
| 1382 |
<div
|
|
|
|
| 25 |
|
| 26 |
const labels = characterData.map((d, i) => `Image ${i + 1} (${d.label})`).join(", ");
|
| 27 |
|
| 28 |
+
return `MERGE TASK: Create a natural, cohesive group photo combining ALL subjects from ${count} provided images.
|
| 29 |
|
| 30 |
Images provided:
|
| 31 |
${characterData.map((d, i) => `- Image ${i + 1}: ${d.label}`).join("\n")}
|
| 32 |
|
| 33 |
+
CRITICAL REQUIREMENTS:
|
| 34 |
+
1. Extract ALL people/subjects from EACH image exactly as they appear
|
| 35 |
+
2. Place them together in a SINGLE UNIFIED SCENE with:
|
| 36 |
+
- Consistent lighting direction and color temperature
|
| 37 |
+
- Matching shadows and ambient lighting
|
| 38 |
+
- Proper scale relationships (realistic relative sizes)
|
| 39 |
+
- Natural spacing as if they were photographed together
|
| 40 |
+
- Shared environment/background that looks cohesive
|
| 41 |
+
|
| 42 |
+
3. Composition guidelines:
|
| 43 |
+
- Arrange subjects at similar depth (not one far behind another)
|
| 44 |
+
- Use natural group photo positioning (slight overlap is ok)
|
| 45 |
+
- Ensure all faces are clearly visible
|
| 46 |
+
- Create visual balance in the composition
|
| 47 |
+
- Apply consistent color grading across all subjects
|
| 48 |
+
|
| 49 |
+
4. Environmental unity:
|
| 50 |
+
- Use a single, coherent background for all subjects
|
| 51 |
+
- Match the perspective as if taken with one camera
|
| 52 |
+
- Ensure ground plane continuity (all standing on same level)
|
| 53 |
+
- Apply consistent atmospheric effects (if any)
|
| 54 |
+
|
| 55 |
+
The result should look like all subjects were photographed together in the same place at the same time, NOT like separate images placed side by side.`;
|
| 56 |
}
|
| 57 |
|
| 58 |
// Types
|
|
|
|
| 592 |
} as CharacterNode,
|
| 593 |
]);
|
| 594 |
|
| 595 |
+
// Theme state
|
| 596 |
+
const [theme, setTheme] = useState<'dark' | 'light'>('dark');
|
| 597 |
+
|
| 598 |
+
// Apply theme to document
|
| 599 |
+
useEffect(() => {
|
| 600 |
+
if (theme === 'light') {
|
| 601 |
+
document.documentElement.classList.remove('dark');
|
| 602 |
+
} else {
|
| 603 |
+
document.documentElement.classList.add('dark');
|
| 604 |
+
}
|
| 605 |
+
}, [theme]);
|
| 606 |
+
|
| 607 |
// Viewport state
|
| 608 |
const [scale, setScale] = useState(1);
|
| 609 |
const [tx, setTx] = useState(0);
|
|
|
|
| 715 |
}
|
| 716 |
setDraggingFrom(null);
|
| 717 |
setDragPos(null);
|
| 718 |
+
// Re-enable text selection
|
| 719 |
+
document.body.style.userSelect = '';
|
| 720 |
+
document.body.style.webkitUserSelect = '';
|
| 721 |
}
|
| 722 |
};
|
| 723 |
|
|
|
|
| 1075 |
// Connection drag handlers
|
| 1076 |
const handleStartConnection = (characterId: string) => {
|
| 1077 |
setDraggingFrom(characterId);
|
| 1078 |
+
// Prevent text selection during dragging
|
| 1079 |
+
document.body.style.userSelect = 'none';
|
| 1080 |
+
document.body.style.webkitUserSelect = 'none';
|
| 1081 |
};
|
| 1082 |
|
| 1083 |
const handleEndConnection = (mergeId: string) => {
|
|
|
|
| 1085 |
connectToMerge(mergeId, draggingFrom);
|
| 1086 |
setDraggingFrom(null);
|
| 1087 |
setDragPos(null);
|
| 1088 |
+
// Re-enable text selection
|
| 1089 |
+
document.body.style.userSelect = '';
|
| 1090 |
+
document.body.style.webkitUserSelect = '';
|
| 1091 |
}
|
| 1092 |
};
|
| 1093 |
|
|
|
|
| 1103 |
if (draggingFrom) {
|
| 1104 |
setDraggingFrom(null);
|
| 1105 |
setDragPos(null);
|
| 1106 |
+
// Re-enable text selection
|
| 1107 |
+
document.body.style.userSelect = '';
|
| 1108 |
+
document.body.style.webkitUserSelect = '';
|
| 1109 |
}
|
| 1110 |
};
|
| 1111 |
const disconnectFromMerge = (mergeId: string, characterId: string) => {
|
|
|
|
| 1409 |
return (
|
| 1410 |
<div className="min-h-[100svh] bg-background text-foreground">
|
| 1411 |
<header className="flex items-center justify-between px-6 py-4 border-b border-border/60 bg-card/70 backdrop-blur">
|
| 1412 |
+
<h1 className="text-lg font-semibold tracking-wide">
|
| 1413 |
+
<span className="mr-2" aria-hidden>🍌</span>Nano Banana Editor
|
| 1414 |
+
</h1>
|
| 1415 |
+
<Button
|
| 1416 |
+
variant="ghost"
|
| 1417 |
+
size="icon"
|
| 1418 |
+
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
|
| 1419 |
+
title={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
|
| 1420 |
+
className="rounded-lg"
|
| 1421 |
+
>
|
| 1422 |
+
{theme === 'dark' ? (
|
| 1423 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
| 1424 |
+
<circle cx="12" cy="12" r="5"/>
|
| 1425 |
+
<line x1="12" y1="1" x2="12" y2="3"/>
|
| 1426 |
+
<line x1="12" y1="21" x2="12" y2="23"/>
|
| 1427 |
+
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
|
| 1428 |
+
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
|
| 1429 |
+
<line x1="1" y1="12" x2="3" y2="12"/>
|
| 1430 |
+
<line x1="21" y1="12" x2="23" y2="12"/>
|
| 1431 |
+
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
|
| 1432 |
+
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
|
| 1433 |
+
</svg>
|
| 1434 |
+
) : (
|
| 1435 |
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
| 1436 |
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
| 1437 |
+
</svg>
|
| 1438 |
+
)}
|
| 1439 |
+
</Button>
|
| 1440 |
</header>
|
| 1441 |
|
| 1442 |
<div
|
app/globals.css
CHANGED
|
@@ -40,8 +40,39 @@
|
|
| 40 |
--chart-5: 27 87% 67%;
|
| 41 |
}
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
@media (prefers-color-scheme: dark) {
|
| 44 |
-
:root {
|
| 45 |
--background: 240 10% 3.9%;
|
| 46 |
--foreground: 0 0% 98%;
|
| 47 |
|
|
@@ -146,6 +177,19 @@ body {
|
|
| 146 |
backface-visibility: hidden;
|
| 147 |
perspective: 1000px;
|
| 148 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
.nb-node .nb-header {
|
| 150 |
background: linear-gradient(to bottom, hsl(var(--muted) / 0.35), hsl(var(--muted) / 0.08));
|
| 151 |
}
|
|
@@ -158,6 +202,8 @@ body {
|
|
| 158 |
cursor: crosshair;
|
| 159 |
transition: transform 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
|
| 160 |
position: relative;
|
|
|
|
|
|
|
| 161 |
}
|
| 162 |
.nb-port:hover {
|
| 163 |
transform: scale(1.25);
|
|
|
|
| 40 |
--chart-5: 27 87% 67%;
|
| 41 |
}
|
| 42 |
|
| 43 |
+
.dark {
|
| 44 |
+
--background: 240 10% 3.9%;
|
| 45 |
+
--foreground: 0 0% 98%;
|
| 46 |
+
|
| 47 |
+
--card: 240 10% 3.9%;
|
| 48 |
+
--card-foreground: 0 0% 98%;
|
| 49 |
+
|
| 50 |
+
--popover: 240 10% 3.9%;
|
| 51 |
+
--popover-foreground: 0 0% 98%;
|
| 52 |
+
|
| 53 |
+
/* Brand in dark mode */
|
| 54 |
+
--primary: 14 87% 60%;
|
| 55 |
+
--primary-foreground: 240 10% 3.9%;
|
| 56 |
+
|
| 57 |
+
--secondary: 240 3.7% 15.9%;
|
| 58 |
+
--secondary-foreground: 0 0% 98%;
|
| 59 |
+
|
| 60 |
+
--muted: 240 3.7% 15.9%;
|
| 61 |
+
--muted-foreground: 240 5% 64.9%;
|
| 62 |
+
|
| 63 |
+
--accent: 14 40% 20%;
|
| 64 |
+
--accent-foreground: 0 0% 98%;
|
| 65 |
+
|
| 66 |
+
--destructive: 0 62.8% 30.6%;
|
| 67 |
+
--destructive-foreground: 0 85.7% 97.3%;
|
| 68 |
+
|
| 69 |
+
--border: 240 3.7% 15.9%;
|
| 70 |
+
--input: 240 3.7% 15.9%;
|
| 71 |
+
--ring: 14 87% 55%;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
@media (prefers-color-scheme: dark) {
|
| 75 |
+
:root:not(.light) {
|
| 76 |
--background: 240 10% 3.9%;
|
| 77 |
--foreground: 0 0% 98%;
|
| 78 |
|
|
|
|
| 177 |
backface-visibility: hidden;
|
| 178 |
perspective: 1000px;
|
| 179 |
}
|
| 180 |
+
|
| 181 |
+
/* Prevent text selection on node elements except inputs */
|
| 182 |
+
.nb-node * {
|
| 183 |
+
user-select: none;
|
| 184 |
+
-webkit-user-select: none;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.nb-node input,
|
| 188 |
+
.nb-node textarea,
|
| 189 |
+
.nb-node select {
|
| 190 |
+
user-select: text;
|
| 191 |
+
-webkit-user-select: text;
|
| 192 |
+
}
|
| 193 |
.nb-node .nb-header {
|
| 194 |
background: linear-gradient(to bottom, hsl(var(--muted) / 0.35), hsl(var(--muted) / 0.08));
|
| 195 |
}
|
|
|
|
| 202 |
cursor: crosshair;
|
| 203 |
transition: transform 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
|
| 204 |
position: relative;
|
| 205 |
+
user-select: none;
|
| 206 |
+
-webkit-user-select: none;
|
| 207 |
}
|
| 208 |
.nb-port:hover {
|
| 209 |
transform: scale(1.25);
|