Add final modal (#270)

Co-authored-by: Christopher Oliphant <RALifeCoach@users.noreply.github.com>
This commit is contained in:
FromTheLOLAshesandChristopher Oliphant authored and GitHub committed 2024-04-29 20:26:13 -04:00
1 parent 6a1d7ee5f5
commit 4ac9ca4d77
17 files changed
+475 -138

No files matched your search

+3 -3
View File
@@ -1,6 +1,5 @@
import MakeRequest from "../MakeRequest";
import { MediaFile } from "./GetMedia";
import { Pagination } from "../_common/SharedFetchTypes";
import { MediaFile, Pagination } from "~/pages/PageEnigma/models";
export interface GetMediaRequest {}
@@ -12,6 +11,7 @@ export interface GetMediaListResponse {
export interface GetMediaParams {
page_index: number;
filter_media_type: string;
}
export const GetMediaByUser = MakeRequest<
@@ -23,4 +23,4 @@ export const GetMediaByUser = MakeRequest<
method: "GET",
routingFunction: (userToken: string) =>
`/v1/media_files/list/user/${userToken}`,
});
});
+11 -2
View File
@@ -1,14 +1,23 @@
import { faFilm } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button } from "~/components";
import { activeJobs, viewMyMovies } from "~/pages/PageEnigma/store";
import {
activeJobs,
generateMovieId,
viewMyMovies,
} from "~/pages/PageEnigma/store";
export const MyMoviesButton = () => {
const activeCount = activeJobs.value.jobs.length;
return (
<div className="relative">
<Button variant="action" onClick={() => (viewMyMovies.value = true)}>
<Button
variant="action"
onClick={() => {
generateMovieId.value = "";
viewMyMovies.value = true;
}}>
<div className="relative flex items-center gap-2">
{activeCount > 0 ? (
<svg
+2 -2
View File
@@ -4,7 +4,7 @@ import { DragComponent } from "~/pages/PageEnigma/comps/DragComponent/DragCompon
import { EngineProvider } from "~/contexts/EngineProvider";
import { useInferenceJobManager } from "~/hooks";
import { LoadingBar } from "~/components";
import { MyMovies } from "~/pages/PageEnigma/comps/MyMovies/MyMovies";
import { GenerateModals } from "~/pages/PageEnigma/comps/GenerateModals/GenerateModals";
import { ErrorDialog } from "~/components/ErrorDialog";
export const PageEnigma = () => {
@@ -14,7 +14,7 @@ export const PageEnigma = () => {
<EngineProvider>
<PageEnigmaComponent />
<DragComponent />
<MyMovies />
<GenerateModals />
<ErrorDialog />
</EngineProvider>
<LoadingBar
@@ -5,14 +5,17 @@ import { useRef, useState } from "react";
import { BucketConfig } from "~/api/BucketConfig";
import dayjs from "dayjs";
import Tooltip from "~/components/Tooltip";
import { environmentVariables } from "~/store";
interface Props {
movie: MediaInfo;
setMovieId: (id: string) => void;
}
export function CompletedCard({ movie }: Props) {
export function CompletedCard({ movie, setMovieId }: Props) {
const bucketConfig = useRef<BucketConfig>(new BucketConfig());
const [loadError, setLoadError] = useState(false);
const downloadLink = `${environmentVariables.value.GOOGLE_API}/vocodes-public${movie.public_bucket_path}`;
const imageUrl = bucketConfig.current.getCdnUrl(
movie.public_bucket_path + "-thumb.gif",
@@ -23,9 +26,9 @@ export function CompletedCard({ movie }: Props) {
return (
<button
className="flex w-full items-center justify-between px-5 py-3 text-start transition-all duration-150 hover:bg-brand-secondary/40"
onClick={() =>
console.log("change current modal to completed video modal")
}>
onClick={() => {
setMovieId(movie.token);
}}>
<div className="flex gap-4">
<div className="rounded-lg">
<img
@@ -51,7 +54,9 @@ export function CompletedCard({ movie }: Props) {
<div className="pr-5">
<Tooltip content="Download" position="top">
<button
onClick={() => console.log("download")}
onClick={() => {
window.open(downloadLink, "_blank");
}}
className="text-xl text-white/50 transition-all duration-150 hover:text-white/90">
<FontAwesomeIcon icon={faArrowDownToLine} />
</button>
@@ -0,0 +1,35 @@
import { TransitionDialogue } from "~/components";
import { generateMovieId, viewMyMovies } from "~/pages/PageEnigma/store";
import { faFilm } from "@fortawesome/pro-solid-svg-icons";
import { useContext, useEffect, useState } from "react";
import { MyMovies } from "~/pages/PageEnigma/comps/GenerateModals/MyMovies";
import { Sharing } from "~/pages/PageEnigma/comps/GenerateModals/Sharing";
import { useSignals } from "@preact/signals-react/runtime";
import dayjs from "dayjs";
import { MediaFile } from "~/pages/PageEnigma/models";
import { GetMediaFileByToken } from "~/pages/PageEnigma/comps/SidePanelTabs/AudioTab/utilities";
import { ToasterContext, ToastTypes } from "~/contexts/ToasterContext";
export function GenerateModals() {
useSignals();
const [movieId, setMovieId] = useState(generateMovieId.value);
const [mediaFile, setMediaFile] = useState<MediaFile | null>(null);
const { addToast } = useContext(ToasterContext);
useEffect(() => {
if (movieId) {
GetMediaFileByToken(movieId).then((res) => {
if (!res.success) {
addToast(ToastTypes.ERROR, "Unable to read media file");
return;
}
setMediaFile(res.media_file ?? null);
});
}
}, [movieId, addToast]);
if (!mediaFile) {
return <MyMovies setMovieId={setMovieId} />;
}
return <Sharing mediaFile={mediaFile!} setMediaFile={setMediaFile} />;
}
@@ -1,11 +1,20 @@
import { TransitionDialogue } from "~/components";
import { activeJobs, movies, viewMyMovies } from "~/pages/PageEnigma/store";
import { CompletedCard } from "~/pages/PageEnigma/comps/MyMovies/CompletedCard";
import { InProgressCard } from "~/pages/PageEnigma/comps/MyMovies/InProgressCard";
import {
activeJobs,
generateMovieId,
movies,
viewMyMovies,
} from "~/pages/PageEnigma/store";
import { CompletedCard } from "~/pages/PageEnigma/comps/GenerateModals/CompletedCard";
import { InProgressCard } from "~/pages/PageEnigma/comps/GenerateModals/InProgressCard";
import { useSignals } from "@preact/signals-react/runtime";
import { faFilm } from "@fortawesome/pro-solid-svg-icons";
import { TransitionDialogue } from "~/components";
export function MyMovies() {
interface Props {
setMovieId: (page: string) => void;
}
export function MyMovies({ setMovieId }: Props) {
useSignals();
return (
@@ -15,7 +24,10 @@ export function MyMovies() {
className="max-w-4xl"
childPadding={false}
isOpen={viewMyMovies.value}
onClose={() => (viewMyMovies.value = false)}>
onClose={() => {
setMovieId("");
viewMyMovies.value = false;
}}>
<div className="h-[560px] overflow-y-auto overflow-x-hidden rounded-b-lg">
{activeJobs.value.jobs.length > 0 && (
<div className="mb-3">
@@ -29,7 +41,11 @@ export function MyMovies() {
<div className="mx-5 mb-1 font-medium">Completed</div>
<div className="flex flex-col">
{movies.value.movies.map((movie) => (
<CompletedCard key={movie.token} movie={movie} />
<CompletedCard
key={movie.token}
movie={movie}
setMovieId={setMovieId}
/>
))}
</div>
</div>
@@ -0,0 +1,133 @@
import { useSignals } from "@preact/signals-react/runtime";
import { useLayoutEffect, useState } from "react";
import { MediaFile } from "~/pages/PageEnigma/models";
import { Button, Input, TransitionDialogue } from "~/components";
import {
faArrowDownToLine,
faFilm,
faLink,
} from "@fortawesome/pro-solid-svg-icons";
import SocialButton from "./SocialButton";
import { generateMovieId, viewMyMovies } from "~/pages/PageEnigma/store";
import dayjs from "dayjs";
import { environmentVariables } from "~/store";
interface Props {
mediaFile: MediaFile;
setMediaFile: (file: MediaFile | null) => void;
}
export function Sharing({ mediaFile, setMediaFile }: Props) {
useSignals();
const shareUrl = `https://storyteller.ai/media/${mediaFile?.token || ""}`;
const shareText = "Check out this media on StoryTeller.ai";
const [buttonLabel, setButtonLabel] = useState("Copy");
const downloadLink = `${environmentVariables.value.GOOGLE_API}/vocodes-public${mediaFile?.public_bucket_path}`;
const openUrl = `/media/${mediaFile.token}`;
const handleCopyLink = () => {
if (navigator.clipboard) {
navigator.clipboard.writeText(shareUrl);
}
setButtonLabel("Copied!");
setTimeout(() => setButtonLabel("Copy"), 2000);
};
const generateTitle = () => {
return (
<div>
<span className="font-xl font-bold">
{mediaFile?.maybe_title ?? mediaFile?.token}
</span>
<span className="ml-2 text-sm text-white/60">
{dayjs(mediaFile?.updated_at).format("MMM DD, YYYY HH:mm:SS")}
</span>
</div>
);
};
return (
<TransitionDialogue
title={generateTitle()}
titleIcon={faFilm}
className="max-w-4xl"
childPadding={false}
isOpen={viewMyMovies.value}
width={1049}
onClose={() => {
if (generateMovieId.value) {
viewMyMovies.value = false;
return;
}
setMediaFile(null);
}}>
<div className="flex gap-[29px] px-5 pb-5">
<div className="w-[616px]">
<video controls width={616} crossOrigin="anonymous">
<source
src={`${environmentVariables.value.GOOGLE_API}/vocodes-public${mediaFile?.public_bucket_path}`}
type="video/mp4"
/>
Your browser does not support the video tag.
</video>
</div>
<div className="w-[355px]">
<div className="mb-2 text-sm">Share movie to</div>
<div className="flex w-full flex-wrap justify-between">
<SocialButton
social="x"
shareUrl={shareUrl}
shareText={shareText}
/>
<SocialButton
social="whatsapp"
shareUrl={shareUrl}
shareText={shareText}
/>
<SocialButton
social="facebook"
shareUrl={shareUrl}
shareText={shareText}
/>
<SocialButton
social="reddit"
shareUrl={shareUrl}
shareText={shareText}
/>
<SocialButton
social="email"
shareUrl={shareUrl}
shareText={shareText}
/>
</div>
<div className="my-4 flex w-full gap-2">
<div className="w-full">
<Input type="text" value={shareUrl} readOnly />
</div>
<Button icon={faLink} onClick={handleCopyLink} variant="primary">
{buttonLabel}
</Button>
</div>
<Button
icon={faArrowDownToLine}
className="my-4 w-full"
onClick={() => {
window.open(downloadLink, "_blank");
}}
variant="secondary">
Download
</Button>
<Button
className="mb-4 w-full"
onClick={() => {
window.open(openUrl, "_blank");
}}
variant="secondary">
View on Media Page
</Button>
</div>
</div>
</TransitionDialogue>
);
}
@@ -0,0 +1,81 @@
import {
faFacebookF,
faRedditAlien,
faWhatsapp,
faTwitter,
} from "@fortawesome/free-brands-svg-icons";
import { faXmark, faEnvelope } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
interface SocialButtonProps {
social: "x" | "whatsapp" | "facebook" | "reddit" | "email";
shareUrl: string;
shareText: string;
}
export default function SocialButton({
social,
shareUrl,
shareText,
}: SocialButtonProps) {
const socialIcons = {
x: faTwitter,
whatsapp: faWhatsapp,
facebook: faFacebookF,
reddit: faRedditAlien,
email: faEnvelope,
};
const socialIcon = socialIcons[social] || faXmark;
const getShareLink = (social: string, url: string, text: string) => {
switch (social) {
case "x":
return `https://twitter.com/intent/tweet?url=${encodeURIComponent(
url,
)}&text=${encodeURIComponent(text)}`;
case "facebook":
return `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(
url,
)}`;
case "whatsapp":
return `https://wa.me/?text=${encodeURIComponent(
text,
)} ${encodeURIComponent(url)}`;
case "reddit":
return `https://www.reddit.com/submit?url=${encodeURIComponent(
url,
)}&title=${encodeURIComponent(text)}`;
case "email":
return `mailto:?subject=${encodeURIComponent(
text,
)}&body=${encodeURIComponent(url)}`;
default:
return "#";
}
};
const handleClick = () => {
const shareLink = getShareLink(social, shareUrl, shareText);
window.open(shareLink, "_blank");
};
return (
<button className="flex flex-col items-center" onClick={handleClick}>
<div
className={[
"flex h-[56px] w-[56px]",
"items-center justify-center rounded-lg",
"text-[32px] text-white",
`bg-${social}`,
"hover:opacity-80",
].join(" ")}>
<FontAwesomeIcon icon={socialIcon} />
</div>
<p className="text-xs">
{social.charAt(0).toUpperCase() + social.slice(1)}
</p>
</button>
);
}
@@ -1,4 +1,11 @@
import { listTts, listV2V, inferTts, listMediaByUser, getMediaFileByToken, inferV2V } from '~/api';
import {
listTts,
listV2V,
inferTts,
listMediaByUser,
getMediaFileByToken,
inferV2V,
} from "~/api";
import {
MediaFile,
GetMediaFileResponse,
@@ -6,7 +13,7 @@ import {
VoiceConversionModelListResponse,
EnqueueVoiceConversionRequest,
EnqueueVoiceConversionResponse,
} from './typesImported';
} from "./typesImported";
import {
TtsModelListItem,
TtsModelListResponsePayload,
@@ -14,51 +21,59 @@ import {
StatusLike,
GenerateTtsAudioRequest,
GenerateTtsAudioResponse,
} from '~/pages/PageEnigma/models/tts';
} from "~/pages/PageEnigma/models/tts";
import { STORAGE_KEYS } from "~/contexts/Authentication/types";
export const ListAudioByUser = async(username:string, sessionToken: string) => {
return await fetch(listMediaByUser(username),{
method: 'GET',
export const ListAudioByUser = async (
username: string,
sessionToken: string,
) => {
return await fetch(listMediaByUser(username), {
method: "GET",
headers: {
"Accept": "application/json",
'session': sessionToken,
Accept: "application/json",
session: sessionToken,
},
})
.then(res => res.json())
.then(res => {
if(res.success && res.results){
return res.results.filter((item:MediaFile)=>item['media_type']==='audio');
}else{
Promise.reject();
}
})
.catch(e => ({ success : false }));
}
.then((res) => res.json())
.then((res) => {
if (res.success && res.results) {
return res.results.filter(
(item: MediaFile) => item["media_type"] === "audio",
);
} else {
Promise.reject();
}
})
.catch(() => ({ success: false }));
};
export async function ListTtsModels(sessionToken:string) : Promise<Array<TtsModelListItem>| undefined> {
export async function ListTtsModels(
sessionToken: string,
): Promise<Array<TtsModelListItem> | undefined> {
return await fetch(listTts, {
method: 'GET',
method: "GET",
headers: {
'Accept': 'application/json',
'session': sessionToken,
Accept: "application/json",
session: sessionToken,
},
})
.then(res => res.json())
.then(res => {
const response : TtsModelListResponsePayload = res;
if (!response.success) {
return;
}
return response?.models;
})
.catch(e => {
return undefined;
});
.then((res) => res.json())
.then((res) => {
const response: TtsModelListResponsePayload = res;
if (!response.success) {
return;
}
return response?.models;
})
.catch(() => {
return undefined;
});
}
export function maybeMapError(statuslike: StatusLike) : GenerateTtsAudioErrorType | undefined {
export function maybeMapError(
statuslike: StatusLike,
): GenerateTtsAudioErrorType | undefined {
switch (statuslike.status) {
case 400:
return GenerateTtsAudioErrorType.BadRequest;
@@ -71,121 +86,124 @@ export function maybeMapError(statuslike: StatusLike) : GenerateTtsAudioErrorTyp
}
}
export async function GenerateTtsAudio(request: GenerateTtsAudioRequest, sessionToken:string) : Promise<GenerateTtsAudioResponse>
{
export async function GenerateTtsAudio(
request: GenerateTtsAudioRequest,
sessionToken: string,
): Promise<GenerateTtsAudioResponse> {
return await fetch(inferTts, {
method: 'POST',
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'session': sessionToken,
Accept: "application/json",
"Content-Type": "application/json",
session: sessionToken,
},
body: JSON.stringify(request),
})
.then(res => res.json())
.then(res => {
if (!('inference_job_token' in res)) {
.then((res) => res.json())
.then((res) => {
if (!("inference_job_token" in res)) {
return { error: GenerateTtsAudioErrorType.UnknownError };
}
const ret: GenerateTtsAudioResponse = {
success: true,
inference_job_token: res.inference_job_token,
inference_job_token_type: res.inference_job_token_type,
};
return ret;
})
.catch((e) => {
const maybeError = maybeMapError(e);
if (maybeError !== undefined) {
return { error: maybeError };
}
return { error: GenerateTtsAudioErrorType.UnknownError };
}
const ret:GenerateTtsAudioResponse = {
success: true,
inference_job_token: res.inference_job_token,
inference_job_token_type: res.inference_job_token_type,
}
return ret;
})
.catch(e => {
let maybeError = maybeMapError(e);
if (maybeError !== undefined) {
return { error: maybeError };
}
return { error: GenerateTtsAudioErrorType.UnknownError };
});
});
}
};
export async function GetMediaFileByToken (fileToken: string, sessionToken: string) : Promise<GetMediaFileResponse>
{
export async function GetMediaFileByToken(
fileToken: string,
): Promise<GetMediaFileResponse> {
const sessionToken = localStorage.getItem(STORAGE_KEYS.SESSION_TOKEN) ?? "";
return await fetch(getMediaFileByToken(fileToken), {
method: "GET",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'session': sessionToken,
Accept: "application/json",
"Content-Type": "application/json",
session: sessionToken,
},
})
.then(res => res.json())
.then(res => {
const response: GetMediaFileResponse = res;
.then((res) => res.json())
.then((res) => {
const response: GetMediaFileResponse = res;
if (response && response.success && response.media_file) {
// NB: Timestamps aren't converted to Date objects on their own!
response.media_file.created_at = new Date(
response.media_file.created_at
response.media_file.created_at,
);
response.media_file.updated_at = new Date(
response.media_file.updated_at
response.media_file.updated_at,
);
return response;
} else {
return { success: false };
}
})
.catch(e => {
return { success: false };
});
;
})
.catch(() => {
return { success: false };
});
}
export async function ListVoiceConversionModels(sessionToken: string) : Promise<Array<VoiceConversionModelListItem>| undefined> {
export async function ListVoiceConversionModels(
sessionToken: string,
): Promise<Array<VoiceConversionModelListItem> | undefined> {
return await fetch(listV2V, {
method: 'GET',
method: "GET",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'session': sessionToken,
Accept: "application/json",
"Content-Type": "application/json",
session: sessionToken,
},
})
.then(res => res.json())
.then(res => {
const response : VoiceConversionModelListResponse = res;
if (!response.success) {
return;
}
return response?.models;
})
.catch(e => {
return undefined;
});
.then((res) => res.json())
.then((res) => {
const response: VoiceConversionModelListResponse = res;
if (!response.success) {
return;
}
return response?.models;
})
.catch(() => {
return undefined;
});
}
export async function GenerateVoiceConversion(
request: EnqueueVoiceConversionRequest,
sessionToken: string,
) : Promise<EnqueueVoiceConversionResponse>
{
): Promise<EnqueueVoiceConversionResponse> {
return await fetch(inferV2V, {
method: 'POST',
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'session': sessionToken,
Accept: "application/json",
"Content-Type": "application/json",
session: sessionToken,
},
body: JSON.stringify(request),
})
.then(res => res.json())
.then(res => {
if (!res) {
return { success : false };
}
.then((res) => res.json())
.then((res) => {
if (!res) {
return { success: false };
}
if (res && 'success' in res) {
return res;
} else {
return { success : false };
}
})
.catch(e => {
return { success : false };
});
}
if (res && "success" in res) {
return res;
} else {
return { success: false };
}
})
.catch(() => {
return { success: false };
});
}
+1
View File
@@ -2,6 +2,7 @@ export * from "./assets";
export * from "./inferenceJobs";
export * from "./message";
export * from "./movies";
export * from "./pagination";
export * from "./sidePanel";
export * from "./track";
export * from "./types";
@@ -0,0 +1,4 @@
export interface Pagination {
current: number;
total_page_count: number;
}
+1
View File
@@ -3,6 +3,7 @@ import { ActiveJob } from "~/pages/PageEnigma/models";
import { MediaInfo } from "~/pages/PageEnigma/models/movies";
export const viewMyMovies = signal(false);
export const generateMovieId = signal("");
export const activeJobs = signal<{ jobs: ActiveJob[] }>({ jobs: [] });
export const movies = signal<{ movies: MediaInfo[] }>({ movies: [] });
+22
View File
@@ -11,6 +11,7 @@
"@ffmpeg/ffmpeg": "0.11.6",
"@ffmpeg/util": "0.12.0",
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-brands-svg-icons": "^6.5.2",
"@fortawesome/pro-solid-svg-icons": "^6.5.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.18",
@@ -1334,6 +1335,27 @@
"node": ">=6"
}
},
"node_modules/@fortawesome/free-brands-svg-icons": {
"version": "6.5.2",
"resolved": "https://npm.fontawesome.com/@fortawesome/free-brands-svg-icons/-/6.5.2/free-brands-svg-icons-6.5.2.tgz",
"integrity": "sha512-zi5FNYdmKLnEc0jc0uuHH17kz/hfYTg4Uei0wMGzcoCL/4d3WM3u1VMc0iGGa31HuhV5i7ZK8ZlTCQrHqRHSGQ==",
"hasInstallScript": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.5.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-brands-svg-icons/node_modules/@fortawesome/fontawesome-common-types": {
"version": "6.5.2",
"resolved": "https://npm.fontawesome.com/@fortawesome/fontawesome-common-types/-/6.5.2/fontawesome-common-types-6.5.2.tgz",
"integrity": "sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==",
"hasInstallScript": true,
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/pro-solid-svg-icons": {
"version": "6.5.1",
"resolved": "https://npm.fontawesome.com/@fortawesome/pro-solid-svg-icons/-/6.5.1/pro-solid-svg-icons-6.5.1.tgz",
+1
View File
@@ -18,6 +18,7 @@
"@ffmpeg/ffmpeg": "0.11.6",
"@ffmpeg/util": "0.12.0",
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-brands-svg-icons": "^6.5.2",
"@fortawesome/pro-solid-svg-icons": "^6.5.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.18",
+16 -5
View File
@@ -147,7 +147,7 @@ export default {
selectedTab: "#55556B",
},
ui:{
ui: {
background: "#1a1a27",
panel: "#242433",
"panel-border": "#393948",
@@ -156,20 +156,20 @@ export default {
divider: "#515168",
},
media:{
media: {
"audio-tts": "#6AAA88",
"audio-v2v": "#6691B9",
"audio-upload": "#9760C2",
"audio-demo" : "#B96675",
"audio-demo": "#B96675",
"is-new": "#FFDC26",
},
inference:{
inference: {
pending: "#6b728b",
generating: "#46527C",
error: "#924C4B",
},
"axis-x": "#D33242",
"axis-y": "#308752",
"axis-z": "#2E70FF",
@@ -177,6 +177,12 @@ export default {
green: "#308752",
blue: "#2E70FF",
facebook: "#4267b2",
reddit: "#ff5700",
whatsapp: "#25d366",
x: "#1da1f2",
email: "#858585",
white: colors.white,
gray: colors.gray,
black: colors.black,
@@ -196,5 +202,10 @@ export default {
"bg-object-selected",
"bg-object-unselected",
"bg-object-clip",
"bg-facebook",
"bg-reddit",
"bg-whatsapp",
"bg-x",
"bg-email",
],
} satisfies Config;