Compare commits
6 Commits
fix/json-t
...
feat/tabs-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf792f1495 | ||
|
|
df3210a663 | ||
|
|
bad7730c32 | ||
|
|
adb6a78549 | ||
|
|
8cc27f20c3 | ||
|
|
8e49a3f5f1 |
152
components/ContextMenu/index.tsx
Normal file
152
components/ContextMenu/index.tsx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { CaretRight, Check, Circle } from "phosphor-react";
|
||||||
|
import { FC, Fragment, ReactNode } from "react";
|
||||||
|
import { Flex, Text } from "../";
|
||||||
|
import {
|
||||||
|
ContextMenuCheckboxItem,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuItemIndicator,
|
||||||
|
ContextMenuLabel,
|
||||||
|
ContextMenuRadioGroup,
|
||||||
|
ContextMenuRadioItem,
|
||||||
|
ContextMenuRoot,
|
||||||
|
ContextMenuSeparator,
|
||||||
|
ContextMenuTrigger,
|
||||||
|
ContextMenuTriggerItem,
|
||||||
|
} from "./primitive";
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: "Show bookmarks",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: true,
|
||||||
|
indicator: "*",
|
||||||
|
onCheckedChange: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "radio",
|
||||||
|
label: "People",
|
||||||
|
value: "pedro",
|
||||||
|
onValueChange: () => {},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "pedro",
|
||||||
|
label: "Pedro Duarte",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "colm",
|
||||||
|
label: "Colm Tuite",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export type TextOption = {
|
||||||
|
type: "text";
|
||||||
|
label: ReactNode;
|
||||||
|
onClick?: () => any;
|
||||||
|
children?: Option[];
|
||||||
|
};
|
||||||
|
export type SeparatorOption = { type: "separator" };
|
||||||
|
export type CheckboxOption = {
|
||||||
|
type: "checkbox";
|
||||||
|
label: ReactNode;
|
||||||
|
checked?: boolean;
|
||||||
|
onCheckedChange?: (isChecked: boolean) => any;
|
||||||
|
};
|
||||||
|
export type RadioOption<T extends string = string> = {
|
||||||
|
type: "radio";
|
||||||
|
label: ReactNode;
|
||||||
|
onValueChange?: (value: string) => any;
|
||||||
|
value: T;
|
||||||
|
options?: { value: T; label?: ReactNode }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type WithCommons = { key: string; disabled?: boolean };
|
||||||
|
|
||||||
|
type Option = (TextOption | SeparatorOption | CheckboxOption | RadioOption) &
|
||||||
|
WithCommons;
|
||||||
|
|
||||||
|
interface IContextMenu {
|
||||||
|
options?: Option[];
|
||||||
|
isNested?: boolean;
|
||||||
|
}
|
||||||
|
export const ContextMenu: FC<IContextMenu> = ({
|
||||||
|
children,
|
||||||
|
options,
|
||||||
|
isNested,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<ContextMenuRoot>
|
||||||
|
{isNested ? (
|
||||||
|
<ContextMenuTriggerItem>{children}</ContextMenuTriggerItem>
|
||||||
|
) : (
|
||||||
|
<ContextMenuTrigger>{children}</ContextMenuTrigger>
|
||||||
|
)}
|
||||||
|
{options && (
|
||||||
|
<ContextMenuContent sideOffset={isNested ? 2 : 5}>
|
||||||
|
{options.map(({ key, ...option }) => {
|
||||||
|
if (option.type === "text") {
|
||||||
|
const { children, label } = option;
|
||||||
|
if (children)
|
||||||
|
return (
|
||||||
|
<ContextMenu isNested key={key} options={children}>
|
||||||
|
<Flex fluid row justify="space-between" align="center">
|
||||||
|
<Text>{label}</Text>
|
||||||
|
<CaretRight />
|
||||||
|
</Flex>
|
||||||
|
</ContextMenu>
|
||||||
|
);
|
||||||
|
return <ContextMenuItem key={key}>{label}</ContextMenuItem>;
|
||||||
|
}
|
||||||
|
if (option.type === "checkbox") {
|
||||||
|
const { label, checked, onCheckedChange } = option;
|
||||||
|
return (
|
||||||
|
<ContextMenuCheckboxItem
|
||||||
|
key={key}
|
||||||
|
checked={checked}
|
||||||
|
onCheckedChange={onCheckedChange}
|
||||||
|
>
|
||||||
|
<Flex row align="center">
|
||||||
|
<ContextMenuItemIndicator>
|
||||||
|
<Check />
|
||||||
|
</ContextMenuItemIndicator>
|
||||||
|
<Text css={{ ml: checked ? "$4" : undefined }}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</ContextMenuCheckboxItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (option.type === "radio") {
|
||||||
|
const { label, options, onValueChange, value } = option;
|
||||||
|
return (
|
||||||
|
<Fragment key={key}>
|
||||||
|
<ContextMenuLabel>{label}</ContextMenuLabel>
|
||||||
|
<ContextMenuRadioGroup
|
||||||
|
value={value}
|
||||||
|
onValueChange={onValueChange}
|
||||||
|
>
|
||||||
|
{options?.map(({ value: v, label }) => {
|
||||||
|
return (
|
||||||
|
<ContextMenuRadioItem key={v} value={v}>
|
||||||
|
<ContextMenuItemIndicator>
|
||||||
|
<Circle weight="fill" />
|
||||||
|
</ContextMenuItemIndicator>
|
||||||
|
<Text css={{ ml: "$4" }}>{label}</Text>
|
||||||
|
</ContextMenuRadioItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ContextMenuRadioGroup>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <ContextMenuSeparator key={key} />;
|
||||||
|
})}
|
||||||
|
</ContextMenuContent>
|
||||||
|
)}
|
||||||
|
</ContextMenuRoot>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContextMenu;
|
||||||
107
components/ContextMenu/primitive.tsx
Normal file
107
components/ContextMenu/primitive.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
|
||||||
|
import { styled } from "../../stitches.config";
|
||||||
|
import {
|
||||||
|
slideDownAndFade,
|
||||||
|
slideLeftAndFade,
|
||||||
|
slideRightAndFade,
|
||||||
|
slideUpAndFade,
|
||||||
|
} from "../../styles/keyframes";
|
||||||
|
|
||||||
|
const StyledContent = styled(ContextMenuPrimitive.Content, {
|
||||||
|
minWidth: 140,
|
||||||
|
backgroundColor: "$backgroundOverlay",
|
||||||
|
borderRadius: 6,
|
||||||
|
overflow: "hidden",
|
||||||
|
padding: "5px",
|
||||||
|
boxShadow:
|
||||||
|
"0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)",
|
||||||
|
"@media (prefers-reduced-motion: no-preference)": {
|
||||||
|
animationDuration: "400ms",
|
||||||
|
animationTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)",
|
||||||
|
willChange: "transform, opacity",
|
||||||
|
'&[data-state="open"]': {
|
||||||
|
'&[data-side="top"]': { animationName: slideDownAndFade },
|
||||||
|
'&[data-side="right"]': { animationName: slideLeftAndFade },
|
||||||
|
'&[data-side="bottom"]': { animationName: slideUpAndFade },
|
||||||
|
'&[data-side="left"]': { animationName: slideRightAndFade },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
".dark &": {
|
||||||
|
boxShadow:
|
||||||
|
"0px 10px 38px -10px rgba(22, 23, 24, 0.85), 0px 10px 20px -15px rgba(22, 23, 24, 0.6)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemStyles = {
|
||||||
|
all: "unset",
|
||||||
|
fontSize: 13,
|
||||||
|
lineHeight: 1,
|
||||||
|
color: "$text",
|
||||||
|
borderRadius: 3,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
height: 28,
|
||||||
|
padding: "0 7px",
|
||||||
|
position: "relative",
|
||||||
|
paddingLeft: 10,
|
||||||
|
userSelect: "none",
|
||||||
|
|
||||||
|
"&[data-disabled]": {
|
||||||
|
color: "$textMuted",
|
||||||
|
pointerEvents: "none",
|
||||||
|
},
|
||||||
|
|
||||||
|
"&:focus": {
|
||||||
|
backgroundColor: "$purple9",
|
||||||
|
color: "$white",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledItem = styled(ContextMenuPrimitive.Item, { ...itemStyles });
|
||||||
|
const StyledCheckboxItem = styled(ContextMenuPrimitive.CheckboxItem, {
|
||||||
|
...itemStyles,
|
||||||
|
});
|
||||||
|
const StyledRadioItem = styled(ContextMenuPrimitive.RadioItem, {
|
||||||
|
...itemStyles,
|
||||||
|
});
|
||||||
|
const StyledTriggerItem = styled(ContextMenuPrimitive.TriggerItem, {
|
||||||
|
'&[data-state="open"]': {
|
||||||
|
backgroundColor: "$purple9",
|
||||||
|
color: "$purple9",
|
||||||
|
},
|
||||||
|
...itemStyles,
|
||||||
|
});
|
||||||
|
|
||||||
|
const StyledLabel = styled(ContextMenuPrimitive.Label, {
|
||||||
|
paddingLeft: 10,
|
||||||
|
fontSize: 12,
|
||||||
|
lineHeight: "25px",
|
||||||
|
color: "$text",
|
||||||
|
});
|
||||||
|
|
||||||
|
const StyledSeparator = styled(ContextMenuPrimitive.Separator, {
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: "$backgroundAlt",
|
||||||
|
margin: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
const StyledItemIndicator = styled(ContextMenuPrimitive.ItemIndicator, {
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
width: 25,
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ContextMenuRoot = ContextMenuPrimitive.Root;
|
||||||
|
export const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
|
||||||
|
export const ContextMenuContent = StyledContent;
|
||||||
|
export const ContextMenuItem = StyledItem;
|
||||||
|
export const ContextMenuCheckboxItem = StyledCheckboxItem;
|
||||||
|
export const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
|
||||||
|
export const ContextMenuRadioItem = StyledRadioItem;
|
||||||
|
export const ContextMenuItemIndicator = StyledItemIndicator;
|
||||||
|
export const ContextMenuTriggerItem = StyledTriggerItem;
|
||||||
|
export const ContextMenuLabel = StyledLabel;
|
||||||
|
export const ContextMenuSeparator = StyledSeparator;
|
||||||
@@ -13,7 +13,7 @@ import state from "../state";
|
|||||||
import wat from "../utils/wat-highlight";
|
import wat from "../utils/wat-highlight";
|
||||||
|
|
||||||
import EditorNavigation from "./EditorNavigation";
|
import EditorNavigation from "./EditorNavigation";
|
||||||
import { Button, Text, Link, Flex } from ".";
|
import { Button, Text, Link, Flex, Tabs, Tab } from ".";
|
||||||
import Monaco from "./Monaco";
|
import Monaco from "./Monaco";
|
||||||
|
|
||||||
const FILESIZE_BREAKPOINTS: [number, number] = [2 * 1024, 5 * 1024];
|
const FILESIZE_BREAKPOINTS: [number, number] = [2 * 1024, 5 * 1024];
|
||||||
@@ -25,9 +25,20 @@ const DeployEditor = () => {
|
|||||||
|
|
||||||
const [showContent, setShowContent] = useState(false);
|
const [showContent, setShowContent] = useState(false);
|
||||||
|
|
||||||
const activeFile = snap.files[snap.active]?.compiledContent
|
const compiledFiles = snap.files.filter(file => file.compiledContent);
|
||||||
? snap.files[snap.active]
|
const activeFile = compiledFiles[snap.activeWat];
|
||||||
: snap.files.filter(file => file.compiledContent)[0];
|
|
||||||
|
const renderNav = () => (
|
||||||
|
<Tabs
|
||||||
|
activeIndex={snap.activeWat}
|
||||||
|
onChangeActive={idx => (state.activeWat = idx)}
|
||||||
|
>
|
||||||
|
{compiledFiles.map((file, index) => {
|
||||||
|
return <Tab key={file.name} header={`${file.name}.wat`} />;
|
||||||
|
})}
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
|
||||||
const compiledSize = activeFile?.compiledContent?.byteLength || 0;
|
const compiledSize = activeFile?.compiledContent?.byteLength || 0;
|
||||||
const color =
|
const color =
|
||||||
compiledSize > FILESIZE_BREAKPOINTS[1]
|
compiledSize > FILESIZE_BREAKPOINTS[1]
|
||||||
@@ -38,7 +49,7 @@ const DeployEditor = () => {
|
|||||||
|
|
||||||
const isContentChanged =
|
const isContentChanged =
|
||||||
activeFile && activeFile.compiledValueSnapshot !== activeFile.content;
|
activeFile && activeFile.compiledValueSnapshot !== activeFile.content;
|
||||||
// const hasDeployErros = activeFile && activeFile.containsErrors;
|
// const hasDeployErrors = activeFile && activeFile.containsErrors;
|
||||||
|
|
||||||
const CompiledStatView = activeFile && (
|
const CompiledStatView = activeFile && (
|
||||||
<Flex
|
<Flex
|
||||||
@@ -99,6 +110,7 @@ const DeployEditor = () => {
|
|||||||
</NextLink>
|
</NextLink>
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|
||||||
const isContent =
|
const isContent =
|
||||||
snap.files?.filter(file => file.compiledWatContent).length > 0 &&
|
snap.files?.filter(file => file.compiledWatContent).length > 0 &&
|
||||||
router.isReady;
|
router.isReady;
|
||||||
@@ -113,7 +125,7 @@ const DeployEditor = () => {
|
|||||||
width: "100%",
|
width: "100%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<EditorNavigation showWat />
|
<EditorNavigation renderNav={renderNav} />
|
||||||
<Container
|
<Container
|
||||||
css={{
|
css={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
|||||||
@@ -1,27 +1,7 @@
|
|||||||
import { keyframes } from "@stitches/react";
|
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
|
|
||||||
import { styled } from "../stitches.config";
|
import { styled } from "../stitches.config";
|
||||||
|
import { slideDownAndFade, slideLeftAndFade, slideRightAndFade, slideUpAndFade } from '../styles/keyframes';
|
||||||
const slideUpAndFade = keyframes({
|
|
||||||
"0%": { opacity: 0, transform: "translateY(2px)" },
|
|
||||||
"100%": { opacity: 1, transform: "translateY(0)" },
|
|
||||||
});
|
|
||||||
|
|
||||||
const slideRightAndFade = keyframes({
|
|
||||||
"0%": { opacity: 0, transform: "translateX(-2px)" },
|
|
||||||
"100%": { opacity: 1, transform: "translateX(0)" },
|
|
||||||
});
|
|
||||||
|
|
||||||
const slideDownAndFade = keyframes({
|
|
||||||
"0%": { opacity: 0, transform: "translateY(-2px)" },
|
|
||||||
"100%": { opacity: 1, transform: "translateY(0)" },
|
|
||||||
});
|
|
||||||
|
|
||||||
const slideLeftAndFade = keyframes({
|
|
||||||
"0%": { opacity: 0, transform: "translateX(2px)" },
|
|
||||||
"100%": { opacity: 1, transform: "translateX(0)" },
|
|
||||||
});
|
|
||||||
|
|
||||||
const StyledContent = styled(DropdownMenuPrimitive.Content, {
|
const StyledContent = styled(DropdownMenuPrimitive.Content, {
|
||||||
minWidth: 220,
|
minWidth: 220,
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import React, { useState, useEffect, useCallback, useRef } from "react";
|
import React, {
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
ReactNode,
|
||||||
|
} from "react";
|
||||||
import {
|
import {
|
||||||
Plus,
|
|
||||||
Share,
|
Share,
|
||||||
DownloadSimple,
|
DownloadSimple,
|
||||||
Gear,
|
Gear,
|
||||||
@@ -28,7 +32,6 @@ import { useSnapshot } from "valtio";
|
|||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createNewFile,
|
|
||||||
syncToGist,
|
syncToGist,
|
||||||
updateEditorSettings,
|
updateEditorSettings,
|
||||||
downloadAsZip,
|
downloadAsZip,
|
||||||
@@ -48,36 +51,23 @@ import {
|
|||||||
import Flex from "./Flex";
|
import Flex from "./Flex";
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
import { Input, Label } from "./Input";
|
import { Input, Label } from "./Input";
|
||||||
import Text from "./Text";
|
|
||||||
import Tooltip from "./Tooltip";
|
import Tooltip from "./Tooltip";
|
||||||
import { styled } from "../stitches.config";
|
|
||||||
import { showAlert } from "../state/actions/showAlert";
|
import { showAlert } from "../state/actions/showAlert";
|
||||||
|
|
||||||
const ErrorText = styled(Text, {
|
|
||||||
color: "$error",
|
|
||||||
mt: "$1",
|
|
||||||
display: "block",
|
|
||||||
});
|
|
||||||
|
|
||||||
const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
|
const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
|
||||||
const snap = useSnapshot(state);
|
const snap = useSnapshot(state);
|
||||||
const [editorSettingsOpen, setEditorSettingsOpen] = useState(false);
|
const [editorSettingsOpen, setEditorSettingsOpen] = useState(false);
|
||||||
const [isNewfileDialogOpen, setIsNewfileDialogOpen] = useState(false);
|
|
||||||
const [newfileError, setNewfileError] = useState<string | null>(null);
|
|
||||||
const [filename, setFilename] = useState("");
|
|
||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession();
|
||||||
const [popup, setPopUp] = useState(false);
|
const [popup, setPopUp] = useState(false);
|
||||||
const [editorSettings, setEditorSettings] = useState(snap.editorSettings);
|
const [editorSettings, setEditorSettings] = useState(snap.editorSettings);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (session && session.user && popup) {
|
if (session && session.user && popup) {
|
||||||
setPopUp(false);
|
setPopUp(false);
|
||||||
}
|
}
|
||||||
}, [session, popup]);
|
}, [session, popup]);
|
||||||
|
|
||||||
// when filename changes, reset error
|
|
||||||
useEffect(() => {
|
|
||||||
setNewfileError(null);
|
|
||||||
}, [filename, setNewfileError]);
|
|
||||||
|
|
||||||
const showNewGistAlert = () => {
|
const showNewGistAlert = () => {
|
||||||
showAlert("Are you sure?", {
|
showAlert("Are you sure?", {
|
||||||
@@ -95,46 +85,8 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateFilename = useCallback(
|
|
||||||
(filename: string): { error: string | null } => {
|
|
||||||
// check if filename already exists
|
|
||||||
if (!filename) {
|
|
||||||
return { error: "You need to add filename" };
|
|
||||||
}
|
|
||||||
if (snap.files.find((file) => file.name === filename)) {
|
|
||||||
return { error: "Filename already exists." };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!filename.includes(".") || filename[filename.length - 1] === ".") {
|
|
||||||
return { error: "Filename should include file extension" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for illegal characters
|
|
||||||
const ALPHA_NUMERICAL_REGEX = /^[A-Za-z0-9_-]+[.][A-Za-z0-9]{1,4}$/g;
|
|
||||||
if (!filename.match(ALPHA_NUMERICAL_REGEX)) {
|
|
||||||
return {
|
|
||||||
error: `Filename can contain only characters from a-z, A-Z, 0-9, "_" and "-" and it needs to have file extension (e.g. ".c")`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return { error: null };
|
|
||||||
},
|
|
||||||
[snap.files]
|
|
||||||
);
|
|
||||||
const handleConfirm = useCallback(() => {
|
|
||||||
// add default extension in case omitted
|
|
||||||
const chk = validateFilename(filename);
|
|
||||||
if (chk && chk.error) {
|
|
||||||
setNewfileError(`Error: ${chk.error}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsNewfileDialogOpen(false);
|
|
||||||
createNewFile(filename);
|
|
||||||
setFilename("");
|
|
||||||
}, [filename, setIsNewfileDialogOpen, setFilename, validateFilename]);
|
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const files = snap.files;
|
|
||||||
return (
|
return (
|
||||||
<Flex css={{ flexShrink: 0, gap: "$0" }}>
|
<Flex css={{ flexShrink: 0, gap: "$0" }}>
|
||||||
<Flex
|
<Flex
|
||||||
@@ -174,131 +126,14 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
|
|||||||
scrollbarWidth: "thin",
|
scrollbarWidth: "thin",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
onWheelCapture={(e) => {
|
onWheelCapture={e => {
|
||||||
if (scrollRef.current) {
|
if (scrollRef.current) {
|
||||||
scrollRef.current.scrollLeft += e.deltaY;
|
scrollRef.current.scrollLeft += e.deltaY;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Container css={{ flex: 1 }} ref={containerRef}>
|
<Container css={{ flex: 1 }} ref={containerRef}>
|
||||||
<Stack
|
{renderNav?.()}
|
||||||
css={{
|
|
||||||
gap: "$3",
|
|
||||||
flex: 1,
|
|
||||||
flexWrap: "nowrap",
|
|
||||||
marginBottom: "-1px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{files &&
|
|
||||||
files.length > 0 &&
|
|
||||||
files.map((file, index) => {
|
|
||||||
if (!file.compiledContent && showWat) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
outline={
|
|
||||||
showWat ? snap.activeWat !== index : snap.active !== index
|
|
||||||
}
|
|
||||||
onClick={() => (state.active = index)}
|
|
||||||
key={file.name + index}
|
|
||||||
css={{
|
|
||||||
"&:hover": {
|
|
||||||
span: {
|
|
||||||
visibility: "visible",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{file.name}
|
|
||||||
{showWat && ".wat"}
|
|
||||||
{!showWat && (
|
|
||||||
<Box
|
|
||||||
as="span"
|
|
||||||
css={{
|
|
||||||
display: "flex",
|
|
||||||
p: "2px",
|
|
||||||
borderRadius: "$full",
|
|
||||||
mr: "-4px",
|
|
||||||
"&:hover": {
|
|
||||||
// boxSizing: "0px 0px 1px",
|
|
||||||
backgroundColor: "$mauve2",
|
|
||||||
color: "$mauve12",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
onClick={(ev: React.MouseEvent<HTMLElement>) => {
|
|
||||||
ev.stopPropagation();
|
|
||||||
// Remove file from state
|
|
||||||
state.files.splice(index, 1);
|
|
||||||
// Change active file state
|
|
||||||
// If deleted file is behind active tab
|
|
||||||
// we keep the current state otherwise
|
|
||||||
// select previous file on the list
|
|
||||||
state.active =
|
|
||||||
index > snap.active ? snap.active : snap.active - 1;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<X size="9px" weight="bold" />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{!showWat && (
|
|
||||||
<Dialog
|
|
||||||
open={isNewfileDialogOpen}
|
|
||||||
onOpenChange={setIsNewfileDialogOpen}
|
|
||||||
>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button
|
|
||||||
ghost
|
|
||||||
size="sm"
|
|
||||||
css={{ alignItems: "center", px: "$2", mr: "$3" }}
|
|
||||||
>
|
|
||||||
<Plus size="16px" />{" "}
|
|
||||||
{snap.files.length === 0 && "Add new file"}
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogTitle>Create new file</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
<Label>Filename</Label>
|
|
||||||
<Input
|
|
||||||
value={filename}
|
|
||||||
onChange={(e) => setFilename(e.target.value)}
|
|
||||||
onKeyPress={(e) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
handleConfirm();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ErrorText>{newfileError}</ErrorText>
|
|
||||||
</DialogDescription>
|
|
||||||
|
|
||||||
<Flex
|
|
||||||
css={{
|
|
||||||
marginTop: 25,
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
gap: "$3",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button outline>Cancel</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<Button variant="primary" onClick={handleConfirm}>
|
|
||||||
Create file
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
|
|
||||||
<X size="20px" />
|
|
||||||
</Box>
|
|
||||||
</DialogClose>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Container>
|
</Container>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex
|
<Flex
|
||||||
@@ -542,8 +377,8 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
|
|||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
value={editorSettings.tabSize}
|
value={editorSettings.tabSize}
|
||||||
onChange={(e) =>
|
onChange={e =>
|
||||||
setEditorSettings((curr) => ({
|
setEditorSettings(curr => ({
|
||||||
...curr,
|
...curr,
|
||||||
tabSize: Number(e.target.value),
|
tabSize: Number(e.target.value),
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useRouter } from "next/router";
|
|||||||
|
|
||||||
import Box from "./Box";
|
import Box from "./Box";
|
||||||
import Container from "./Container";
|
import Container from "./Container";
|
||||||
import { saveFile } from "../state/actions";
|
import { createNewFile, saveFile } from "../state/actions";
|
||||||
import { apiHeaderFiles } from "../state/constants";
|
import { apiHeaderFiles } from "../state/constants";
|
||||||
import state from "../state";
|
import state from "../state";
|
||||||
|
|
||||||
@@ -20,7 +20,8 @@ import ReconnectingWebSocket from "reconnecting-websocket";
|
|||||||
|
|
||||||
import docs from "../xrpl-hooks-docs/docs";
|
import docs from "../xrpl-hooks-docs/docs";
|
||||||
import Monaco from "./Monaco";
|
import Monaco from "./Monaco";
|
||||||
import { saveAllFiles } from '../state/actions/saveFile';
|
import { saveAllFiles } from "../state/actions/saveFile";
|
||||||
|
import { Tab, Tabs } from "./Tabs";
|
||||||
|
|
||||||
const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => {
|
const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => {
|
||||||
const currPath = editor.getModel()?.uri.path;
|
const currPath = editor.getModel()?.uri.path;
|
||||||
@@ -119,6 +120,26 @@ const HooksEditor = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const file = snap.files[snap.active];
|
const file = snap.files[snap.active];
|
||||||
|
|
||||||
|
const renderNav = () => (
|
||||||
|
<Tabs
|
||||||
|
label="File"
|
||||||
|
activeIndex={snap.active}
|
||||||
|
onChangeActive={idx => (state.active = idx)}
|
||||||
|
extensionRequired
|
||||||
|
onCreateNewTab={createNewFile}
|
||||||
|
onCloseTab={idx => state.files.splice(idx, 1)}
|
||||||
|
headerExtraValidation={{
|
||||||
|
regex: /^[A-Za-z0-9_-]+[.][A-Za-z0-9]{1,4}$/g,
|
||||||
|
error:
|
||||||
|
'Filename can contain only characters from a-z, A-Z, 0-9, "_" and "-"',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{snap.files.map((file, index) => {
|
||||||
|
return <Tab key={file.name} header={file.name} />;
|
||||||
|
})}
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
css={{
|
css={{
|
||||||
@@ -131,7 +152,7 @@ const HooksEditor = () => {
|
|||||||
width: "100%",
|
width: "100%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<EditorNavigation />
|
<EditorNavigation renderNav={renderNav} />
|
||||||
{snap.files.length > 0 && router.isReady ? (
|
{snap.files.length > 0 && router.isReady ? (
|
||||||
<Monaco
|
<Monaco
|
||||||
keepCurrentModel
|
keepCurrentModel
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
import { TTS, tts } from "../utils/hookOnCalculator";
|
import { TTS, tts } from "../utils/hookOnCalculator";
|
||||||
import { deployHook } from "../state/actions";
|
import { deployHook } from "../state/actions";
|
||||||
import { useSnapshot } from "valtio";
|
import { useSnapshot } from "valtio";
|
||||||
import state, { SelectOption } from "../state";
|
import state, { IFile, SelectOption } from "../state";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { prepareDeployHookTx, sha256 } from "../state/actions/deployHook";
|
import { prepareDeployHookTx, sha256 } from "../state/actions/deployHook";
|
||||||
import estimateFee from "../utils/estimateFee";
|
import estimateFee from "../utils/estimateFee";
|
||||||
@@ -56,9 +56,9 @@ export type SetHookData = {
|
|||||||
export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
|
export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
|
||||||
({ accountAddress }) => {
|
({ accountAddress }) => {
|
||||||
const snap = useSnapshot(state);
|
const snap = useSnapshot(state);
|
||||||
const activeFile = snap.files[snap.active]?.compiledContent
|
const compiledFiles = snap.files.filter(file => file.compiledContent);
|
||||||
? snap.files[snap.active]
|
const activeFile = compiledFiles[snap.activeWat] as IFile | undefined;
|
||||||
: snap.files.filter(file => file.compiledContent)[0];
|
|
||||||
const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false);
|
const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false);
|
||||||
|
|
||||||
const accountOptions: SelectOption[] = snap.accounts.map(acc => ({
|
const accountOptions: SelectOption[] = snap.accounts.map(acc => ({
|
||||||
@@ -72,6 +72,15 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
|
|||||||
const account = snap.accounts.find(
|
const account = snap.accounts.find(
|
||||||
acc => acc.address === selectedAccount?.value
|
acc => acc.address === selectedAccount?.value
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getHookNamespace = useCallback(
|
||||||
|
() =>
|
||||||
|
activeFile && snap.deployValues[activeFile.name]
|
||||||
|
? snap.deployValues[activeFile.name].HookNamespace
|
||||||
|
: activeFile?.name.split(".")[0] || "",
|
||||||
|
[activeFile, snap.deployValues]
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -81,13 +90,10 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
|
|||||||
getValues,
|
getValues,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<SetHookData>({
|
} = useForm<SetHookData>({
|
||||||
defaultValues: snap.deployValues?.[activeFile?.name]
|
defaultValues: (activeFile && snap.deployValues[activeFile.name]) || {
|
||||||
? snap.deployValues[activeFile?.name]
|
HookNamespace: activeFile?.name.split(".")[0] || "",
|
||||||
: {
|
Invoke: transactionOptions.filter(to => to.label === "ttPAYMENT"),
|
||||||
HookNamespace:
|
},
|
||||||
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || "",
|
|
||||||
Invoke: transactionOptions.filter(to => to.label === "ttPAYMENT"),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
const { fields, append, remove } = useFieldArray({
|
const { fields, append, remove } = useFieldArray({
|
||||||
control,
|
control,
|
||||||
@@ -97,20 +103,15 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
|
|||||||
const [estimateLoading, setEstimateLoading] = useState(false);
|
const [estimateLoading, setEstimateLoading] = useState(false);
|
||||||
const watchedFee = watch("Fee");
|
const watchedFee = watch("Fee");
|
||||||
|
|
||||||
// Update value if activeWat changes
|
// Update value if activeFile changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const defaultValue = snap.deployValues?.[activeFile?.name]
|
if (!activeFile) return;
|
||||||
? snap.deployValues?.[activeFile?.name].HookNamespace
|
const defaultValue = getHookNamespace();
|
||||||
: snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || "";
|
|
||||||
setValue("HookNamespace", defaultValue);
|
setValue("HookNamespace", defaultValue);
|
||||||
setFormInitialized(true);
|
setFormInitialized(true);
|
||||||
}, [
|
}, [setValue, activeFile, snap.deployValues, getHookNamespace]);
|
||||||
snap.activeWat,
|
|
||||||
snap.files,
|
|
||||||
setValue,
|
|
||||||
activeFile?.name,
|
|
||||||
snap.deployValues,
|
|
||||||
]);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
watchedFee &&
|
watchedFee &&
|
||||||
@@ -128,21 +129,19 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
|
|||||||
// name: "HookGrants", // unique name for your Field Array
|
// name: "HookGrants", // unique name for your Field Array
|
||||||
// });
|
// });
|
||||||
const [hashedNamespace, setHashedNamespace] = useState("");
|
const [hashedNamespace, setHashedNamespace] = useState("");
|
||||||
const namespace = watch(
|
|
||||||
"HookNamespace",
|
const namespace = watch("HookNamespace", getHookNamespace());
|
||||||
snap.deployValues?.[activeFile?.name]
|
|
||||||
? snap.deployValues?.[activeFile?.name].HookNamespace
|
|
||||||
: snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || ""
|
|
||||||
);
|
|
||||||
const calculateHashedValue = useCallback(async () => {
|
const calculateHashedValue = useCallback(async () => {
|
||||||
const hashedVal = await sha256(namespace);
|
const hashedVal = await sha256(namespace);
|
||||||
setHashedNamespace(hashedVal.toUpperCase());
|
setHashedNamespace(hashedVal.toUpperCase());
|
||||||
}, [namespace]);
|
}, [namespace]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
calculateHashedValue();
|
calculateHashedValue();
|
||||||
}, [namespace, calculateHashedValue]);
|
}, [namespace, calculateHashedValue]);
|
||||||
|
|
||||||
// Calcucate initial fee estimate when modal opens
|
// Calculate initial fee estimate when modal opens
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (formInitialized && account) {
|
if (formInitialized && account) {
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -161,18 +160,15 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
|
|||||||
}, [formInitialized]);
|
}, [formInitialized]);
|
||||||
|
|
||||||
const tooLargeFile = () => {
|
const tooLargeFile = () => {
|
||||||
const activeFile = snap.files[snap.active].compiledContent
|
|
||||||
? snap.files[snap.active]
|
|
||||||
: snap.files.filter(file => file.compiledContent)[0];
|
|
||||||
return Boolean(
|
return Boolean(
|
||||||
activeFile?.compiledContent?.byteLength &&
|
activeFile?.compiledContent?.byteLength &&
|
||||||
activeFile?.compiledContent?.byteLength >= 64000
|
activeFile?.compiledContent?.byteLength >= 64000
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<SetHookData> = async (data) => {
|
const onSubmit: SubmitHandler<SetHookData> = async data => {
|
||||||
const currAccount = state.accounts.find(
|
const currAccount = state.accounts.find(
|
||||||
(acc) => acc.address === account?.address
|
acc => acc.address === account?.address
|
||||||
);
|
);
|
||||||
if (!account) return;
|
if (!account) return;
|
||||||
if (currAccount) currAccount.isLoading = true;
|
if (currAccount) currAccount.isLoading = true;
|
||||||
@@ -194,10 +190,7 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
|
|||||||
uppercase
|
uppercase
|
||||||
variant={"secondary"}
|
variant={"secondary"}
|
||||||
disabled={
|
disabled={
|
||||||
!account ||
|
!account || account.isLoading || !activeFile || tooLargeFile()
|
||||||
account.isLoading ||
|
|
||||||
!snap.files.filter(file => file.compiledWatContent).length ||
|
|
||||||
tooLargeFile()
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Set Hook
|
Set Hook
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
} from "./Dialog";
|
} from "./Dialog";
|
||||||
import { Plus, X } from "phosphor-react";
|
import { Plus, X } from "phosphor-react";
|
||||||
import { styled } from "../stitches.config";
|
import { styled } from "../stitches.config";
|
||||||
|
import { capitalize } from "../utils/helpers";
|
||||||
|
import ContextMenu from "./ContextMenu";
|
||||||
|
|
||||||
const ErrorText = styled(Text, {
|
const ErrorText = styled(Text, {
|
||||||
color: "$error",
|
color: "$error",
|
||||||
@@ -25,11 +27,11 @@ const ErrorText = styled(Text, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
interface TabProps {
|
interface TabProps {
|
||||||
header?: string;
|
header: string;
|
||||||
children: ReactNode;
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO customise messages shown
|
// TODO customize messages shown
|
||||||
interface Props {
|
interface Props {
|
||||||
label?: string;
|
label?: string;
|
||||||
activeIndex?: number;
|
activeIndex?: number;
|
||||||
@@ -38,8 +40,12 @@ interface Props {
|
|||||||
children: ReactElement<TabProps>[];
|
children: ReactElement<TabProps>[];
|
||||||
keepAllAlive?: boolean;
|
keepAllAlive?: boolean;
|
||||||
defaultExtension?: string;
|
defaultExtension?: string;
|
||||||
appendDefaultExtension?: boolean;
|
extensionRequired?: boolean;
|
||||||
allowedExtensions?: string[];
|
allowedExtensions?: string[];
|
||||||
|
headerExtraValidation?: {
|
||||||
|
regex: string | RegExp;
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
onCreateNewTab?: (name: string) => any;
|
onCreateNewTab?: (name: string) => any;
|
||||||
onCloseTab?: (index: number, header?: string) => any;
|
onCloseTab?: (index: number, header?: string) => any;
|
||||||
onChangeActive?: (index: number, header?: string) => any;
|
onChangeActive?: (index: number, header?: string) => any;
|
||||||
@@ -57,8 +63,9 @@ export const Tabs = ({
|
|||||||
onCreateNewTab,
|
onCreateNewTab,
|
||||||
onCloseTab,
|
onCloseTab,
|
||||||
onChangeActive,
|
onChangeActive,
|
||||||
|
headerExtraValidation,
|
||||||
|
extensionRequired,
|
||||||
defaultExtension = "",
|
defaultExtension = "",
|
||||||
appendDefaultExtension = false,
|
|
||||||
allowedExtensions,
|
allowedExtensions,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [active, setActive] = useState(activeIndex || 0);
|
const [active, setActive] = useState(activeIndex || 0);
|
||||||
@@ -87,16 +94,38 @@ export const Tabs = ({
|
|||||||
|
|
||||||
const validateTabname = useCallback(
|
const validateTabname = useCallback(
|
||||||
(tabname: string): { error: string | null } => {
|
(tabname: string): { error: string | null } => {
|
||||||
if (tabs.find(tab => tab.header === tabname)) {
|
if (!tabname) {
|
||||||
return { error: "Name already exists." };
|
return { error: `Please enter ${label.toLocaleLowerCase()} name.` };
|
||||||
|
}
|
||||||
|
if (tabs.find(tab => tab.header === tabname)) {
|
||||||
|
return { error: `${capitalize(label)} name already exists.` };
|
||||||
|
}
|
||||||
|
const ext =
|
||||||
|
(tabname.includes(".") && tabname.split(".").pop()) ||
|
||||||
|
defaultExtension ||
|
||||||
|
"";
|
||||||
|
if (extensionRequired && !ext) {
|
||||||
|
return { error: "File extension is required!" };
|
||||||
}
|
}
|
||||||
const ext = tabname.split(".").pop() || "";
|
|
||||||
if (allowedExtensions && !allowedExtensions.includes(ext)) {
|
if (allowedExtensions && !allowedExtensions.includes(ext)) {
|
||||||
return { error: "This file extension is not allowed!" };
|
return { error: "This file extension is not allowed!" };
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
headerExtraValidation &&
|
||||||
|
!tabname.match(headerExtraValidation.regex)
|
||||||
|
) {
|
||||||
|
return { error: headerExtraValidation.error };
|
||||||
|
}
|
||||||
return { error: null };
|
return { error: null };
|
||||||
},
|
},
|
||||||
[allowedExtensions, tabs]
|
[
|
||||||
|
allowedExtensions,
|
||||||
|
defaultExtension,
|
||||||
|
extensionRequired,
|
||||||
|
headerExtraValidation,
|
||||||
|
label,
|
||||||
|
tabs,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleActiveChange = useCallback(
|
const handleActiveChange = useCallback(
|
||||||
@@ -108,31 +137,25 @@ export const Tabs = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleCreateTab = useCallback(() => {
|
const handleCreateTab = useCallback(() => {
|
||||||
// add default extension in case omitted
|
const chk = validateTabname(tabname);
|
||||||
let _tabname = tabname.includes(".")
|
|
||||||
? tabname
|
|
||||||
: `${tabname}.${defaultExtension}`;
|
|
||||||
if (appendDefaultExtension && !_tabname.endsWith(defaultExtension)) {
|
|
||||||
_tabname = `${_tabname}.${defaultExtension}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chk = validateTabname(_tabname);
|
|
||||||
if (chk.error) {
|
if (chk.error) {
|
||||||
setNewtabError(`Error: ${chk.error}`);
|
setNewtabError(`Error: ${chk.error}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let _tabname = tabname;
|
||||||
|
if (defaultExtension && !_tabname.endsWith(defaultExtension)) {
|
||||||
|
_tabname = `${_tabname}.${defaultExtension}`;
|
||||||
|
}
|
||||||
|
|
||||||
setIsNewtabDialogOpen(false);
|
setIsNewtabDialogOpen(false);
|
||||||
setTabname("");
|
setTabname("");
|
||||||
|
|
||||||
onCreateNewTab?.(_tabname);
|
onCreateNewTab?.(_tabname);
|
||||||
|
|
||||||
// switch to new tab?
|
|
||||||
handleActiveChange(tabs.length, _tabname);
|
handleActiveChange(tabs.length, _tabname);
|
||||||
}, [
|
}, [
|
||||||
tabname,
|
tabname,
|
||||||
defaultExtension,
|
defaultExtension,
|
||||||
appendDefaultExtension,
|
|
||||||
validateTabname,
|
validateTabname,
|
||||||
onCreateNewTab,
|
onCreateNewTab,
|
||||||
handleActiveChange,
|
handleActiveChange,
|
||||||
@@ -146,8 +169,10 @@ export const Tabs = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
onCloseTab?.(idx, tabs[idx].header);
|
onCloseTab?.(idx, tabs[idx].header);
|
||||||
|
|
||||||
|
handleActiveChange(idx, tabs[idx].header);
|
||||||
},
|
},
|
||||||
[active, onCloseTab, tabs]
|
[active, handleActiveChange, onCloseTab, tabs]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -164,46 +189,47 @@ export const Tabs = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tabs.map((tab, idx) => (
|
{tabs.map((tab, idx) => (
|
||||||
<Button
|
<ContextMenu key={tab.header}>
|
||||||
key={tab.header}
|
<Button
|
||||||
role="tab"
|
role="tab"
|
||||||
tabIndex={idx}
|
tabIndex={idx}
|
||||||
onClick={() => handleActiveChange(idx, tab.header)}
|
onClick={() => handleActiveChange(idx, tab.header)}
|
||||||
onKeyPress={() => handleActiveChange(idx, tab.header)}
|
onKeyPress={() => handleActiveChange(idx, tab.header)}
|
||||||
outline={active !== idx}
|
outline={active !== idx}
|
||||||
size="sm"
|
size="sm"
|
||||||
css={{
|
css={{
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
span: {
|
span: {
|
||||||
visibility: "visible",
|
visibility: "visible",
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tab.header || idx}
|
|
||||||
{onCloseTab && (
|
|
||||||
<Box
|
|
||||||
as="span"
|
|
||||||
css={{
|
|
||||||
display: "flex",
|
|
||||||
p: "2px",
|
|
||||||
borderRadius: "$full",
|
|
||||||
mr: "-4px",
|
|
||||||
"&:hover": {
|
|
||||||
// boxSizing: "0px 0px 1px",
|
|
||||||
backgroundColor: "$mauve2",
|
|
||||||
color: "$mauve12",
|
|
||||||
},
|
},
|
||||||
}}
|
},
|
||||||
onClick={(ev: React.MouseEvent<HTMLElement>) => {
|
}}
|
||||||
ev.stopPropagation();
|
>
|
||||||
handleCloseTab(idx);
|
{tab.header || idx}
|
||||||
}}
|
{onCloseTab && (
|
||||||
>
|
<Box
|
||||||
<X size="9px" weight="bold" />
|
as="span"
|
||||||
</Box>
|
css={{
|
||||||
)}
|
display: "flex",
|
||||||
</Button>
|
p: "2px",
|
||||||
|
borderRadius: "$full",
|
||||||
|
mr: "-4px",
|
||||||
|
"&:hover": {
|
||||||
|
// boxSizing: "0px 0px 1px",
|
||||||
|
backgroundColor: "$mauve2",
|
||||||
|
color: "$mauve12",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={(ev: React.MouseEvent<HTMLElement>) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
handleCloseTab(idx);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X size="9px" weight="bold" />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</ContextMenu>
|
||||||
))}
|
))}
|
||||||
{onCreateNewTab && (
|
{onCreateNewTab && (
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -216,11 +242,14 @@ export const Tabs = ({
|
|||||||
size="sm"
|
size="sm"
|
||||||
css={{ alignItems: "center", px: "$2", mr: "$3" }}
|
css={{ alignItems: "center", px: "$2", mr: "$3" }}
|
||||||
>
|
>
|
||||||
<Plus size="16px" /> {tabs.length === 0 && `Add new ${label.toLocaleLowerCase()}`}
|
<Plus size="16px" />{" "}
|
||||||
|
{tabs.length === 0 && `Add new ${label.toLocaleLowerCase()}`}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogTitle>Create new {label.toLocaleLowerCase()}</DialogTitle>
|
<DialogTitle>
|
||||||
|
Create new {label.toLocaleLowerCase()}
|
||||||
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
<Label>{label} name</Label>
|
<Label>{label} name</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -259,29 +288,32 @@ export const Tabs = ({
|
|||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
{keepAllAlive ? (
|
{keepAllAlive
|
||||||
tabs.map((tab, idx) => {
|
? tabs.map((tab, idx) => {
|
||||||
// TODO Maybe rule out fragments as children
|
// TODO Maybe rule out fragments as children
|
||||||
if (!isValidElement(tab.children)) {
|
if (!isValidElement(tab.children)) {
|
||||||
if (active !== idx) return null;
|
if (active !== idx) return null;
|
||||||
return tab.children;
|
return tab.children;
|
||||||
}
|
}
|
||||||
let key = tab.children.key || tab.header || idx;
|
let key = tab.children.key || tab.header || idx;
|
||||||
let { children } = tab;
|
let { children } = tab;
|
||||||
let { style, ...props } = children.props;
|
let { style, ...props } = children.props;
|
||||||
return (
|
return (
|
||||||
<children.type
|
<children.type
|
||||||
key={key}
|
key={key}
|
||||||
{...props}
|
{...props}
|
||||||
style={{ ...style, display: active !== idx ? "none" : undefined }}
|
style={{
|
||||||
/>
|
...style,
|
||||||
);
|
display: active !== idx ? "none" : undefined,
|
||||||
})
|
}}
|
||||||
) : (
|
/>
|
||||||
<Fragment key={tabs[active].header || active}>
|
);
|
||||||
{tabs[active].children}
|
})
|
||||||
</Fragment>
|
: tabs[active] && (
|
||||||
)}
|
<Fragment key={tabs[active].header || active}>
|
||||||
|
{tabs[active].children}
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
10
package.json
10
package.json
@@ -16,8 +16,9 @@
|
|||||||
"@octokit/core": "^3.5.1",
|
"@octokit/core": "^3.5.1",
|
||||||
"@radix-ui/colors": "^0.1.7",
|
"@radix-ui/colors": "^0.1.7",
|
||||||
"@radix-ui/react-alert-dialog": "^0.1.1",
|
"@radix-ui/react-alert-dialog": "^0.1.1",
|
||||||
|
"@radix-ui/react-context-menu": "^0.1.6",
|
||||||
"@radix-ui/react-dialog": "^0.1.1",
|
"@radix-ui/react-dialog": "^0.1.1",
|
||||||
"@radix-ui/react-dropdown-menu": "^0.1.1",
|
"@radix-ui/react-dropdown-menu": "^0.1.6",
|
||||||
"@radix-ui/react-id": "^0.1.1",
|
"@radix-ui/react-id": "^0.1.1",
|
||||||
"@radix-ui/react-label": "^0.1.5",
|
"@radix-ui/react-label": "^0.1.5",
|
||||||
"@radix-ui/react-popover": "^0.1.6",
|
"@radix-ui/react-popover": "^0.1.6",
|
||||||
@@ -35,7 +36,7 @@
|
|||||||
"lodash.xor": "^4.5.0",
|
"lodash.xor": "^4.5.0",
|
||||||
"monaco-editor": "^0.33.0",
|
"monaco-editor": "^0.33.0",
|
||||||
"next": "^12.0.4",
|
"next": "^12.0.4",
|
||||||
"next-auth": "^4.0.0-beta.5",
|
"next-auth": "^4.10.1",
|
||||||
"next-plausible": "^3.2.0",
|
"next-plausible": "^3.2.0",
|
||||||
"next-themes": "^0.1.1",
|
"next-themes": "^0.1.1",
|
||||||
"normalize-url": "^7.0.2",
|
"normalize-url": "^7.0.2",
|
||||||
@@ -75,5 +76,8 @@
|
|||||||
"eslint-config-next": "11.1.2",
|
"eslint-config-next": "11.1.2",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"typescript": "4.4.4"
|
"typescript": "4.4.4"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"ripple-binary-codec": "=1.4.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type monaco from "monaco-editor";
|
import type monaco from "monaco-editor";
|
||||||
import { proxy, ref, subscribe } from "valtio";
|
import { proxy, ref, subscribe } from "valtio";
|
||||||
import { devtools } from 'valtio/utils';
|
import { devtools, subscribeKey } from 'valtio/utils';
|
||||||
import { XrplClient } from "xrpl-client";
|
import { XrplClient } from "xrpl-client";
|
||||||
import { SplitSize } from "./actions/persistSplits";
|
import { SplitSize } from "./actions/persistSplits";
|
||||||
|
|
||||||
@@ -168,16 +168,23 @@ if (process.env.NODE_ENV !== "production") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
subscribe(state, () => {
|
subscribe(state.accounts, () => {
|
||||||
const { accounts, active } = state;
|
const { accounts } = state;
|
||||||
const accountsNoLoading = accounts.map(acc => ({ ...acc, isLoading: false }))
|
const accountsNoLoading = accounts.map(acc => ({ ...acc, isLoading: false }))
|
||||||
localStorage.setItem("hooksIdeAccounts", JSON.stringify(accountsNoLoading));
|
localStorage.setItem("hooksIdeAccounts", JSON.stringify(accountsNoLoading));
|
||||||
if (!state.files[active]?.compiledWatContent) {
|
|
||||||
state.activeWat = 0;
|
|
||||||
} else {
|
|
||||||
state.activeWat = active;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const updateActiveWat = () => {
|
||||||
|
const filename = state.files[state.active]?.name
|
||||||
|
|
||||||
|
const compiledFiles = state.files.filter(
|
||||||
|
file => file.compiledContent)
|
||||||
|
const idx = compiledFiles.findIndex(file => file.name === filename)
|
||||||
|
|
||||||
|
if (idx !== -1) state.activeWat = idx
|
||||||
|
}
|
||||||
|
subscribeKey(state, 'active', updateActiveWat)
|
||||||
|
subscribeKey(state, 'files', updateActiveWat)
|
||||||
}
|
}
|
||||||
export default state
|
export default state
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export const {
|
|||||||
accent: "#9D2DFF",
|
accent: "#9D2DFF",
|
||||||
background: "$gray1",
|
background: "$gray1",
|
||||||
backgroundAlt: "$gray4",
|
backgroundAlt: "$gray4",
|
||||||
|
backgroundOverlay: "$mauve2",
|
||||||
text: "$gray12",
|
text: "$gray12",
|
||||||
textMuted: "$gray10",
|
textMuted: "$gray10",
|
||||||
primary: "$plum",
|
primary: "$plum",
|
||||||
@@ -365,6 +366,7 @@ export const darkTheme = createTheme("dark", {
|
|||||||
...greenDark,
|
...greenDark,
|
||||||
...redDark,
|
...redDark,
|
||||||
deep: "rgb(10, 10, 10)",
|
deep: "rgb(10, 10, 10)",
|
||||||
|
backgroundOverlay: "$mauve5"
|
||||||
// backgroundA: transparentize(0.1, grayDark.gray1),
|
// backgroundA: transparentize(0.1, grayDark.gray1),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
21
styles/keyframes.ts
Normal file
21
styles/keyframes.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { keyframes } from '../stitches.config';
|
||||||
|
|
||||||
|
export const slideUpAndFade = keyframes({
|
||||||
|
"0%": { opacity: 0, transform: "translateY(2px)" },
|
||||||
|
"100%": { opacity: 1, transform: "translateY(0)" },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const slideRightAndFade = keyframes({
|
||||||
|
"0%": { opacity: 0, transform: "translateX(-2px)" },
|
||||||
|
"100%": { opacity: 1, transform: "translateX(0)" },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const slideDownAndFade = keyframes({
|
||||||
|
"0%": { opacity: 0, transform: "translateY(-2px)" },
|
||||||
|
"100%": { opacity: 1, transform: "translateY(0)" },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const slideLeftAndFade = keyframes({
|
||||||
|
"0%": { opacity: 0, transform: "translateX(2px)" },
|
||||||
|
"100%": { opacity: 1, transform: "translateX(0)" },
|
||||||
|
});
|
||||||
40
yarn.lock
40
yarn.lock
@@ -594,6 +594,18 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
|
|
||||||
|
"@radix-ui/react-context-menu@^0.1.6":
|
||||||
|
version "0.1.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-context-menu/-/react-context-menu-0.1.6.tgz#0c75f2faffec6c8697247a4b685a432b3c4d07f0"
|
||||||
|
integrity sha512-0qa6ABaeqD+WYI+8iT0jH0QLLcV8Kv0xI+mZL4FFnG4ec9H0v+yngb5cfBBfs9e/KM8mDzFFpaeegqsQlLNqyQ==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.13.10"
|
||||||
|
"@radix-ui/primitive" "0.1.0"
|
||||||
|
"@radix-ui/react-context" "0.1.1"
|
||||||
|
"@radix-ui/react-menu" "0.1.6"
|
||||||
|
"@radix-ui/react-primitive" "0.1.4"
|
||||||
|
"@radix-ui/react-use-callback-ref" "0.1.0"
|
||||||
|
|
||||||
"@radix-ui/react-context@0.1.1":
|
"@radix-ui/react-context@0.1.1":
|
||||||
version "0.1.1"
|
version "0.1.1"
|
||||||
resolved "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-0.1.1.tgz"
|
resolved "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-0.1.1.tgz"
|
||||||
@@ -635,9 +647,9 @@
|
|||||||
"@radix-ui/react-use-callback-ref" "0.1.0"
|
"@radix-ui/react-use-callback-ref" "0.1.0"
|
||||||
"@radix-ui/react-use-escape-keydown" "0.1.0"
|
"@radix-ui/react-use-escape-keydown" "0.1.0"
|
||||||
|
|
||||||
"@radix-ui/react-dropdown-menu@^0.1.1":
|
"@radix-ui/react-dropdown-menu@^0.1.6":
|
||||||
version "0.1.6"
|
version "0.1.6"
|
||||||
resolved "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-0.1.6.tgz"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-0.1.6.tgz#3203229788cd57e552c9f19dcc7008e2b545919c"
|
||||||
integrity sha512-RZhtzjWwJ4ZBN7D8ek4Zn+ilHzYuYta9yIxFnbC0pfqMnSi67IQNONo1tuuNqtFh9SRHacPKc65zo+kBBlxtdg==
|
integrity sha512-RZhtzjWwJ4ZBN7D8ek4Zn+ilHzYuYta9yIxFnbC0pfqMnSi67IQNONo1tuuNqtFh9SRHacPKc65zo+kBBlxtdg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
@@ -2941,10 +2953,10 @@ natural-compare@^1.4.0:
|
|||||||
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
|
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
|
||||||
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
|
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
|
||||||
|
|
||||||
next-auth@^4.0.0-beta.5:
|
next-auth@^4.10.1:
|
||||||
version "4.2.1"
|
version "4.10.1"
|
||||||
resolved "https://registry.npmjs.org/next-auth/-/next-auth-4.2.1.tgz"
|
resolved "https://registry.yarnpkg.com/next-auth/-/next-auth-4.10.1.tgz#33b29265d12287bb2f6d267c8d415a407c27f0e9"
|
||||||
integrity sha512-XDtt7nqevkNf4EJ2zKAKkI+MFsURf11kx11vPwxrBYA1MHeqWwaWbGOUOI2ekNTvfAg4nTEJJUH3LV2cLrH3Tg==
|
integrity sha512-F00vtwBdyMIIJ8IORHOAOHjVGTDEhhm9+HpB2BQ8r40WtGxqToWWLN7Z+2ZW/z2RFlo3zhcuAtUCPUzVJxtZwQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.16.3"
|
"@babel/runtime" "^7.16.3"
|
||||||
"@panva/hkdf" "^1.0.1"
|
"@panva/hkdf" "^1.0.1"
|
||||||
@@ -3638,19 +3650,7 @@ ripple-address-codec@^4.2.4:
|
|||||||
base-x "3.0.9"
|
base-x "3.0.9"
|
||||||
create-hash "^1.1.2"
|
create-hash "^1.1.2"
|
||||||
|
|
||||||
ripple-binary-codec@^1.1.3:
|
ripple-binary-codec@=1.4.2, ripple-binary-codec@^0.2.4, ripple-binary-codec@^1.1.3, ripple-binary-codec@^1.4.2:
|
||||||
version "1.3.2"
|
|
||||||
resolved "https://registry.npmjs.org/ripple-binary-codec/-/ripple-binary-codec-1.3.2.tgz"
|
|
||||||
integrity sha512-8VG1vfb3EM1J7ZdPXo9E57Zv2hF4cxT64gP6rGSQzODVgMjiBCWozhN3729qNTGtHItz0e82Oix8v95vWYBQ3A==
|
|
||||||
dependencies:
|
|
||||||
assert "^2.0.0"
|
|
||||||
big-integer "^1.6.48"
|
|
||||||
buffer "5.6.0"
|
|
||||||
create-hash "^1.2.0"
|
|
||||||
decimal.js "^10.2.0"
|
|
||||||
ripple-address-codec "^4.2.3"
|
|
||||||
|
|
||||||
ripple-binary-codec@^1.4.2:
|
|
||||||
version "1.4.2"
|
version "1.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/ripple-binary-codec/-/ripple-binary-codec-1.4.2.tgz#cdc35353e4bc7c3a704719247c82b4c4d0b57dd3"
|
resolved "https://registry.yarnpkg.com/ripple-binary-codec/-/ripple-binary-codec-1.4.2.tgz#cdc35353e4bc7c3a704719247c82b4c4d0b57dd3"
|
||||||
integrity sha512-EDKIyZMa/6Ay/oNgCwjD9b9CJv0zmBreeHVQeG4BYwy+9GPnIQjNeT5e/aB6OjAnhcmpgbPeBmzwmNVwzxlt0w==
|
integrity sha512-EDKIyZMa/6Ay/oNgCwjD9b9CJv0zmBreeHVQeG4BYwy+9GPnIQjNeT5e/aB6OjAnhcmpgbPeBmzwmNVwzxlt0w==
|
||||||
@@ -3685,7 +3685,7 @@ ripple-hashes@^0.3.4, ripple-hashes@latest:
|
|||||||
bignumber.js "^4.1.0"
|
bignumber.js "^4.1.0"
|
||||||
create-hash "^1.1.2"
|
create-hash "^1.1.2"
|
||||||
ripple-address-codec "^3.0.4"
|
ripple-address-codec "^3.0.4"
|
||||||
ripple-binary-codec "^1.4.2"
|
ripple-binary-codec "^0.2.4"
|
||||||
|
|
||||||
ripple-keypairs@^1.0.3, ripple-keypairs@latest:
|
ripple-keypairs@^1.0.3, ripple-keypairs@latest:
|
||||||
version "1.1.3"
|
version "1.1.3"
|
||||||
|
|||||||
Reference in New Issue
Block a user