mirror of
https://github.com/r2r90/canvas-label.git
synced 2025-12-16 21:19:38 +00:00
Started test version
This commit is contained in:
@@ -23,6 +23,18 @@ const config = {
|
||||
locales: ["en"],
|
||||
defaultLocale: "en",
|
||||
},
|
||||
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'images.unsplash.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@hookform/resolvers": "^3.3.2",
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@prisma/client": "^5.1.1",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
@@ -27,6 +28,7 @@
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-separator": "^1.0.3",
|
||||
"@radix-ui/react-slider": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toggle": "^1.0.3",
|
||||
"@radix-ui/react-toggle-group": "^1.0.4",
|
||||
@@ -54,6 +56,7 @@
|
||||
"next-auth": "^4.23.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.49.0",
|
||||
"react-icons": "^4.11.0",
|
||||
"react-konva": "^18.2.10",
|
||||
"react-konva-utils": "^1.0.5",
|
||||
@@ -63,7 +66,7 @@
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"use-image": "^1.1.1",
|
||||
"uuid": "^9.0.1",
|
||||
"zod": "^3.21.4"
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/eslint": "^8.44.2",
|
||||
|
||||
28
pnpm-lock.yaml
generated
28
pnpm-lock.yaml
generated
@@ -17,6 +17,9 @@ dependencies:
|
||||
'@headlessui/react':
|
||||
specifier: ^1.7.17
|
||||
version: 1.7.17(react-dom@18.2.0)(react@18.2.0)
|
||||
'@hookform/resolvers':
|
||||
specifier: ^3.3.2
|
||||
version: 3.3.2(react-hook-form@7.49.0)
|
||||
'@next-auth/prisma-adapter':
|
||||
specifier: ^1.0.7
|
||||
version: 1.0.7(@prisma/client@5.5.0)(next-auth@4.24.3)
|
||||
@@ -53,6 +56,9 @@ dependencies:
|
||||
'@radix-ui/react-slider':
|
||||
specifier: ^1.1.2
|
||||
version: 1.1.2(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@radix-ui/react-slot':
|
||||
specifier: ^1.0.2
|
||||
version: 1.0.2(@types/react@18.2.31)(react@18.2.0)
|
||||
'@radix-ui/react-tabs':
|
||||
specifier: ^1.0.4
|
||||
version: 1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
|
||||
@@ -134,6 +140,9 @@ dependencies:
|
||||
react-dom:
|
||||
specifier: 18.2.0
|
||||
version: 18.2.0(react@18.2.0)
|
||||
react-hook-form:
|
||||
specifier: ^7.49.0
|
||||
version: 7.49.0(react@18.2.0)
|
||||
react-icons:
|
||||
specifier: ^4.11.0
|
||||
version: 4.11.0(react@18.2.0)
|
||||
@@ -162,7 +171,7 @@ dependencies:
|
||||
specifier: ^9.0.1
|
||||
version: 9.0.1
|
||||
zod:
|
||||
specifier: ^3.21.4
|
||||
specifier: ^3.22.4
|
||||
version: 3.22.4
|
||||
|
||||
devDependencies:
|
||||
@@ -349,6 +358,14 @@ packages:
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@hookform/resolvers@3.3.2(react-hook-form@7.49.0):
|
||||
resolution: {integrity: sha512-Tw+GGPnBp+5DOsSg4ek3LCPgkBOuOgS5DsDV7qsWNH9LZc433kgsWICjlsh2J9p04H2K66hsXPPb9qn9ILdUtA==}
|
||||
peerDependencies:
|
||||
react-hook-form: ^7.0.0
|
||||
dependencies:
|
||||
react-hook-form: 7.49.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@humanwhocodes/config-array@0.11.13:
|
||||
resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
@@ -4526,6 +4543,15 @@ packages:
|
||||
scheduler: 0.23.0
|
||||
dev: false
|
||||
|
||||
/react-hook-form@7.49.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-gf4qyY4WiqK2hP/E45UUT6wt3Khl49pleEVcIzxhLBrD6m+GMWtLRk0vMrRv45D1ZH8PnpXFwRPv0Pewske2jw==}
|
||||
engines: {node: '>=18', pnpm: '8'}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17 || ^18
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/react-icons@4.11.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-V+4khzYcE5EBk/BvcuYRq6V/osf11ODUM2J8hg2FDSswRrGvqiYUYPRy4OdrWaQOBj4NcpJfmHZLNaD+VH0TyA==}
|
||||
peerDependencies:
|
||||
|
||||
10
src/components/Test/content/edit-utils/sidebar.tsx
Normal file
10
src/components/Test/content/edit-utils/sidebar.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import StageMode from "@/components/Test/content/edit-utils/stage-mode";
|
||||
|
||||
export default function Sidebar() {
|
||||
return (
|
||||
<>
|
||||
<StageMode />
|
||||
</>
|
||||
);
|
||||
}
|
||||
40
src/components/Test/content/edit-utils/stage-mode.tsx
Normal file
40
src/components/Test/content/edit-utils/stage-mode.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from "react";
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "@/components/ui/hover-card";
|
||||
import { TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { PiBeerBottle } from "react-icons/pi";
|
||||
|
||||
export default function StageMode() {
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
<HoverCard openDelay={200}>
|
||||
<HoverCardTrigger asChild>
|
||||
<span className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||
Taille de l'etquieutte
|
||||
</span>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="w-[320px] text-sm" side="left">
|
||||
Sélectionnez la taille de votre raquette parmi les trois tailles
|
||||
disponibles : S, M, L.
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
<TabsList className="grid h-[2.5rem] grid-cols-3">
|
||||
<TabsTrigger value="complete">
|
||||
<span className="sr-only">Complete</span>
|
||||
<PiBeerBottle size={10} />
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="insert">
|
||||
<span className="sr-only">Insert</span>
|
||||
<PiBeerBottle size={15} />
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="edit">
|
||||
<span className="sr-only">Edit</span>
|
||||
<PiBeerBottle size={22} />
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
25
src/components/Test/content/playground-area/stage-area.tsx
Normal file
25
src/components/Test/content/playground-area/stage-area.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from "react";
|
||||
import { TabsContent } from "@/components/ui/tabs";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CounterClockwiseClockIcon } from "@radix-ui/react-icons";
|
||||
import Canvas from "@/components/canvas";
|
||||
|
||||
export default function StageArea() {
|
||||
return (
|
||||
<TabsContent value="complete" className="mt-0 border-0 p-0">
|
||||
<div className="flex h-full flex-col space-y-4">
|
||||
<div className="min-h-[400px] flex-1 rounded-md border border-input bg-background p-4 2xl:min-h-[750px]">
|
||||
<Canvas />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button>Submit</Button>
|
||||
<Button variant="secondary">
|
||||
<span className="sr-only">Show history</span>
|
||||
<CounterClockwiseClockIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
);
|
||||
}
|
||||
21
src/components/Test/content/playground.tsx
Normal file
21
src/components/Test/content/playground.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import { Tabs } from "@/components/ui/tabs";
|
||||
import Sidebar from "@/components/Test/content/edit-utils/sidebar";
|
||||
import StageArea from "@/components/Test/content/playground-area/stage-area";
|
||||
|
||||
export default function Playground() {
|
||||
return (
|
||||
<Tabs defaultValue="complete" className="flex-1">
|
||||
<div className="container h-full py-6">
|
||||
<div className="grid h-full items-stretch gap-6 md:grid-cols-[1fr_200px]">
|
||||
<div className="md:order-1">
|
||||
<StageArea />
|
||||
</div>
|
||||
<div className="hidden flex-col space-y-4 sm:flex md:order-2">
|
||||
<Sidebar />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
22
src/components/Test/navbar/Navbar.tsx
Normal file
22
src/components/Test/navbar/Navbar.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import { Save } from "@/components/Test/navbar/Save";
|
||||
import { Preview } from "@/components/Test/navbar/Preview";
|
||||
import { SelectVigneron } from "@/components/Test/navbar/select-vigneron";
|
||||
import { presets } from "@/components/Test/navbar/list-de-vignerons";
|
||||
import { UserNav } from "@/components/layout/user-nav";
|
||||
|
||||
export default function Navbar() {
|
||||
return (
|
||||
<div className="container flex flex-col items-start justify-between space-y-2 py-4 sm:flex-row sm:items-center sm:space-y-0 md:h-16">
|
||||
<h2 className="text-lg font-semibold">Vitiquette</h2>
|
||||
<div className="ml-auto flex w-full space-x-2 sm:justify-end">
|
||||
<SelectVigneron presets={presets} />
|
||||
<Save />
|
||||
<Preview />
|
||||
<div className="ml-auto flex items-center space-x-4">
|
||||
<UserNav />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
89
src/components/Test/navbar/Preview.tsx
Normal file
89
src/components/Test/navbar/Preview.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
export function Preview() {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="secondary">Preview</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[625px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>View code</DialogTitle>
|
||||
<DialogDescription>
|
||||
You can use the following code to start integrating your current
|
||||
prompt and settings into your application.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4">
|
||||
<div className="rounded-md bg-black p-6">
|
||||
<pre>
|
||||
<code className="grid gap-1 text-sm text-muted-foreground [&_span]:h-4">
|
||||
<span>
|
||||
<span className="text-sky-300">import</span> os
|
||||
</span>
|
||||
<span>
|
||||
<span className="text-sky-300">import</span> openai
|
||||
</span>
|
||||
<span />
|
||||
<span>
|
||||
openai.api_key = os.getenv(
|
||||
<span className="text-green-300">
|
||||
"OPENAI_API_KEY"
|
||||
</span>
|
||||
)
|
||||
</span>
|
||||
<span />
|
||||
<span>response = openai.Completion.create(</span>
|
||||
<span>
|
||||
{" "}
|
||||
model=
|
||||
<span className="text-green-300">"davinci"</span>,
|
||||
</span>
|
||||
<span>
|
||||
{" "}
|
||||
prompt=<span className="text-amber-300">""</span>,
|
||||
</span>
|
||||
<span>
|
||||
{" "}
|
||||
temperature=<span className="text-amber-300">0.9</span>,
|
||||
</span>
|
||||
<span>
|
||||
{" "}
|
||||
max_tokens=<span className="text-amber-300">5</span>,
|
||||
</span>
|
||||
<span>
|
||||
{" "}
|
||||
top_p=<span className="text-amber-300">1</span>,
|
||||
</span>
|
||||
<span>
|
||||
{" "}
|
||||
frequency_penalty=<span className="text-amber-300">0</span>,
|
||||
</span>
|
||||
<span>
|
||||
{" "}
|
||||
presence_penalty=<span className="text-green-300">0</span>,
|
||||
</span>
|
||||
<span>)</span>
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Your API Key can be found here. You should use environment
|
||||
variables or a secret management tool to expose your key to your
|
||||
applications.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
44
src/components/Test/navbar/Save.tsx
Normal file
44
src/components/Test/navbar/Save.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
export function Save() {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="secondary">Save</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[475px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Save preset</DialogTitle>
|
||||
<DialogDescription>
|
||||
This will save the current playground state as a preset which you
|
||||
can access later or share with others.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input id="name" autoFocus />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Input id="description" />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="submit">Save</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
47
src/components/Test/navbar/list-de-vignerons.ts
Normal file
47
src/components/Test/navbar/list-de-vignerons.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
export interface Preset {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const presets: Preset[] = [
|
||||
{
|
||||
id: "9cb0e66a-9937-465d-a188-2c4c4ae2401f",
|
||||
name: "Grammatical Standard English",
|
||||
},
|
||||
{
|
||||
id: "61eb0e32-2391-4cd3-adc3-66efe09bc0b7",
|
||||
name: "Summarize for a 2nd grader",
|
||||
},
|
||||
{
|
||||
id: "a4e1fa51-f4ce-4e45-892c-224030a00bdd",
|
||||
name: "Text to command",
|
||||
},
|
||||
{
|
||||
id: "cc198b13-4933-43aa-977e-dcd95fa30770",
|
||||
name: "Q&A",
|
||||
},
|
||||
{
|
||||
id: "adfa95be-a575-45fd-a9ef-ea45386c64de",
|
||||
name: "English to other languages",
|
||||
},
|
||||
{
|
||||
id: "c569a06a-0bd6-43a7-adf9-bf68c09e7a79",
|
||||
name: "Parse unstructured data",
|
||||
},
|
||||
{
|
||||
id: "15ccc0d7-f37a-4f0a-8163-a37e162877dc",
|
||||
name: "Classification",
|
||||
},
|
||||
{
|
||||
id: "4641ef41-1c0f-421d-b4b2-70fe431081f3",
|
||||
name: "Natural language to Python",
|
||||
},
|
||||
{
|
||||
id: "48d34082-72f3-4a1b-a14d-f15aca4f57a0",
|
||||
name: "Explain code",
|
||||
},
|
||||
{
|
||||
id: "dfd42fd5-0394-4810-92c6-cc907d3bfd1a",
|
||||
name: "Chat",
|
||||
},
|
||||
];
|
||||
82
src/components/Test/navbar/select-vigneron.tsx
Normal file
82
src/components/Test/navbar/select-vigneron.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
|
||||
import { type PopoverProps } from "@radix-ui/react-popover";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
|
||||
import { type Preset } from "./list-de-vignerons";
|
||||
|
||||
interface PresetSelectorProps extends PopoverProps {
|
||||
presets: Preset[];
|
||||
}
|
||||
|
||||
export function SelectVigneron({ presets, ...props }: PresetSelectorProps) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [selectedPreset, setSelectedPreset] = React.useState<Preset>();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen} {...props}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-label="Load a preset..."
|
||||
aria-expanded={open}
|
||||
className="flex-1 justify-between md:max-w-[200px] lg:max-w-[300px]"
|
||||
>
|
||||
{selectedPreset ? selectedPreset.name : "Load a preset..."}
|
||||
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[300px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search presets..." />
|
||||
<CommandEmpty>No presets found.</CommandEmpty>
|
||||
<CommandGroup heading="Examples">
|
||||
{presets.map((preset) => (
|
||||
<CommandItem
|
||||
key={preset.id}
|
||||
onSelect={() => {
|
||||
setSelectedPreset(preset);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{preset.name}
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
selectedPreset?.id === preset.id
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
<CommandGroup className="pt-0">
|
||||
<CommandItem onSelect={() => router.push("/examples")}>
|
||||
More examples
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -9,16 +9,36 @@ import {
|
||||
updateImage,
|
||||
updateText,
|
||||
} from "@/store/app.slice";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Toolbar } from "@/components/toolbar";
|
||||
import TransformableText from "@/components/transformable-text";
|
||||
import LayerBorder from "@/components/layer-border";
|
||||
import { CANVAS_PADDING_X, CANVAS_PADDING_Y } from "@/consts/canvas-params";
|
||||
import Legacy from "@/components/layout/sidebar/legacy";
|
||||
import { type Shape } from "konva/lib/Shape";
|
||||
import Konva from "konva";
|
||||
|
||||
type Snap = "start" | "center" | "end";
|
||||
type SnappingEdges = {
|
||||
vertical: Array<{
|
||||
guide: number;
|
||||
offset: number;
|
||||
snap: Snap;
|
||||
}>;
|
||||
horizontal: Array<{
|
||||
guide: number;
|
||||
offset: number;
|
||||
snap: Snap;
|
||||
}>;
|
||||
};
|
||||
|
||||
const GUIDELINE_OFFSET = 5;
|
||||
|
||||
const Canvas = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const stage = useAppSelector((state) => state.app.stage);
|
||||
const stageRef = useRef<Konva.Stage>(null);
|
||||
const layerRef = useRef<Konva.Layer>(null);
|
||||
const selectedItemId = useAppSelector((state) => state.app.selectedItemId);
|
||||
const selectItem = (id: string) => dispatch(appSlice.actions.selectItem(id));
|
||||
const items = useAppSelector((state) => state.app.items);
|
||||
@@ -56,6 +76,267 @@ const Canvas = () => {
|
||||
setSelected(!isEditing);
|
||||
}
|
||||
|
||||
/* Drag Drop logic */
|
||||
|
||||
const getObjectSnappingEdges = useCallback((node: Shape): SnappingEdges => {
|
||||
const box = node.getClientRect();
|
||||
const absPos = node.absolutePosition();
|
||||
|
||||
return {
|
||||
vertical: [
|
||||
{
|
||||
guide: Math.round(box.x),
|
||||
offset: Math.round(absPos.x - box.x),
|
||||
snap: "start",
|
||||
},
|
||||
{
|
||||
guide: Math.round(box.x + box.width / 2),
|
||||
offset: Math.round(absPos.x - box.x - box.width / 2),
|
||||
snap: "center",
|
||||
},
|
||||
{
|
||||
guide: Math.round(box.x + box.width),
|
||||
offset: Math.round(absPos.x - box.x - box.width),
|
||||
snap: "end",
|
||||
},
|
||||
],
|
||||
horizontal: [
|
||||
{
|
||||
guide: Math.round(box.y),
|
||||
offset: Math.round(absPos.y - box.y),
|
||||
snap: "start",
|
||||
},
|
||||
{
|
||||
guide: Math.round(box.y + box.height / 2),
|
||||
offset: Math.round(absPos.y - box.y - box.height / 2),
|
||||
snap: "center",
|
||||
},
|
||||
{
|
||||
guide: Math.round(box.y + box.height),
|
||||
offset: Math.round(absPos.y - box.y - box.height),
|
||||
snap: "end",
|
||||
},
|
||||
],
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getLineGuideStops = (skipShape: Konva.Shape) => {
|
||||
const stage = skipShape.getStage();
|
||||
if (!stage) return { vertical: [], horizontal: [] };
|
||||
|
||||
// we can snap to playground-area borders and the center of the playground-area
|
||||
const vertical = [0, stage.width() / 2, stage.width()];
|
||||
const horizontal = [0, stage.height() / 2, stage.height()];
|
||||
|
||||
// and we snap over edges and center of each object on the canvas
|
||||
stage.find(".object").forEach((guideItem) => {
|
||||
if (guideItem === skipShape) {
|
||||
return;
|
||||
}
|
||||
const box = guideItem.getClientRect();
|
||||
// and we can snap to all edges of shapes
|
||||
vertical.push(box.x, box.x + box.width, box.x + box.width / 2);
|
||||
horizontal.push(box.y, box.y + box.height, box.y + box.height / 2);
|
||||
});
|
||||
return {
|
||||
vertical,
|
||||
horizontal,
|
||||
};
|
||||
};
|
||||
|
||||
const getGuides = useCallback(
|
||||
(
|
||||
lineGuideStops: ReturnType<typeof getLineGuideStops>,
|
||||
itemBounds: ReturnType<typeof getObjectSnappingEdges>,
|
||||
) => {
|
||||
const resultV: Array<{
|
||||
lineGuide: number;
|
||||
diff: number;
|
||||
snap: Snap;
|
||||
offset: number;
|
||||
}> = [];
|
||||
|
||||
const resultH: Array<{
|
||||
lineGuide: number;
|
||||
diff: number;
|
||||
snap: Snap;
|
||||
offset: number;
|
||||
}> = [];
|
||||
|
||||
lineGuideStops.vertical.forEach((lineGuide) => {
|
||||
itemBounds.vertical.forEach((itemBound) => {
|
||||
const diff = Math.abs(lineGuide - itemBound.guide);
|
||||
if (diff < GUIDELINE_OFFSET) {
|
||||
resultV.push({
|
||||
lineGuide: lineGuide,
|
||||
diff: diff,
|
||||
snap: itemBound.snap,
|
||||
offset: itemBound.offset,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
lineGuideStops.horizontal.forEach((lineGuide) => {
|
||||
itemBounds.horizontal.forEach((itemBound) => {
|
||||
const diff = Math.abs(lineGuide - itemBound.guide);
|
||||
if (diff < GUIDELINE_OFFSET) {
|
||||
resultH.push({
|
||||
lineGuide: lineGuide,
|
||||
diff: diff,
|
||||
snap: itemBound.snap,
|
||||
offset: itemBound.offset,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const guides: Array<{
|
||||
lineGuide: number;
|
||||
offset: number;
|
||||
orientation: "V" | "H";
|
||||
snap: "start" | "center" | "end";
|
||||
}> = [];
|
||||
|
||||
const minV = resultV.sort((a, b) => a.diff - b.diff)[0];
|
||||
const minH = resultH.sort((a, b) => a.diff - b.diff)[0];
|
||||
|
||||
if (minV) {
|
||||
guides.push({
|
||||
lineGuide: minV.lineGuide,
|
||||
offset: minV.offset,
|
||||
orientation: "V",
|
||||
snap: minV.snap,
|
||||
});
|
||||
}
|
||||
|
||||
if (minH) {
|
||||
guides.push({
|
||||
lineGuide: minH.lineGuide,
|
||||
offset: minH.offset,
|
||||
orientation: "H",
|
||||
snap: minH.snap,
|
||||
});
|
||||
}
|
||||
|
||||
return guides;
|
||||
},
|
||||
[],
|
||||
);
|
||||
const drawGuides = useCallback(
|
||||
(guides: ReturnType<typeof getGuides>, layer: Konva.Layer) => {
|
||||
guides.forEach((lg) => {
|
||||
if (lg.orientation === "H") {
|
||||
const line = new Konva.Line({
|
||||
points: [-6000, 0, 6000, 0],
|
||||
stroke: "rgb(0, 161, 255)",
|
||||
strokeWidth: 1,
|
||||
name: "guid-line",
|
||||
dash: [4, 6],
|
||||
});
|
||||
layer.add(line);
|
||||
line.absolutePosition({
|
||||
x: 0,
|
||||
y: lg.lineGuide,
|
||||
});
|
||||
} else if (lg.orientation === "V") {
|
||||
const line = new Konva.Line({
|
||||
points: [0, -6000, 0, 6000],
|
||||
stroke: "rgb(0, 161, 255)",
|
||||
strokeWidth: 1,
|
||||
name: "guid-line",
|
||||
dash: [4, 6],
|
||||
});
|
||||
layer.add(line);
|
||||
line.absolutePosition({
|
||||
x: lg.lineGuide,
|
||||
y: 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
const onDragMove = useCallback(
|
||||
(e: Konva.KonvaEventObject<DragEvent>) => {
|
||||
const layer = e.target.getLayer();
|
||||
if (!layer) {
|
||||
return;
|
||||
}
|
||||
// clear all previous lines on the screen
|
||||
layer.find(".guid-line").forEach((l) => l.destroy());
|
||||
|
||||
// find possible snapping lines
|
||||
const lineGuideStops = getLineGuideStops(e.target as Konva.Shape);
|
||||
// find snapping points of current object
|
||||
const itemBounds = getObjectSnappingEdges(e.target as Konva.Shape);
|
||||
|
||||
// now find where can we snap current object
|
||||
const guides = getGuides(lineGuideStops, itemBounds);
|
||||
|
||||
// do nothing if no snapping
|
||||
if (!guides.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
drawGuides(guides, layer);
|
||||
|
||||
const absPos = e.target.absolutePosition();
|
||||
// now force object position
|
||||
guides.forEach((lg) => {
|
||||
switch (lg.snap) {
|
||||
case "start": {
|
||||
switch (lg.orientation) {
|
||||
case "V": {
|
||||
absPos.x = lg.lineGuide + lg.offset;
|
||||
break;
|
||||
}
|
||||
case "H": {
|
||||
absPos.y = lg.lineGuide + lg.offset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "center": {
|
||||
switch (lg.orientation) {
|
||||
case "V": {
|
||||
absPos.x = lg.lineGuide + lg.offset;
|
||||
break;
|
||||
}
|
||||
case "H": {
|
||||
absPos.y = lg.lineGuide + lg.offset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "end": {
|
||||
switch (lg.orientation) {
|
||||
case "V": {
|
||||
absPos.x = lg.lineGuide + lg.offset;
|
||||
break;
|
||||
}
|
||||
case "H": {
|
||||
absPos.y = lg.lineGuide + lg.offset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
e.target.absolutePosition(absPos);
|
||||
},
|
||||
[drawGuides, getGuides, getObjectSnappingEdges],
|
||||
);
|
||||
|
||||
const onDragEnd = (e: Konva.KonvaEventObject<DragEvent>) => {
|
||||
const layer = e.target.getLayer();
|
||||
// clear all previous lines on the screen
|
||||
layer?.find(".guid-line").forEach((l) => l.destroy());
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full flex-col items-center">
|
||||
<Toolbar />
|
||||
@@ -72,6 +353,7 @@ const Canvas = () => {
|
||||
>
|
||||
<Stage
|
||||
className="bg-white"
|
||||
ref={stageRef}
|
||||
width={stage.width}
|
||||
height={stage.height}
|
||||
onTouchStart={deselectHandler}
|
||||
@@ -98,6 +380,7 @@ const Canvas = () => {
|
||||
clipY={CANVAS_PADDING_Y}
|
||||
clipWidth={stage.width - 2 * CANVAS_PADDING_X}
|
||||
clipHeight={stage.height - 2 * CANVAS_PADDING_Y}
|
||||
ref={layerRef}
|
||||
>
|
||||
{items.toReversed().map((item) => {
|
||||
if (item.type === StageItemType.Image) {
|
||||
@@ -109,7 +392,12 @@ const Canvas = () => {
|
||||
onChange={(newAttrs) => {
|
||||
dispatch(updateImage({ id: item.id, ...newAttrs }));
|
||||
}}
|
||||
imageProps={item.params}
|
||||
imageProps={{
|
||||
...item.params,
|
||||
onDragMove,
|
||||
onDragEnd,
|
||||
name: "object",
|
||||
}}
|
||||
key={item.id}
|
||||
isBlocked={item.isBlocked}
|
||||
/>
|
||||
@@ -126,7 +414,12 @@ const Canvas = () => {
|
||||
onChange={(newAttrs) => {
|
||||
dispatch(updateText({ id: item.id, ...newAttrs }));
|
||||
}}
|
||||
textProps={item.params}
|
||||
textProps={{
|
||||
...item.params,
|
||||
onDragMove,
|
||||
onDragEnd,
|
||||
name: "object",
|
||||
}}
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Sidebar } from "./sidebar/sidebar";
|
||||
import { Navbar } from "./navbar";
|
||||
import React from "react";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Tabs } from "@/components/ui/tabs";
|
||||
import { Sidebar } from "@/components/layout/sidebar/sidebar";
|
||||
import { Navbar } from "@/components/layout/navbar";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
@@ -14,6 +17,23 @@ export const Layout = ({ children }: Props) => {
|
||||
<Sidebar />
|
||||
<main className="h-full w-full bg-slate-300">{children}</main>
|
||||
</div>
|
||||
|
||||
{/*Test Update Version */}
|
||||
|
||||
{/* <div className="hidden h-full flex-col md:flex">
|
||||
<Navbar />
|
||||
<Separator />
|
||||
<Tabs defaultValue="complete" className="flex-1">
|
||||
<div className="container h-full py-6">
|
||||
<div className="grid h-full items-stretch gap-6 md:grid-cols-[1fr_200px]">
|
||||
<div className="md:order-1">{children}</div>
|
||||
<div className="hidden flex-col space-y-4 sm:flex md:order-2">
|
||||
<Sidebar />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>*/}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
140
src/components/layout/sidebar/image-gallery.tsx
Normal file
140
src/components/layout/sidebar/image-gallery.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import React, { type ElementRef, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import axios from "axios";
|
||||
import * as process from "process";
|
||||
import { LuLayoutTemplate } from "react-icons/lu";
|
||||
import Image from "next/image";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
export const ImageGallery = () => {
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [query, setQuery] = useState<string>("");
|
||||
const [page, setPage] = useState<number>(1);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const [photos, setPhotos] = useState<any[]>([]);
|
||||
|
||||
const scrollAreaRef = useRef<ElementRef<"div">>(null);
|
||||
|
||||
const mainUrl = "https://api.unsplash.com/photos";
|
||||
const searchUrl = "https://api.unsplash.com/search/photos";
|
||||
const clientID = `?client_id=${process.env.NEXT_PUBLIC_ACCESS_KEY}`;
|
||||
|
||||
useEffect(() => {
|
||||
fetchImages();
|
||||
}, [query, page]);
|
||||
|
||||
const fetchImages = async () => {
|
||||
setIsLoading(true);
|
||||
let url;
|
||||
const urlPage = `&page=${page}`;
|
||||
const urlQuery = `&query=${query}`;
|
||||
|
||||
if (query) {
|
||||
url = `${searchUrl}${clientID}${urlPage}${urlQuery}`;
|
||||
console.log(url);
|
||||
} else {
|
||||
url = `${mainUrl}${clientID}${urlPage}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await axios.get(url);
|
||||
|
||||
setPhotos((oldPhotos) => {
|
||||
if (query && page === 1) {
|
||||
return data.results;
|
||||
} else if (query) {
|
||||
return [...oldPhotos, ...data.results];
|
||||
} else {
|
||||
console.log(data);
|
||||
return [...oldPhotos, ...data];
|
||||
}
|
||||
});
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const ref = scrollAreaRef.current;
|
||||
const handler = () => {
|
||||
if (scrollAreaRef.current) {
|
||||
console.log(
|
||||
scrollAreaRef.current.clientHeight + scrollAreaRef.current.scrollTop,
|
||||
scrollAreaRef.current.scrollHeight - 2,
|
||||
);
|
||||
}
|
||||
if (
|
||||
isLoading ||
|
||||
!scrollAreaRef.current ||
|
||||
scrollAreaRef.current.clientHeight + scrollAreaRef.current.scrollTop <
|
||||
scrollAreaRef.current.scrollHeight - 2
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPage((oldPage) => {
|
||||
return oldPage + 1;
|
||||
});
|
||||
};
|
||||
|
||||
scrollAreaRef.current?.addEventListener("scroll", handler);
|
||||
return () => ref?.removeEventListener("scroll", handler);
|
||||
}, [isLoading, open]);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
onClick={() => setOpen(true)}
|
||||
variant="outline"
|
||||
className="text-xl"
|
||||
>
|
||||
<LuLayoutTemplate />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="right" className="mt-4">
|
||||
<Card className="p-3">
|
||||
<CardHeader className="mb-2 p-2 text-center">
|
||||
<CardTitle>Template</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search Images"
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
/>
|
||||
|
||||
<div
|
||||
className=" grid h-64 w-full grid-cols-2 gap-4 overflow-auto"
|
||||
ref={scrollAreaRef}
|
||||
>
|
||||
{photos?.map((p, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="rounded-md border-2 border-muted bg-popover p-2 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary"
|
||||
>
|
||||
<Image
|
||||
key={p.i}
|
||||
src={p.urls.small}
|
||||
alt={"image"}
|
||||
height={100}
|
||||
width={100}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import SizeSelect from "@/components/layout/sidebar/size-select";
|
||||
import { BackgroundSelect } from "@/components/layout/sidebar/background-select";
|
||||
import { Layers } from "@/components/layout/sidebar/layers";
|
||||
import { SelectTemplate } from "@/components/layout/sidebar/select-template";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { ImageGallery } from "@/components/layout/sidebar/image-gallery";
|
||||
|
||||
export function Sidebar() {
|
||||
return (
|
||||
@@ -14,6 +14,7 @@ export function Sidebar() {
|
||||
<ImageInput />
|
||||
<SelectTemplate />
|
||||
<BackgroundSelect />
|
||||
<ImageGallery />
|
||||
<Layers />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -41,21 +41,11 @@ export const TransformableImage = ({
|
||||
<>
|
||||
<Image
|
||||
onDragMove={(e) => {
|
||||
console.log(e);
|
||||
const positionX = e.target.x() + e.target.width() / 2;
|
||||
const positionY = e.target.y() + e.target.height() / 2;
|
||||
const canvasXCenter = (e.target.getStage()?.width() ?? 0) / 2;
|
||||
const canvasYCenter = (e.target.getStage()?.height() ?? 0) / 2;
|
||||
const isInTheXCenter =
|
||||
Math.abs(Math.round(canvasXCenter) - Math.round(positionX)) <= 5;
|
||||
const isInTheYCenter =
|
||||
Math.abs(Math.round(canvasYCenter) - Math.round(positionY)) <= 5;
|
||||
if (isBlocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.body.classList.toggle("show-vertical-line", isInTheXCenter);
|
||||
document.body.classList.toggle(
|
||||
"show-horizontal-line",
|
||||
isInTheYCenter,
|
||||
);
|
||||
imageProps.onDragMove(e);
|
||||
}}
|
||||
id={id}
|
||||
alt={"canvas image"}
|
||||
@@ -69,18 +59,7 @@ export const TransformableImage = ({
|
||||
if (isBlocked) {
|
||||
return;
|
||||
}
|
||||
const positionX = e.target.x() + e.target.width() / 2;
|
||||
const canvasXCenter = (e.target.getStage()?.width() ?? 0) / 2;
|
||||
const isInTheCenter =
|
||||
Math.abs(Math.round(canvasXCenter) - Math.round(positionX)) <= 5;
|
||||
onChange({
|
||||
...imageProps,
|
||||
image,
|
||||
x: isInTheCenter
|
||||
? canvasXCenter - e.target.width() / 2
|
||||
: e.target.x(),
|
||||
y: e.target.y(),
|
||||
});
|
||||
imageProps.onDragEnd?.(e);
|
||||
}}
|
||||
onTransformEnd={() => {
|
||||
const node = imageRef.current;
|
||||
|
||||
171
src/components/ui/form.tsx
Normal file
171
src/components/ui/form.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import * as React from "react";
|
||||
import type * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { Controller, FormProvider, useFormContext } from "react-hook-form";
|
||||
import type { ControllerProps, FieldPath, FieldValues } from "react-hook-form";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
const Form = FormProvider;
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> = {
|
||||
name: TName;
|
||||
};
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue,
|
||||
);
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext);
|
||||
const itemContext = React.useContext(FormItemContext);
|
||||
const { getFieldState, formState } = useFormContext();
|
||||
|
||||
const fieldState = getFieldState(fieldContext.name, formState);
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>");
|
||||
}
|
||||
|
||||
const { id } = itemContext;
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
};
|
||||
};
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue,
|
||||
);
|
||||
|
||||
const FormItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const id = React.useId();
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
);
|
||||
});
|
||||
FormItem.displayName = "FormItem";
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField();
|
||||
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && "text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormLabel.displayName = "FormLabel";
|
||||
|
||||
const FormControl = React.forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||
useFormField();
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormControl.displayName = "FormControl";
|
||||
|
||||
const FormDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField();
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormDescription.displayName = "FormDescription";
|
||||
|
||||
const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField();
|
||||
const body = error ? String(error?.message) : children;
|
||||
|
||||
if (!body) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn("text-sm font-medium text-destructive", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
);
|
||||
});
|
||||
FormMessage.displayName = "FormMessage";
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
};
|
||||
@@ -1,11 +1,11 @@
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
import * as React from "react";
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Popover = PopoverPrimitive.Root
|
||||
const Popover = PopoverPrimitive.Root;
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
@@ -18,12 +18,12 @@ const PopoverContent = React.forwardRef<
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
))
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||
));
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent }
|
||||
export { Popover, PopoverTrigger, PopoverContent };
|
||||
|
||||
@@ -8,9 +8,9 @@ const ScrollArea = React.forwardRef<
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
|
||||
0
src/consts/sources-api.ts
Normal file
0
src/consts/sources-api.ts
Normal file
@@ -1,6 +1,10 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import Head from "next/head";
|
||||
const Canvas = dynamic(() => import("../components/canvas"), { ssr: false });
|
||||
/*const Canvas = dynamic(
|
||||
() => import("../components/Test/content/playground-area/stage-area"),
|
||||
{ ssr: false },
|
||||
);*/
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user