Compare commits

...
Author SHA1 Message Date
Michael Chung e4bbafeae6 Used Aspect Ratio to fix.
Prompt box handles both web and desktop.
2025-04-21 16:58:02 -04:00
bflatastic e39a7f0360 bug fix: asset modal flickers when items are drag and dropped too fast 2025-04-22 01:47:03 +07:00
bflatastic 5ea90154b1 bug fix: selecting object in scene crashes (Headless UI Transition component bug) 2025-04-22 01:29:51 +07:00
bflatastic f627da6d8f updated readme: ui component import 2025-04-22 01:19:30 +07:00
ArEnScandMichael Chung 682f11c79e Tauri and login for open ai (#540)
Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-04-21 14:15:14 -04:00
BFlat 0bdbb5214b settings modal (#539)
new shared components:
- button
- close button
- input
- label
- modal
- select
- settings modal
2025-04-22 01:00:58 +07:00
Brandon Thomas 94f2d2ec68 fix tauri build configs 2025-04-21 13:13:23 -04:00
Michael Chung 384b841528 Add a scale factor to the capture canvas
just use a hack to get it released
2025-04-21 12:55:02 -04:00
Michael Chung 62f97317ff Add component information into README 2025-04-21 09:23:30 -04:00
BFlat 8210a9f063 update headless ui version (#538) 2025-04-21 14:36:55 +07:00
BFlat b4d603098d working upload 3d asset modal (#537)
![image](https://github.com/user-attachments/assets/01df49cd-82a2-4f67-a02c-8adec34d1794)
2025-04-21 12:23:46 +07:00
121 changed files with 3317 additions and 632 deletions
@@ -1,37 +0,0 @@
{
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
"productName": "ArtCraft 2D",
"version": "0.1.0",
"identifier": "ai.artcraft.app",
"build": {
"frontendDist": "../../../frontend/apps/editor2d/dist",
"devUrl": "http://localhost:5173",
"beforeDevCommand": "nx run editor2d:dev",
"beforeBuildCommand": "nx run editor2d:build"
},
"app": {
"windows": [
{
"title": "ArtCraft 2D",
"width": 2400,
"height": 1300,
"resizable": true,
"fullscreen": false
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
@@ -1,37 +0,0 @@
{
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
"productName": "ArtCraft 3D",
"version": "0.1.0",
"identifier": "ai.artcraft.app",
"build": {
"frontendDist": "../../../frontend/apps/editor3d/dist",
"devUrl": "http://localhost:5173",
"beforeDevCommand": "nx run editor3d:dev",
"beforeBuildCommand": "nx run editor3d:build"
},
"app": {
"windows": [
{
"title": "ArtCraft 3D",
"width": 2400,
"height": 1300,
"resizable": true,
"fullscreen": false
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
@@ -0,0 +1,5 @@
# For some reason, we can't pass json files to the tauri dev command
[build]
frontendDist = "../../../frontend/apps/editor3d/dist"
beforeDevCommand = "nx run editor3d:dev"
devUrl = "http://localhost:5173"
@@ -4,10 +4,10 @@
"version": "0.1.0",
"identifier": "ai.artcraft.app",
"build": {
"frontendDist": "../../../frontend/apps/editor3d/dist",
"devUrl": "http://localhost:5173",
"beforeDevCommand": "nx run editor3d:dev",
"beforeBuildCommand": "nx run editor3d:build"
"frontendDist": "../../../frontend/apps/editor2d/dist",
"devUrl": "http://localhost:5741",
"beforeDevCommand": "nx run editor2d:dev",
"beforeBuildCommand": "nx run editor2d:build"
},
"app": {
"windows": [
+15 -4
View File
@@ -2,8 +2,7 @@
This is an `nx` monorepo that can contain multiple apps and shared libraries.
All commands to run these projects are performed from _this_ directory.
All commands to run these projects are performed from _this_ directory.
## Install dependencies
@@ -35,6 +34,10 @@ The names for @frontend and @storyteller come from the package.json file in the
import { Login } from "@frontend/login";
import { api } from "@storyteller/api";
For shared UI components, import from @storyteller/ui-[componentname].
import { Button } from "@storyteller/ui-button";
import { Modal } from "@storyteller/ui-modal";
building:
npx nx build editor3d
npx nx build editor2d
@@ -42,6 +45,14 @@ npx nx build editor2d
dev:
npx nx dev editor3d
npx nx dev editor2d
# component build
1.
npx nx g @nx/react:library libs/components/toaster --import-path=@storyteller/ui-toaster --bundler=vite
npm install
2.import it first in code
3.nx sync
4.nx build
```
-1
View File
@@ -28,7 +28,6 @@
"@fortawesome/pro-regular-svg-icons": "^6.7.2",
"@fortawesome/pro-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@headlessui/react": "^2.1.3",
"@preact/signals-react": "^2.2.0",
"@tauri-apps/api": "^2.2.0",
"@techstark/opencv-js": "^4.10.0-release.1",
@@ -62,6 +62,7 @@ export class EditEngine {
this.editMode = EditModes.SELECT;
this.boardCanvasRef = boardCanvasRef;
this.stage = new Konva.Stage({
container: this.boardCanvasRef,
width: window.innerWidth,
@@ -238,13 +239,19 @@ export class EditEngine {
}
private onBoardCanvasResize() {
this.realTimeDrawEngine.updateCaptureCanvas(undefined, undefined);
this.realTimeDrawEngine.updateCaptureCanvas(0, 0);
console.log("onBoardCanvasResize Event called");
console.log("Board Canvas values below");
console.log(this.boardCanvasRef.offsetWidth);
console.log(this.boardCanvasRef.offsetHeight);
this.matteBox.updateSize({
boardCanvasSize: {
width: this.boardCanvasRef.offsetWidth,
height: this.boardCanvasRef.offsetHeight,
},
});
this.uiLayer.draw();
this.stage.width(this.boardCanvasRef.offsetWidth);
this.stage.height(this.boardCanvasRef.offsetHeight);
+14 -1
View File
@@ -510,7 +510,20 @@ export class Engine {
}
private onBoardCanvasResize() {
this.realTimeDrawEngine.updateCaptureCanvas(undefined, undefined);
console.log("onBoardCanvasResize Event called");
// Print window dimensions for debugging
console.log("Window dimensions:", {
innerWidth: window.innerWidth,
innerHeight: window.innerHeight,
outerWidth: window.outerWidth,
outerHeight: window.outerHeight,
});
this.realTimeDrawEngine.updateCaptureCanvas(
window.innerWidth,
window.innerHeight,
);
this.matteBox.updateSize({
boardCanvasSize: {
width: this.boardCanvasRef.offsetWidth,
@@ -516,36 +516,74 @@ export class RealTimeDrawEngine {
width: number | undefined,
height: number | undefined,
) {
console.log("updateCaptureCanvas");
if (!this.captureCanvas) {
return;
}
if (width) {
this.width = width;
}
if (height) {
this.height = height;
}
// Ensures that all the nodes stag in the same place should
// there be a window resize.
// recompute the position
// to ensure that the position of this stays
const scaleFactor = 0.9;
// Store old values for calculating delta
const oldPositionX = this.positionX;
const oldPositionY = this.positionY;
// recompute the position
const oldWidth = this.width;
const oldHeight = this.height;
// Original aspect ratio
const originalAspectRatio = oldWidth / oldHeight;
// Calculate new dimensions based on window size
let newWidth: number;
let newHeight: number;
if (width === undefined) {
newWidth = window.innerWidth * scaleFactor;
} else {
newWidth = width * scaleFactor;
}
if (height === undefined) {
newHeight = window.innerHeight * scaleFactor;
} else {
newHeight = height * scaleFactor;
}
// Preserve aspect ratio
const newAspectRatio = newWidth / newHeight;
if (newAspectRatio > originalAspectRatio) {
// Width is constraining factor
newWidth = newHeight * originalAspectRatio;
} else {
// Height is constraining factor
newHeight = newWidth / originalAspectRatio;
}
this.width = newWidth;
this.height = newHeight;
console.log("New dimensions:", this.width, this.height);
// Calculate new position to center the canvas in the window
this.positionX = (window.innerWidth - this.width) / 2;
this.positionY = (window.innerHeight - this.height) / 2;
console.log("New centered position:", this.positionX, this.positionY);
// Update the canvas position and size
this.captureCanvas.setPosition({
x: this.positionX / 2,
y: this.positionY / 2,
x: this.positionX,
y: this.positionY,
});
this.captureCanvas.size({ width: this.width, height: this.height });
// this is the change in positions
const deltaX = this.positionX - oldPositionX;
const deltaY = this.positionY - oldPositionY;
// Calculate scale factors to maintain proportions
const scaleX = this.width / oldWidth;
const scaleY = this.height / oldHeight;
// Use a uniform scale factor to preserve aspect ratio
const uniformScale = Math.min(scaleX, scaleY);
// Update all children except the capture canvas
var children = this.mediaLayerRef.getChildren();
for (let i = 0; i < children.length; i++) {
let node = children[i];
@@ -554,11 +592,27 @@ export class RealTimeDrawEngine {
if (node.name() === "CaptureCanvas") {
continue;
}
const pos = node.getPosition();
const oldScale = node.scale();
// Apply both position shift and scaling to maintain relative positioning
node.setPosition({
x: pos.x + deltaX,
y: pos.y + deltaY,
x: (pos.x - oldPositionX) * uniformScale + this.positionX,
y: (pos.y - oldPositionY) * uniformScale + this.positionY,
});
// Update scale proportionally if the node has a scale property
if (
oldScale &&
typeof oldScale.x === "number" &&
typeof oldScale.y === "number"
) {
node.scale({
x: oldScale.x * uniformScale,
y: oldScale.y * uniformScale,
});
}
}
this.mediaLayerRef.batchDraw();
+33 -22
View File
@@ -2,6 +2,38 @@
@tailwind components;
@tailwind utilities;
* {
scrollbar-width: thin;
scrollbar-color: #363636 #171717;
}
/* Works on Chrome, Edge, and Safari */
*::-webkit-scrollbar {
width: 8px;
}
*::-webkit-scrollbar-track {
background: #171717;
}
*::-webkit-scrollbar-thumb {
background-color: #363636;
border-radius: 4px;
border: 2px solid #171717;
}
/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type="number"] {
-moz-appearance: textfield;
}
@layer base {
:root {
@apply select-none bg-ui-background font-sans text-white;
@@ -27,28 +59,7 @@
background-size: 36px 36px;
@apply bg-center;
}
h1 {
@apply text-3xl font-medium md:text-5xl;
}
h2 {
@apply text-2xl font-medium md:text-4xl;
}
h3 {
@apply text-xl font-medium md:text-3xl;
}
h4 {
@apply text-lg font-medium;
}
h5,
label {
@apply text-base;
}
h6 {
@apply text-sm;
}
p {
@apply text-sm font-normal;
}
a {
@apply font-medium text-primary-400 hover:text-primary-300;
}
@@ -1,6 +1,7 @@
import { useParams } from "react-router-dom";
import { withProtectionRoute } from "~/components/hoc";
import { useRenderCounter } from "~/hooks/useRenderCounter";
import { Resource, invoke } from '@tauri-apps/api/core';
// Components of the page
import { ToolbarUserProfile } from "~/components/features";
@@ -26,6 +27,11 @@ import {
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export const isTauri = (): boolean => {
console.log(window.__TAURI_INTERNALS__);
return window.__TAURI_INTERNALS__;
};
export const Main = withProtectionRoute(() => {
// This is a hook that will log the number of times the component has rerendered
// Let's make sure we only log once
@@ -40,7 +46,7 @@ export const Main = withProtectionRoute(() => {
const realtimeEngineRef = useRef<EngineType | null>(null);
const generationEngineRef = useRef<GenerationEngine | null>(null);
const [firstTimeDialogOpen, setFirstTimeDialogOpen] = useState(true);
const [signinDialogOpen, setSigninDialogOpen] = useState(false);
const appModeValue = appMode.value;
let childView;
@@ -97,7 +103,7 @@ export const Main = withProtectionRoute(() => {
}}
className="max-w-5xl"
>
<div className="flex flex-col items-center justify-center gap-6">
<div className="flex flex-col items-center justify-center">
<div className="flex flex-col items-center gap-3">
<h1 className="text-3xl font-bold">
<FontAwesomeIcon
@@ -123,7 +129,22 @@ export const Main = withProtectionRoute(() => {
</video>
</div>
<Button
className="px-4 py-3 font-semibold"
className="font-semibold"
icon={faArrowRight}
iconFlip={true}
onClick={async () => {
if (isTauri()) {
await invoke('open_login_command');
setFirstTimeDialogOpen(false);
} else {
console.error('Tauri is not available in this environment');
}
}}
>
Signin to OpenAI to get Started
</Button>
{/* <Button
className="font-semibold"
icon={faArrowRight}
iconFlip={true}
onClick={() => {
@@ -131,7 +152,7 @@ export const Main = withProtectionRoute(() => {
}}
>
Start creating now
</Button>
</Button> */}
</div>
</BaseDialog>
</>
@@ -177,7 +177,100 @@ export const PromptBox = () => {
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setPrompt(e.target.value);
};
const handleTauriEnqueue = async () => {
const api = new Api();
let image = getCanvasRenderBitmap();
if (image === undefined) {
return;
}
const base64Bitmap = await EncodeImageBitmapToBase64(image);
const byteString = atob(base64Bitmap);
const mimeString = "image/png";
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
const uuid = crypto.randomUUID(); // Generate a new UUID
const file = new File([ab], `${uuid}.png`, { type: mimeString });
const snapshotMediaToken = await api.uploadSceneSnapshot({
screenshot: file,
});
if (snapshotMediaToken.data === undefined) {
toast.error("Error: Unable to upload scene snapshot Please try again.");
return;
}
console.log("useSystemPrompt", useSystemPrompt);
console.log("Snapshot media token:", snapshotMediaToken.data);
const generateResponse = await invoke("sora_image_generation_command", {
snapshot_media_token: snapshotMediaToken.data,
disable_system_prompt: !useSystemPrompt,
prompt: prompt,
maybe_additional_images: referenceImages.map((image) => image.mediaToken),
maybe_number_of_samples: 1,
});
toast.success("Please wait while we process your image.");
};
const handleWebEnqueue = async () => {
const api = new Api();
let image = getCanvasRenderBitmap();
if (image === undefined) {
toast.error(
"Error: Unable to generate image. Please check the input and try again.",
);
return;
}
const base64Bitmap = await EncodeImageBitmapToBase64(image);
const byteString = atob(base64Bitmap);
const mimeString = "image/png";
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
const uuid = crypto.randomUUID(); // Generate a new UUID
const file = new File([ab], `${uuid}.png`, { type: mimeString });
const snapshotMediaToken = await api.uploadSceneSnapshot({
screenshot: file,
});
if (snapshotMediaToken.data === undefined) {
toast.error("Error: Unable to upload scene snapshot Please try again.");
return;
}
console.log("useSystemPrompt", useSystemPrompt);
console.log("Snapshot media token:", snapshotMediaToken.data);
const response = await api.enqueueImageGeneration({
disableSystemPrompt: !useSystemPrompt,
prompt: prompt,
snapshotMediaToken: snapshotMediaToken.data,
additionalImages: referenceImages.map((image) => image.mediaToken),
});
if (response.success === true) {
toast.success("Please wait while we process your image.");
if (response.data) {
addJobToken(response.data);
}
return;
} else {
toast.error("Failed to enqueue image generation. Please try again.");
}
};
const handleEnqueue = async () => {
if (!prompt.trim()) return;
@@ -191,69 +284,10 @@ export const PromptBox = () => {
referenceImages,
);
if (isDesktopApp) {
let image = getCanvasRenderBitmap();
if (image === undefined) {
return;
}
const base64Bitmap = await EncodeImageBitmapToBase64(image);
const generateResponse = await invoke("image_generation_command", {
image: base64Bitmap,
prompt: prompt,
});
await new Promise((resolve) => setTimeout(resolve, 2000));
}
if (!isDesktopApp) {
const api = new Api();
let image = getCanvasRenderBitmap();
if (image === undefined) {
toast.error(
"Error: Unable to generate image. Please check the input and try again.",
);
return;
}
const base64Bitmap = await EncodeImageBitmapToBase64(image);
const byteString = atob(base64Bitmap);
const mimeString = "image/png";
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
const uuid = crypto.randomUUID(); // Generate a new UUID
const file = new File([ab], `${uuid}.png`, { type: mimeString });
const snapshotMediaToken = await api.uploadSceneSnapshot({
screenshot: file,
});
if (snapshotMediaToken.data === undefined) {
toast.error(
"Error: Unable to upload scene snapshot Please try again.",
);
return;
}
console.log("useSystemPrompt", useSystemPrompt);
console.log("Snapshot media token:", snapshotMediaToken.data);
const response = await api.enqueueImageGeneration({
disableSystemPrompt: !useSystemPrompt,
prompt: prompt,
snapshotMediaToken: snapshotMediaToken.data,
additionalImages: referenceImages.map((image) => image.mediaToken),
});
if (response.success === true) {
toast.success("Please wait while we process your image.");
if (response.data) {
addJobToken(response.data);
}
return;
} else {
toast.error("Failed to enqueue image generation. Please try again.");
}
if (isDesktopApp == true) {
handleTauriEnqueue();
} else {
handleWebEnqueue();
}
} catch (error) {
console.error("Error during image generation:", error);
@@ -1,16 +1,32 @@
import { faGear } from "@fortawesome/pro-solid-svg-icons";
import { twMerge } from "tailwind-merge";
import { Login } from "~/app/pages/login";
import { Activity } from "~/components/ui/Activity/Activity";
// import { paperWrapperStyles } from "~/components/styles";
// import { faPlus, faQuestion } from "@fortawesome/pro-solid-svg-icons";
// import { ToolbarButton } from "~/components/features/ToolbarButton";
import { Button } from "@storyteller/ui-button";
import { SettingsModal } from "@storyteller/ui-settings-modal";
import { useState } from "react";
export const ToolbarTopRight = () => {
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
return (
<div
className={twMerge("relative z-50 flex h-fit w-fit items-center gap-2")}
>
<Activity />
</div>
<>
<div
className={twMerge("relative z-50 flex h-fit w-fit items-center gap-2")}
>
<Button
icon={faGear}
className="h-[42px] w-[42px] bg-[#5F5F68]/60 backdrop-blur-lg hover:bg-[#5F5F68]/90"
onClick={() => setIsSettingsModalOpen(true)}
/>
<Activity />
</div>
<SettingsModal
isOpen={isSettingsModalOpen}
onClose={() => setIsSettingsModalOpen(false)}
/>
</>
);
};
+35 -3
View File
@@ -4,7 +4,11 @@ import defaultTheme from "tailwindcss/defaultTheme";
import { storytellerColors } from "./tailwind.stcolors";
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
"../../libs/components/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {
fontFamily: {
@@ -44,14 +48,42 @@ export default {
info: colors.sky[500],
// brand colors
primary: storytellerColors.azureRadiance,
secondary: storytellerColors.gunpowder,
primary: {
DEFAULT: "#2d81ff",
50: "#eef6ff",
100: "#d9ebff",
200: "#bcdcff",
300: "#8ec7ff",
400: "#59a7ff",
500: "#2d81ff",
600: "#1b63f5",
700: "#144ee1",
800: "#173fb6",
900: "#19398f",
950: "#142457",
},
secondary: {
DEFAULT: "#3E3E41",
50: "#f6f6f6",
100: "#e7e7e7",
200: "#d1d1d1",
300: "#b0b0b0",
400: "#888888",
500: "#6d6d6d",
600: "#5d5d5d",
700: "#4f4f4f",
800: "#454545",
900: "#3d3d3d",
950: "#3E3E41",
},
tertiary: storytellerColors.aquamarineBlue,
ui: {
background: "#101014",
panel: "#3E3E41",
border: "#FFFFFF0D",
"panel-border": "#3F3F3F",
controls: "#3E3E41",
},
},
+11 -1
View File
@@ -24,5 +24,15 @@
"@ApiManager/*": ["./src/Classes/ApiManager/*"]
}
},
"include": ["src"]
"include": [
"src"
],
"references": [
{
"path": "../../libs/components/settings-modal/tsconfig.lib.json"
},
{
"path": "../../libs/components/button/tsconfig.lib.json"
}
]
}
+12 -2
View File
@@ -1,7 +1,17 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
{
"path": "../../libs/components/settings-modal"
},
{
"path": "../../libs/components/button"
},
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
]
}
@@ -52,7 +52,7 @@ export const Input = React.forwardRef(
id={id ? id : label ? kebabCase(label) : undefined}
className={twMerge(
"h-10 w-full rounded-lg bg-[#242424] px-3 py-2.5 text-white placeholder-white/50 outline-none",
"border border-[#363636] transition-all duration-150 ease-in-out hover:border-brand-primary/60 focus:border-brand-primary focus:!outline-none",
"hover:border-primary/60 focus:border-primary border border-[#363636] transition-all duration-150 ease-in-out focus:!outline-none",
icon && "pl-10",
isError && "outline-red focus:outline-red",
inputClassName,
@@ -60,6 +60,7 @@ export const LoadingBar = ({
return (
<Transition
as="div"
className={wrapperClassName}
show={show}
enter="transition-opacity duration-150"
@@ -1,41 +1,41 @@
import { Transition } from '@headlessui/react';
import { twMerge } from 'tailwind-merge';
import { Transition } from "@headlessui/react";
import { twMerge } from "tailwind-merge";
import { H4 } from '.';
import { H4 } from ".";
interface LoadingDotsProps {
className?: string;
isShowing?: boolean;
};
}
interface LoadingDotsInnerProps {
className?: string;
isShowing?: boolean;
type?: 'typing'|'bricks';
type?: "typing" | "bricks";
message?: string;
};
}
export const LoadingDotsTyping = (props: LoadingDotsProps)=>{
return (
<LoadingDots {...props} />
)
}
export const LoadingDotsBricks = (props: LoadingDotsProps)=>{
return (
<LoadingDots {...props} type="bricks"/>
)
}
export const LoadingDotsTyping = (props: LoadingDotsProps) => {
return <LoadingDots {...props} />;
};
export const LoadingDotsBricks = (props: LoadingDotsProps) => {
return <LoadingDots {...props} type="bricks" />;
};
export function LoadingDots({
className,
isShowing=true,
type = 'typing',
isShowing = true,
type = "typing",
message,
}: LoadingDotsInnerProps){
const classNames = twMerge("w-full h-full flex flex-col justify-center items-center bg-ui-background gap-6", className);
}: LoadingDotsInnerProps) {
const classNames = twMerge(
"w-full h-full flex flex-col justify-center items-center bg-ui-background gap-6",
className,
);
return(
return (
<Transition
as="div"
className={classNames}
show={isShowing}
enter="transition-opacity duration-150"
@@ -45,17 +45,9 @@ export function LoadingDots({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
{ type==='typing' &&
<div className="dot-typing"></div>
}
{ type==='bricks' &&
<div className="dot-bricks"></div>
}
{
message &&
<H4>{message}</H4>
}
{type === "typing" && <div className="dot-typing"></div>}
{type === "bricks" && <div className="dot-bricks"></div>}
{message && <H4>{message}</H4>}
</Transition>
);
};
}
@@ -1,76 +1,95 @@
import { Fragment } from "react";
import { Listbox, Transition } from "@headlessui/react";
import { faCheck, faChevronDown } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { MediaFileAnimationType } from "~/enums";
import { twMerge } from "tailwind-merge";
export type SelectValue = string | number;
export type SelectOption = { label: string; value: SelectValue };
interface ListDropdownProps {
options: SelectOption[];
onChange: (val: MediaFileAnimationType) => void;
onChange: (val: SelectValue) => void;
placeholder?: string;
value?: SelectValue;
id?: string;
}
export const Select = ({
onChange,
options,
placeholder,
value,
id,
}: ListDropdownProps) => {
const selectedOption = options.find(
(option: SelectOption) => option.value === value,
) || { label: placeholder || "", value: "" };
return (
<Listbox value={value} onChange={onChange}>
<div className="relative mt-1">
<Listbox.Button className="relative h-10 w-full cursor-pointer rounded-md bg-brand-secondary py-2 pl-3 pr-10 text-left outline-none outline-offset-0 transition-all duration-150 ease-in-out focus:outline-brand-primary sm:text-sm">
<span className={`block truncate${value ? "" : " opacity-50"}`}>
{selectedOption.label}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<FontAwesomeIcon icon={faChevronDown} aria-hidden="true" />
</span>
</Listbox.Button>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-brand-secondary py-1 text-base shadow-lg focus:outline-none sm:text-sm">
{options.map((option, itemIdx) => (
<Listbox.Option
key={itemIdx}
className={({ active }) =>
`relative cursor-pointer select-none py-2 pl-10 pr-4 text-white ${
active ? "text-white" : "text-gray-400"
}`
}
value={option.value}
<div className="relative mt-1">
<Listbox value={value || ""} onChange={onChange}>
{({ open }) => (
<>
<Listbox.Button
id={id}
className="relative h-10 w-full cursor-pointer rounded-md bg-brand-secondary py-2 pl-3 pr-10 text-left outline-none outline-offset-0 transition-all duration-150 ease-in-out focus:outline-brand-primary sm:text-sm"
>
<span
className={twMerge("block truncate", !value && "opacity-50")}
>
{({ selected }) => (
<>
<span
className={`block truncate ${
selected ? "font-medium" : "font-normal"
}`}
>
{option.label}
</span>
{selected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3">
<FontAwesomeIcon icon={faCheck} aria-hidden="true" />
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</Listbox>
{selectedOption.label}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2.5">
<FontAwesomeIcon icon={faChevronDown} aria-hidden="true" />
</span>
</Listbox.Button>
<Transition
show={open}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-brand-secondary py-1 text-base shadow-xl focus:outline-none sm:text-sm">
{options.map((option, itemIdx) => (
<Listbox.Option
key={itemIdx}
className={({ active, selected }) =>
twMerge(
"relative cursor-pointer select-none py-2.5 pl-8 pr-2 text-white transition-all duration-150 ease-in-out",
active && "bg-brand-primary/20 text-white",
selected && "bg-brand-primary/40",
!selected && "text-white/90",
)
}
value={option.value}
>
{({ selected }) => (
<>
<span
className={`block truncate ${
selected ? "font-medium" : "font-normal"
}`}
>
{option.label}
</span>
{selected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-2">
<FontAwesomeIcon
icon={faCheck}
aria-hidden="true"
/>
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</>
)}
</Listbox>
</div>
);
};
@@ -27,11 +27,17 @@ const DialogBackdrop = ({ className }: { className?: string }) => {
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leave="ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className={twMerge("fixed inset-0 bg-black/60", className)} />
<div
className={twMerge("fixed inset-0 bg-black/60", className)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
/>
</Transition.Child>
);
};
@@ -71,7 +77,7 @@ export const TransitionDialogue = ({
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enter="ease-out duration-200"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
@@ -86,6 +92,10 @@ export const TransitionDialogue = ({
className,
)}
style={{ minWidth: width }}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
{title && (
<Dialog.Title
@@ -35,13 +35,15 @@ export const H4 = ({ className, children }: TypoProps) => (
export const Label = ({
className,
children,
required,
...rest
}: LabelHTMLAttributes<HTMLLabelElement>) => (
}: LabelHTMLAttributes<HTMLLabelElement> & { required?: boolean }) => (
<label
className={twMerge(baseTypo, "mb-2 text-[15px] font-medium", className)}
{...rest}
>
{children}
{required && <span className="ml-0.5 text-[#ff6467]">*</span>}
</label>
);
@@ -1,5 +1,6 @@
import { FileUploader as DragDropFilesUploader } from "react-drag-drop-files";
import { DragAndDropZone } from "./DragAndDropZone";
import { Label } from "../Typography";
interface Props {
title: string;
@@ -10,8 +11,8 @@ interface Props {
export const FileUploader = ({ fileTypes, title, file, setFile }: Props) => {
return (
<div className="flex w-full flex-col gap-3">
{title}
<div className="flex w-full flex-col">
<Label required>{title}</Label>
<DragDropFilesUploader
handleChange={(file: File | null) => {
setFile(file);
@@ -1,11 +1,12 @@
import { useCallback, useEffect, useState, useRef } from "react";
import { Button, H6, Input, ListDropdown } from "~/components";
import { Button, H6, Input, Label, ListDropdown } from "~/components";
import { FileUploader } from "../UploadModal/FileUploader";
import { loadPreviewOnCanvas, snapshotCanvasAsThumbnail } from "./utilities";
import { upload3DObjects } from "./utilities/upload3DObjects";
import { UploaderState } from "~/models";
import { FilterEngineCategories, MediaFileAnimationType } from "~/enums";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCube } from "@fortawesome/pro-solid-svg-icons";
interface Props {
title: string;
@@ -21,7 +22,6 @@ interface Props {
}
export const UploadFiles3D = ({
title,
fileTypes,
engineCategory,
options,
@@ -121,16 +121,19 @@ export const UploadFiles3D = ({
return (
<>
<div className="mb-4 flex flex-col gap-4">
<Input
placeholder="Enter the title here"
errorMessage={uploadTitle.error}
value={uploadTitle.value}
onChange={(event) => setUploadTitle({ value: event.target.value })}
className={uploadTitle.error ? "mb-3" : ""}
/>
<div className="flex flex-col">
<Label required>Asset Name</Label>
<Input
placeholder="Enter the asset name here"
errorMessage={uploadTitle.error}
value={uploadTitle.value}
onChange={(event) => setUploadTitle({ value: event.target.value })}
className={uploadTitle.error ? "mb-3" : ""}
/>
</div>
<FileUploader
title={title}
title="Upload 3D Model"
fileTypes={fileTypes}
file={assetFile.value}
setFile={(file: File | null) => {
@@ -149,16 +152,20 @@ export const UploadFiles3D = ({
<H6 className="z-10 text-red">{assetFile.error}</H6>
)}
<div className="relative m-auto aspect-square w-full overflow-hidden rounded-lg bg-brand-secondary">
<canvas className="h-full w-full" ref={canvasCallbackRef} />
<div className="relative m-auto w-full overflow-hidden rounded-lg bg-brand-secondary">
<canvas
className="pointer-events-none h-full w-full"
ref={canvasCallbackRef}
/>
{!assetFile.value && (
<H6 className="absolute left-0 top-1/2 -mt-5 w-full text-center">
File Preview
<H6 className="pointer-events-auto absolute left-0 top-1/2 -mt-5 flex w-full items-center justify-center gap-2.5 text-center opacity-50">
<FontAwesomeIcon icon={faCube} />
Your model preview will appear here
</H6>
)}
{previewStatus.type.includes("Error") && (
<>
<H6 className="absolute left-0 top-1/2 -mt-5 w-full text-center">
<H6 className="pointer-events-auto absolute left-0 top-1/2 -mt-5 w-full text-center">
{previewStatus.type}
{previewStatus.message && <br />}
{previewStatus.message}
@@ -167,13 +174,17 @@ export const UploadFiles3D = ({
)}
</div>
<div className="flex justify-end gap-4">
<Button variant="primary" onClick={handleSubmit}>
Upload
</Button>
<div className="flex justify-end gap-2">
<Button variant="secondary" onClick={onClose}>
Cancel
</Button>
<Button
variant="primary"
onClick={handleSubmit}
disabled={!uploadTitle.value || !assetFile.value}
>
Upload
</Button>
</div>
</div>
</>
@@ -1,20 +1,31 @@
import { useEffect, useState } from "react";
import { TransitionDialogue, LoadingDots } from "~/components";
import {
TransitionDialogue,
LoadingDots,
Label,
CloseButton,
} from "~/components";
import { UploadAssetError } from "../UploadModal/UploadAssetError";
import { UploadSuccess } from "../UploadModal/UploadSuccess";
import { UploadFiles3D } from "./UploadFiles3D";
import { FilterEngineCategories, UploaderStates } from "~/enums";
import { initialUploaderState, UploaderState } from "~/models";
import { Select } from "../Select";
import {
FilterEngineCategories,
UploaderStates,
OBJECT_FILE_TYPE,
CHARACTER_MIXAMO_FILE_TYPE,
IMAGEPLANE_FILE_TYPE,
} from "~/enums";
import { assetModalVisible } from "~/pages/PageEnigma/signals";
interface Props {
onClose: () => void;
onSuccess: () => void;
isOpen: boolean;
title: string;
fileTypes: string[];
engineCategory: FilterEngineCategories;
options?: {
fileSubtypes?: { [key: string]: string }[];
hasLength?: boolean;
@@ -22,17 +33,61 @@ interface Props {
};
}
// Map categories to their allowed file types
const categoryFileTypes: Partial<Record<FilterEngineCategories, string[]>> = {
[FilterEngineCategories.OBJECT]: Object.values(OBJECT_FILE_TYPE),
[FilterEngineCategories.CHARACTER]: [
...Object.values(CHARACTER_MIXAMO_FILE_TYPE),
],
[FilterEngineCategories.IMAGE_PLANE]: Object.values(IMAGEPLANE_FILE_TYPE),
[FilterEngineCategories.LOCATION]: Object.values(OBJECT_FILE_TYPE),
[FilterEngineCategories.CREATURE]: Object.values(OBJECT_FILE_TYPE),
};
// Map engine categories to asset modal tabs
const categoryToAssetTab: Partial<Record<FilterEngineCategories, string>> = {
[FilterEngineCategories.OBJECT]: "objects",
[FilterEngineCategories.CHARACTER]: "character",
[FilterEngineCategories.IMAGE_PLANE]: "image-planes",
[FilterEngineCategories.LOCATION]: "sets",
[FilterEngineCategories.CREATURE]: "creatures",
};
// Excluded category options for the upload modal
const categoryOptions = Object.entries(FilterEngineCategories)
.filter(
([, value]) =>
![
"audio",
"video_plane",
"skybox",
"set_dressing",
"expression",
"animation",
"scene",
].includes(value),
)
.sort(([, a], [, b]) => (a === "object" ? -1 : b === "object" ? 1 : 0))
.map(([key, value]) => ({
// This is because sets is location i guess - BFlat
label:
key === "LOCATION"
? "Set"
: key.charAt(0) + key.slice(1).toLowerCase().replace(/_/g, " "),
value: value,
}));
export function UploadModal3D({
isOpen,
onClose,
onSuccess,
title,
fileTypes,
engineCategory,
options,
}: Props) {
const [uploaderState, setUploaderState] =
useState<UploaderState>(initialUploaderState);
const [selectedCategory, setSelectedCategory] =
useState<FilterEngineCategories>(FilterEngineCategories.OBJECT);
const updateUploaderState = (newState: UploaderState) => {
setUploaderState(newState);
@@ -52,14 +107,35 @@ export function UploadModal3D({
switch (uploaderState.status) {
case UploaderStates.ready:
return (
<UploadFiles3D
title={title}
engineCategory={engineCategory}
fileTypes={fileTypes}
options={options}
onClose={onClose}
onUploadProgress={updateUploaderState}
/>
<div className="space-y-4">
<div className="flex w-full flex-col">
<Label>Category</Label>
<Select
id="category-select"
options={categoryOptions}
value={selectedCategory}
onChange={(value) => {
if (
typeof value === "string" &&
Object.values(FilterEngineCategories).includes(
value as FilterEngineCategories,
)
) {
setSelectedCategory(value as FilterEngineCategories);
}
}}
placeholder="Select a category"
/>
</div>
<UploadFiles3D
title={title}
engineCategory={selectedCategory}
fileTypes={categoryFileTypes[selectedCategory] || []}
options={options}
onClose={onClose}
onUploadProgress={updateUploaderState}
/>
</div>
);
case UploaderStates.uploadingAsset:
case UploaderStates.uploadingCover:
@@ -73,10 +149,25 @@ export function UploadModal3D({
case UploaderStates.success:
return (
<UploadSuccess
title={title}
title="3D model"
onOk={() => {
onSuccess();
onClose();
// Store the selected category and tab in sessionStorage
const assetTab = categoryToAssetTab[selectedCategory];
if (assetTab) {
sessionStorage.setItem(
"lastUploadedCategory",
selectedCategory,
);
sessionStorage.setItem("lastUploadedTab", assetTab);
// Reopen the asset modal
assetModalVisible.value = true;
} else {
// Clear any existing stored values if we don't have a valid tab
sessionStorage.removeItem("lastUploadedCategory");
sessionStorage.removeItem("lastUploadedTab");
}
}}
/>
);
@@ -87,7 +178,7 @@ export function UploadModal3D({
onRetry={() => {
resetModalState();
}}
type={FilterEngineCategories.OBJECT}
type={selectedCategory}
errorMessage={uploaderState.errorMessage}
/>
);
@@ -104,10 +195,18 @@ export function UploadModal3D({
/>
);
}
return undefined;
};
return (
<TransitionDialogue isOpen={isOpen} onClose={onClose} title={title}>
<TransitionDialogue
isOpen={isOpen}
onClose={onClose}
title={title}
className="max-w-xl"
showClose={false}
>
<CloseButton className="absolute right-4 top-4" onClick={onClose} />
<UploaderModalContent />
</TransitionDialogue>
);
@@ -22,7 +22,14 @@ export const loadPreviewOnCanvas = ({
}) => {
// Setup of scene, camera, and renderer in the canvas
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000);
// Calculate proper aspect ratio from canvas dimensions
const width = canvas.getBoundingClientRect().width || 0;
const height = canvas.getBoundingClientRect().height || 0;
const aspectRatio = width / height;
// Reduce FOV to make objects appear larger
const camera = new THREE.PerspectiveCamera(35, aspectRatio, 0.1, 1000);
camera.position.z = 2;
const renderer = new THREE.WebGLRenderer({
@@ -32,14 +39,16 @@ export const loadPreviewOnCanvas = ({
preserveDrawingBuffer: true,
});
renderer.setSize(
canvas.getBoundingClientRect().width || 0,
canvas.getBoundingClientRect().height || 0,
);
renderer.setSize(width, height);
const color = 0xfcece7;
const light = new THREE.HemisphereLight(color, 0x8d8d8d, 3.0);
// Add some additional lighting to better show the model
const frontLight = new THREE.DirectionalLight(0xffffff, 2);
frontLight.position.set(0, 0, 10);
scene.add(frontLight);
scene.add(light);
// load the file into the preview mini-scene depending of the file's type
@@ -88,33 +97,41 @@ const glbLoader = ({
child.userData["color"] = "#FFFFFF";
scene.add(child);
let maxSize = 2;
if (scene.children.length > 0) {
scene.children.forEach((child) => {
child.traverse((object: THREE.Object3D | THREE.Mesh) => {
// Assuming `object` is your Three.js object and you know it's a Mesh
if (object instanceof THREE.Mesh) {
object.geometry.computeBoundingBox();
const boundingBox = object.geometry.boundingBox;
const center = new THREE.Vector3();
boundingBox.getCenter(center);
const dimensions = new THREE.Vector3();
boundingBox.getSize(dimensions);
const maxDim = Math.max(
dimensions.x,
dimensions.y,
dimensions.z,
);
if (maxSize < maxDim) {
maxSize = maxDim;
camera.position.set(-maxDim, maxDim, maxDim);
camera.lookAt(center);
camera.updateProjectionMatrix();
}
}
});
});
}
// Calculate bounding box for all objects in the scene
const box = new THREE.Box3();
scene.traverse((object) => {
if (object instanceof THREE.Mesh) {
object.geometry.computeBoundingBox();
box.expandByObject(object);
}
});
// Get the center and size of the bounding box
const center = new THREE.Vector3();
const size = new THREE.Vector3();
box.getCenter(center);
box.getSize(size);
// Calculate the radius of the bounding sphere
const radius = Math.max(size.x, size.y, size.z) * 0.5;
// Position camera to fit the object in view with minimal padding
const fov = camera.fov * (Math.PI / 180);
const distance = (radius * 1.2) / Math.tan(fov * 0.5);
// Position camera at a better angle for preview
camera.position.set(
center.x + distance * 0.6,
center.y + distance * 0.4,
center.z + distance * 0.6,
);
camera.lookAt(center);
// Adjust the camera's near and far planes to ensure the model is visible
camera.near = distance * 0.01;
camera.far = distance * 100;
camera.updateProjectionMatrix();
renderer.render(scene, camera);
statusCallback({
type: "OK",
@@ -1,6 +1,8 @@
import { useState } from "react";
import {
faChevronLeft,
faChevronRight,
faGear,
faImages,
} from "@fortawesome/pro-solid-svg-icons";
import { useLocation, useParams } from "react-router-dom";
@@ -10,7 +12,7 @@ import { SceneTitleInput } from "./SceneTitleInput";
import { getCurrentLocationWithoutParams } from "~/utilities";
import { Activity } from "~/pages/PageEnigma/comps/GenerateModals/Activity";
import { LibraryModal } from "~/pages/PageEnigma/comps/LibraryModal/LibraryModal";
import { useState } from "react";
import { SettingsModal } from "@storyteller/ui-settings-modal";
function isEditorPath(path: string) {
if (path === "/") return true;
@@ -28,6 +30,7 @@ export const TopBar = ({ pageName }: Props) => {
);
const [isLibraryModalOpen, setIsLibraryModalOpen] = useState(false);
const [activeLibraryTab, setActiveLibraryTab] = useState("my-media");
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
const handleClick = () => {
window.location.href = "https://storyteller-2d.netlify.app/";
@@ -72,6 +75,12 @@ export const TopBar = ({ pageName }: Props) => {
<div className="flex justify-end gap-3.5">
<div className="flex gap-2">
<Button
variant="secondary"
icon={faGear}
className="h-[38px] w-[38px]"
onClick={() => setIsSettingsModalOpen(true)}
/>
<Button
variant="secondary"
icon={faImages}
@@ -79,6 +88,7 @@ export const TopBar = ({ pageName }: Props) => {
>
My Gallery
</Button>
<Activity />
</div>
<div className="flex justify-end gap-2">
@@ -88,6 +98,11 @@ export const TopBar = ({ pageName }: Props) => {
</nav>
</header>
<SettingsModal
isOpen={isSettingsModalOpen}
onClose={() => setIsSettingsModalOpen(false)}
/>
<LibraryModal
isOpen={isLibraryModalOpen}
onClose={() => setIsLibraryModalOpen(false)}
@@ -59,14 +59,7 @@ class DndAsset {
this.overElement = null;
overTimeline.value = false;
this.notDropText = "";
if (reopenAfterDragSignal.value) {
assetModalVisibleDuringDrag.value = true;
} else {
assetModalVisibleDuringDrag.value = true;
setTimeout(() => {
assetModalVisibleDuringDrag.value = false;
}, 0);
}
assetModalVisibleDuringDrag.value = reopenAfterDragSignal.value;
}
}
@@ -120,7 +120,7 @@ export const PageEditor = () => {
onClick={handleOverlayClick}
>
<div
className="absolute bottom-0 mb-2 ml-2 flex origin-bottom-left flex-col gap-2"
className="absolute bottom-0 mb-4 ml-4 flex origin-bottom-left flex-col gap-2"
style={{ transform: `scale(${getScale()})` }}
>
<Outliner />
@@ -1,4 +1,5 @@
import { TransitionDialogue } from "~/components/reusable/TransitionDialogue";
import { Modal } from "@storyteller/ui-modal";
import { UploadModal3D } from "~/components/reusable/UploadModal3D";
import {
faPlus,
faSearch,
@@ -10,6 +11,7 @@ import {
faChevronRight,
faMountainCity,
faDog,
faImage,
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button, CloseButton, Input, Tooltip } from "~/components";
@@ -26,14 +28,10 @@ import {
} from "../../signals";
import { MediaItem } from "../../models";
import { useUserObjects, useFeaturedObjects } from "../SidePanelTabs/hooks";
import { FilterEngineCategories } from "~/enums";
import { FilterEngineCategories, FilterMediaType } from "~/enums";
import { twMerge } from "tailwind-merge";
import { useSignals } from "@preact/signals-react/runtime";
interface AssetModalProps {
isOpen: boolean;
onClose: () => void;
}
import { isAnyStatusFetching } from "../SidePanelTabs/utilities";
type AssetTab = {
id: string;
@@ -79,130 +77,236 @@ export const AssetModal = () => {
const [activeLibraryTab, setActiveLibraryTab] = useState("library");
const [activeAssetTab, setActiveAssetTab] = useState("all");
const [searchTerm, setSearchTerm] = useState("");
const [reopenAfterAdd, setReopenAfterAdd] = useState(false);
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
const searchInputRef = useRef<HTMLInputElement>(null);
const handleReopenChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.checked;
// Force the update to be asynchronous
setTimeout(() => {
reopenAfterDragSignal.value = newValue;
}, 0);
};
const handleClose = () => {
assetModalVisible.value = false;
};
// Update the signal when the preference changes
useEffect(() => {
reopenAfterDragSignal.value = reopenAfterAdd;
}, [reopenAfterAdd]);
useEffect(() => {
if (assetModalVisible.value) {
// Check for stored category and tab from upload
const lastUploadedTab = sessionStorage.getItem("lastUploadedTab");
if (lastUploadedTab) {
setActiveLibraryTab("mine");
setActiveAssetTab(lastUploadedTab);
// Clear the stored values
sessionStorage.removeItem("lastUploadedTab");
sessionStorage.removeItem("lastUploadedCategory");
}
// Small delay to ensure the modal is fully mounted
const timer = setTimeout(() => {
searchInputRef.current?.focus();
}, 100);
return () => clearTimeout(timer);
}
return undefined;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [assetModalVisible.value]);
// Fetch objects for different categories
const { userObjects: userCharacters } = useUserObjects({
const {
userObjects: userCharacters,
userFetchStatus: userCharactersFetchStatus,
fetchUserObjects: fetchUserCharacters,
} = useUserObjects({
filterEngineCategories: [FilterEngineCategories.CHARACTER],
defaultErrorMessage: "Error fetching user characters",
});
const { userObjects: userObjects } = useUserObjects({
const {
userObjects: userObjects,
userFetchStatus: userObjectsFetchStatus,
fetchUserObjects: fetchUserObjects,
} = useUserObjects({
filterEngineCategories: [FilterEngineCategories.OBJECT],
defaultErrorMessage: "Error fetching user objects",
});
const { featuredObjects: featuredCharacters } = useFeaturedObjects({
filterEngineCategories: [FilterEngineCategories.CHARACTER],
defaultErrorMessage: "Error fetching featured characters",
});
const { featuredObjects: featuredObjects } = useFeaturedObjects({
filterEngineCategories: [FilterEngineCategories.OBJECT],
defaultErrorMessage: "Error fetching featured objects",
});
const { userObjects: userSets } = useUserObjects({
const {
userObjects: userSets,
userFetchStatus: userSetsFetchStatus,
fetchUserObjects: fetchUserSets,
} = useUserObjects({
filterEngineCategories: [FilterEngineCategories.LOCATION],
defaultErrorMessage: "Error fetching user sets",
});
const { featuredObjects: featuredSets } = useFeaturedObjects({
filterEngineCategories: [FilterEngineCategories.LOCATION],
defaultErrorMessage: "Error fetching featured sets",
});
const { userObjects: userCreatures } = useUserObjects({
const {
userObjects: userCreatures,
userFetchStatus: userCreaturesFetchStatus,
fetchUserObjects: fetchUserCreatures,
} = useUserObjects({
filterEngineCategories: [FilterEngineCategories.CREATURE],
defaultErrorMessage: "Error fetching user creatures",
});
const { featuredObjects: featuredCreatures } = useFeaturedObjects({
const {
userObjects: userImagePlanes,
userFetchStatus: userImagePlanesFetchStatus,
fetchUserObjects: fetchUserImagePlanes,
} = useUserObjects({
filterEngineCategories: [FilterEngineCategories.IMAGE_PLANE],
defaultErrorMessage: "Error fetching user image planes",
});
const {
featuredObjects: featuredCharacters,
featuredFetchStatus: featuredCharactersFetchStatus,
} = useFeaturedObjects({
filterEngineCategories: [FilterEngineCategories.CHARACTER],
filterMediaTypes: [FilterMediaType.GLB],
defaultErrorMessage: "Error fetching featured characters",
});
const {
featuredObjects: featuredObjects,
featuredFetchStatus: featuredObjectsFetchStatus,
} = useFeaturedObjects({
filterEngineCategories: [FilterEngineCategories.OBJECT],
defaultErrorMessage: "Error fetching featured objects",
});
const {
featuredObjects: featuredSets,
featuredFetchStatus: featuredSetsFetchStatus,
} = useFeaturedObjects({
filterEngineCategories: [FilterEngineCategories.LOCATION],
defaultErrorMessage: "Error fetching featured sets",
});
const {
featuredObjects: featuredCreatures,
featuredFetchStatus: featuredCreaturesFetchStatus,
} = useFeaturedObjects({
filterEngineCategories: [FilterEngineCategories.CREATURE],
defaultErrorMessage: "Error fetching featured creatures",
});
const {
featuredObjects: featuredImagePlanes,
featuredFetchStatus: featuredImagePlanesFetchStatus,
} = useFeaturedObjects({
filterEngineCategories: [FilterEngineCategories.IMAGE_PLANE],
defaultErrorMessage: "Error fetching featured image planes",
});
const isFetching = isAnyStatusFetching([
userCharactersFetchStatus,
userObjectsFetchStatus,
userSetsFetchStatus,
userCreaturesFetchStatus,
userImagePlanesFetchStatus,
featuredCharactersFetchStatus,
featuredObjectsFetchStatus,
featuredSetsFetchStatus,
featuredCreaturesFetchStatus,
featuredImagePlanesFetchStatus,
]);
const libraryTabs = [
{ id: "library", label: "Library" },
{ id: "mine", label: "Mine" },
];
// eslint-disable-next-line react-hooks/exhaustive-deps
const assetTabs: AssetTab[] = [
{ id: "all", label: "All", icon: faLayerGroup, items: [] },
{
id: "character",
label: "Character",
icon: faUser,
engineCategory: FilterEngineCategories.CHARACTER,
items:
activeLibraryTab === "library"
? [...demoCharacterItems.value, ...(featuredCharacters ?? [])]
: (userCharacters ?? []),
},
{
id: "objects",
label: "Objects",
icon: faCube,
engineCategory: FilterEngineCategories.OBJECT,
items:
activeLibraryTab === "library"
? [...demoShapeItems.value, ...(featuredObjects ?? [])]
: (userObjects ?? []),
},
{
id: "sets",
label: "Sets",
icon: faMountainCity,
engineCategory: FilterEngineCategories.LOCATION,
items:
activeLibraryTab === "library"
? (featuredSets ?? [])
: (userSets ?? []),
},
{
id: "creatures",
label: "Creatures",
icon: faDog,
engineCategory: FilterEngineCategories.CREATURE,
items:
activeLibraryTab === "library"
? (featuredCreatures ?? [])
: (userCreatures ?? []),
},
{
id: "skybox",
label: "Skybox",
icon: faSun,
items: demoSkyboxItems.value,
},
];
const assetTabs = useMemo<AssetTab[]>(
() => [
{ id: "all", label: "All", icon: faLayerGroup, items: [] },
{
id: "character",
label: "Character",
icon: faUser,
engineCategory: FilterEngineCategories.CHARACTER,
items:
activeLibraryTab === "library"
? [...demoCharacterItems.value, ...(featuredCharacters ?? [])]
: (userCharacters ?? []),
},
{
id: "objects",
label: "Objects",
icon: faCube,
engineCategory: FilterEngineCategories.OBJECT,
items:
activeLibraryTab === "library"
? [...demoShapeItems.value, ...(featuredObjects ?? [])]
: (userObjects ?? []),
},
{
id: "sets",
label: "Sets",
icon: faMountainCity,
engineCategory: FilterEngineCategories.LOCATION,
items:
activeLibraryTab === "library"
? (featuredSets ?? [])
: (userSets ?? []),
},
{
id: "creatures",
label: "Creatures",
icon: faDog,
engineCategory: FilterEngineCategories.CREATURE,
items:
activeLibraryTab === "library"
? (featuredCreatures ?? [])
: (userCreatures ?? []),
},
{
id: "skybox",
label: "Skybox",
icon: faSun,
items: activeLibraryTab === "library" ? demoSkyboxItems.value : [],
},
{
id: "image-planes",
label: "Image Planes",
icon: faImage,
engineCategory: FilterEngineCategories.IMAGE_PLANE,
items:
activeLibraryTab === "library"
? (featuredImagePlanes ?? [])
: (userImagePlanes ?? []),
},
],
// eslint-disable-next-line react-hooks/exhaustive-deps
[
activeLibraryTab,
demoCharacterItems.value,
demoShapeItems.value,
demoSkyboxItems.value,
featuredCharacters,
featuredCreatures,
featuredObjects,
featuredSets,
featuredImagePlanes,
userCharacters,
userCreatures,
userObjects,
userSets,
userImagePlanes,
],
);
// Combine all items for the "All" tab
const allItems = useMemo(() => {
return assetTabs
const items = assetTabs
.filter((tab) => tab.id !== "all")
.flatMap((tab) => tab.items);
// console.log("All items:", items);
// console.log("Active library tab:", activeLibraryTab);
// console.log("User creatures:", userCreatures);
// console.log("User objects:", userObjects);
return items;
}, [assetTabs]);
// Update the items array for the "All" tab
@@ -238,11 +342,31 @@ export const AssetModal = () => {
setSearchTerm("");
}, [activeAssetTab]);
// Refresh user content when switching to mine tab
useEffect(() => {
if (activeLibraryTab === "mine") {
// Trigger a refetch of user content
fetchUserCharacters();
fetchUserObjects();
fetchUserSets();
fetchUserCreatures();
fetchUserImagePlanes();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeLibraryTab]);
const renderContent = () => {
if (activeAssetTab === "all" && !searchTerm) {
// Filter out empty categories when in "mine" tab
const tabsToShow = assetTabs
.slice(1)
.filter(
(tab) => activeLibraryTab === "library" || tab.items.length > 0,
);
return (
<div className="h-full space-y-2 overflow-y-auto">
{assetTabs.slice(1).map((tab) => (
{tabsToShow.map((tab) => (
<AllTabSection
key={tab.id}
label={tab.label}
@@ -257,14 +381,19 @@ export const AssetModal = () => {
return (
<ItemElements
items={displayedItems}
busy={false}
busy={isFetching}
debug={`asset-modal-${currentTab.id}`}
/>
);
};
const handleAddAsset = () => {
if (reopenAfterAdd) {
assetModalVisible.value = false;
setIsUploadModalOpen(true);
};
const handleUploadSuccess = () => {
if (reopenAfterDragSignal.value) {
// Small delay to allow the modal to close and reopen
setTimeout(() => {
handleClose();
@@ -277,113 +406,144 @@ export const AssetModal = () => {
const clearSearch = () => setSearchTerm("");
return (
<TransitionDialogue
isOpen={assetModalVisible.value && assetModalVisibleDuringDrag.value}
onClose={handleClose}
className="h-[640px] max-w-4xl"
childPadding={false}
>
<div className="grid h-full grid-cols-12 gap-3">
<div className="relative col-span-3 flex h-full flex-col p-3 pt-2 after:absolute after:right-0 after:top-0 after:h-full after:w-px after:bg-gray-200 after:dark:bg-white/10">
<div className="flex items-center justify-between gap-2.5 py-0.5">
<h2 className="text-[18px] font-semibold opacity-80">3D Assets</h2>
<Tooltip content="Upload model" position="top" delay={200}>
<Button
className="h-6 w-6 rounded-full border-none bg-transparent text-white/70 transition-colors hover:bg-transparent hover:text-white/100"
onClick={handleAddAsset}
>
<FontAwesomeIcon icon={faPlus} className="text-xl" />
</Button>
</Tooltip>
</div>
<hr className="my-2 w-full border-white/10" />
<div className="space-y-2">
{assetTabs.map((tab) => (
<Button
key={tab.id}
variant={activeAssetTab === tab.id ? "primary" : "secondary"}
className={twMerge(
"w-full justify-start rounded-xl border border-white/[2%] bg-white/[4%] px-3.5 py-2.5 text-left hover:bg-white/15",
activeAssetTab === tab.id &&
"border-brand-primary bg-brand-primary/10 hover:bg-brand-primary/10",
)}
onClick={() => setActiveAssetTab(tab.id)}
>
<FontAwesomeIcon icon={tab.icon} className="mr-2 opacity-70" />
{tab.label}
</Button>
))}
</div>
<div className="mt-auto flex items-center gap-2 pt-3">
<input
type="checkbox"
id="reopen-after-add"
checked={reopenAfterAdd}
onChange={(e) => setReopenAfterAdd(e.target.checked)}
className="h-4 w-4 rounded-lg border-gray-300 bg-gray-700 text-brand-primary focus:ring-brand-primary"
/>
<label htmlFor="reopen-after-add" className="text-sm text-white/70">
Reopen after adding
</label>
</div>
</div>
<div className="col-span-9 p-3 pb-0 ps-0 pt-2">
<div className="flex h-full flex-col">
<div className="h-full">
<div className="flex items-center gap-4">
<TabSelector
tabs={libraryTabs}
activeTab={activeLibraryTab}
onTabChange={setActiveLibraryTab}
className="w-auto"
/>
<div className="relative grow">
<Input
ref={searchInputRef}
placeholder="Search"
className="grow"
inputClassName="pr-2.5"
icon={faSearch}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
iconClassName="text-white/60"
/>
{searchTerm && (
<CloseButton
onClick={clearSearch}
className="absolute right-2.5 top-1/2 h-4 w-4 -translate-y-1/2 bg-white/10 text-[10px] hover:bg-white/20"
/>
<>
<Modal
isOpen={assetModalVisible.value && assetModalVisibleDuringDrag.value}
onClose={handleClose}
className="h-[640px] max-w-4xl"
childPadding={false}
>
<div className="grid h-full grid-cols-12 gap-3">
<div className="relative col-span-3 flex h-full flex-col p-3 pt-2 after:absolute after:right-0 after:top-0 after:h-full after:w-px after:bg-gray-200 after:dark:bg-white/10">
<div className="flex items-center justify-between gap-2.5 py-0.5">
<h2 className="text-[18px] font-semibold opacity-80">
3D Assets
</h2>
<Tooltip content="Upload model" position="top" delay={200}>
<Button
className="h-6 w-6 rounded-full border-none bg-transparent text-white/70 transition-colors hover:bg-transparent hover:text-white/100"
onClick={handleAddAsset}
>
<FontAwesomeIcon icon={faPlus} className="text-xl" />
</Button>
</Tooltip>
</div>
<hr className="my-2 w-full border-white/10" />
<div className="space-y-2">
{assetTabs.map((tab) => (
<Button
key={tab.id}
variant={activeAssetTab === tab.id ? "primary" : "secondary"}
className={twMerge(
"w-full justify-start rounded-xl border border-white/[2%] bg-white/[4%] px-3.5 py-2.5 text-left hover:bg-white/15",
activeAssetTab === tab.id &&
"border-brand-primary bg-brand-primary/10 hover:bg-brand-primary/10",
)}
</div>
<CloseButton onClick={handleClose} />
</div>
onClick={() => setActiveAssetTab(tab.id)}
>
<FontAwesomeIcon
icon={tab.icon}
className="mr-2 opacity-70"
/>
{tab.label}
</Button>
))}
</div>
<div className="mt-auto flex items-center gap-2 pt-3">
<div
className={twMerge(
"overflow-auto-y mt-4 h-[574px]",
activeAssetTab !== "all" && "h-[552px]",
)}
className="flex cursor-pointer items-center"
onClick={(e) => {
// Prevent double-firing when clicking the checkbox itself
if (e.target instanceof HTMLInputElement) return;
const checkbox = document.getElementById(
"reopen-after-add",
) as HTMLInputElement;
if (checkbox) {
checkbox.checked = !checkbox.checked;
reopenAfterDragSignal.value = checkbox.checked;
}
}}
>
{activeAssetTab !== "all" && !searchTerm && (
<div className="mb-2 flex items-center font-semibold">
<Button
variant="secondary"
className="flex items-center gap-2 border-none bg-transparent px-3 py-1.5 text-sm text-white/70 hover:bg-transparent hover:text-white/100"
onClick={() => setActiveAssetTab("all")}
>
<FontAwesomeIcon
icon={faChevronLeft}
className="text-sm font-semibold opacity-70"
<input
type="checkbox"
id="reopen-after-add"
checked={reopenAfterDragSignal.value}
onChange={handleReopenChange}
className="h-4 w-4 cursor-pointer rounded-lg border-gray-300 bg-gray-700 text-brand-primary focus:ring-brand-primary"
/>
<label
htmlFor="reopen-after-add"
className="ml-2 cursor-pointer select-none text-sm text-white/70"
>
Reopen after adding
</label>
</div>
</div>
</div>
<div className="col-span-9 p-3 pb-0 ps-0 pt-2">
<div className="flex h-full flex-col">
<div className="h-full">
<div className="flex items-center gap-4">
<TabSelector
tabs={libraryTabs}
activeTab={activeLibraryTab}
onTabChange={setActiveLibraryTab}
className="w-auto"
/>
<div className="relative grow">
<Input
ref={searchInputRef}
placeholder="Search"
className="grow"
inputClassName="pr-2.5"
icon={faSearch}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
iconClassName="text-white/60"
/>
{searchTerm && (
<CloseButton
onClick={clearSearch}
className="absolute right-2.5 top-1/2 h-4 w-4 -translate-y-1/2 bg-white/10 text-[10px] hover:bg-white/20"
/>
</Button>
{currentTab.label}
)}
</div>
)}
{renderContent()}
<CloseButton onClick={handleClose} />
</div>
<div
className={twMerge(
"overflow-auto-y mt-4 h-[574px]",
activeAssetTab !== "all" && "h-[552px]",
)}
>
{activeAssetTab !== "all" && !searchTerm && (
<div className="mb-2 flex items-center font-semibold">
<Button
variant="secondary"
className="flex items-center gap-2 border-none bg-transparent px-3 py-1.5 text-sm text-white/70 hover:bg-transparent hover:text-white/100"
onClick={() => setActiveAssetTab("all")}
>
<FontAwesomeIcon
icon={faChevronLeft}
className="text-sm font-semibold opacity-70"
/>
</Button>
{currentTab.label}
</div>
)}
{renderContent()}
</div>
</div>
</div>
</div>
</div>
</div>
</TransitionDialogue>
</Modal>
<UploadModal3D
onClose={() => setIsUploadModalOpen(false)}
onSuccess={handleUploadSuccess}
isOpen={isUploadModalOpen}
title="Upload 3D Asset"
/>
</>
);
};
@@ -155,7 +155,7 @@ export const ControlPanelSceneObject = () => {
updatedScaleValues.z = (parseFloat(localScale.z) + scale).toString();
handleScaleChange(updatedScaleValues);
}
};
const handleScaleChange = (xyz: Record<string, string>) => {
if (isInvalid(xyz)) {
@@ -180,9 +180,10 @@ export const ControlPanelSceneObject = () => {
return (
<Transition
as="div"
show={isShowing.value}
className={twMerge(
"glass absolute bottom-0 right-0 mb-2 mr-2 flex h-fit w-56 origin-bottom-right flex-col gap-2 rounded-lg border border-ui-panel-border p-3.5 text-white shadow-lg",
"glass absolute bottom-0 right-0 mb-4 mr-4 flex h-fit w-56 origin-bottom-right flex-col gap-2 rounded-lg border border-ui-panel-border p-3.5 text-white shadow-lg",
)}
enter="transition-opacity duration-150"
enterFrom="opacity-0"
@@ -208,6 +209,7 @@ export const ControlPanelSceneObject = () => {
</div>
<Transition
as="div"
show={!isCollapsed}
enter="transition-all duration-200 ease-in-out"
enterFrom="opacity-0 max-h-0"
@@ -179,6 +179,7 @@ export const Outliner = () => {
return (
<Transition
as="div"
show={outlinerIsShowing.value}
className={twMerge(
"glass flex max-h-[34vh] w-[240px] origin-bottom-left flex-col overflow-hidden rounded-lg shadow-lg",
@@ -2,11 +2,7 @@ import React from "react";
import { useSignals } from "@preact/signals-react/runtime";
import { poseMode, showPoseControls } from "../signals/selectedMode";
import { Button, Tooltip } from "~/components";
import {
faCheck,
faCheckDouble,
faPersonRunning,
} from "@fortawesome/pro-solid-svg-icons";
import { faCheck, faPersonRunning } from "@fortawesome/pro-solid-svg-icons";
declare global {
interface Window {
@@ -0,0 +1 @@
export { SettingsModal } from "./SettingsModal";
@@ -5,11 +5,7 @@ import { Badge } from "~/components";
import { AssetType } from "~/enums";
import { useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faHandPointer } from "@fortawesome/pro-regular-svg-icons";
import {
faArrowPointer,
faUpDownLeftRight,
} from "@fortawesome/pro-solid-svg-icons";
import { faUpDownLeftRight } from "@fortawesome/pro-solid-svg-icons";
interface Props {
debug?: string;
@@ -44,10 +40,7 @@ export const ItemElement = ({ item }: Props) => {
const [imageError, setImageError] = useState(false);
return (
<div
className="group relative w-full cursor-pointer select-none overflow-hidden transition-all duration-200"
onPointerDown={(event) => DndAsset.onPointerDown(event, item)}
>
<div className="group relative w-full select-none overflow-hidden transition-all duration-200">
{item.media_type && (
<Badge
label={
@@ -61,7 +54,11 @@ export const ItemElement = ({ item }: Props) => {
/>
)}
<div className="pointer-events-none relative aspect-[16/12] w-full select-none overflow-hidden rounded-xl border-[3px] border-white/5 bg-brand-secondary-600 object-cover object-center transition-all group-hover:border-brand-primary">
<div
className="pointer-events-none relative aspect-[16/12] w-full select-none overflow-hidden rounded-xl border-[3px] border-white/5 bg-brand-secondary-600 object-cover object-center transition-all group-hover:border-brand-primary"
onPointerDown={(event) => DndAsset.onPointerDown(event, item)}
style={{ cursor: "grab", pointerEvents: "auto" }}
>
{item.thumbnail && !imageError && (
<img
crossOrigin="anonymous"
-1
View File
@@ -35,7 +35,6 @@
"@fortawesome/pro-regular-svg-icons": "^6.5.1",
"@fortawesome/pro-solid-svg-icons": "^6.5.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"@headlessui/react": "^1.7.18",
"@mediapipe/tasks-vision": "^0.10.20",
"@netlify/edge-functions": "^2.3.1",
"@netlify/remix-edge-adapter": "^3.2.0",
+32 -13
View File
@@ -6,6 +6,7 @@ export default {
"./app/index.html",
"./app/src/**/*.{js,jsx,ts,tsx}",
"./app/src/*.{js,jsx,ts,tsx}",
"../../libs/components/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {
@@ -25,7 +26,37 @@ export default {
colors: {
transparent: "transparent",
current: "currentColor",
white: colors.white,
gray: colors.gray,
black: colors.black,
primary: {
DEFAULT: "#2d81ff",
"50": "#eef6ff",
"100": "#d9ebff",
"200": "#bcdcff",
"300": "#8ec7ff",
"400": "#59a7ff",
"500": "#2d81ff",
"600": "#1b63f5",
"700": "#144ee1",
"800": "#173fb6",
"900": "#19398f",
"950": "#142457",
},
secondary: {
DEFAULT: "#3E3E41",
"50": "#f6f6f6",
"100": "#e7e7e7",
"200": "#d1d1d1",
"300": "#b0b0b0",
"400": "#888888",
"500": "#6d6d6d",
"600": "#5d5d5d",
"700": "#4f4f4f",
"800": "#454545",
"900": "#3d3d3d",
"950": "#3E3E41",
},
"brand-primary": {
//sunglo
DEFAULT: "#2d81ff", //=500
@@ -85,7 +116,6 @@ export default {
"900": "#64373f",
"950": "#361b1e",
},
success: {
//apple
DEFAULT: "#40ad48", //=500
@@ -101,7 +131,6 @@ export default {
"900": "#1f4823",
"950": "#0c270f",
},
action: {
DEFAULT: "#3E3E41",
"50": "#f6f6f6",
@@ -116,7 +145,6 @@ export default {
"900": "#3d3d3d",
"950": "#3E3E41",
},
character: {
unselected: "#46527C",
selected: "#6384F4",
@@ -169,7 +197,6 @@ export default {
cannotDrop: "#904948",
wrapper: "#39394d",
},
ui: {
background: "#242424",
panel: "#1F1F1F",
@@ -178,7 +205,6 @@ export default {
"controls-button": "#3E3E41",
divider: "#515168",
},
media: {
"audio-tts": "#6AAA88",
"audio-v2v": "#6691B9",
@@ -186,29 +212,22 @@ export default {
"audio-demo": "#B96675",
"is-new": "#FFDC26",
},
inference: {
pending: "#6b728b",
generating: "#46527C",
error: "#924C4B",
},
"axis-x": "#D33242",
"axis-y": "#308752",
"axis-z": "#2E70FF",
red: "#D33242",
green: "#308752",
blue: "#2E70FF",
facebook: "#4267b2",
reddit: "#ff5700",
whatsapp: "#25d366",
x: "#000000",
email: "#858585",
white: colors.white,
gray: colors.gray,
black: colors.black,
},
},
plugins: [],
+3
View File
@@ -23,6 +23,9 @@
"references": [
{
"path": "../../libs/components/toaster"
},
{
"path": "../../libs/components/settings-modal"
}
]
}
+12
View File
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
@@ -0,0 +1,7 @@
# @storyteller/ui-button
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test @storyteller/ui-button` to execute the unit tests via [Vitest](https://vitest.dev/).
@@ -0,0 +1,17 @@
{
"name": "@storyteller/ui-button",
"version": "0.0.1",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
}
}
@@ -0,0 +1,2 @@
export * from './lib/button';
@@ -0,0 +1,7 @@
/*
* Replace this with your own classes
*
* e.g.
* .container {
* }
*/
@@ -0,0 +1,12 @@
import { render } from '@testing-library/react';
import Button from './button';
describe('Button', () => {
it('should render successfully', () => {
const { baseElement } = render(<Button />);
expect(baseElement).toBeTruthy();
});
});
@@ -0,0 +1,108 @@
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { ButtonHTMLAttributes } from "react";
import { faSpinnerThird } from "@fortawesome/pro-solid-svg-icons";
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
icon?: IconDefinition;
iconClassName?: string;
iconFlip?: boolean;
htmlFor?: string;
variant?: "primary" | "secondary" | "action";
loading?: boolean;
}
export const Button = ({
icon,
iconClassName,
children,
className: propsClassName,
htmlFor,
variant: propsVariant = "primary",
disabled,
iconFlip = false,
loading,
...rest
}: ButtonProps) => {
function getVariantClassNames(variant: string) {
switch (variant) {
case "secondary": {
return "bg-secondary hover:bg-secondary-700 text-white focus-visible:outline-secondary";
}
case "action": {
return "bg-action hover:bg-action-900 text-white focus-visible:outline-action";
}
case "primary":
default: {
return "bg-primary hover:bg-primary-400 text-white focus-visible:outline-primary-600";
}
}
}
const disabledClass = twMerge(
disabled || loading ? "opacity-50 pointer-events-none" : ""
);
const className = twMerge(
"text-sm font-medium rounded-lg px-3 py-2 border border-transparent shadow-sm focus-visible:outline focus-visible:outline-0 focus-visible:outline-offset-0 transition-all duration-150 flex gap-2 items-center justify-center",
getVariantClassNames(propsVariant),
propsClassName,
disabledClass
);
if (htmlFor) {
return (
<label className={className} htmlFor={htmlFor} style={rest.style}>
{loading && !iconFlip ? (
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
) : (
<>
{icon && !iconFlip ? (
<FontAwesomeIcon icon={icon} className={iconClassName} />
) : null}
</>
)}
{children}
{loading && iconFlip ? (
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
) : (
<>
{icon && iconFlip ? (
<FontAwesomeIcon icon={icon} className={iconClassName} />
) : null}
</>
)}
</label>
);
}
return (
<button
className={className}
disabled={disabled || loading}
{...{ ...rest, htmlFor }}
>
{loading && !iconFlip ? (
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
) : (
<>
{icon && !iconFlip ? (
<FontAwesomeIcon icon={icon} className={iconClassName} />
) : null}
</>
)}
{children}
{loading && iconFlip ? (
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
) : (
<>
{icon && iconFlip ? (
<FontAwesomeIcon icon={icon} className={iconClassName} />
) : null}
</>
)}
</button>
);
};
export default Button;
@@ -0,0 +1,13 @@
{
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../../tsconfig.base.json"
}
@@ -0,0 +1,45 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"types": [
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts",
"vite/client"
],
"rootDir": "src",
"jsx": "react-jsx",
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo"
},
"exclude": [
"out-tsc",
"dist",
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx"
],
"include": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.ts",
"src/**/*.tsx"
]
}
@@ -0,0 +1,34 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out-tsc/vitest",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest"
],
"jsx": "react-jsx"
},
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}
@@ -0,0 +1,49 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import dts from 'vite-plugin-dts';
import * as path from 'path';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../../node_modules/.vite/libs/components/button',
plugins: [react(), dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: './dist',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
name: '@storyteller/ui-button',
fileName: 'index',
// Change this to the formats you want to support.
// Don't forget to update your package.json as well.
formats: ['es' as const]
},
rollupOptions: {
// External packages that should not be bundled into your library.
external: ['react','react-dom','react/jsx-runtime']
},
},
test: {
watch: false,
globals: true,
environment: 'jsdom',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: './test-output/vitest/coverage',
provider: 'v8' as const,
}
},
}));
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
@@ -0,0 +1,7 @@
# @storyteller/ui-close-button
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test @storyteller/ui-close-button` to execute the unit tests via [Vitest](https://vitest.dev/).
@@ -0,0 +1,17 @@
{
"name": "@storyteller/ui-close-button",
"version": "0.0.1",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
}
}
@@ -0,0 +1,2 @@
export * from './lib/close-button';
@@ -0,0 +1,7 @@
/*
* Replace this with your own classes
*
* e.g.
* .container {
* }
*/
@@ -0,0 +1,12 @@
import { render } from '@testing-library/react';
import CloseButton from './close-button';
describe('CloseButton', () => {
it('should render successfully', () => {
const { baseElement } = render(<CloseButton />);
expect(baseElement).toBeTruthy();
});
});
@@ -0,0 +1,36 @@
import { faXmark } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
interface CloseButtonProps {
onClick: () => void;
className?: string;
size?: "sm" | "md" | "lg";
}
export const CloseButton = ({
onClick,
className,
size = "md",
}: CloseButtonProps) => {
const sizeClasses = {
sm: "h-5 w-5 text-sm",
md: "h-7 w-7 text-md",
lg: "h-9 w-9 text-xl",
};
return (
<button
onClick={onClick}
className={twMerge(
"flex items-center justify-center rounded-full bg-black/40 text-white/60 transition-all hover:bg-black/70 hover:text-white",
sizeClasses[size],
className
)}
>
<FontAwesomeIcon icon={faXmark} />
</button>
);
};
export default CloseButton;
@@ -0,0 +1,13 @@
{
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../../tsconfig.base.json"
}
@@ -0,0 +1,45 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"types": [
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts",
"vite/client"
],
"rootDir": "src",
"jsx": "react-jsx",
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo"
},
"exclude": [
"out-tsc",
"dist",
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx"
],
"include": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.ts",
"src/**/*.tsx"
]
}
@@ -0,0 +1,34 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out-tsc/vitest",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest"
],
"jsx": "react-jsx"
},
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}
@@ -0,0 +1,49 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import dts from 'vite-plugin-dts';
import * as path from 'path';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../../node_modules/.vite/libs/components/close-button',
plugins: [react(), dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: './dist',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
name: '@storyteller/ui-close-button',
fileName: 'index',
// Change this to the formats you want to support.
// Don't forget to update your package.json as well.
formats: ['es' as const]
},
rollupOptions: {
// External packages that should not be bundled into your library.
external: ['react','react-dom','react/jsx-runtime']
},
},
test: {
watch: false,
globals: true,
environment: 'jsdom',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: './test-output/vitest/coverage',
provider: 'v8' as const,
}
},
}));
+12
View File
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
+7
View File
@@ -0,0 +1,7 @@
# @storyteller/ui-input
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test @storyteller/ui-input` to execute the unit tests via [Vitest](https://vitest.dev/).
@@ -0,0 +1,17 @@
{
"name": "@storyteller/ui-input",
"version": "0.0.1",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
}
}
@@ -0,0 +1,2 @@
export * from './lib/input';
@@ -0,0 +1,7 @@
/*
* Replace this with your own classes
*
* e.g.
* .container {
* }
*/
@@ -0,0 +1,12 @@
import { render } from '@testing-library/react';
import Input from './input';
describe('Input', () => {
it('should render successfully', () => {
const { baseElement } = render(<Input />);
expect(baseElement).toBeTruthy();
});
});
@@ -0,0 +1,77 @@
import React from "react";
import { twMerge } from "tailwind-merge";
import { IconDefinition } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Label } from "@storyteller/ui-label";
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
inputClassName?: string;
iconClassName?: string;
label?: string;
icon?: IconDefinition;
isError?: boolean;
errorMessage?: string;
}
export const Input = React.forwardRef(
(
{
label,
icon,
inputClassName,
iconClassName,
className,
id,
isError,
onBlur,
onFocus,
errorMessage,
...rest
}: InputProps,
ref: React.ForwardedRef<HTMLInputElement>
) => {
return (
<div className={twMerge("flex flex-col", className)}>
{label && <Label htmlFor={id ? id : label}>{label}</Label>}
<div className="relative w-full">
{icon && (
<FontAwesomeIcon
icon={icon}
className={twMerge("text-md absolute pl-3 pt-3", iconClassName)}
/>
)}
<input
ref={ref}
id={id ? id : label ? label : undefined}
className={twMerge(
"h-10 w-full rounded-lg bg-[#242424] px-3 py-2.5 text-white placeholder-white/50 outline-none",
"border border-[#363636] transition-all duration-150 ease-in-out hover:border-brand-primary/60 focus:border-brand-primary focus:!outline-none",
icon && "pl-10",
isError && "outline-red focus:outline-red",
inputClassName
)}
onFocus={(e: React.FocusEvent<HTMLInputElement>) => {
if (onFocus) {
onFocus(e);
}
}}
onBlur={(e: React.FocusEvent<HTMLInputElement>) => {
if (onBlur) {
onBlur(e);
}
}}
{...rest}
/>
{errorMessage && (
<h6 className="absolute z-10 text-red">{errorMessage}</h6>
)}
</div>
</div>
);
}
);
Input.displayName = "Input";
export default Input;
@@ -0,0 +1,16 @@
{
"files": [],
"include": [],
"references": [
{
"path": "../label"
},
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../../tsconfig.base.json"
}
@@ -0,0 +1,50 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"types": [
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts",
"vite/client"
],
"rootDir": "src",
"jsx": "react-jsx",
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo"
},
"exclude": [
"out-tsc",
"dist",
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx"
],
"include": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.ts",
"src/**/*.tsx"
],
"references": [
{
"path": "../label/tsconfig.lib.json"
}
]
}
@@ -0,0 +1,34 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out-tsc/vitest",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest"
],
"jsx": "react-jsx"
},
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}
@@ -0,0 +1,49 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import dts from 'vite-plugin-dts';
import * as path from 'path';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../../node_modules/.vite/libs/components/input',
plugins: [react(), dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: './dist',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
name: '@storyteller/ui-input',
fileName: 'index',
// Change this to the formats you want to support.
// Don't forget to update your package.json as well.
formats: ['es' as const]
},
rollupOptions: {
// External packages that should not be bundled into your library.
external: ['react','react-dom','react/jsx-runtime']
},
},
test: {
watch: false,
globals: true,
environment: 'jsdom',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: './test-output/vitest/coverage',
provider: 'v8' as const,
}
},
}));
+12
View File
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
+7
View File
@@ -0,0 +1,7 @@
# @storyteller/ui-label
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test @storyteller/ui-label` to execute the unit tests via [Vitest](https://vitest.dev/).
@@ -0,0 +1,17 @@
{
"name": "@storyteller/ui-label",
"version": "0.0.1",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
}
}
@@ -0,0 +1,2 @@
export * from './lib/label';
@@ -0,0 +1,7 @@
/*
* Replace this with your own classes
*
* e.g.
* .container {
* }
*/
@@ -0,0 +1,12 @@
import { render } from '@testing-library/react';
import Label from './label';
describe('Label', () => {
it('should render successfully', () => {
const { baseElement } = render(<Label />);
expect(baseElement).toBeTruthy();
});
});
@@ -0,0 +1,25 @@
import { LabelHTMLAttributes, ReactNode } from "react";
import { twMerge } from "tailwind-merge";
interface LabelProps extends LabelHTMLAttributes<HTMLLabelElement> {
className?: string;
children: ReactNode;
required?: boolean;
}
export const Label = ({
className,
children,
required,
...rest
}: LabelProps) => (
<label
className={twMerge("text-white mb-2 text-[15px] font-medium", className)}
{...rest}
>
{children}
{required && <span className="ml-0.5 text-[#ff6467]">*</span>}
</label>
);
export default Label;
@@ -0,0 +1,13 @@
{
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../../tsconfig.base.json"
}
@@ -0,0 +1,45 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"types": [
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts",
"vite/client"
],
"rootDir": "src",
"jsx": "react-jsx",
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo"
},
"exclude": [
"out-tsc",
"dist",
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx"
],
"include": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.ts",
"src/**/*.tsx"
]
}
@@ -0,0 +1,34 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out-tsc/vitest",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest"
],
"jsx": "react-jsx"
},
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}
@@ -0,0 +1,49 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import dts from 'vite-plugin-dts';
import * as path from 'path';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../../node_modules/.vite/libs/components/label',
plugins: [react(), dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: './dist',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
name: '@storyteller/ui-label',
fileName: 'index',
// Change this to the formats you want to support.
// Don't forget to update your package.json as well.
formats: ['es' as const]
},
rollupOptions: {
// External packages that should not be bundled into your library.
external: ['react','react-dom','react/jsx-runtime']
},
},
test: {
watch: false,
globals: true,
environment: 'jsdom',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: './test-output/vitest/coverage',
provider: 'v8' as const,
}
},
}));
+12
View File
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
+7
View File
@@ -0,0 +1,7 @@
# @storyteller/ui-modal
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test @storyteller/ui-modal` to execute the unit tests via [Vitest](https://vitest.dev/).
@@ -0,0 +1,17 @@
{
"name": "@storyteller/ui-modal",
"version": "0.0.1",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
}
}
@@ -0,0 +1,2 @@
export * from './lib/modal';
@@ -0,0 +1,7 @@
/*
* Replace this with your own classes
*
* e.g.
* .container {
* }
*/
@@ -0,0 +1,12 @@
import { render } from '@testing-library/react';
import Modal from './modal';
describe('Modal', () => {
it('should render successfully', () => {
const { baseElement } = render(<Modal />);
expect(baseElement).toBeTruthy();
});
});
@@ -0,0 +1,138 @@
import { Fragment, ReactNode } from "react";
import {
Dialog,
DialogPanel,
DialogTitle,
Transition,
TransitionChild,
} from "@headlessui/react";
import { twMerge } from "tailwind-merge";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconDefinition } from "@fortawesome/pro-solid-svg-icons";
import { CloseButton } from "@storyteller/ui-close-button";
const DialogBackdrop = ({ className }: { className?: string }) => {
return (
<TransitionChild
as="div"
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div
className={twMerge("fixed inset-0 bg-black/60", className)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
/>
</TransitionChild>
);
};
export const Modal = ({
isOpen,
title,
titleIcon,
onTitleIconClick,
onClose,
className,
backdropClassName,
width,
children,
childPadding = true,
titleIconClassName,
showClose = true,
}: {
isOpen: boolean;
title?: ReactNode;
titleIcon?: IconDefinition;
onTitleIconClick?: () => void;
titleIconClassName?: string;
onClose: () => void;
className?: string;
backdropClassName?: string;
width?: number;
children: ReactNode;
childPadding?: boolean;
showClose?: boolean;
}) => {
return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-[70]" onClose={onClose}>
<DialogBackdrop className={backdropClassName} />
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<TransitionChild
as="div"
enter="ease-out duration-200"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
className={twMerge(
"w-full max-w-lg transform rounded-xl relative",
"border border-ui-panel-border bg-[#2C2C2C]",
"text-left align-middle shadow-xl transition-all",
childPadding ? "p-4" : "",
className
)}
>
<DialogPanel
className="w-full relative"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
{title && (
<DialogTitle
as="div"
className="mb-5 flex justify-between pb-0 text-xl font-bold text-white"
>
<>
{onTitleIconClick ? (
<button
className="flex items-center gap-3"
onClick={onTitleIconClick}
>
{titleIcon && (
<FontAwesomeIcon
icon={titleIcon}
className={titleIconClassName}
/>
)}
{title}
</button>
) : (
<div className="flex items-center gap-3">
{titleIcon && (
<FontAwesomeIcon
icon={titleIcon}
className={titleIconClassName}
/>
)}
{title}
</div>
)}
</>
</DialogTitle>
)}
<div className={`h-full`.trim()}>{children}</div>
</DialogPanel>
{showClose && (
<div className="absolute top-0 right-0 p-2.5">
<CloseButton onClick={onClose} />
</div>
)}
</TransitionChild>
</div>
</div>
</Dialog>
</Transition>
);
};
@@ -0,0 +1,16 @@
{
"files": [],
"include": [],
"references": [
{
"path": "../close-button"
},
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../../tsconfig.base.json"
}
@@ -0,0 +1,50 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"types": [
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts",
"vite/client"
],
"rootDir": "src",
"jsx": "react-jsx",
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo"
},
"exclude": [
"out-tsc",
"dist",
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx"
],
"include": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.ts",
"src/**/*.tsx"
],
"references": [
{
"path": "../close-button/tsconfig.lib.json"
}
]
}
@@ -0,0 +1,34 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out-tsc/vitest",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest"
],
"jsx": "react-jsx"
},
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}
@@ -0,0 +1,49 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import dts from 'vite-plugin-dts';
import * as path from 'path';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../../node_modules/.vite/libs/components/modal',
plugins: [react(), dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: './dist',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
name: '@storyteller/ui-modal',
fileName: 'index',
// Change this to the formats you want to support.
// Don't forget to update your package.json as well.
formats: ['es' as const]
},
rollupOptions: {
// External packages that should not be bundled into your library.
external: ['react','react-dom','react/jsx-runtime']
},
},
test: {
watch: false,
globals: true,
environment: 'jsdom',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: './test-output/vitest/coverage',
provider: 'v8' as const,
}
},
}));
+12
View File
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
@@ -0,0 +1,7 @@
# @storyteller/ui-select
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test @storyteller/ui-select` to execute the unit tests via [Vitest](https://vitest.dev/).
@@ -0,0 +1,17 @@
{
"name": "@storyteller/ui-select",
"version": "0.0.1",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
}
}
@@ -0,0 +1,2 @@
export * from './lib/select';
@@ -0,0 +1,7 @@
/*
* Replace this with your own classes
*
* e.g.
* .container {
* }
*/
@@ -0,0 +1,12 @@
import { render } from '@testing-library/react';
import Select from './select';
describe('Select', () => {
it('should render successfully', () => {
const { baseElement } = render(<Select />);
expect(baseElement).toBeTruthy();
});
});

Some files were not shown because too many files have changed in this diff Show More