Compare commits

...
Author SHA1 Message Date
bflatastic ce60583675 shared component toast and activity 2025-04-17 17:11:47 +07:00
bflatastic 496fc46300 init shared component 2025-04-17 15:35:23 +07:00
bflatastic c84c36d774 escape key to deselect objects and exit FK pose mode 2025-04-17 13:33:12 +07:00
bflatastic 3269b62e14 editor2d: artcraft logo 2025-04-17 13:11:55 +07:00
BFlat cd49fe0015 pose mode toggle button on top when character is selected (#520)
- pose mode toggle button to go in and out of pose mode when a character
is selected
- tooltip text size change
- button styling fixes
2025-04-17 13:04:53 +07:00
28 changed files with 414 additions and 207 deletions
+1
View File
@@ -15,6 +15,7 @@
"preview": "vite preview"
},
"dependencies": {
"@storyteller/shared": "file:../../shared/*",
"@devnomic/marquee": "^1.0.3",
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2",
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

+3 -3
View File
@@ -5,7 +5,7 @@ import { RouterProvider } from "react-router-dom";
import { router } from "./router";
import { posthog } from "posthog-js";
import { PostHogProvider } from "posthog-js/react";
import { Toaster } from "~/components/ui/Toast";
import { Toaster } from "@storyteller/shared/components";
import { JobProvider } from "~/components/JobContext";
import "./global.css";
import { useRenderCounter } from "~/hooks/useRenderCounter";
@@ -33,7 +33,7 @@ const App = () => {
<JobProvider>
<PostHogProvider client={posthog}>
<RouterProvider router={router} />
<Toaster />
<Toaster topOffset={70} rightOffset={12} />
</PostHogProvider>
</JobProvider>
</StrictMode>
@@ -43,7 +43,7 @@ const App = () => {
<>
<JobProvider>
<RouterProvider router={router} />
<Toaster />
<Toaster topOffset={70} rightOffset={12} />
</JobProvider>
</>
);
@@ -77,8 +77,9 @@ export const Main = withProtectionRoute(() => {
</div>
</div>
</div>
{appModeValue === "gallery" &&
(<GalleryRootComponent className="fixed h-full w-full z-1 bg-black" />)}
{appModeValue === "gallery" && (
<GalleryRootComponent className="z-1 fixed h-full w-full bg-black" />
)}
</div>
);
});
@@ -41,9 +41,9 @@ export const ToolbarTopLeft = () => {
)}
>
<img
src="/brand/mira-logo.png"
src="/brand/artcraft-logo.png"
alt="logo"
className="h-[30px] select-none pb-1"
className="h-[28px] select-none"
/>
<TabSelector
@@ -6,7 +6,7 @@ import { CompletedCard } from "./CompletedCard";
import { InProgressCard } from "./InProgressCard";
import { useJobContext } from "~/components/JobContext";
import { Api } from "~/KonvaApp/Api";
import { toast } from "sonner";
import { toast } from "@storyteller/shared/components";
// Dummy data types
interface CompletedItem {
@@ -199,7 +199,7 @@ export function Activity() {
</div>
) : completedItems.length === 0 && jobs.length === 0 ? (
<div className="flex h-48 w-full flex-col justify-center gap-4 p-4 text-center align-middle">
<h3 className="text-gray-300">No activities yet</h3>
<h3 className="text-lg text-gray-300">No activities yet</h3>
</div>
) : (
<div>
+2 -1
View File
@@ -21,7 +21,8 @@
"noFallthroughCasesInSwitch": true,
"paths": {
"~/*": ["./src/*"],
"@ApiManager/*": ["./src/Classes/ApiManager/*"]
"@ApiManager/*": ["./src/Classes/ApiManager/*"],
"@storyteller/shared/*": ["../../shared/*"]
}
},
"include": ["src"]
@@ -45,7 +45,7 @@ export const Button = ({
);
const className = twMerge(
"text-sm font-medium rounded-lg px-3 py-2 border border-[#363636] shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 transition-all duration-150 flex gap-2 items-center justify-center",
"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,
@@ -137,7 +137,7 @@ export const Tooltip = ({
transitionTimingFunction: "ease-out",
}}
className={twMerge(
"pointer-events-none absolute z-10 w-max rounded-lg bg-[#5F5F68] px-2.5 py-1.5 text-sm font-medium text-white shadow-xl",
"pointer-events-none absolute z-10 w-max rounded-lg bg-[#5F5F68] px-2.5 py-1.5 text-[13px] font-medium text-white shadow-xl",
className ? className : "",
)}
>
@@ -52,7 +52,7 @@ export const TopBar = ({ pageName }: Props) => {
<SceneTitleInput pageName={pageName} />
</div>
<div className="flex justify-end gap-5">
<div className="flex justify-end gap-3">
<div className="flex gap-2">
<Button
variant="secondary"
@@ -17,7 +17,11 @@ import { SceneManager, SceneObject } from "./scene_manager_api";
import { FreeCam } from "./free_cam";
import Stats from "three/examples/jsm/libs/stats.module.js";
import { FKHelper } from "./KinHelpers/FKHelper";
import { selectedMode } from "../signals/selectedMode";
import {
selectedMode,
poseMode,
showPoseControls,
} from "../signals/selectedMode";
export enum KinMode {
FK,
@@ -122,6 +126,10 @@ export class MouseControls {
scene: this.scene,
onDragChange: this.onFKControlsDragging.bind(this),
});
// Expose handlePoseModeChange to window
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).__mouseControls = this;
}
onFKControlsDragging(dragging: boolean) {
@@ -185,7 +193,11 @@ export class MouseControls {
this.setSelected(this.selected);
this.publishSelect();
// this.editor.update_properties()
if (currentObject.userData.isCharacter) {
showPoseControls.value = true;
}
// Normal selection behavior
if (currentObject.userData["locked"] !== true && this.control) {
this.scene.add(this.control);
this.control.attach(currentObject);
@@ -235,6 +247,9 @@ export class MouseControls {
this.fkHelper.clear();
this.kinMode = KinMode.NONE;
this.reattachTransformControls();
if (poseMode.value === "pose") {
poseMode.value = "select";
}
console.log("FK mode off");
return;
}
@@ -255,6 +270,9 @@ export class MouseControls {
this.hideTransformControls();
this.kinMode = KinMode.FK;
this.fkHelper.setTarget(this.selected[0]);
if (poseMode.value === "select") {
poseMode.value = "pose";
}
console.log("FK mode on");
return;
}
@@ -305,30 +323,20 @@ export class MouseControls {
if ((event.ctrlKey || event.metaKey) && !this.isProcessing) {
if (event.key === "Z" || event.key === "z") {
if (event.shiftKey) {
// redo
this.isProcessing = true;
await this.sceneManager?.redo();
this.isProcessing = false;
} else {
// undo
this.isProcessing = true;
await this.sceneManager?.undo();
this.isProcessing = false;
}
return;
// redo
this.isProcessing = true;
await this.sceneManager?.redo();
this.isProcessing = false;
} else if (event.key === "c") {
// redo
this.isProcessing = true;
await this.sceneManager?.copy();
this.isProcessing = false;
return;
} else if (event.key === "v") {
// redo
this.isProcessing = true;
await this.sceneManager?.paste();
this.isProcessing = false;
return;
} else if (event.key === "0") {
// Stats Menu
this.enable_stats();
@@ -351,6 +359,17 @@ export class MouseControls {
} else {
this.cameraViewControls.movementSpeed = 1.15;
}
if (event.key === "Escape") {
if (poseMode.value === "pose") {
this.toggleFKMode();
return;
} else if (this.selected && this.selected.length > 0) {
this.removeTransformControls();
hideObjectPanel();
showPoseControls.value = false;
}
}
}
// Sets new mouse location usually used in raycasts.
@@ -454,7 +473,8 @@ export class MouseControls {
this.selected = [];
this.setSelected(this.selected);
this.removeTransformControls();
//hideObjectPanel();
hideObjectPanel();
showPoseControls.value = false;
}
if (this.sceneManager) {
@@ -7,6 +7,7 @@ import { ControlPanelSceneObject } from "./comps/ControlPanelSceneObject";
import { PreviewEngineCamera } from "./comps/PreviewEngineCamera";
import { PreviewFrameImage } from "./comps/PreviewFrameImage";
import { pageHeight, pageWidth } from "~/signals";
import { PoseModeSelector } from "./comps/PoseModeSelector";
import {
timelineHeight,
@@ -24,7 +25,6 @@ import { Outliner } from "./comps/Outliner";
import { CameraAspectRatio } from "./enums";
import { PromptBox } from "./comps/PromptBox";
import { OnboardingHelper } from "./comps/OnboardingHelper";
import { focalLengthDragging } from "./signals/camera";
import { FocalLengthDisplay } from "./comps/FocalLengthDisplay/FocalLengthDisplay";
export const PageEditor = () => {
@@ -106,6 +106,9 @@ export const PageEditor = () => {
{/* Focal Length Display */}
<FocalLengthDisplay />
{/* Pose Mode Selector */}
<PoseModeSelector />
{/* Top controls */}
<div
className="absolute left-0 top-0 w-full"
@@ -6,9 +6,8 @@ import { useBackgroundLoadingMedia } from "~/hooks/useBackgroundLoadingMedia";
import { useQueueHandler } from "./hooks/useQueueHandler";
import { ErrorDialog, LoadingDots } from "~/components";
import { GenerateModals } from "~/pages/PageEnigma/comps/GenerateModals/GenerateModals";
import { Toaster } from "@storyteller/shared/components";
import { EditorLoadingBar } from "./comps/EditorLoadingBar";
import { Wizard } from "~/pages/PageEnigma/Wizard/Wizard";
import { useSignals } from "@preact/signals-react/runtime";
import { useEffect, useState } from "react";
@@ -17,7 +16,12 @@ import { UsersApi } from "~/Classes/ApiManager";
import { PageEnigmaComponent } from "~/pages/PageEnigma/PageEnigmaComponent";
import { TurnOnGpu } from "~/pages/PageEnigma/TurnOnGpu";
import { PrecisionSelector } from "./comps/PrecisionSelector/PrecisionSelector";
import { precisionSelectedValue, precisionSelectorMenuCoords, precisionSelectorValues, showPrecisionSelector } from "./signals/precisionSelectorMenu";
import {
precisionSelectedValue,
precisionSelectorMenuCoords,
precisionSelectorValues,
showPrecisionSelector,
} from "./signals/precisionSelectorMenu";
export const PageEnigma = ({ sceneToken }: { sceneToken?: string }) => {
useSignals();
@@ -104,6 +108,7 @@ export const PageEnigma = ({ sceneToken }: { sceneToken?: string }) => {
<GenerateModals />
<ErrorDialog />
<Wizard />
<Toaster topOffset={70} rightOffset={12} />
</EngineProvider>
<EditorLoadingBar />
</TrackProvider>
@@ -15,7 +15,7 @@ export const FocalLengthDisplay = () => {
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="absolute left-1/2 top-16 -translate-x-1/2 transform">
<div className="absolute left-1/2 top-16 z-10 -translate-x-1/2 transform">
<div className="glass rounded-xl px-5 py-2.5 text-center text-2xl font-bold text-white">
{focalLengthDragging.value.focalLength}mm
</div>
@@ -4,11 +4,10 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { PopoverMenu } from "~/components/reusable/Popover/Popover";
import { CompletedCard } from "./CompletedCard";
import { InProgressCard } from "./InProgressCard";
import { H3, P, Tooltip } from "~/components";
import { activeImageGenerationJobs, toasts, userMovies } from "~/signals";
import { useEffect, useState } from "react";
import { H3, Tooltip } from "~/components";
import { useEffect, useState, useRef, useCallback } from "react";
import { JobsApi } from "~/Classes/ApiManager/JobsApi";
import { toast } from "sonner";
import { toast } from "@storyteller/shared/components";
import { ActiveJob } from "~/pages/PageEnigma/models";
// TODO ensure we de-dupe all this extra code.
interface CompletedItem {
@@ -60,26 +59,49 @@ interface CompletedItem {
export function Activity() {
useSignals();
const [completedItems, setCompletedItems] = useState<CompletedItem[]>([]);
const [loading, setLoading] = useState(true);
const [jobs, setJobs] = useState<ActiveJob[]>([]);
const processedJobs = useRef(new Set<string>());
const isFirstLoad = useRef(true);
const toastTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// Debounced toast function
const showToast = useCallback((items: CompletedItem[]) => {
if (toastTimeoutRef.current) {
clearTimeout(toastTimeoutRef.current);
}
toastTimeoutRef.current = setTimeout(() => {
const count = items.length;
toast.success(
count === 1
? `${items[0].maybe_title} completed successfully`
: `${count} images completed successfully`,
);
toastTimeoutRef.current = null;
}, 100); // Small delay to catch potential double calls
}, []);
useEffect(() => {
const pollData = async () => {
const fetchJobs = async () => {
try {
const jobsApi = new JobsApi();
const jobsResponse = await jobsApi.ListRecentJobs();
let mounted = true;
if (jobsResponse.success && jobsResponse.data) {
// Process active jobs
const activeJobs = jobsResponse.data
.filter((job) => {
const status = job.status?.status?.toLowerCase();
return (
status && status !== "complete_success" && status !== "failed"
);
})
.map((job) => ({
async function fetchJobs() {
try {
const jobsApi = new JobsApi();
const response = await jobsApi.ListRecentJobs();
if (!mounted || !response.success || !response.data) return;
// Handle active jobs
const activeJobs = response.data
.filter((job) => {
const status = job.status?.status?.toLowerCase();
return (
status && status !== "complete_success" && status !== "failed"
);
})
.map(
(job) =>
({
job_token: job.job_token,
request: {
maybe_model_title:
@@ -89,79 +111,78 @@ export function Activity() {
status: job.status?.status?.toUpperCase() || "",
progress_percentage: job.status?.progress_percentage || 0,
},
}));
// Process completed items
// Get completed items and filter out any that are already in our list
const successfulJobs = jobsResponse.data.filter(
(job) => job.status?.status?.toLowerCase() === "complete_success",
);
// Create a set of existing tokens for faster lookup
const existingTokens = new Set(
completedItems.map((item) => item.token),
);
// Filter out jobs we already have in our list
const newCompletedItems = successfulJobs
.filter((job) => !existingTokens.has(job.job_token))
.map((job) => ({
token: job.job_token,
maybe_title:
job.request?.maybe_model_title || "Image Generation",
public_bucket_path:
job.maybe_result?.maybe_public_bucket_media_path || "",
created_at: job.created_at || new Date().toISOString(),
updated_at: job.updated_at || new Date().toISOString(),
}));
maybe_result: job.maybe_result || null,
}) as ActiveJob,
);
// Show toast notification for newly completed items
if (newCompletedItems.length > 0) {
const count = newCompletedItems.length;
const message =
count === 1
? `${newCompletedItems[0].maybe_title} completed successfully`
: `${count} images completed successfully`;
toast.success(message);
}
// Handle completed jobs
const newlyCompleted = response.data.filter(
(job) =>
job.status?.status?.toLowerCase() === "complete_success" &&
!processedJobs.current.has(job.job_token),
);
// Update completed items with a maximum limit of 50 items
setCompletedItems((prev) => {
const combined = [...newCompletedItems, ...prev];
// Remove duplicates based on token
const uniqueItems = Array.from(
new Map(combined.map((item) => [item.token, item])).values(),
);
// Sort by updated_at in descending order and limit to 50 items
return uniqueItems
.sort(
(a, b) =>
new Date(b.updated_at).getTime() -
new Date(a.updated_at).getTime(),
)
.slice(0, 50);
});
setJobs(activeJobs as ActiveJob[]);
setLoading(false);
if (newlyCompleted.length > 0) {
// Add to processed set
newlyCompleted.forEach((job) =>
processedJobs.current.add(job.job_token),
);
// Create completed items
const newItems = newlyCompleted.map((job) => ({
token: job.job_token,
maybe_title: job.request?.maybe_model_title || "Image Generation",
public_bucket_path:
job.maybe_result?.maybe_public_bucket_media_path || "",
updated_at: job.updated_at || new Date().toISOString(),
}));
// Show toast only if not first load
if (!isFirstLoad.current) {
showToast(newItems);
}
} catch (error) {
toast.error("Error Fetching Recent Jobs");
console.error("Failed to fetch recent jobs:", error);
// Update state
setCompletedItems((prev) => {
const combined = [...newItems, ...prev];
return combined
.filter(
(item, index, self) =>
index === self.findIndex((t) => t.token === item.token),
)
.sort(
(a, b) =>
new Date(b.updated_at).getTime() -
new Date(a.updated_at).getTime(),
)
.slice(0, 50);
});
}
};
await fetchJobs();
setJobs(activeJobs);
isFirstLoad.current = false;
} catch (error) {
console.error("Failed to fetch jobs:", error);
}
}
// Initial fetch
fetchJobs();
// Set up polling
const interval = setInterval(fetchJobs, 5000);
// Cleanup
return () => {
mounted = false;
clearInterval(interval);
if (toastTimeoutRef.current) {
clearTimeout(toastTimeoutRef.current);
}
};
// Initial load
// Initial call to pollData
pollData();
// Set up polling interval (every 5 seconds)
const interval = setInterval(async () => {
await pollData();
}, 5000);
return () => clearInterval(interval);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [showToast]);
return (
<Tooltip content="Activity" position="bottom" closeOnClick={true}>
@@ -173,20 +194,19 @@ export function Activity() {
align="end"
triggerIcon={
<div>
{jobs && jobs.length > 0 ? (
{jobs.length > 0 ? (
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
) : (
<FontAwesomeIcon icon={faBell} />
)}
{jobs && jobs.length > 0 && (
<div className="bg-blue-500 absolute -right-1 -top-1 h-3 w-3 animate-pulse rounded-full" />
{jobs.length > 0 && (
<div className="absolute -right-1 -top-1 h-3 w-3 animate-pulse rounded-full bg-brand-primary-400" />
)}
</div>
}
>
<div className="max-h-[480px] overflow-y-auto">
{/* In progress */}
{jobs && jobs.length > 0 && (
{jobs.length > 0 && (
<div>
{jobs.map((job) => (
<InProgressCard key={job.job_token} job={job} />
@@ -194,7 +214,7 @@ export function Activity() {
</div>
)}
{!userMovies.value && !activeImageGenerationJobs.value ? (
{isFirstLoad.current ? (
<div className="flex h-48 w-full flex-col justify-center gap-4 p-4 text-center align-middle">
<FontAwesomeIcon
icon={faSpinnerThird}
@@ -204,18 +224,12 @@ export function Activity() {
/>
<H3 className="text-gray-300">Retrieving Activities</H3>
</div>
) : userMovies.value &&
userMovies.value.length === 0 &&
!activeImageGenerationJobs.value ? (
) : jobs.length === 0 && completedItems.length === 0 ? (
<div className="flex h-48 w-full flex-col justify-center gap-4 p-4 text-center align-middle">
<H3 className="text-gray-300">No activities yet</H3>
<P className="text-gray-400">
Try generating something new from the featured scenes
</P>
<h3 className="text-lg text-gray-300">No activities yet</h3>
</div>
) : (
<div>
{/* Completed */}
<div className="flex flex-col">
{completedItems.map((item) => (
<CompletedCard key={item.token} job={item} />
@@ -0,0 +1,73 @@
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";
declare global {
interface Window {
__mouseControls?: {
toggleFKMode: () => void;
};
}
}
interface PoseModeSelectorProps {}
export const PoseModeSelector: React.FC<PoseModeSelectorProps> = () => {
useSignals();
const handleModeChange = () => {
if (poseMode.value === "select") {
poseMode.value = "pose";
} else {
poseMode.value = "select";
}
window.__mouseControls?.toggleFKMode();
};
if (showPoseControls.value === false) {
return null;
}
return (
<>
<div
className="fixed left-1/2 top-32 flex -translate-x-1/2 transform items-center justify-center gap-2"
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
>
<Tooltip
content={"Toggle pose mode (K)"}
position={"bottom"}
delay={300}
closeOnClick={true}
>
<>
{poseMode.value === "select" ? (
<Button
icon={faPersonRunning}
onClick={handleModeChange}
className="rounded-xl shadow-xl outline-none focus-visible:outline-none"
>
Enter Pose Mode
</Button>
) : (
<Button
icon={faCheck}
onClick={handleModeChange}
className="rounded-xl outline-none focus-visible:outline-none"
>
Done
</Button>
)}
</>
</Tooltip>
</div>
</>
);
};
@@ -58,6 +58,7 @@ interface ReferenceImage {
import { EngineApi } from "~/Classes/ApiManager/EngineApi";
import { UploaderStates } from "~/enums/UploaderStates";
import { CameraSettingsModal } from "../CameraSettingsModal";
import { toast } from "@storyteller/shared/components";
export const PromptBox = () => {
useSignals();
@@ -430,7 +431,7 @@ export const PromptBox = () => {
screenshot: snapshot.file,
sceneMediaToken: "",
});
console.log("useSystemPrompt", useSystemPrompt);
await engineApi.enqueueImageGeneration({
disableSystemPrompt: !useSystemPrompt,
@@ -451,6 +452,7 @@ export const PromptBox = () => {
await new Promise((resolve) => setTimeout(resolve, 2000));
} finally {
setisEnqueueing(false);
toast.success("Image generation enqueued");
}
}
setisEnqueueing(false);
@@ -1,3 +1,5 @@
import { signal } from "@preact/signals-react";
export const selectedMode = signal("move");
export const showPoseControls = signal(false);
export const poseMode = signal("select");
+10 -9
View File
@@ -22,13 +22,14 @@
},
"dependencies": {
"@storyteller/api": "file:../../libs/api",
"@storyteller/shared": "file:../../shared/*",
"@awesome.me/kit-fde2be5eb0": "^1.0.7",
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-brands-svg-icons": "^6.5.2",
"@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",
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@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",
"@mediapipe/tasks-vision": "^0.10.20",
"@netlify/edge-functions": "^2.3.1",
"@netlify/remix-edge-adapter": "^3.2.0",
@@ -53,9 +54,9 @@
"isbot": "^4.1.0",
"lodash.debounce": "^4.0.8",
"posthog-js": "^1.141.0",
"react": "^18.2.0",
"react": "^18.3.1",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18.2.0",
"react-dom": "^18.3.1",
"react-drag-drop-files": "^2.3.10",
"react-helmet": "^6.1.0",
"react-image-crop": "^11.0.5",
@@ -63,7 +64,7 @@
"tailwind-merge": "^2.2.2",
"terser": "^5.30.3",
"three": "^0.162.0",
"uuid": "^9.0.1",
"uuid": "^10.0.0",
"wavesurfer.js": "^7.7.11",
"webm-writer": "^1.0.0"
},
+4 -14
View File
@@ -1,16 +1,7 @@
{
"include": [
"env.d.ts",
"**/*.ts",
"**/*.tsx",
"netlify-edge-plugin.ts"
],
"include": ["env.d.ts", "**/*.ts", "**/*.tsx", "netlify-edge-plugin.ts"],
"compilerOptions": {
"lib": [
"DOM",
"DOM.Iterable",
"ESNext"
],
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"isolatedModules": true,
"esModuleInterop": true,
"jsx": "react-jsx",
@@ -24,9 +15,8 @@
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"paths": {
"~/*": [
"./app/*"
]
"~/*": ["./app/*"],
"@storyteller/shared/*": ["../../shared/*"]
},
// Vite takes care of building everything, not tsc.
"noEmit": true
+80 -43
View File
@@ -69,6 +69,7 @@
"@fortawesome/react-fontawesome": "^0.2.2",
"@headlessui/react": "^2.1.3",
"@preact/signals-react": "^2.2.0",
"@storyteller/shared": "file:../../shared/*",
"@tauri-apps/api": "^2.2.0",
"@techstark/opencv-js": "^4.10.0-release.1",
"@zip.js/zip.js": "^2.7.52",
@@ -570,25 +571,6 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"apps/editor2d/node_modules/@headlessui/react": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.1.tgz",
"integrity": "sha512-daiUqVLae8CKVjEVT19P/izW0aGK0GNhMSAeMlrDebKmoVZHcRRwbxzgtnEadUVDXyBsWo9/UH4KHeniO+0tMg==",
"license": "MIT",
"dependencies": {
"@floating-ui/react": "^0.26.16",
"@react-aria/focus": "^3.17.1",
"@react-aria/interactions": "^3.21.3",
"@tanstack/react-virtual": "^3.11.1"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": "^18 || ^19 || ^19.0.0-rc",
"react-dom": "^18 || ^19 || ^19.0.0-rc"
}
},
"apps/editor2d/node_modules/@preact/signals-react": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@preact/signals-react/-/signals-react-2.3.0.tgz",
@@ -1179,12 +1161,12 @@
"apps/editor3d": {
"dependencies": {
"@awesome.me/kit-fde2be5eb0": "^1.0.7",
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-brands-svg-icons": "^6.5.2",
"@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",
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@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",
"@mediapipe/tasks-vision": "^0.10.20",
"@netlify/edge-functions": "^2.3.1",
"@netlify/remix-edge-adapter": "^3.2.0",
@@ -1196,6 +1178,7 @@
"@remix-run/router": "^1.15.3",
"@remix-run/serve": "^2.8.1",
"@storyteller/api": "file:../../libs/api",
"@storyteller/shared": "file:../../shared/*",
"@tweenjs/tween.js": "^18.6.4",
"@types/lodash": "^4.17.6",
"@types/react-virtualized": "^9.22.2",
@@ -1210,9 +1193,9 @@
"isbot": "^4.1.0",
"lodash.debounce": "^4.0.8",
"posthog-js": "^1.141.0",
"react": "^18.2.0",
"react": "^18.3.1",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18.2.0",
"react-dom": "^18.3.1",
"react-drag-drop-files": "^2.3.10",
"react-helmet": "^6.1.0",
"react-image-crop": "^11.0.5",
@@ -1220,7 +1203,7 @@
"tailwind-merge": "^2.2.2",
"terser": "^5.30.3",
"three": "^0.162.0",
"uuid": "^9.0.1",
"uuid": "^10.0.0",
"wavesurfer.js": "^7.7.11",
"webm-writer": "^1.0.0"
},
@@ -1685,6 +1668,10 @@
"react": "^16.14.0 || 17.x || 18.x || 19.x"
}
},
"apps/editor3d/node_modules/@storyteller/shared": {
"resolved": "shared/components",
"link": true
},
"apps/editor3d/node_modules/@types/react": {
"version": "18.3.20",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.20.tgz",
@@ -1932,6 +1919,41 @@
}
}
},
"apps/editor3d/node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"apps/editor3d/node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
},
"peerDependencies": {
"react": "^18.3.1"
}
},
"apps/editor3d/node_modules/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"bin": {
"uuid": "dist/bin/uuid"
}
},
"apps/editor3d/node_modules/vite": {
"version": "5.4.18",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.18.tgz",
@@ -4715,20 +4737,21 @@
}
},
"node_modules/@headlessui/react": {
"version": "1.7.19",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.19.tgz",
"integrity": "sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw==",
"license": "MIT",
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.1.tgz",
"integrity": "sha512-daiUqVLae8CKVjEVT19P/izW0aGK0GNhMSAeMlrDebKmoVZHcRRwbxzgtnEadUVDXyBsWo9/UH4KHeniO+0tMg==",
"dependencies": {
"@tanstack/react-virtual": "^3.0.0-beta.60",
"client-only": "^0.0.1"
"@floating-ui/react": "^0.26.16",
"@react-aria/focus": "^3.17.1",
"@react-aria/interactions": "^3.21.3",
"@tanstack/react-virtual": "^3.11.1"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": "^16 || ^17 || ^18",
"react-dom": "^16 || ^17 || ^18"
"react": "^18 || ^19 || ^19.0.0-rc",
"react-dom": "^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/@humanfs/core": {
@@ -9897,6 +9920,10 @@
"resolved": "libs/api",
"link": true
},
"node_modules/@storyteller/shared": {
"resolved": "shared/*",
"link": true
},
"node_modules/@svgr/babel-plugin-add-jsx-attribute": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz",
@@ -14582,12 +14609,6 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -30560,6 +30581,7 @@
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"dev": true,
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
@@ -31853,6 +31875,21 @@
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
}
},
"shared": {
"name": "@storyteller/shared",
"version": "0.0.1",
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0",
"sonner": "^1.4.3"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"shared/*": {},
"shared/components": {}
}
}
+3 -3
View File
@@ -1,4 +1,4 @@
packages:
- 'apps/*'
- 'libs/*'
- "apps/*"
- "libs/*"
- "shared/*"
@@ -0,0 +1,23 @@
import { Toaster as SonnerToaster } from "sonner";
interface ToasterProps {
topOffset?: number;
rightOffset?: number;
}
export const Toaster = ({ topOffset = 0, rightOffset = 0 }: ToasterProps) => {
return (
<div className="relative z-[9]">
<SonnerToaster
position="top-right"
expand={false}
richColors
closeButton
style={{ top: topOffset, right: rightOffset }}
/>
</div>
);
};
// Re-export toast function from sonner for easy access
export { toast } from "sonner";
+1
View File
@@ -0,0 +1 @@
export * from "./Toaster";
+1
View File
@@ -0,0 +1 @@
+19
View File
@@ -0,0 +1,19 @@
{
"name": "@storyteller/shared",
"version": "0.0.1",
"private": true,
"main": "./components/index.ts",
"exports": {
"./components": "./components/index.ts",
"./contexts": "./contexts/index.ts"
},
"dependencies": {
"sonner": "^1.4.3",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": ".",
"declaration": true,
"composite": true,
"jsx": "react-jsx"
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["dist", "node_modules"]
}
+2 -1
View File
@@ -16,6 +16,7 @@
"skipLibCheck": true,
"strict": true,
"target": "es2022",
"customConditions": ["development"]
"customConditions": ["development"],
"baseUrl": "."
}
}