feat: wireframe-to-code (#7334)
This commit is contained in:
@ -1,7 +1,7 @@
|
||||
import React, { useState } from "react";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { ExcalidrawElement, ExcalidrawElementType } from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
import { useDevice } from "../components/App";
|
||||
import {
|
||||
@ -36,6 +36,8 @@ import {
|
||||
frameToolIcon,
|
||||
mermaidLogoIcon,
|
||||
laserPointerToolIcon,
|
||||
OpenAIIcon,
|
||||
MagicIcon,
|
||||
} from "./icons";
|
||||
import { KEYS } from "../keys";
|
||||
|
||||
@ -79,7 +81,8 @@ export const SelectedShapeActions = ({
|
||||
const showLinkIcon =
|
||||
targetElements.length === 1 || isSingleElementBoundContainer;
|
||||
|
||||
let commonSelectedType: string | null = targetElements[0]?.type || null;
|
||||
let commonSelectedType: ExcalidrawElementType | null =
|
||||
targetElements[0]?.type || null;
|
||||
|
||||
for (const element of targetElements) {
|
||||
if (element.type !== commonSelectedType) {
|
||||
@ -94,7 +97,8 @@ export const SelectedShapeActions = ({
|
||||
{((hasStrokeColor(appState.activeTool.type) &&
|
||||
appState.activeTool.type !== "image" &&
|
||||
commonSelectedType !== "image" &&
|
||||
commonSelectedType !== "frame") ||
|
||||
commonSelectedType !== "frame" &&
|
||||
commonSelectedType !== "magicframe") ||
|
||||
targetElements.some((element) => hasStrokeColor(element.type))) &&
|
||||
renderAction("changeStrokeColor")}
|
||||
</div>
|
||||
@ -331,6 +335,9 @@ export const ShapesSwitcher = ({
|
||||
>
|
||||
{t("toolBar.laser")}
|
||||
</DropdownMenu.Item>
|
||||
<div style={{ margin: "6px 0", fontSize: 14, fontWeight: 600 }}>
|
||||
Generate
|
||||
</div>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => app.setOpenDialog("mermaid")}
|
||||
icon={mermaidLogoIcon}
|
||||
@ -338,6 +345,25 @@ export const ShapesSwitcher = ({
|
||||
>
|
||||
{t("toolBar.mermaidToExcalidraw")}
|
||||
</DropdownMenu.Item>
|
||||
|
||||
{app.props.aiEnabled !== false && (
|
||||
<>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => app.onMagicButtonSelect()}
|
||||
icon={MagicIcon}
|
||||
data-testid="toolbar-magicframe"
|
||||
>
|
||||
{t("toolBar.magicframe")}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => app.setOpenDialog("magicSettings")}
|
||||
icon={OpenAIIcon}
|
||||
data-testid="toolbar-magicSettings"
|
||||
>
|
||||
{t("toolBar.magicSettings")}
|
||||
</DropdownMenu.Item>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,12 @@
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { CLASSES, DEFAULT_SIDEBAR, LIBRARY_SIDEBAR_WIDTH } from "../constants";
|
||||
import {
|
||||
CLASSES,
|
||||
DEFAULT_SIDEBAR,
|
||||
LIBRARY_SIDEBAR_WIDTH,
|
||||
TOOL_TYPE,
|
||||
} from "../constants";
|
||||
import { showSelectedShapeActions } from "../element";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { Language, t } from "../i18n";
|
||||
@ -56,6 +61,7 @@ import { mutateElement } from "../element/mutateElement";
|
||||
import { ShapeCache } from "../scene/ShapeCache";
|
||||
import Scene from "../scene/Scene";
|
||||
import { LaserPointerButton } from "./LaserTool/LaserPointerButton";
|
||||
import { MagicSettings } from "./MagicSettings";
|
||||
|
||||
interface LayerUIProps {
|
||||
actionManager: ActionManager;
|
||||
@ -77,6 +83,10 @@ interface LayerUIProps {
|
||||
children?: React.ReactNode;
|
||||
app: AppClassProperties;
|
||||
isCollaborating: boolean;
|
||||
openAIKey: string | null;
|
||||
isOpenAIKeyPersisted: boolean;
|
||||
onOpenAIAPIKeyChange: (apiKey: string, shouldPersist: boolean) => void;
|
||||
onMagicSettingsConfirm: (apiKey: string, shouldPersist: boolean) => void;
|
||||
}
|
||||
|
||||
const DefaultMainMenu: React.FC<{
|
||||
@ -133,6 +143,10 @@ const LayerUI = ({
|
||||
children,
|
||||
app,
|
||||
isCollaborating,
|
||||
openAIKey,
|
||||
isOpenAIKeyPersisted,
|
||||
onOpenAIAPIKeyChange,
|
||||
onMagicSettingsConfirm,
|
||||
}: LayerUIProps) => {
|
||||
const device = useDevice();
|
||||
const tunnels = useInitializeTunnels();
|
||||
@ -295,9 +309,11 @@ const LayerUI = ({
|
||||
>
|
||||
<LaserPointerButton
|
||||
title={t("toolBar.laser")}
|
||||
checked={appState.activeTool.type === "laser"}
|
||||
checked={
|
||||
appState.activeTool.type === TOOL_TYPE.laser
|
||||
}
|
||||
onChange={() =>
|
||||
app.setActiveTool({ type: "laser" })
|
||||
app.setActiveTool({ type: TOOL_TYPE.laser })
|
||||
}
|
||||
isMobile
|
||||
/>
|
||||
@ -439,6 +455,20 @@ const LayerUI = ({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{appState.openDialog === "magicSettings" && (
|
||||
<MagicSettings
|
||||
openAIKey={openAIKey}
|
||||
isPersisted={isOpenAIKeyPersisted}
|
||||
onChange={onOpenAIAPIKeyChange}
|
||||
onConfirm={(apiKey, shouldPersist) => {
|
||||
setAppState({ openDialog: null });
|
||||
onMagicSettingsConfirm(apiKey, shouldPersist);
|
||||
}}
|
||||
onClose={() => {
|
||||
setAppState({ openDialog: null });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ActiveConfirmDialog />
|
||||
<tunnels.OverwriteConfirmDialogTunnel.Out />
|
||||
{renderImageExportDialog()}
|
||||
|
38
src/components/MagicButton.tsx
Normal file
38
src/components/MagicButton.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import "./ToolIcon.scss";
|
||||
|
||||
import clsx from "clsx";
|
||||
import { ToolButtonSize } from "./ToolButton";
|
||||
|
||||
const DEFAULT_SIZE: ToolButtonSize = "small";
|
||||
|
||||
export const ElementCanvasButton = (props: {
|
||||
title?: string;
|
||||
icon: JSX.Element;
|
||||
name?: string;
|
||||
checked: boolean;
|
||||
onChange?(): void;
|
||||
isMobile?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<label
|
||||
className={clsx(
|
||||
"ToolIcon ToolIcon__MagicButton",
|
||||
`ToolIcon_size_${DEFAULT_SIZE}`,
|
||||
{
|
||||
"is-mobile": props.isMobile,
|
||||
},
|
||||
)}
|
||||
title={`${props.title}`}
|
||||
>
|
||||
<input
|
||||
className="ToolIcon_type_checkbox"
|
||||
type="checkbox"
|
||||
name={props.name}
|
||||
onChange={props.onChange}
|
||||
checked={props.checked}
|
||||
aria-label={props.title}
|
||||
/>
|
||||
<div className="ToolIcon__icon">{props.icon}</div>
|
||||
</label>
|
||||
);
|
||||
};
|
9
src/components/MagicSettings.scss
Normal file
9
src/components/MagicSettings.scss
Normal file
@ -0,0 +1,9 @@
|
||||
.excalidraw {
|
||||
.MagicSettings-confirm {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.MagicSettings__confirm {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
}
|
145
src/components/MagicSettings.tsx
Normal file
145
src/components/MagicSettings.tsx
Normal file
@ -0,0 +1,145 @@
|
||||
import { useState } from "react";
|
||||
import { Dialog } from "./Dialog";
|
||||
import { TextField } from "./TextField";
|
||||
import { MagicIcon, OpenAIIcon } from "./icons";
|
||||
|
||||
import "./MagicSettings.scss";
|
||||
import { FilledButton } from "./FilledButton";
|
||||
import { CheckboxItem } from "./CheckboxItem";
|
||||
import { KEYS } from "../keys";
|
||||
import { useUIAppState } from "../context/ui-appState";
|
||||
|
||||
const InlineButton = ({ icon }: { icon: JSX.Element }) => {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
width: "1em",
|
||||
margin: "0 0.5ex 0 0.5ex",
|
||||
display: "inline-block",
|
||||
lineHeight: 0,
|
||||
verticalAlign: "middle",
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export const MagicSettings = (props: {
|
||||
openAIKey: string | null;
|
||||
isPersisted: boolean;
|
||||
onChange: (key: string, shouldPersist: boolean) => void;
|
||||
onConfirm: (key: string, shouldPersist: boolean) => void;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { theme } = useUIAppState();
|
||||
const [keyInputValue, setKeyInputValue] = useState(props.openAIKey || "");
|
||||
const [shouldPersist, setShouldPersist] = useState<boolean>(
|
||||
props.isPersisted,
|
||||
);
|
||||
|
||||
const onConfirm = () => {
|
||||
props.onConfirm(keyInputValue.trim(), shouldPersist);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
onCloseRequest={() => {
|
||||
props.onClose();
|
||||
props.onConfirm(keyInputValue.trim(), shouldPersist);
|
||||
}}
|
||||
title={
|
||||
<div style={{ display: "flex" }}>
|
||||
Diagram to Code (AI){" "}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: "0.1rem 0.5rem",
|
||||
marginLeft: "1rem",
|
||||
fontSize: 14,
|
||||
borderRadius: "12px",
|
||||
background: theme === "light" ? "#FFCCCC" : "#703333",
|
||||
}}
|
||||
>
|
||||
Experimental
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
className="MagicSettings"
|
||||
autofocus={false}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
marginBottom: 0,
|
||||
}}
|
||||
>
|
||||
For the diagram-to-code feature we use{" "}
|
||||
<InlineButton icon={OpenAIIcon} />
|
||||
OpenAI.
|
||||
</p>
|
||||
<p>
|
||||
While the OpenAI API is in beta, its use is strictly limited — as such
|
||||
we require you use your own API key. You can create an{" "}
|
||||
<a
|
||||
href="https://platform.openai.com/login?launch"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
OpenAI account
|
||||
</a>
|
||||
, add a small credit (5 USD minimum), and{" "}
|
||||
<a
|
||||
href="https://platform.openai.com/api-keys"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
generate your own API key
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
Your OpenAI key does not leave the browser, and you can also set your
|
||||
own limit in your OpenAI account dashboard if needed.
|
||||
</p>
|
||||
<TextField
|
||||
isPassword
|
||||
value={keyInputValue}
|
||||
placeholder="Paste your API key here"
|
||||
label="OpenAI API key"
|
||||
onChange={(value) => {
|
||||
setKeyInputValue(value);
|
||||
props.onChange(value.trim(), shouldPersist);
|
||||
}}
|
||||
selectOnRender
|
||||
onKeyDown={(event) => event.key === KEYS.ENTER && onConfirm()}
|
||||
/>
|
||||
<p>
|
||||
By default, your API token is not persisted anywhere so you'll need to
|
||||
insert it again after reload. But, you can persist locally in your
|
||||
browser below.
|
||||
</p>
|
||||
|
||||
<CheckboxItem checked={shouldPersist} onChange={setShouldPersist}>
|
||||
Persist API key in browser storage
|
||||
</CheckboxItem>
|
||||
|
||||
<p>
|
||||
Once API key is set, you can use the <InlineButton icon={MagicIcon} />{" "}
|
||||
tool to wrap your elements in a frame that will then allow you to turn
|
||||
it into code. This dialog can be accessed using the <b>AI Settings</b>{" "}
|
||||
<InlineButton icon={OpenAIIcon} />.
|
||||
</p>
|
||||
|
||||
<FilledButton
|
||||
className="MagicSettings__confirm"
|
||||
size="large"
|
||||
label="Confirm"
|
||||
onClick={onConfirm}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
@ -94,7 +94,7 @@ export const PasteChartDialog = ({
|
||||
|
||||
const handleChartClick = (chartType: ChartType, elements: ChartElements) => {
|
||||
onInsertElements(elements);
|
||||
trackEvent("magic", "chart", chartType);
|
||||
trackEvent("paste", "chart", chartType);
|
||||
setAppState({
|
||||
currentChartType: chartType,
|
||||
pasteDialog: {
|
||||
|
@ -8,6 +8,7 @@ import Trans from "./Trans";
|
||||
import { LibraryItems, LibraryItem, UIAppState } from "../types";
|
||||
import { exportToCanvas, exportToSvg } from "../packages/utils";
|
||||
import {
|
||||
EDITOR_LS_KEYS,
|
||||
EXPORT_DATA_TYPES,
|
||||
EXPORT_SOURCE,
|
||||
MIME_TYPES,
|
||||
@ -19,6 +20,7 @@ import { chunk } from "../utils";
|
||||
import DialogActionButton from "./DialogActionButton";
|
||||
import { CloseIcon } from "./icons";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
import { EditorLocalStorage } from "../data/EditorLocalStorage";
|
||||
|
||||
import "./PublishLibrary.scss";
|
||||
|
||||
@ -31,34 +33,6 @@ interface PublishLibraryDataParams {
|
||||
website: string;
|
||||
}
|
||||
|
||||
const LOCAL_STORAGE_KEY_PUBLISH_LIBRARY = "publish-library-data";
|
||||
|
||||
const savePublishLibDataToStorage = (data: PublishLibraryDataParams) => {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
LOCAL_STORAGE_KEY_PUBLISH_LIBRARY,
|
||||
JSON.stringify(data),
|
||||
);
|
||||
} catch (error: any) {
|
||||
// Unable to access window.localStorage
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const importPublishLibDataFromStorage = () => {
|
||||
try {
|
||||
const data = localStorage.getItem(LOCAL_STORAGE_KEY_PUBLISH_LIBRARY);
|
||||
if (data) {
|
||||
return JSON.parse(data);
|
||||
}
|
||||
} catch (error: any) {
|
||||
// Unable to access localStorage
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const generatePreviewImage = async (libraryItems: LibraryItems) => {
|
||||
const MAX_ITEMS_PER_ROW = 6;
|
||||
const BOX_SIZE = 128;
|
||||
@ -255,7 +229,9 @@ const PublishLibrary = ({
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const data = importPublishLibDataFromStorage();
|
||||
const data = EditorLocalStorage.get<PublishLibraryDataParams>(
|
||||
EDITOR_LS_KEYS.PUBLISH_LIBRARY,
|
||||
);
|
||||
if (data) {
|
||||
setLibraryData(data);
|
||||
}
|
||||
@ -328,7 +304,7 @@ const PublishLibrary = ({
|
||||
if (response.ok) {
|
||||
return response.json().then(({ url }) => {
|
||||
// flush data from local storage
|
||||
localStorage.removeItem(LOCAL_STORAGE_KEY_PUBLISH_LIBRARY);
|
||||
EditorLocalStorage.delete(EDITOR_LS_KEYS.PUBLISH_LIBRARY);
|
||||
onSuccess({
|
||||
url,
|
||||
authorName: libraryData.authorName,
|
||||
@ -384,7 +360,7 @@ const PublishLibrary = ({
|
||||
|
||||
const onDialogClose = useCallback(() => {
|
||||
updateItemsInStorage(clonedLibItems);
|
||||
savePublishLibDataToStorage(libraryData);
|
||||
EditorLocalStorage.set(EDITOR_LS_KEYS.PUBLISH_LIBRARY, libraryData);
|
||||
onClose();
|
||||
}, [clonedLibItems, onClose, updateItemsInStorage, libraryData]);
|
||||
|
||||
|
@ -4,12 +4,15 @@ import {
|
||||
useImperativeHandle,
|
||||
KeyboardEvent,
|
||||
useLayoutEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import "./TextField.scss";
|
||||
import { Button } from "./Button";
|
||||
import { eyeIcon, eyeClosedIcon } from "./icons";
|
||||
|
||||
export type TextFieldProps = {
|
||||
type TextFieldProps = {
|
||||
value?: string;
|
||||
|
||||
onChange?: (value: string) => void;
|
||||
@ -22,6 +25,7 @@ export type TextFieldProps = {
|
||||
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
isPassword?: boolean;
|
||||
};
|
||||
|
||||
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
@ -35,6 +39,7 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
readonly,
|
||||
selectOnRender,
|
||||
onKeyDown,
|
||||
isPassword = false,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
@ -48,6 +53,8 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
}
|
||||
}, [selectOnRender]);
|
||||
|
||||
const [isVisible, setIsVisible] = useState<boolean>(true);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx("ExcTextField", {
|
||||
@ -64,14 +71,22 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
})}
|
||||
>
|
||||
<input
|
||||
type={isPassword && isVisible ? "password" : undefined}
|
||||
readOnly={readonly}
|
||||
type="text"
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
ref={innerRef}
|
||||
onChange={(event) => onChange?.(event.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
{isPassword && (
|
||||
<Button
|
||||
onSelect={() => setIsVisible(!isVisible)}
|
||||
style={{ border: 0 }}
|
||||
>
|
||||
{isVisible ? eyeIcon : eyeClosedIcon}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -175,7 +175,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ToolIcon__LaserPointer .ToolIcon__icon {
|
||||
.ToolIcon__LaserPointer .ToolIcon__icon,
|
||||
.ToolIcon__MagicButton .ToolIcon__icon {
|
||||
width: var(--default-button-size);
|
||||
height: var(--default-button-size);
|
||||
}
|
||||
|
@ -189,8 +189,6 @@ const getRelevantAppStateProps = (
|
||||
suggestedBindings: appState.suggestedBindings,
|
||||
isRotating: appState.isRotating,
|
||||
elementsToHighlight: appState.elementsToHighlight,
|
||||
openSidebar: appState.openSidebar,
|
||||
showHyperlinkPopup: appState.showHyperlinkPopup,
|
||||
collaborators: appState.collaborators, // Necessary for collab. sessions
|
||||
activeEmbeddable: appState.activeEmbeddable,
|
||||
snapLines: appState.snapLines,
|
||||
|
@ -1688,3 +1688,57 @@ export const laserPointerToolIcon = createIcon(
|
||||
|
||||
20,
|
||||
);
|
||||
|
||||
export const MagicIcon = createIcon(
|
||||
<g stroke="currentColor" fill="none">
|
||||
<path stroke="none" d="M0 0h24v24H0z" />
|
||||
<path d="M6 21l15 -15l-3 -3l-15 15l3 3" />
|
||||
<path d="M15 6l3 3" />
|
||||
<path d="M9 3a2 2 0 0 0 2 2a2 2 0 0 0 -2 2a2 2 0 0 0 -2 -2a2 2 0 0 0 2 -2" />
|
||||
<path d="M19 13a2 2 0 0 0 2 2a2 2 0 0 0 -2 2a2 2 0 0 0 -2 -2a2 2 0 0 0 2 -2" />
|
||||
</g>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
export const OpenAIIcon = createIcon(
|
||||
<g stroke="currentColor" fill="none">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M11.217 19.384a3.501 3.501 0 0 0 6.783 -1.217v-5.167l-6 -3.35" />
|
||||
<path d="M5.214 15.014a3.501 3.501 0 0 0 4.446 5.266l4.34 -2.534v-6.946" />
|
||||
<path d="M6 7.63c-1.391 -.236 -2.787 .395 -3.534 1.689a3.474 3.474 0 0 0 1.271 4.745l4.263 2.514l6 -3.348" />
|
||||
<path d="M12.783 4.616a3.501 3.501 0 0 0 -6.783 1.217v5.067l6 3.45" />
|
||||
<path d="M18.786 8.986a3.501 3.501 0 0 0 -4.446 -5.266l-4.34 2.534v6.946" />
|
||||
<path d="M18 16.302c1.391 .236 2.787 -.395 3.534 -1.689a3.474 3.474 0 0 0 -1.271 -4.745l-4.308 -2.514l-5.955 3.42" />
|
||||
</g>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
export const fullscreenIcon = createIcon(
|
||||
<g stroke="currentColor" fill="none">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M4 8v-2a2 2 0 0 1 2 -2h2" />
|
||||
<path d="M4 16v2a2 2 0 0 0 2 2h2" />
|
||||
<path d="M16 4h2a2 2 0 0 1 2 2v2" />
|
||||
<path d="M16 20h2a2 2 0 0 0 2 -2v-2" />
|
||||
</g>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
export const eyeIcon = createIcon(
|
||||
<g stroke="currentColor" fill="none">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||
<path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" />
|
||||
</g>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
export const eyeClosedIcon = createIcon(
|
||||
<g stroke="currentColor" fill="none">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" />
|
||||
<path d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87" />
|
||||
<path d="M3 3l18 18" />
|
||||
</g>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
Reference in New Issue
Block a user