add base layout

This commit is contained in:
yzned
2025-07-19 12:55:03 +03:00
committed by yzned
parent d249f0d291
commit de28d80769
13 changed files with 238 additions and 17 deletions

View File

@@ -20,6 +20,8 @@
"@types/node": "^24.0.14",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"mobx": "^6.13.7",
"mobx-react-lite": "^4.1.0",
"radix-ui": "^1.4.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",

32
pnpm-lock.yaml generated
View File

@@ -38,6 +38,12 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
mobx:
specifier: ^6.13.7
version: 6.13.7
mobx-react-lite:
specifier: ^4.1.0
version: 4.1.0(mobx@6.13.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
radix-ui:
specifier: ^1.4.2
version: 1.4.2(@types/react-dom@19.1.6(@types/react@19.1.8))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -2037,6 +2043,22 @@ packages:
engines: {node: '>=10'}
hasBin: true
mobx-react-lite@4.1.0:
resolution: {integrity: sha512-QEP10dpHHBeQNv1pks3WnHRCem2Zp636lq54M2nKO2Sarr13pL4u6diQXf65yzXUn0mkk18SyIDCm9UOJYTi1w==}
peerDependencies:
mobx: ^6.9.0
react: ^16.8.0 || ^17 || ^18 || ^19
react-dom: '*'
react-native: '*'
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
mobx@6.13.7:
resolution: {integrity: sha512-aChaVU/DO5aRPmk1GX8L+whocagUUpBQqoPtJk+cm7UOXUk87J4PeWCh6nNmTTIfEhiR9DI/+FnA8dln/hTK7g==}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -4439,6 +4461,16 @@ snapshots:
mkdirp@3.0.1: {}
mobx-react-lite@4.1.0(mobx@6.13.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
mobx: 6.13.7
react: 19.1.0
use-sync-external-store: 1.5.0(react@19.1.0)
optionalDependencies:
react-dom: 19.1.0(react@19.1.0)
mobx@6.13.7: {}
ms@2.1.3: {}
nanoid@3.3.11: {}

View File

@@ -0,0 +1,17 @@
import { Button } from "../ui/button";
import { observer } from "mobx-react-lite";
import { useAccountStore } from "@/contexts/AccountContext";
export const MainMenu = observer(() => {
const { isMainMenuOpen, setIsMainMenuOpen } = useAccountStore();
return (
<div
data-open={isMainMenuOpen}
className="w-[30px] data-[open=true]:w-[124px] transition-all border-r p-2"
>
<Button onClick={() => setIsMainMenuOpen(!isMainMenuOpen)}>
{isMainMenuOpen ? "Close" : "Open"}
</Button>
</div>
);
});

View File

@@ -0,0 +1,24 @@
import { useAccountStore } from "@/contexts/AccountContext";
import { Button } from "../ui/button";
import { observer } from "mobx-react-lite";
export const Chat = observer(() => {
const { isSandboxOpen, setIsSandboxOpen, isMainMenuOpen, setIsMainMenuOpen } =
useAccountStore();
return (
<div className="flex-1">
<Button
onClick={() => {
setIsSandboxOpen(!isSandboxOpen);
if (isMainMenuOpen && !isSandboxOpen) {
setIsMainMenuOpen(false);
}
}}
className="ml-10"
>
OPEN SANDBOX
</Button>
</div>
);
});

View File

@@ -0,0 +1,15 @@
import { useAccountStore } from "@/contexts/AccountContext";
import { observer } from "mobx-react-lite";
export const Sandbox = observer(() => {
const { isSandboxOpen } = useAccountStore();
return (
<div
data-open={isSandboxOpen}
className="w-0 data-[open=true]:w-[40svw] border-l-1 transition-all border-r p-0 bg-red-600"
>
SANDBOOOOX
</div>
);
});

View File

@@ -0,0 +1,47 @@
import { createContext, useContext, useState } from "react";
import { AccountStore } from "@/store/account";
interface StoreProviderState {
store?: AccountStore;
setStore: (store: AccountStore) => void;
}
interface StoreProviderProps {
children: React.ReactNode;
}
const initialState: StoreProviderState = {
store: undefined,
setStore: () => null,
};
const AccountStoreProviderContext = createContext(initialState);
const _AccountStore = new AccountStore();
export function AccountStoreProvider({
children,
...props
}: StoreProviderProps) {
const [_store, _setStore] = useState(_AccountStore);
return (
<AccountStoreProviderContext.Provider
{...props}
value={{
store: _store,
setStore: _setStore,
}}
>
{children}
</AccountStoreProviderContext.Provider>
);
}
export function useAccountStore() {
const context = useContext(AccountStoreProviderContext);
if (context === undefined) {
throw new Error("useStoreProvider must be used within a StoreProvider");
}
return _AccountStore;
}

3
src/lib/constants.ts Normal file
View File

@@ -0,0 +1,3 @@
export const LS_TOKENS = {
isMainMenuOpen: "isMainMenuOpen",
};

View File

@@ -9,6 +9,7 @@ import "./styles.css";
import reportWebVitals from "./reportWebVitals.ts";
import { Toaster } from "./components/ui/toaster.tsx";
import { TooltipProvider } from "./components/ui/tooltip.tsx";
import { AccountStoreProvider } from "./contexts/AccountContext.tsx";
// Create a new router instance
const router = createRouter({
@@ -33,10 +34,12 @@ if (rootElement && !rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement);
root.render(
<StrictMode>
<TooltipProvider delayDuration={50}>
<RouterProvider router={router} />
<Toaster />
</TooltipProvider>
<AccountStoreProvider>
<TooltipProvider delayDuration={50}>
<RouterProvider router={router} />
<Toaster />
</TooltipProvider>
</AccountStoreProvider>
</StrictMode>,
);
}

View File

@@ -9,8 +9,14 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as InteractionPanelRouteImport } from './routes/interaction-panel'
import { Route as IndexRouteImport } from './routes/index'
const InteractionPanelRoute = InteractionPanelRouteImport.update({
id: '/interaction-panel',
path: '/interaction-panel',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
@@ -19,28 +25,39 @@ const IndexRoute = IndexRouteImport.update({
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/interaction-panel': typeof InteractionPanelRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/interaction-panel': typeof InteractionPanelRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/interaction-panel': typeof InteractionPanelRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/'
fullPaths: '/' | '/interaction-panel'
fileRoutesByTo: FileRoutesByTo
to: '/'
id: '__root__' | '/'
to: '/' | '/interaction-panel'
id: '__root__' | '/' | '/interaction-panel'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
InteractionPanelRoute: typeof InteractionPanelRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/interaction-panel': {
id: '/interaction-panel'
path: '/interaction-panel'
fullPath: '/interaction-panel'
preLoaderRoute: typeof InteractionPanelRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
@@ -53,6 +70,7 @@ declare module '@tanstack/react-router' {
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
InteractionPanelRoute: InteractionPanelRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)

View File

@@ -1,11 +1,24 @@
import { Outlet, createRootRoute } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
// src/routes/__root.tsx
import { Outlet, createRootRoute } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
import { MainMenu } from "@/components/common/main-menu";
export const Route = createRootRoute({
component: () => (
<>
<Outlet />
<TanStackRouterDevtools />
</>
),
})
component: RootLayout,
});
function RootLayout() {
return (
<div className="flex h-screen">
<MainMenu />
<div className="flex flex-col flex-1">
<div className="flex-1 overflow-hidden">
<Outlet />
</div>
</div>
<TanStackRouterDevtools />
</div>
);
}

View File

@@ -1,3 +1,4 @@
import { TextLink } from "@/components/ui/text-link";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/")({
@@ -5,5 +6,9 @@ export const Route = createFileRoute("/")({
});
function App() {
return <div className="p-4">hi</div>;
return (
<div className="p-4">
<TextLink>Lable</TextLink>
</div>
);
}

View File

@@ -0,0 +1,17 @@
// src/routes/chat.tsx
import { Chat } from "@/components/interaction-panel/chat";
import { Sandbox } from "@/components/interaction-panel/sandbox";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/interaction-panel")({
component: InteractionPanelRoute,
});
function InteractionPanelRoute() {
return (
<div className="flex h-full">
<Chat />
<Sandbox />
</div>
);
}

25
src/store/account.ts Normal file
View File

@@ -0,0 +1,25 @@
import { LS_TOKENS } from "@/lib/constants";
import { makeAutoObservable } from "mobx";
export class AccountStore {
isMainMenuOpen: boolean;
isSandboxOpen: boolean;
constructor() {
makeAutoObservable(this, {}, { autoBind: true });
const savedState = localStorage.getItem(LS_TOKENS.isMainMenuOpen);
this.isMainMenuOpen = savedState ? savedState === "true" : false;
this.isSandboxOpen = false;
}
setIsMainMenuOpen(value: boolean) {
this.isMainMenuOpen = value;
localStorage.setItem(LS_TOKENS.isMainMenuOpen, String(this.isMainMenuOpen));
}
setIsSandboxOpen(value: boolean) {
this.isSandboxOpen = value;
}
}