mirror of
https://github.com/r2r90/canvas-label.git
synced 2025-12-18 05:29:26 +00:00
text editing finitions - text align option
This commit is contained in:
@@ -14,10 +14,18 @@
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@prisma/client": "^5.1.1",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-hover-card": "^1.0.7",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-popover": "^1.0.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-toggle": "^1.0.3",
|
||||
"@radix-ui/react-toggle-group": "^1.0.4",
|
||||
"@radix-ui/themes": "^2.0.0",
|
||||
"@reduxjs/toolkit": "^1.9.6",
|
||||
"@remotion/google-fonts": "^4.0.51",
|
||||
|
||||
1557
pnpm-lock.yaml
generated
1557
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,39 +0,0 @@
|
||||
import { HoverCard, HoverCardTrigger } from "./ui/hover-card";
|
||||
import { Label } from "./ui/label";
|
||||
import { Slider } from "./ui/slider";
|
||||
|
||||
interface FontSizeSelector {
|
||||
value: number;
|
||||
fontSizeHandler: (e: number | undefined) => void;
|
||||
}
|
||||
|
||||
export function FontSizeSelector({ value, fontSizeHandler }: FontSizeSelector) {
|
||||
return (
|
||||
<>
|
||||
<HoverCard openDelay={200}>
|
||||
<HoverCardTrigger asChild>
|
||||
<div className="grid gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="fontSize">Font Size</Label>
|
||||
<span className="w-12 rounded-md border border-transparent px-2 py-0.5 text-right text-sm text-muted-foreground hover:border-border">
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
id="fontSize"
|
||||
min={1}
|
||||
max={100}
|
||||
defaultValue={[value ?? 16]}
|
||||
step={1}
|
||||
onValueChange={(e) => {
|
||||
fontSizeHandler(e[0]);
|
||||
}}
|
||||
className="[&_[role=slider]]:h-4 [&_[role=slider]]:w-4"
|
||||
aria-label="fontSize"
|
||||
/>
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
</HoverCard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import { Input } from "../ui/input";
|
||||
import { useAppDispatch } from "@/hooks";
|
||||
import { ChangeEvent, useState } from "react";
|
||||
|
||||
|
||||
export function Sidebar() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@@ -18,12 +17,10 @@ export function Sidebar() {
|
||||
setInputText(e.target.value);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleTextAdd = () => dispatch(addText({initialValue: inputText}));
|
||||
const handleTextAdd = () => dispatch(addText({ initialValue: inputText }));
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col ">
|
||||
<div className="flex h-full max-w-[20rem] flex-col">
|
||||
<Input
|
||||
type="file"
|
||||
className="m-[2rem] w-auto "
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { GiSettingsKnobs } from "react-icons/gi";
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
import { Label } from "./ui/label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
||||
import { Slider } from "./ui/slider";
|
||||
|
||||
export const SpacingSettings = () => {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<GiSettingsKnobs />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80">
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<div className="grid grid-cols-3 items-center gap-4">
|
||||
<Label htmlFor="width">Width</Label>
|
||||
<Slider
|
||||
id="fontSize"
|
||||
min={1}
|
||||
max={100}
|
||||
step={1}
|
||||
|
||||
className="[&_[role=slider]]:h-4 [&_[role=slider]]:w-4"
|
||||
aria-label="fontSize"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 items-center gap-4">
|
||||
<Label htmlFor="maxWidth">Letter Spacing</Label>
|
||||
<Slider
|
||||
id="fontSize"
|
||||
min={1}
|
||||
max={100}
|
||||
step={1}
|
||||
|
||||
className="[&_[role=slider]]:h-4 [&_[role=slider]]:w-4"
|
||||
aria-label="fontSize"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</PopoverContent>
|
||||
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
52
src/components/texte-editing-tools/text-align-selector.tsx
Normal file
52
src/components/texte-editing-tools/text-align-selector.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
AiOutlineAlignCenter,
|
||||
AiOutlineAlignLeft,
|
||||
AiOutlineAlignRight,
|
||||
} from "react-icons/ai";
|
||||
import type { TextConfig } from "konva/lib/shapes/Text";
|
||||
import { updateText } from "@/store/app.slice";
|
||||
import { useAppDispatch } from "@/hooks";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
|
||||
type Props = {
|
||||
currentText: TextConfig;
|
||||
selectedItemId: string | null;
|
||||
};
|
||||
|
||||
export const FontAlign = ({ currentText, selectedItemId }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const handleTextAlignChange = (position: string) => {
|
||||
if (!selectedItemId) return;
|
||||
dispatch(
|
||||
updateText({
|
||||
id: selectedItemId,
|
||||
align: position,
|
||||
}),
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Tabs
|
||||
value={currentText.align}
|
||||
className="flex-1"
|
||||
onValueChange={(value: string) => {
|
||||
if (value) handleTextAlignChange(value);
|
||||
}}
|
||||
>
|
||||
<div className="grid gap-2">
|
||||
<TabsList className="grid grid-cols-3">
|
||||
<TabsTrigger value="left">
|
||||
<AiOutlineAlignLeft />
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="center">
|
||||
<AiOutlineAlignCenter />
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="right">
|
||||
<AiOutlineAlignRight />
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import fonts from "../assets/fonts.json";
|
||||
import fonts from "../../assets/fonts.json";
|
||||
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -21,16 +21,29 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { updateText } from "@/store/app.slice";
|
||||
import { useAppDispatch } from "@/hooks";
|
||||
import type { TextConfig } from "konva/lib/shapes/Text";
|
||||
|
||||
type Props = {
|
||||
handleTextFontFamilyChange: (fontFamiy: string | undefined) => void;
|
||||
selectedFont: string;
|
||||
currentText: TextConfig;
|
||||
selectedItemId: string | null;
|
||||
};
|
||||
|
||||
export function FontFamilyPicker({ handleTextFontFamilyChange, selectedFont }: Props) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
export function FontFamilyPicker({ currentText, selectedItemId }: Props) {
|
||||
const dispatch = useAppDispatch();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleTextFontFamilyChange = (e: string | undefined) => {
|
||||
if (!selectedItemId) return;
|
||||
dispatch(
|
||||
updateText({
|
||||
id: selectedItemId,
|
||||
fontFamily: e,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fonts.items.map((font): void => {
|
||||
@@ -45,6 +58,8 @@ export function FontFamilyPicker({ handleTextFontFamilyChange, selectedFont }: P
|
||||
});
|
||||
}, []);
|
||||
|
||||
const selectedFont = currentText.fontFamily ?? "Roboto";
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
55
src/components/texte-editing-tools/text-size-selector.tsx
Normal file
55
src/components/texte-editing-tools/text-size-selector.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { HoverCard, HoverCardTrigger } from "@/components/ui/hover-card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { updateText } from "@/store/app.slice";
|
||||
import { useAppDispatch } from "@/hooks";
|
||||
import type { TextConfig } from "konva/lib/shapes/Text";
|
||||
|
||||
type Props = {
|
||||
currentText: TextConfig;
|
||||
selectedItemId: string | null;
|
||||
};
|
||||
|
||||
export function FontSizeSelector({ currentText, selectedItemId }: Props) {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const value = currentText.fontSize ?? 16;
|
||||
|
||||
const handleTextFontSizeChange = (e: number | undefined) => {
|
||||
if (!selectedItemId) return;
|
||||
dispatch(
|
||||
updateText({
|
||||
id: selectedItemId,
|
||||
fontSize: e,
|
||||
}),
|
||||
);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<HoverCard openDelay={200}>
|
||||
<HoverCardTrigger asChild>
|
||||
<div className="grid gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="fontSize">Font Size</Label>
|
||||
<span className="w-12 rounded-md border border-transparent px-2 py-0.5 text-right text-sm text-muted-foreground hover:border-border">
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
id="fontSize"
|
||||
min={1}
|
||||
max={100}
|
||||
defaultValue={[currentText.fontSize ?? 16]}
|
||||
step={1}
|
||||
onValueChange={(e) => {
|
||||
handleTextFontSizeChange(e[0]);
|
||||
}}
|
||||
className="[&_[role=slider]]:h-4 [&_[role=slider]]:w-4"
|
||||
aria-label="fontSize"
|
||||
/>
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
</HoverCard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
78
src/components/texte-editing-tools/text-spacing-settings.tsx
Normal file
78
src/components/texte-editing-tools/text-spacing-settings.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { GiSettingsKnobs } from "react-icons/gi";
|
||||
import { Button } from "../ui/button";
|
||||
import { Label } from "../ui/label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
||||
import { Slider } from "../ui/slider";
|
||||
import type { TextConfig } from "konva/lib/shapes/Text";
|
||||
import { useAppDispatch } from "@/hooks";
|
||||
import { updateText } from "@/store/app.slice";
|
||||
|
||||
type Props = {
|
||||
currentText: TextConfig;
|
||||
selectedItemId: string | null;
|
||||
};
|
||||
|
||||
export const SpacingSettings = ({ currentText, selectedItemId }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleTextLetterSpacing = (e: number | undefined) => {
|
||||
if (!selectedItemId) return;
|
||||
dispatch(
|
||||
updateText({
|
||||
id: selectedItemId,
|
||||
letterSpacing: e,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleLineHeight = (e: number | undefined) => {
|
||||
if (!selectedItemId) return;
|
||||
dispatch(
|
||||
updateText({
|
||||
id: selectedItemId,
|
||||
lineHeight: e,
|
||||
}),
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<GiSettingsKnobs />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80">
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<div className="grid grid-cols-3 items-center gap-4">
|
||||
<Label htmlFor="width">Letter Spacing</Label>
|
||||
<Slider
|
||||
id="fontSize"
|
||||
defaultValue={[currentText.letterSpacing ?? 0]}
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.1}
|
||||
className="[&_[role=slider]]:h-4 [&_[role=slider]]:w-4"
|
||||
aria-label="letterSpacing"
|
||||
onValueChange={(e) => handleTextLetterSpacing(e[0])}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 items-center gap-4">
|
||||
<Label htmlFor="maxWidth">Line Height</Label>
|
||||
<Slider
|
||||
id="fontSize"
|
||||
defaultValue={[currentText.lineHeight ?? 1]}
|
||||
min={1}
|
||||
max={10}
|
||||
step={1}
|
||||
className="[&_[role=slider]]:h-4 [&_[role=slider]]:w-4"
|
||||
aria-label="lineHeight"
|
||||
onValueChange={(e) => handleLineHeight(e[0])}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
117
src/components/texte-editing-tools/text-style-selector.tsx
Normal file
117
src/components/texte-editing-tools/text-style-selector.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { Toggle } from "../ui/toggle";
|
||||
import { updateText } from "@/store/app.slice";
|
||||
import type { TextConfig } from "konva/lib/shapes/Text";
|
||||
import {
|
||||
FontBoldIcon,
|
||||
FontItalicIcon,
|
||||
StrikethroughIcon,
|
||||
UnderlineIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import { Separator } from "../ui/separator";
|
||||
import { useAppDispatch } from "@/hooks";
|
||||
|
||||
type Props = {
|
||||
currentText: TextConfig;
|
||||
selectedItemId: string | null;
|
||||
};
|
||||
|
||||
export function FontStyle({ currentText, selectedItemId }: Props) {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleFontStyleToggle = (button: "bold" | "italic") => () => {
|
||||
if (!selectedItemId) return;
|
||||
dispatch(
|
||||
updateText({
|
||||
id: selectedItemId,
|
||||
fontStyle: getFontStyle(currentText.fontStyle, button),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleTextDecorationToggle =
|
||||
(button: "underline" | "line-through") => () => {
|
||||
if (!selectedItemId) return;
|
||||
|
||||
dispatch(
|
||||
updateText({
|
||||
id: selectedItemId,
|
||||
textDecoration: getFontStyle(currentText.textDecoration, button),
|
||||
}),
|
||||
);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-1">
|
||||
<Toggle
|
||||
aria-label="Toggle italic"
|
||||
onClick={handleFontStyleToggle("bold")}
|
||||
value={currentText.fontStyle}
|
||||
>
|
||||
<FontBoldIcon className="mr-2 h-4 w-4" />
|
||||
Bold
|
||||
</Toggle>
|
||||
|
||||
<Toggle
|
||||
aria-label="Toggle italic"
|
||||
onClick={handleFontStyleToggle("italic")}
|
||||
value={currentText.fontStyle}
|
||||
>
|
||||
<FontItalicIcon className="mr-2 h-4 w-4" />
|
||||
Italic
|
||||
</Toggle>
|
||||
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mx-2 my-auto h-[2rem] items-center "
|
||||
/>
|
||||
|
||||
<Toggle
|
||||
aria-label="Toggle italic"
|
||||
onClick={handleTextDecorationToggle("underline")}
|
||||
value={currentText.textDecoration}
|
||||
>
|
||||
<UnderlineIcon className="mr-2 h-4 w-4" />
|
||||
Souligné
|
||||
</Toggle>
|
||||
<Toggle
|
||||
aria-label="Toggle italic"
|
||||
onClick={handleTextDecorationToggle("line-through")}
|
||||
value={currentText.textDecoration}
|
||||
>
|
||||
<StrikethroughIcon className="mr-2 h-4 w-4" />
|
||||
Barré
|
||||
</Toggle>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const getFontStyle = (
|
||||
currentStyle: string | undefined,
|
||||
buttonClicked: "bold" | "italic" | "underline" | "line-through",
|
||||
) => {
|
||||
if (buttonClicked === "bold") {
|
||||
if (currentStyle?.includes("bold")) {
|
||||
return currentStyle?.replace("bold", "").trim();
|
||||
}
|
||||
return ((currentStyle ?? "") + " bold").trim();
|
||||
}
|
||||
if (buttonClicked === "italic") {
|
||||
if (currentStyle?.includes("italic")) {
|
||||
return currentStyle?.replace("italic", "").trim();
|
||||
}
|
||||
return ((currentStyle ?? "") + " italic").trim();
|
||||
}
|
||||
if (buttonClicked === "underline") {
|
||||
if (currentStyle?.includes("underline")) {
|
||||
return currentStyle?.replace("underline", "").trim();
|
||||
}
|
||||
return ((currentStyle ?? "") + " underline").trim();
|
||||
}
|
||||
if (buttonClicked === "line-through") {
|
||||
if (currentStyle?.includes("line-through")) {
|
||||
return currentStyle?.replace("line-through", "").trim();
|
||||
}
|
||||
return ((currentStyle ?? "") + " line-through").trim();
|
||||
}
|
||||
};
|
||||
@@ -1,62 +1,32 @@
|
||||
import { useAppDispatch, useAppSelector } from "@/hooks";
|
||||
import { updateText } from "@/store/app.slice";
|
||||
import { Toggle } from "@/components/ui/toggle";
|
||||
import { FontFamilyPicker } from "./font-family-picker";
|
||||
import { FontSizeSelector } from "./font-size-selector";
|
||||
import { useState, type ChangeEvent } from "react";
|
||||
import { SpacingSettings } from "./spacing-settings";
|
||||
import { FontFamilyPicker } from "./texte-editing-tools/text-family-picker";
|
||||
import { FontSizeSelector } from "./texte-editing-tools/text-size-selector";
|
||||
import type { ChangeEvent } from "react";
|
||||
import { FontStyle } from "./texte-editing-tools/text-style-selector";
|
||||
import { FontAlign } from "./texte-editing-tools/text-align-selector";
|
||||
import { Separator } from "./ui/separator";
|
||||
import { SpacingSettings } from "./texte-editing-tools/text-spacing-settings";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "@/components/ui/hover-card";
|
||||
import {
|
||||
AiOutlineAlignCenter,
|
||||
AiOutlineAlignLeft,
|
||||
AiOutlineAlignRight,
|
||||
} from "react-icons/ai";
|
||||
|
||||
export const Toolbar = () => {
|
||||
const [alignment, setAlignment] = useState("left");
|
||||
const dispatch = useAppDispatch();
|
||||
const selectedItemId = useAppSelector((state) => state.app.selectedItemId);
|
||||
const texts = useAppSelector((state) => state.app.texts);
|
||||
|
||||
const currentText = texts.find((t) => t.id === selectedItemId);
|
||||
|
||||
if (!currentText) return null;
|
||||
|
||||
const handleFontStyleToggle = (button: "bold" | "italic") => () => {
|
||||
if (!selectedItemId) return;
|
||||
dispatch(
|
||||
updateText({
|
||||
id: selectedItemId,
|
||||
fontStyle: getFontStyle(currentText.fontStyle, button),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleTextDecorationToggle =
|
||||
(button: "underline" | "line-through") => () => {
|
||||
if (!selectedItemId) return;
|
||||
|
||||
dispatch(
|
||||
updateText({
|
||||
id: selectedItemId,
|
||||
textDecoration: getFontStyle(currentText.textDecoration, button),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleTextFontSizeChange = (e: number | undefined) => {
|
||||
if (!selectedItemId) return;
|
||||
dispatch(
|
||||
updateText({
|
||||
id: selectedItemId,
|
||||
fontSize: e,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleTextFontFamilyChange = (e: string | undefined) => {
|
||||
if (!selectedItemId) return;
|
||||
dispatch(
|
||||
updateText({
|
||||
id: selectedItemId,
|
||||
fontFamily: e,
|
||||
}),
|
||||
);
|
||||
};
|
||||
const handleTextColorChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!selectedItemId) return;
|
||||
dispatch(
|
||||
@@ -67,95 +37,33 @@ export const Toolbar = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleTextAlignChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!selectedItemId) return;
|
||||
dispatch(
|
||||
updateText({
|
||||
id: selectedItemId,
|
||||
align: e.target.value,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleAlignmentChange = (newAlignment: string | undefined) => {};
|
||||
|
||||
return (
|
||||
<div className="flex h-[5rem] w-full gap-6 border bg-white p-[1rem] transition">
|
||||
<Toggle
|
||||
className="data-[state=on]:font-bold"
|
||||
onClick={handleFontStyleToggle("bold")}
|
||||
>
|
||||
B
|
||||
</Toggle>
|
||||
<Toggle
|
||||
className="data-[state=on]:italic"
|
||||
onClick={handleFontStyleToggle("italic")}
|
||||
>
|
||||
I
|
||||
</Toggle>
|
||||
<Toggle
|
||||
className="underline data-[state=on]:no-underline"
|
||||
onClick={handleTextDecorationToggle("underline")}
|
||||
>
|
||||
U
|
||||
</Toggle>
|
||||
<Toggle
|
||||
className="line-through data-[state=on]:no-underline"
|
||||
onClick={handleTextDecorationToggle("line-through")}
|
||||
>
|
||||
S
|
||||
</Toggle>
|
||||
|
||||
<FontSizeSelector
|
||||
value={currentText.fontSize ?? 16}
|
||||
fontSizeHandler={handleTextFontSizeChange}
|
||||
<div className="flex h-[5rem] w-full gap-6 border bg-white p-[1rem]">
|
||||
<FontFamilyPicker
|
||||
currentText={currentText}
|
||||
selectedItemId={selectedItemId}
|
||||
/>
|
||||
<Separator orientation="vertical" />
|
||||
<FontStyle currentText={currentText} selectedItemId={selectedItemId} />
|
||||
<Separator orientation="vertical" />
|
||||
<FontAlign currentText={currentText} selectedItemId={selectedItemId} />
|
||||
<Separator orientation="vertical" />
|
||||
<FontSizeSelector
|
||||
currentText={currentText}
|
||||
selectedItemId={selectedItemId}
|
||||
/>
|
||||
<Separator orientation="vertical" />
|
||||
<input
|
||||
className="my-auto "
|
||||
type="color"
|
||||
onChange={handleTextColorChange}
|
||||
value={currentText.fill}
|
||||
/>
|
||||
<FontFamilyPicker
|
||||
handleTextFontFamilyChange={handleTextFontFamilyChange}
|
||||
selectedFont={currentText.fontFamily}
|
||||
<Separator orientation="vertical" />
|
||||
<SpacingSettings
|
||||
currentText={currentText}
|
||||
selectedItemId={selectedItemId}
|
||||
/>
|
||||
|
||||
<Toggle
|
||||
className="data-[state=on]:font-bold"
|
||||
onClick={handleAlignmentChange("left")}
|
||||
></Toggle>
|
||||
|
||||
<SpacingSettings />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getFontStyle = (
|
||||
currentStyle: string | undefined,
|
||||
buttonClicked: "bold" | "italic" | "underline" | "line-through",
|
||||
) => {
|
||||
if (buttonClicked === "bold") {
|
||||
if (currentStyle?.includes("bold")) {
|
||||
return currentStyle?.replace("bold", "").trim();
|
||||
}
|
||||
return ((currentStyle ?? "") + " bold").trim();
|
||||
}
|
||||
if (buttonClicked === "italic") {
|
||||
if (currentStyle?.includes("italic")) {
|
||||
return currentStyle?.replace("italic", "").trim();
|
||||
}
|
||||
return ((currentStyle ?? "") + " italic").trim();
|
||||
}
|
||||
if (buttonClicked === "underline") {
|
||||
if (currentStyle?.includes("underline")) {
|
||||
return currentStyle?.replace("underline", "").trim();
|
||||
}
|
||||
return ((currentStyle ?? "") + " underline").trim();
|
||||
}
|
||||
if (buttonClicked === "line-through") {
|
||||
if (currentStyle?.includes("line-through")) {
|
||||
return currentStyle?.replace("line-through", "").trim();
|
||||
}
|
||||
return ((currentStyle ?? "") + " line-through").trim();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from '@radix-ui/react-icons'
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Slot } from "@radix-ui/themes";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
@@ -30,27 +30,27 @@ const buttonVariants = cva(
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants }
|
||||
export { Button, buttonVariants };
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
import * as React from "react";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
@@ -27,16 +27,16 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||
inset && "pl-8",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
));
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName
|
||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
@@ -46,13 +46,13 @@ const DropdownMenuSubContent = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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}
|
||||
/>
|
||||
))
|
||||
));
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
@@ -64,18 +64,18 @@ const DropdownMenuContent = React.forwardRef<
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
@@ -83,12 +83,12 @@ const DropdownMenuItem = React.forwardRef<
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
@@ -98,7 +98,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
@@ -110,9 +110,9 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
));
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
@@ -122,7 +122,7 @@ const DropdownMenuRadioItem = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -133,13 +133,13 @@ const DropdownMenuRadioItem = React.forwardRef<
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
@@ -147,12 +147,12 @@ const DropdownMenuLabel = React.forwardRef<
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
@@ -163,8 +163,8 @@ const DropdownMenuSeparator = React.forwardRef<
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
@@ -175,9 +175,9 @@ const DropdownMenuShortcut = ({
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
);
|
||||
};
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
@@ -195,4 +195,4 @@ export {
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
};
|
||||
|
||||
29
src/components/ui/separator.tsx
Normal file
29
src/components/ui/separator.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|
||||
53
src/components/ui/tabs.tsx
Normal file
53
src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Tabs = TabsPrimitive.Root
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
@@ -14,9 +14,9 @@ const toggleVariants = cva(
|
||||
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-3",
|
||||
sm: "h-9 px-2.5",
|
||||
lg: "h-11 px-5",
|
||||
default: "h-8 px-2",
|
||||
sm: "h-7 px-1.5",
|
||||
lg: "h-8 px-3",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
Reference in New Issue
Block a user