feat: full studio build -- light theme, canvas thumbnails, i18n (fa/en)

This commit is contained in:
Soroush.Asadi
2026-05-24 17:37:21 +03:30
parent d962483359
commit c61f587767
295 changed files with 29797 additions and 265 deletions
+129
View File
@@ -0,0 +1,129 @@
"use client";
import * as React from "react";
import type { ToastProps } from "@radix-ui/react-toast";
const TOAST_LIMIT = 1;
const TOAST_REMOVE_DELAY = 3000;
type ToasterToast = ToastProps & {
id: string;
title?: React.ReactNode;
};
type ActionType = {
ADD_TOAST: "ADD_TOAST";
DISMISS_TOAST: "DISMISS_TOAST";
REMOVE_TOAST: "REMOVE_TOAST";
};
type Action =
| { type: ActionType["ADD_TOAST"]; toast: ToasterToast }
| { type: ActionType["DISMISS_TOAST"]; toastId?: string }
| { type: ActionType["REMOVE_TOAST"]; toastId?: string };
interface State {
toasts: ToasterToast[];
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) return;
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
dispatch({ type: "REMOVE_TOAST", toastId });
}, TOAST_REMOVE_DELAY);
toastTimeouts.set(toastId, timeout);
};
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
};
case "DISMISS_TOAST": {
const { toastId } = action;
if (toastId) {
addToRemoveQueue(toastId);
} else {
state.toasts.forEach((toast) => addToRemoveQueue(toast.id));
}
return {
...state,
toasts: state.toasts.map((toast) =>
toast.id === toastId || toastId === undefined
? { ...toast, open: false }
: toast
),
};
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return { ...state, toasts: [] };
}
return {
...state,
toasts: state.toasts.filter((toast) => toast.id !== action.toastId),
};
default:
return state;
}
};
const listeners: Array<(state: State) => void> = [];
let memoryState: State = { toasts: [] };
let count = 0;
const dispatch = (action: Action) => {
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => listener(memoryState));
};
export function toast({ title, ...props }: Omit<ToasterToast, "id">) {
const id = String(++count);
const update = (next: ToasterToast) =>
dispatch({
type: "ADD_TOAST",
toast: { ...next, id },
});
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
update({
id,
title,
open: true,
onOpenChange: (open) => {
if (!open) dismiss();
},
...props,
});
return { id, dismiss };
}
export function useToast() {
const [state, setState] = React.useState<State>(memoryState);
React.useEffect(() => {
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState);
if (index > -1) listeners.splice(index, 1);
};
}, []);
return {
...state,
toast,
dismiss: (toastId?: string) =>
dispatch({ type: "DISMISS_TOAST", toastId }),
};
}