mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-09 19:41:32 +00:00
Feature/ add text 2 (#45)
* combobox styling * text editor can not autofocus text area * better styles and input method for the color picker
This commit is contained in:
6 files changed
+233
-117
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { useState, ChangeEvent } from "react";
|
||||
import { useState } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Dialog, DialogPanel, DialogTitle } from "@headlessui/react";
|
||||
import { Button, TextEditor } from "~/components/ui";
|
||||
@@ -72,7 +72,7 @@ export const DialogEditText = ({
|
||||
<DialogPanel
|
||||
className={twMerge(
|
||||
paperWrapperStyles,
|
||||
"flex w-full max-w-5xl flex-col justify-between gap-4 px-6 pb-6 pt-4",
|
||||
"flex w-fit max-w-5xl flex-col justify-between gap-4 px-6 pb-6 pt-4",
|
||||
)}
|
||||
// style={{ height: "calc(100vh - 200px)" }}
|
||||
>
|
||||
|
||||
@@ -31,7 +31,7 @@ export const Button = ({
|
||||
return "bg-tertiary hover:bg-tertiary-400 text-white focus-visible:outline-tertiary-600";
|
||||
}
|
||||
case "secondary": {
|
||||
return "bg-secondary hover:bg-secondary-900 text-white focus-visible:outline-secondary";
|
||||
return "bg-secondary-600 hover:bg-secondary-400 text-white focus-visible:outline-secondary";
|
||||
}
|
||||
case "action": {
|
||||
return " bg-action hover:bg-action-500 text-white focus-visible:outline-action";
|
||||
|
||||
@@ -29,25 +29,25 @@ export const Combobox = ({ options, value, onChange }: ComboBoxInterface) => {
|
||||
onChange={onChange}
|
||||
onClose={() => setQuery("")}
|
||||
>
|
||||
<div className="flex">
|
||||
<div className="relative flex h-10 rounded-md border">
|
||||
<ComboboxInput
|
||||
aria-label="Assignee"
|
||||
className="rounded-md"
|
||||
displayValue={(font: string) => font}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
/>
|
||||
<ComboboxButton className="">
|
||||
<FontAwesomeIcon
|
||||
icon={faChevronDown}
|
||||
className="size-4 fill-black/60 group-data-[hover]:fill-black"
|
||||
/>
|
||||
<ComboboxButton className="absolute right-0 top-0 flex h-10 items-center justify-center hover:text-primary">
|
||||
<FontAwesomeIcon icon={faChevronDown} className="size-4 p-2" />
|
||||
</ComboboxButton>
|
||||
</div>
|
||||
<ComboboxOptions anchor="bottom" className="border empty:invisible">
|
||||
<ComboboxOptions
|
||||
anchor="bottom"
|
||||
className="mt-1 w-[var(--input-width)] rounded-lg border empty:invisible"
|
||||
>
|
||||
{filteredOptions.map((option, idx) => (
|
||||
<ComboboxOption
|
||||
key={idx}
|
||||
value={option}
|
||||
className="data-[focus]:bg-blue-100"
|
||||
className="cursor-pointer bg-white p-2 data-[focus]:bg-primary-200 data-[selected]:font-medium"
|
||||
>
|
||||
{option}
|
||||
</ComboboxOption>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TextareaHTMLAttributes } from "react";
|
||||
import { TextareaHTMLAttributes, forwardRef } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { kebabCase } from "~/utilities";
|
||||
|
||||
@@ -16,39 +16,39 @@ export interface TextareaInterface
|
||||
label?: string;
|
||||
resize?: ResizeType;
|
||||
}
|
||||
export const Textarea = ({
|
||||
className,
|
||||
label,
|
||||
resize = "vertical",
|
||||
id,
|
||||
...rest
|
||||
}: TextareaInterface) => {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{label && <label htmlFor={id ? id : kebabCase(label)}>{label}</label>}
|
||||
export const Textarea = forwardRef<HTMLTextAreaElement>(
|
||||
(
|
||||
{ className, label, resize = "vertical", id, ...rest }: TextareaInterface,
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{label && <label htmlFor={id ? id : kebabCase(label)}>{label}</label>}
|
||||
|
||||
<textarea
|
||||
id={id ? id : label ? kebabCase(label) : undefined}
|
||||
className={twMerge(
|
||||
"border-ui-panel-border bg-ui-controls rounded-lg border px-3 py-2",
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
outline: "2px solid transparent",
|
||||
transition: "outline-color 0.15s ease-in-out",
|
||||
resize: resize,
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
// disableHotkeyInput(DomLevels.INPUT);
|
||||
e.currentTarget.style.outlineColor = "#e66462";
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
// enableHotkeyInput(DomLevels.INPUT);
|
||||
e.currentTarget.style.outlineColor = "transparent";
|
||||
}}
|
||||
onKeyDown={(event) => event.stopPropagation()}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
<textarea
|
||||
ref={ref}
|
||||
id={id ? id : label ? kebabCase(label) : undefined}
|
||||
className={twMerge(
|
||||
"border-ui-panel-border bg-ui-controls rounded-lg border px-3 py-2",
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
outline: "2px solid transparent",
|
||||
transition: "outline-color 0.15s ease-in-out",
|
||||
resize: resize,
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
// disableHotkeyInput(DomLevels.INPUT);
|
||||
e.currentTarget.style.outlineColor = "#e66462";
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
// enableHotkeyInput(DomLevels.INPUT);
|
||||
e.currentTarget.style.outlineColor = "transparent";
|
||||
}}
|
||||
onKeyDown={(event) => event.stopPropagation()}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,98 @@
|
||||
import { ChangeEvent, useState } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { faFont, faHashtag } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||
import { HexColorPicker } from "react-colorful";
|
||||
|
||||
import { Button } from "../Button";
|
||||
import { Input } from "../Input";
|
||||
import { paperWrapperStyles } from "~/components/styles";
|
||||
|
||||
export const ColorPicker = ({
|
||||
color: prevColor,
|
||||
onChange,
|
||||
}: {
|
||||
color: string;
|
||||
onChange: (newColor: string) => void;
|
||||
}) => {
|
||||
const [{ currColor, textInput }, setStates] = useState<{
|
||||
currColor: string;
|
||||
textInput: string;
|
||||
}>({
|
||||
currColor: prevColor,
|
||||
textInput: prevColor.substring(1),
|
||||
});
|
||||
|
||||
const handleSelectColor = (newColor: string) => {
|
||||
setStates({ currColor: newColor, textInput: newColor.substring(1) });
|
||||
};
|
||||
const isHexColorCode = (color: string) => {
|
||||
const hexColorRegex = /^(?:[0-9a-fA-F]{3}){1,2}$/;
|
||||
return hexColorRegex.test(color);
|
||||
};
|
||||
const handleTextInput = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (isHexColorCode(e.target.value)) {
|
||||
setStates({
|
||||
currColor: `#${e.target.value}`,
|
||||
textInput: e.target.value,
|
||||
});
|
||||
} else {
|
||||
setStates((curr) => ({
|
||||
...curr,
|
||||
textInput: e.target.value,
|
||||
}));
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Popover className="relative">
|
||||
<PopoverButton className="flex size-10 flex-col items-center gap-1 rounded-md border p-2">
|
||||
<FontAwesomeIcon icon={faFont} />
|
||||
<span className="h-1 w-full" style={{ backgroundColor: prevColor }} />
|
||||
</PopoverButton>
|
||||
<PopoverPanel
|
||||
anchor="bottom"
|
||||
className={twMerge(
|
||||
paperWrapperStyles,
|
||||
"flex flex-col items-center gap-2",
|
||||
)}
|
||||
>
|
||||
{({ close }) => {
|
||||
return (
|
||||
<>
|
||||
<HexColorPicker color={currColor} onChange={handleSelectColor} />
|
||||
<Input
|
||||
style={{ width: "198px" }}
|
||||
icon={faHashtag}
|
||||
value={textInput}
|
||||
onChange={handleTextInput}
|
||||
/>
|
||||
<div className="flex w-full justify-center gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setStates({
|
||||
currColor: prevColor,
|
||||
textInput: prevColor.substring(1),
|
||||
});
|
||||
close();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onChange(currColor);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
OK
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -1,20 +1,12 @@
|
||||
import { ChangeEvent } from "react";
|
||||
import { faFont } from "@fortawesome/pro-solid-svg-icons";
|
||||
import {
|
||||
CloseButton,
|
||||
Popover,
|
||||
PopoverButton,
|
||||
PopoverPanel,
|
||||
} from "@headlessui/react";
|
||||
import { HexColorPicker } from "react-colorful";
|
||||
import { ChangeEvent, useCallback, useEffect, useRef } from "react";
|
||||
|
||||
import { Textarea, TextareaInterface, ResizeType } from "../TextArea";
|
||||
import { InputNumber } from "../InputNumber";
|
||||
import { Combobox } from "../ComboBox";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { ButtonsAlignments } from "./ButtonsAlignment";
|
||||
import { ButtonsTextStyles } from "./ButtonTextStyles";
|
||||
|
||||
import { ColorPicker } from "./ColorPicker";
|
||||
import {
|
||||
TextFormatData,
|
||||
TextAlign,
|
||||
@@ -38,57 +30,99 @@ export const TextEditor = ({
|
||||
onChangeFormatting: (newFormatData: Partial<TextFormatData>) => void;
|
||||
TextareaProps?: TextareaInterface;
|
||||
}) => {
|
||||
const handleOnChangeText = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
onChangeText(e.target.value);
|
||||
};
|
||||
const onChangeTextAlignment = (newAlignment: TextAlign) => {
|
||||
onChangeFormatting({
|
||||
textAlign: newAlignment,
|
||||
});
|
||||
};
|
||||
const onChangeFontStyle = (newStyle: FontStyle) => {
|
||||
onChangeFormatting({
|
||||
fontStyle: newStyle,
|
||||
});
|
||||
};
|
||||
const onChangeFontWeight = (newWeight: FontWeight) => {
|
||||
onChangeFormatting({
|
||||
fontWeight: newWeight,
|
||||
});
|
||||
};
|
||||
const onChangeTextDecoration = (newDecor: TextDecoration) => {
|
||||
onChangeFormatting({
|
||||
textDecoration: newDecor,
|
||||
});
|
||||
};
|
||||
const onChangeFontFamily = (newFontFamily: string) => {
|
||||
onChangeFormatting({
|
||||
fontFamily: newFontFamily,
|
||||
});
|
||||
};
|
||||
const onChangeTextColor = (newTextColor: string) => {
|
||||
onChangeFormatting({
|
||||
color: newTextColor,
|
||||
});
|
||||
};
|
||||
const onChangeFontSize = (newVal: number) => {
|
||||
onChangeFormatting({
|
||||
fontSize: newVal,
|
||||
});
|
||||
};
|
||||
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const focusOnTextArea = useCallback(() => {
|
||||
if (textAreaRef.current === null) {
|
||||
return;
|
||||
}
|
||||
textAreaRef.current.focus();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
focusOnTextArea();
|
||||
}, []);
|
||||
|
||||
const handleOnChangeText = useCallback(
|
||||
(e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
onChangeText(e.target.value);
|
||||
focusOnTextArea();
|
||||
},
|
||||
[onChangeText],
|
||||
);
|
||||
const onChangeTextAlignment = useCallback(
|
||||
(newAlignment: TextAlign) => {
|
||||
onChangeFormatting({
|
||||
textAlign: newAlignment,
|
||||
});
|
||||
focusOnTextArea();
|
||||
},
|
||||
[onChangeFormatting],
|
||||
);
|
||||
const onChangeFontStyle = useCallback(
|
||||
(newStyle: FontStyle) => {
|
||||
onChangeFormatting({
|
||||
fontStyle: newStyle,
|
||||
});
|
||||
focusOnTextArea();
|
||||
},
|
||||
[onChangeFormatting],
|
||||
);
|
||||
const onChangeFontWeight = useCallback(
|
||||
(newWeight: FontWeight) => {
|
||||
onChangeFormatting({
|
||||
fontWeight: newWeight,
|
||||
});
|
||||
focusOnTextArea();
|
||||
},
|
||||
[onChangeFormatting],
|
||||
);
|
||||
const onChangeTextDecoration = useCallback(
|
||||
(newDecor: TextDecoration) => {
|
||||
onChangeFormatting({
|
||||
textDecoration: newDecor,
|
||||
});
|
||||
focusOnTextArea();
|
||||
},
|
||||
[onChangeFormatting],
|
||||
);
|
||||
const onChangeFontFamily = useCallback(
|
||||
(newFontFamily: string) => {
|
||||
onChangeFormatting({
|
||||
fontFamily: newFontFamily,
|
||||
});
|
||||
focusOnTextArea();
|
||||
},
|
||||
[onChangeFormatting],
|
||||
);
|
||||
const onChangeTextColor = useCallback(
|
||||
(newTextColor: string) => {
|
||||
onChangeFormatting({
|
||||
color: newTextColor,
|
||||
});
|
||||
},
|
||||
[onChangeFormatting],
|
||||
);
|
||||
const onChangeFontSize = useCallback(
|
||||
(newVal: number) => {
|
||||
onChangeFormatting({
|
||||
fontSize: newVal,
|
||||
});
|
||||
// focusOnTextArea();
|
||||
},
|
||||
[onChangeFormatting],
|
||||
);
|
||||
|
||||
const TextAreaStates = {
|
||||
style: {
|
||||
...formatData,
|
||||
width: "500px",
|
||||
height: "500px",
|
||||
},
|
||||
rows: 7,
|
||||
placeholder: "...",
|
||||
resize: "none" as ResizeType,
|
||||
onChange: handleOnChangeText,
|
||||
value: text,
|
||||
};
|
||||
const unionTextAreaProps = {
|
||||
const unionedTextAreaProps = {
|
||||
...TextareaProps,
|
||||
...TextAreaStates,
|
||||
};
|
||||
@@ -100,23 +134,7 @@ export const TextEditor = ({
|
||||
value={formatData.fontFamily}
|
||||
onChange={onChangeFontFamily}
|
||||
/>
|
||||
<Popover className="relative">
|
||||
<PopoverButton className="flex size-10 flex-col items-center gap-1 rounded-md border p-2">
|
||||
<FontAwesomeIcon icon={faFont} />
|
||||
<span
|
||||
className="h-1 w-full"
|
||||
style={{ backgroundColor: formatData.color }}
|
||||
/>
|
||||
</PopoverButton>
|
||||
<PopoverPanel anchor="bottom" className="flex flex-col">
|
||||
<HexColorPicker
|
||||
color={formatData.color}
|
||||
onChange={onChangeTextColor}
|
||||
/>
|
||||
<p>{formatData.color}</p>
|
||||
<CloseButton>OK</CloseButton>
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
<ColorPicker color={formatData.color} onChange={onChangeTextColor} />
|
||||
<InputNumber value={formatData.fontSize} onChange={onChangeFontSize} />
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
@@ -133,7 +151,7 @@ export const TextEditor = ({
|
||||
onChangeTextDecoration={onChangeTextDecoration}
|
||||
/>
|
||||
</div>
|
||||
<Textarea {...unionTextAreaProps} />
|
||||
<Textarea {...unionedTextAreaProps} ref={textAreaRef} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user