From 0d9bbb140fccc484d9e2979671eb17ec87c1665c Mon Sep 17 00:00:00 2001
From: itoshi <kkzemeow@gmail.com>
Date: Sat, 31 May 2025 21:17:27 +0300
Subject: [PATCH] fix: Language cache sight

---
 .env                                          |   3 +-
 package.json                                  |   7 +-
 src/app/index.tsx                             |   4 +-
 src/app/router/index.tsx                      |   8 +-
 src/pages/EditSightPage/index.tsx             |  63 +-
 src/shared/const/index.ts                     |   8 +
 src/shared/lib/mui/theme.ts                   |  42 +-
 .../modals/PreviewMediaDialog/index.tsx       | 229 ++++++
 .../modals/SelectArticleDialog/index.tsx      | 298 +++++---
 src/shared/modals/SelectMediaDialog/index.tsx | 178 +++++
 src/shared/modals/index.ts                    |   2 +
 src/shared/store/ArticlesStore/index.tsx      |  56 +-
 src/shared/store/EditSightStore/index.tsx     | 151 ++++
 src/shared/store/MediaStore/index.tsx         |  27 +
 src/shared/store/SightsStore/index.tsx        |  89 ++-
 src/shared/store/index.ts                     |   2 +
 src/shared/ui/CoordinatesInput/index.tsx      |   6 +-
 src/widgets/LanguageSwitcher/index.tsx        |   2 +-
 src/widgets/MediaViewer/ThreeView.tsx         |  22 +
 src/widgets/MediaViewer/index.tsx             | 107 +++
 src/widgets/ReactMarkdownEditor/index.tsx     |  41 +-
 .../SightTabs/InformationTab/index.tsx        | 634 +++++++++-------
 src/widgets/SightTabs/LeftWidgetTab/index.tsx | 522 +++++++------
 .../SightTabs/RightWidgetTab/index.tsx        | 685 +++++++++---------
 src/widgets/index.ts                          |   1 +
 .../modals/SelectArticleDialog/index.tsx      | 188 +++++
 src/widgets/modals/index.ts                   |   1 +
 yarn.lock                                     | 397 +++++++++-
 28 files changed, 2760 insertions(+), 1013 deletions(-)
 create mode 100644 src/shared/modals/PreviewMediaDialog/index.tsx
 create mode 100644 src/shared/modals/SelectMediaDialog/index.tsx
 create mode 100644 src/shared/store/EditSightStore/index.tsx
 create mode 100644 src/shared/store/MediaStore/index.tsx
 create mode 100644 src/widgets/MediaViewer/ThreeView.tsx
 create mode 100644 src/widgets/MediaViewer/index.tsx
 create mode 100644 src/widgets/modals/SelectArticleDialog/index.tsx
 create mode 100644 src/widgets/modals/index.ts

diff --git a/.env b/.env
index 6517129..59c6180 100644
--- a/.env
+++ b/.env
@@ -1 +1,2 @@
-VITE_REACT_APP = 'https://wn.krbl.ru/'
\ No newline at end of file
+VITE_REACT_APP ='https://wn.krbl.ru/'
+VITE_KRBL_MEDIA='https://wn.krbl.ru/media/'
\ No newline at end of file
diff --git a/package.json b/package.json
index 0a7ad51..54bdc7f 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,9 @@
     "@emotion/react": "^11.14.0",
     "@emotion/styled": "^11.14.0",
     "@mui/material": "^7.1.0",
+    "@photo-sphere-viewer/core": "^5.13.2",
+    "@react-three/drei": "^10.1.2",
+    "@react-three/fiber": "^9.1.2",
     "@tailwindcss/vite": "^4.1.8",
     "axios": "^1.9.0",
     "easymde": "^2.20.0",
@@ -23,11 +26,13 @@
     "react": "^19.1.0",
     "react-dom": "^19.1.0",
     "react-markdown": "^10.1.0",
+    "react-photo-sphere-viewer": "^6.2.3",
     "react-router-dom": "^7.6.1",
     "react-simplemde-editor": "^5.2.0",
     "react-toastify": "^11.0.5",
     "rehype-raw": "^7.0.0",
-    "tailwindcss": "^4.1.8"
+    "tailwindcss": "^4.1.8",
+    "three": "^0.177.0"
   },
   "devDependencies": {
     "@eslint/js": "^9.25.0",
diff --git a/src/app/index.tsx b/src/app/index.tsx
index b435b31..1a152cd 100644
--- a/src/app/index.tsx
+++ b/src/app/index.tsx
@@ -3,12 +3,12 @@ import * as React from "react";
 import { BrowserRouter } from "react-router-dom";
 
 import { Router } from "./router";
-import { theme } from "@shared";
+import { CustomTheme } from "@shared";
 import { ThemeProvider } from "@mui/material/styles";
 import { ToastContainer } from "react-toastify";
 
 export const App: React.FC = () => (
-  <ThemeProvider theme={theme}>
+  <ThemeProvider theme={CustomTheme.Light}>
     <ToastContainer />
     <BrowserRouter>
       <Router />
diff --git a/src/app/router/index.tsx b/src/app/router/index.tsx
index 5889955..8eabdb9 100644
--- a/src/app/router/index.tsx
+++ b/src/app/router/index.tsx
@@ -6,8 +6,9 @@ import {
   MainPage,
   SightPage,
 } from "@pages";
-import { authStore } from "@shared";
+import { authStore, editSightStore, sightsStore } from "@shared";
 import { Layout } from "@widgets";
+import { useEffect } from "react";
 
 import { Navigate, Outlet, Route, Routes, useLocation } from "react-router-dom";
 
@@ -32,6 +33,11 @@ const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
 };
 
 export const Router = () => {
+  const pathname = useLocation();
+  useEffect(() => {
+    editSightStore.clearSightInfo();
+    sightsStore.clearCreateSight();
+  }, [pathname]);
   return (
     <Routes>
       <Route
diff --git a/src/pages/EditSightPage/index.tsx b/src/pages/EditSightPage/index.tsx
index 8774d80..b74e10b 100644
--- a/src/pages/EditSightPage/index.tsx
+++ b/src/pages/EditSightPage/index.tsx
@@ -16,7 +16,7 @@ function a11yProps(index: number) {
 export const EditSightPage = observer(() => {
   const [value, setValue] = useState(0);
   const { sight, getSight } = sightsStore;
-  const { getArticles } = articlesStore;
+  const { articles, getArticles } = articlesStore;
   const { language } = languageStore;
   const { id } = useParams();
 
@@ -28,53 +28,54 @@ export const EditSightPage = observer(() => {
     const fetchData = async () => {
       if (id) {
         await getSight(Number(id));
-        await getArticles();
+        await getArticles(language);
       }
     };
     fetchData();
   }, [id, language]);
 
   return (
-    <Box
-      sx={{
-        width: "100%",
-        display: "flex",
-        flexDirection: "column",
-        minHeight: "100vh",
-      }}
-    >
+    articles &&
+    sight && (
       <Box
         sx={{
-          borderBottom: 1,
-          borderColor: "divider",
+          width: "100%",
           display: "flex",
-          justifyContent: "center",
+          flexDirection: "column",
+          minHeight: "100vh",
         }}
       >
-        <Tabs
-          value={value}
-          onChange={handleChange}
-          aria-label="sight tabs"
+        <Box
           sx={{
-            width: "100%",
-            "& .MuiTabs-flexContainer": {
-              justifyContent: "center",
-            },
+            borderBottom: 1,
+            borderColor: "divider",
+            display: "flex",
+            justifyContent: "center",
           }}
         >
-          <Tab sx={{ flex: 1 }} label="Общая информация" {...a11yProps(0)} />
-          <Tab sx={{ flex: 1 }} label="Левый виджет" {...a11yProps(1)} />
-          <Tab sx={{ flex: 1 }} label="Правый виджет" {...a11yProps(2)} />
-        </Tabs>
-      </Box>
+          <Tabs
+            value={value}
+            onChange={handleChange}
+            aria-label="sight tabs"
+            sx={{
+              width: "100%",
+              "& .MuiTabs-flexContainer": {
+                justifyContent: "center",
+              },
+            }}
+          >
+            <Tab sx={{ flex: 1 }} label="Общая информация" {...a11yProps(0)} />
+            <Tab sx={{ flex: 1 }} label="Левый виджет" {...a11yProps(1)} />
+            <Tab sx={{ flex: 1 }} label="Правый виджет" {...a11yProps(2)} />
+          </Tabs>
+        </Box>
 
-      {sight && (
         <div className="flex-1">
           <InformationTab value={value} index={0} />
-          <LeftWidgetTab data={sight} value={value} index={1} />
-          <RightWidgetTab data={sight} value={value} index={2} />
+          <LeftWidgetTab value={value} index={1} />
+          <RightWidgetTab value={value} index={2} />
         </div>
-      )}
-    </Box>
+      </Box>
+    )
   );
 });
diff --git a/src/shared/const/index.ts b/src/shared/const/index.ts
index 6903264..28586ba 100644
--- a/src/shared/const/index.ts
+++ b/src/shared/const/index.ts
@@ -1 +1,9 @@
 export const API_URL = "https://wn.krbl.ru";
+export const MEDIA_TYPE_LABELS = {
+  1: "Фото",
+  2: "Видео",
+  3: "Иконка",
+  4: "Водяной знак",
+  5: "Панорама",
+  6: "3Д-модель",
+};
diff --git a/src/shared/lib/mui/theme.ts b/src/shared/lib/mui/theme.ts
index fb1b266..185133c 100644
--- a/src/shared/lib/mui/theme.ts
+++ b/src/shared/lib/mui/theme.ts
@@ -1,17 +1,45 @@
 import { createTheme } from "@mui/material/styles";
 
-export const theme = createTheme({
-  // You can customize your theme here
+export const COLORS = {
+  primary: "#7f6b58",
+  secondary: "#48989f",
+};
+
+const theme = {
   palette: {
-    mode: "light",
+    primary: {
+      main: COLORS.primary,
+    },
+    secondary: {
+      main: COLORS.secondary,
+    },
   },
   components: {
-    MuiDrawer: {
+    MuiAppBar: {
       styleOverrides: {
-        paper: {
-          backgroundColor: "#fff",
+        root: {
+          backgroundColor: COLORS.secondary,
         },
       },
     },
   },
-});
+};
+
+export const CustomTheme = {
+  Light: createTheme({
+    palette: {
+      ...theme.palette,
+    },
+    components: {
+      ...theme.components,
+    },
+  }),
+  Dark: createTheme({
+    palette: {
+      ...theme.palette,
+    },
+    components: {
+      ...theme.components,
+    },
+  }),
+};
diff --git a/src/shared/modals/PreviewMediaDialog/index.tsx b/src/shared/modals/PreviewMediaDialog/index.tsx
new file mode 100644
index 0000000..43d6a15
--- /dev/null
+++ b/src/shared/modals/PreviewMediaDialog/index.tsx
@@ -0,0 +1,229 @@
+import {
+  articlesStore,
+  authStore,
+  Language,
+  mediaStore,
+  MEDIA_TYPE_LABELS,
+  API_URL,
+} from "@shared";
+import { observer } from "mobx-react-lite";
+import { useEffect, useRef, useState, useCallback } from "react";
+import {
+  Dialog,
+  DialogTitle,
+  DialogContent,
+  DialogActions,
+  Button,
+  TextField,
+  Paper,
+  Box,
+  Typography,
+  CircularProgress,
+  Alert,
+  Snackbar,
+} from "@mui/material";
+import { Download, Save } from "lucide-react";
+import { ReactMarkdownComponent, MediaViewer } from "@widgets";
+import { authInstance } from "@shared";
+
+interface PreviewMediaDialogProps {
+  open: boolean;
+  onClose: () => void;
+  mediaId: string;
+}
+
+export const PreviewMediaDialog = observer(
+  ({ open, onClose, mediaId }: PreviewMediaDialogProps) => {
+    const [isLoading, setIsLoading] = useState(false);
+    const [error, setError] = useState<string | null>(null);
+    const [success, setSuccess] = useState(false);
+    const media = mediaId
+      ? mediaStore.media.find((m) => m.id === mediaId)
+      : null;
+    const [mediaName, setMediaName] = useState(media?.media_name ?? "");
+    const [mediaFilename, setMediaFilename] = useState(media?.filename ?? "");
+
+    // Reset form when media changes
+    useEffect(() => {
+      if (media) {
+        setMediaName(media.media_name);
+        setMediaFilename(media.filename);
+      }
+    }, [media]);
+
+    useEffect(() => {
+      const handleKeyPress = (event: KeyboardEvent) => {
+        if (event.key.toLowerCase() === "enter" && !event.ctrlKey) {
+          event.preventDefault();
+          onClose();
+        }
+      };
+
+      window.addEventListener("keydown", handleKeyPress);
+      return () => window.removeEventListener("keydown", handleKeyPress);
+    }, [onClose]);
+
+    const handleSave = async () => {
+      if (!mediaId) return;
+
+      setIsLoading(true);
+      setError(null);
+
+      try {
+        await authInstance.patch(`/media/${mediaId}`, {
+          media_name: mediaName,
+          filename: mediaFilename,
+          type: media?.media_type,
+        });
+
+        // Update local store
+        await mediaStore.getMedia();
+        setSuccess(true);
+      } catch (err) {
+        setError(err instanceof Error ? err.message : "Failed to save media");
+      } finally {
+        setIsLoading(false);
+      }
+    };
+
+    const handleClose = () => {
+      setError(null);
+      setSuccess(false);
+      onClose();
+    };
+
+    if (!media) {
+      return null;
+    }
+
+    return (
+      <>
+        <Dialog open={open} onClose={handleClose} maxWidth="md" fullWidth>
+          <DialogTitle>Просмотр медиа</DialogTitle>
+          <DialogContent
+            className="flex gap-4"
+            dividers
+            sx={{
+              height: "600px",
+              display: "flex",
+              flexDirection: "column",
+              gap: 2,
+              pt: 2,
+            }}
+          >
+            <Box className="flex flex-col gap-4">
+              <Box className="flex gap-2">
+                <TextField
+                  fullWidth
+                  value={mediaName}
+                  onChange={(e) => setMediaName(e.target.value)}
+                  label="Название медиа"
+                  disabled={isLoading}
+                />
+                <TextField
+                  fullWidth
+                  value={mediaFilename}
+                  onChange={(e) => setMediaFilename(e.target.value)}
+                  label="Название файла"
+                  disabled={isLoading}
+                />
+              </Box>
+
+              <TextField
+                fullWidth
+                label="Тип медиа"
+                value={
+                  MEDIA_TYPE_LABELS[
+                    media.media_type as keyof typeof MEDIA_TYPE_LABELS
+                  ]
+                }
+                disabled
+                sx={{ width: "50%" }}
+              />
+
+              <Box className="flex gap-4 h-full">
+                <Paper
+                  elevation={2}
+                  sx={{
+                    flex: 1,
+                    p: 2,
+                    display: "flex",
+                    alignItems: "center",
+                    justifyContent: "center",
+                    minHeight: 400,
+                  }}
+                >
+                  <MediaViewer
+                    media={{
+                      id: mediaId,
+                      media_type: media.media_type,
+                      filename: media.filename,
+                    }}
+                  />
+                </Paper>
+
+                <Box className="flex flex-col gap-2 self-end">
+                  <Button
+                    variant="contained"
+                    color="primary"
+                    startIcon={<Download size={16} />}
+                    component="a"
+                    href={`${
+                      import.meta.env.VITE_KRBL_MEDIA
+                    }${mediaId}/download?token=${localStorage.getItem(
+                      "token"
+                    )}`}
+                    target="_blank"
+                    disabled={isLoading}
+                  >
+                    Скачать
+                  </Button>
+                  <Button
+                    variant="contained"
+                    color="success"
+                    startIcon={
+                      isLoading ? (
+                        <CircularProgress size={16} />
+                      ) : (
+                        <Save size={16} />
+                      )
+                    }
+                    onClick={handleSave}
+                    disabled={isLoading || (!mediaName && !mediaFilename)}
+                  >
+                    Сохранить
+                  </Button>
+                </Box>
+              </Box>
+            </Box>
+          </DialogContent>
+          <DialogActions>
+            <Button onClick={handleClose} disabled={isLoading}>
+              Отмена
+            </Button>
+          </DialogActions>
+        </Dialog>
+
+        <Snackbar
+          open={!!error}
+          autoHideDuration={6000}
+          onClose={() => setError(null)}
+        >
+          <Alert severity="error" onClose={() => setError(null)}>
+            {error}
+          </Alert>
+        </Snackbar>
+
+        <Snackbar
+          open={success}
+          autoHideDuration={3000}
+          onClose={() => setSuccess(false)}
+        >
+          <Alert severity="success" onClose={() => setSuccess(false)}>
+            Медиа успешно сохранено
+          </Alert>
+        </Snackbar>
+      </>
+    );
+  }
+);
diff --git a/src/shared/modals/SelectArticleDialog/index.tsx b/src/shared/modals/SelectArticleDialog/index.tsx
index e59b718..cf27742 100644
--- a/src/shared/modals/SelectArticleDialog/index.tsx
+++ b/src/shared/modals/SelectArticleDialog/index.tsx
@@ -1,6 +1,6 @@
-import { articlesStore } from "@shared";
+import { articlesStore, authStore, Language } from "@shared";
 import { observer } from "mobx-react-lite";
-import { useEffect, useRef, useState } from "react";
+import { useEffect, useState } from "react";
 import {
   Dialog,
   DialogTitle,
@@ -31,38 +31,56 @@ export const SelectArticleModal = observer(
     open,
     onClose,
     onSelectArticle,
-
-    linkedArticleIds = [], // Default to empty array if not provided
+    linkedArticleIds = [],
   }: SelectArticleModalProps) => {
-    const { articles, getArticle } = articlesStore; // articles here refers to fetched articles from store
+    const { articles, getArticle, getArticleMedia } = articlesStore;
     const [searchQuery, setSearchQuery] = useState("");
-    const [hoveredArticleId, setHoveredArticleId] = useState<string | null>(
+    const [selectedArticleId, setSelectedArticleId] = useState<string | null>(
       null
     );
-    const hoverTimerRef = useRef<NodeJS.Timeout | null>(null);
+    const [isLoading, setIsLoading] = useState(false);
+
+    // Reset selection when modal opens/closes
+    useEffect(() => {
+      if (open) {
+        setSelectedArticleId(null);
+        articlesStore.articleData = null;
+        articlesStore.articleMedia = null;
+      }
+    }, [open]);
 
     useEffect(() => {
-      if (hoveredArticleId) {
-        hoverTimerRef.current = setTimeout(() => {
-          getArticle(hoveredArticleId);
-        }, 200);
-      }
-
-      return () => {
-        if (hoverTimerRef.current) {
-          clearTimeout(hoverTimerRef.current);
+      const handleKeyPress = (event: KeyboardEvent) => {
+        if (event.key.toLowerCase() === "enter") {
+          event.preventDefault();
+          if (selectedArticleId) {
+            onSelectArticle(selectedArticleId);
+            onClose();
+          }
         }
       };
-    }, [hoveredArticleId, getArticle]);
 
-    const handleArticleHover = (articleId: string) => {
-      setHoveredArticleId(articleId);
-    };
+      window.addEventListener("keydown", handleKeyPress);
+      return () => {
+        window.removeEventListener("keydown", handleKeyPress);
+      };
+    }, [selectedArticleId, onSelectArticle, onClose]);
 
-    const handleArticleLeave = () => {
-      setHoveredArticleId(null);
-      if (hoverTimerRef.current) {
-        clearTimeout(hoverTimerRef.current);
+    const handleArticleClick = async (articleId: string) => {
+      if (selectedArticleId === articleId) return;
+
+      setSelectedArticleId(articleId);
+      setIsLoading(true);
+
+      try {
+        await Promise.all([getArticle(articleId), getArticleMedia(articleId)]);
+      } catch (error) {
+        console.error("Failed to fetch article data:", error);
+        // Reset article data on error
+        articlesStore.articleData = null;
+        articlesStore.articleMedia = null;
+      } finally {
+        setIsLoading(false);
       }
     };
 
@@ -72,21 +90,38 @@ export const SelectArticleModal = observer(
         article.service_name.toLowerCase().includes(searchQuery.toLowerCase())
       );
 
+    const token = localStorage.getItem("token");
     return (
-      <Dialog open={open} onClose={onClose} maxWidth="lg" fullWidth>
+      <Dialog
+        open={open}
+        onClose={onClose}
+        maxWidth="lg"
+        fullWidth
+        PaperProps={{
+          sx: {
+            minHeight: "80vh",
+            maxHeight: "90vh",
+          },
+        }}
+      >
         <DialogTitle>Выберите существующую статью</DialogTitle>
         <DialogContent
           className="flex gap-4"
-          dividers // Adds a divider below the title and above the actions
-          sx={{ height: "600px", display: "flex", flexDirection: "row" }} // Fixed height for DialogContent
+          dividers
+          sx={{
+            height: "600px",
+            display: "flex",
+            flexDirection: "row",
+            p: 2,
+          }}
         >
-          <Paper className="w-[66%] flex flex-col">
+          <Paper className="w-[66%] flex flex-col" elevation={2}>
             <TextField
               fullWidth
               placeholder="Поиск статей..."
               value={searchQuery}
               onChange={(e) => setSearchQuery(e.target.value)}
-              sx={{ mb: 2, mt: 1 }}
+              sx={{ mb: 2, mt: 1, px: 2 }}
               InputProps={{
                 startAdornment: (
                   <InputAdornment position="start">
@@ -95,27 +130,51 @@ export const SelectArticleModal = observer(
                 ),
               }}
             />
-            <List sx={{ flexGrow: 1, overflowY: "auto" }}>
-              {filteredArticles.map((article) => (
-                <ListItemButton
-                  key={article.id}
-                  onClick={() => onSelectArticle(article.id)}
-                  onMouseEnter={() => handleArticleHover(article.id)}
-                  onMouseLeave={handleArticleLeave}
-                  sx={{
-                    borderRadius: 1,
-                    mb: 0.5,
-                    "&:hover": {
-                      backgroundColor: "action.hover",
-                    },
-                  }}
+            <List sx={{ flexGrow: 1, overflowY: "auto", px: 2 }}>
+              {filteredArticles.length === 0 ? (
+                <Typography
+                  variant="body2"
+                  color="text.secondary"
+                  sx={{ p: 2, textAlign: "center" }}
                 >
-                  <ListItemText primary={article.service_name} />
-                </ListItemButton>
-              ))}
+                  {searchQuery ? "Статьи не найдены" : "Нет доступных статей"}
+                </Typography>
+              ) : (
+                filteredArticles.map((article) => (
+                  <ListItemButton
+                    key={article.id}
+                    onClick={() => handleArticleClick(article.id)}
+                    onDoubleClick={() => onSelectArticle(article.id)}
+                    selected={selectedArticleId === article.id}
+                    disabled={isLoading}
+                    sx={{
+                      borderRadius: 1,
+                      mb: 0.5,
+                      "&:hover": {
+                        backgroundColor: "action.hover",
+                      },
+                      "&.Mui-selected": {
+                        backgroundColor: "primary.main",
+                        color: "primary.contrastText",
+                        "&:hover": {
+                          backgroundColor: "primary.dark",
+                        },
+                      },
+                    }}
+                  >
+                    <ListItemText
+                      primary={article.service_name}
+                      primaryTypographyProps={{
+                        fontWeight:
+                          selectedArticleId === article.id ? "bold" : "normal",
+                      }}
+                    />
+                  </ListItemButton>
+                ))
+              )}
             </List>
           </Paper>
-          <Paper className="flex-1 flex flex-col">
+          <Paper className="flex-1 flex flex-col" elevation={2}>
             <Box
               className="rounded-2xl overflow-hidden"
               sx={{
@@ -127,60 +186,109 @@ export const SelectArticleModal = observer(
                 flexDirection: "column",
               }}
             >
-              {/* Media Preview Area */}
-              <Box
-                sx={{
-                  width: "100%",
-                  height: 200,
-                  flexShrink: 0,
-                  backgroundColor: "grey.300",
-                  display: "flex",
-                  alignItems: "center",
-                  justifyContent: "center",
-                }}
-              >
-                <ImagePlus size={48} color="grey" />
-              </Box>
+              {isLoading ? (
+                <Box
+                  sx={{
+                    width: "100%",
+                    height: "100%",
+                    display: "flex",
+                    alignItems: "center",
+                    justifyContent: "center",
+                  }}
+                >
+                  <Typography color="white">Загрузка...</Typography>
+                </Box>
+              ) : (
+                <>
+                  {articlesStore.articleMedia && (
+                    <Box sx={{ p: 2, backgroundColor: "rgba(0,0,0,0.1)" }}>
+                      <img
+                        src={`${import.meta.env.VITE_KRBL_MEDIA}${
+                          articlesStore.articleMedia.id
+                        }/download?token=${token}`}
+                        alt={articlesStore.articleMedia.filename}
+                        style={{
+                          maxWidth: "100%",
+                          height: "auto",
+                          maxHeight: "300px",
+                          objectFit: "contain",
+                          borderRadius: 8,
+                        }}
+                      />
+                    </Box>
+                  )}
+                  {!articlesStore.articleMedia && (
+                    <Box
+                      sx={{
+                        width: "100%",
+                        height: 200,
+                        flexShrink: 0,
+                        backgroundColor: "rgba(0,0,0,0.1)",
+                        display: "flex",
+                        alignItems: "center",
+                        justifyContent: "center",
+                      }}
+                    >
+                      <ImagePlus size={48} color="white" />
+                    </Box>
+                  )}
 
-              {/* Title Area */}
-              <Box
-                sx={{
-                  width: "100%",
-                  height: "70px",
-                  background: "#877361",
-                  display: "flex",
-                  flexShrink: 0,
-                  alignItems: "center",
-                  borderBottom: "1px solid",
-                  px: 2,
-                }}
-              >
-                <Typography variant="h6" color="white">
-                  {articlesStore.articleData?.heading ||
-                    "Нет данных для предпросмотра"}
-                </Typography>
-              </Box>
+                  <Box
+                    sx={{
+                      width: "100%",
+                      minHeight: "70px",
+                      background: "#877361",
+                      display: "flex",
+                      flexShrink: 0,
+                      alignItems: "center",
+                      borderBottom: "1px solid rgba(255,255,255,0.1)",
+                      px: 2,
+                    }}
+                  >
+                    <Typography variant="h6" color="white">
+                      {articlesStore.articleData?.heading || "Выберите статью"}
+                    </Typography>
+                  </Box>
 
-              {/* Body Preview Area */}
-              <Box
-                sx={{
-                  px: 2,
-                  flexGrow: 1,
-                  overflowY: "auto",
-                  backgroundColor: "#877361", // To make markdown readable
-                  color: "white",
-                  py: 1,
-                }}
-              >
-                <ReactMarkdownComponent
-                  value={articlesStore.articleData?.body || ""}
-                />
-              </Box>
+                  <Box
+                    sx={{
+                      px: 2,
+                      flexGrow: 1,
+                      overflowY: "auto",
+                      backgroundColor: "#877361",
+                      color: "white",
+                      py: 1,
+                    }}
+                  >
+                    {articlesStore.articleData?.body ? (
+                      <ReactMarkdownComponent
+                        value={articlesStore.articleData.body}
+                      />
+                    ) : (
+                      <Typography
+                        color="rgba(255,255,255,0.7)"
+                        sx={{ textAlign: "center", mt: 4 }}
+                      >
+                        Предпросмотр статьи появится здесь
+                      </Typography>
+                    )}
+                  </Box>
+                </>
+              )}
             </Box>
           </Paper>
         </DialogContent>
-        <DialogActions>
+        <DialogActions sx={{ p: 2 }}>
           <Button onClick={onClose}>Отмена</Button>
+          <Button
+            variant="contained"
+            onClick={() =>
+              selectedArticleId && onSelectArticle(selectedArticleId)
+            }
+            disabled={!selectedArticleId || isLoading}
+          >
+            Выбрать
+          </Button>
         </DialogActions>
       </Dialog>
     );
diff --git a/src/shared/modals/SelectMediaDialog/index.tsx b/src/shared/modals/SelectMediaDialog/index.tsx
new file mode 100644
index 0000000..c5676c6
--- /dev/null
+++ b/src/shared/modals/SelectMediaDialog/index.tsx
@@ -0,0 +1,178 @@
+import { articlesStore, authStore, Language, mediaStore } from "@shared";
+import { observer } from "mobx-react-lite";
+import { useEffect, useRef, useState } from "react";
+import {
+  Dialog,
+  DialogTitle,
+  DialogContent,
+  DialogActions,
+  Button,
+  TextField,
+  List,
+  ListItemButton,
+  ListItemText,
+  Paper,
+  Box,
+  Typography,
+  InputAdornment,
+} from "@mui/material";
+import { ImagePlus, Search } from "lucide-react";
+import { ReactMarkdownComponent, MediaViewer } from "@widgets";
+
+interface SelectMediaDialogProps {
+  open: boolean; // Corrected prop name
+  onClose: () => void;
+  onSelectMedia: (mediaId: string) => void; // Renamed from onSelectArticle
+  linkedMediaIds?: string[]; // Renamed from linkedArticleIds, assuming it refers to media already in use
+}
+
+export const SelectMediaDialog = observer(
+  ({
+    open, // Corrected prop name
+    onClose,
+    onSelectMedia, // Renamed prop
+    linkedMediaIds = [], // Default to empty array if not provided, renamed
+  }: SelectMediaDialogProps) => {
+    const { media, getMedia } = mediaStore; // Confirmed: using mediaStore for media
+    const [searchQuery, setSearchQuery] = useState("");
+    const [hoveredMediaId, setHoveredMediaId] = useState<string | null>(null);
+    const hoverTimerRef = useRef<NodeJS.Timeout | null>(null);
+
+    // Fetch media on component mount
+    useEffect(() => {
+      getMedia();
+    }, [getMedia]); // getMedia should be a dependency to avoid lint warnings if it's not stable
+
+    // Keyboard event listener for "Enter" key to select hovered media
+    useEffect(() => {
+      const handleKeyPress = (event: KeyboardEvent) => {
+        if (event.key === "Enter") {
+          event.preventDefault(); // Prevent browser default action (e.g., form submission)
+
+          if (hoveredMediaId) {
+            onSelectMedia(hoveredMediaId); // Call onSelectMedia
+            onClose();
+          }
+        }
+      };
+
+      window.addEventListener("keydown", handleKeyPress);
+      return () => {
+        window.removeEventListener("keydown", handleKeyPress);
+      };
+    }, [hoveredMediaId, onSelectMedia, onClose]); // Dependencies for keyboard listener
+
+    // Effect for handling hover timeout (if you want to clear the preview after a delay)
+    // Based on the original code, it seemed like you wanted a delay for showing,
+    // but typically for a preview, it's immediate on hover and cleared on mouse leave.
+    // I've removed the 5-second timeout for setting the ID as it's counter-intuitive for a live preview.
+    // If you intend for the preview to disappear after a short while *after* the mouse leaves,
+    // you would implement a mouseleave timer. For now, it will clear on mouseleave.
+
+    const handleMouseEnter = (mediaId: string) => {
+      if (hoverTimerRef.current) {
+        clearTimeout(hoverTimerRef.current);
+      }
+      setHoveredMediaId(mediaId);
+    };
+
+    const handleMouseLeave = () => {
+      // You can add a small delay here if you want the preview to linger for a moment
+      // before disappearing, e.g., setTimeout(() => setHoveredMediaId(null), 200);
+      setHoveredMediaId(null);
+    };
+
+    const filteredMedia = media
+      .filter((mediaItem) => !linkedMediaIds.includes(mediaItem.id)) // Use mediaItem to avoid name collision
+      .filter((mediaItem) =>
+        mediaItem.media_name.toLowerCase().includes(searchQuery.toLowerCase())
+      );
+
+    // Find the currently hovered media object for MediaViewer
+    const currentHoveredMedia = hoveredMediaId
+      ? media.find((m) => m.id === hoveredMediaId)
+      : null;
+
+    return (
+      <Dialog open={open} onClose={onClose} maxWidth="lg" fullWidth>
+        <DialogTitle>Выберите существующее медиа</DialogTitle>
+        <DialogContent
+          className="flex gap-4"
+          dividers // Adds a divider below the title and above the actions
+          sx={{ height: "600px", display: "flex", flexDirection: "row" }} // Fixed height for DialogContent
+        >
+          <Paper className="w-[66%] flex flex-col" sx={{ p: 2 }}>
+            {" "}
+            {/* Added padding for consistency */}
+            <TextField
+              fullWidth
+              placeholder="Поиск медиа..." // Changed placeholder for clarity
+              value={searchQuery}
+              onChange={(e) => setSearchQuery(e.target.value)}
+              sx={{ mb: 2, mt: 1 }}
+              InputProps={{
+                startAdornment: (
+                  <InputAdornment position="start">
+                    <Search size={20} />
+                  </InputAdornment>
+                ),
+              }}
+            />
+            <List sx={{ flexGrow: 1, overflowY: "auto" }}>
+              {filteredMedia.length > 0 ? (
+                filteredMedia.map(
+                  (
+                    mediaItem // Use mediaItem to avoid confusion
+                  ) => (
+                    <ListItemButton
+                      key={mediaItem.id}
+                      onClick={() => onSelectMedia(mediaItem.id)} // Call onSelectMedia
+                      onMouseEnter={() => handleMouseEnter(mediaItem.id)}
+                      onMouseLeave={handleMouseLeave}
+                      sx={{
+                        borderRadius: 1,
+                        mb: 0.5,
+                        "&:hover": {
+                          backgroundColor: "action.hover",
+                        },
+                      }}
+                    >
+                      <ListItemText primary={mediaItem.media_name} />
+                    </ListItemButton>
+                  )
+                )
+              ) : (
+                <Typography
+                  sx={{ mt: 2, textAlign: "center" }}
+                  color="text.secondary"
+                >
+                  Медиа не найдено или все медиа уже прикреплены.
+                </Typography>
+              )}
+            </List>
+          </Paper>
+          {currentHoveredMedia ? ( // Only render MediaViewer if currentHoveredMedia is found
+            <Paper className="w-[33%] h-[100%] flex justify-center items-center">
+              <MediaViewer
+                media={{
+                  id: currentHoveredMedia.id,
+                  media_type: currentHoveredMedia.media_type ?? 1, // Provide a default if media_type can be undefined
+                  filename: currentHoveredMedia.filename,
+                }}
+              />
+            </Paper>
+          ) : (
+            <Paper className="w-[33%] h-[100%] flex justify-center items-center">
+              <Typography variant="body2" color="text.secondary">
+                Наведите на медиа в списке для предпросмотра.
+              </Typography>
+            </Paper>
+          )}
+        </DialogContent>
+        <DialogActions>
+          <Button onClick={onClose}>Отмена</Button>
+        </DialogActions>
+      </Dialog>
+    );
+  }
+);
diff --git a/src/shared/modals/index.ts b/src/shared/modals/index.ts
index e714367..22df340 100644
--- a/src/shared/modals/index.ts
+++ b/src/shared/modals/index.ts
@@ -1 +1,3 @@
 export * from "./SelectArticleDialog";
+export * from "./SelectMediaDialog";
+export * from "./PreviewMediaDialog";
diff --git a/src/shared/store/ArticlesStore/index.tsx b/src/shared/store/ArticlesStore/index.tsx
index 600eb8f..5048469 100644
--- a/src/shared/store/ArticlesStore/index.tsx
+++ b/src/shared/store/ArticlesStore/index.tsx
@@ -1,36 +1,78 @@
-import { authInstance } from "@shared";
-import { makeAutoObservable, runInAction } from "mobx";
+import { authInstance, editSightStore, Language, languageStore } from "@shared";
+import { computed, makeAutoObservable, runInAction } from "mobx";
 
 export type Article = {
-  id: string;
+  id: number;
   heading: string;
   body: string;
   service_name: string;
 };
 
+type Media = {
+  id: string;
+  filename: string;
+  media_name: string;
+  media_type: number;
+};
+
 class ArticlesStore {
   constructor() {
     makeAutoObservable(this);
   }
 
-  articles: Article[] = [];
+  articles: { [key in Language]: Article[] } = {
+    ru: [],
+    en: [],
+    zh: [],
+  };
   articleData: Article | null = null;
+  articleMedia: Media | null = null;
+  articleLoading: boolean = false;
 
-  getArticles = async () => {
+  getArticles = async (language: Language) => {
+    this.articleLoading = true;
     const response = await authInstance.get("/article");
 
     runInAction(() => {
-      this.articles = response.data;
+      this.articles[language] = response.data;
     });
+    this.articleLoading = false;
   };
 
-  getArticle = async (id: string) => {
+  getArticle = async (id: number) => {
+    this.articleLoading = true;
     const response = await authInstance.get(`/article/${id}`);
 
     runInAction(() => {
       this.articleData = response.data;
     });
+    this.articleLoading = false;
   };
+
+  getSightArticles = async (id: number) => {
+    const response = await authInstance.get(`/sight/${id}/article`);
+
+    runInAction(() => {
+      editSightStore.sightInfo[languageStore.language].right = response.data;
+    });
+  };
+
+  getArticleMedia = async (id: number) => {
+    const response = await authInstance.get(`/article/${id}/media`);
+
+    runInAction(() => {
+      this.articleMedia = response.data[0];
+    });
+  };
+
+  getArticleByArticleId = computed(() => {
+    if (editSightStore.sightInfo.left_article) {
+      return this.articles[languageStore.language].find(
+        (a) => a.id == editSightStore.sightInfo.left_article
+      );
+    }
+    return null;
+  });
 }
 
 export const articlesStore = new ArticlesStore();
diff --git a/src/shared/store/EditSightStore/index.tsx b/src/shared/store/EditSightStore/index.tsx
new file mode 100644
index 0000000..8faf2d9
--- /dev/null
+++ b/src/shared/store/EditSightStore/index.tsx
@@ -0,0 +1,151 @@
+// @shared/stores/editSightStore.ts
+import { Language } from "@shared";
+import { makeAutoObservable } from "mobx";
+
+export interface MediaObject {
+  id: string;
+  filename: string;
+  media_type: number;
+}
+
+type SightBaseInfo = {
+  id: number;
+  city_id: number;
+  city: string;
+  latitude: number;
+  longitude: number;
+  thumbnail: string;
+  watermark_lu: string;
+  watermark_rd: string;
+  left_article: number;
+  preview_media: string;
+  video_preview: string;
+};
+
+export interface RightArticleBlock {
+  id: string;
+  type: "article" | "preview_media";
+  name: string;
+  linkedArticleId?: string;
+  heading: string;
+  body: string;
+  media: MediaObject | null;
+}
+
+type SightInfo = SightBaseInfo & {
+  [key in Language]: {
+    info: {
+      name: string;
+      address: string;
+    };
+    left: {
+      loaded: boolean; // Означает, что данные для этого языка были инициализированы/загружены
+      heading: string;
+      body: string;
+      media: MediaObject | null;
+    };
+    right: RightArticleBlock[];
+  };
+};
+
+class EditSightStore {
+  sightInfo: SightInfo = {
+    id: 0,
+    city_id: 0,
+    city: "",
+    latitude: 0,
+    longitude: 0,
+    thumbnail: "",
+    watermark_lu: "",
+    watermark_rd: "",
+    left_article: 0,
+    preview_media: "",
+    video_preview: "",
+    ru: {
+      info: { name: "", address: "" },
+      left: { loaded: false, heading: "", body: "", media: null },
+      right: [],
+    },
+    en: {
+      info: { name: "", address: "" },
+      left: { loaded: false, heading: "", body: "", media: null },
+      right: [],
+    },
+    zh: {
+      info: { name: "", address: "" },
+      left: { loaded: false, heading: "", body: "", media: null },
+      right: [],
+    },
+  };
+
+  constructor() {
+    makeAutoObservable(this);
+  }
+
+  // loadSightInfo: Используется для первоначальной загрузки данных для ЯЗЫКА.
+  // Она устанавливает loaded: true, чтобы в будущем не перезатирать данные.
+  loadSightInfo = (
+    language: Language,
+    heading: string,
+    body: string,
+    media: MediaObject | null
+  ) => {
+    // Важно: если данные уже были загружены или изменены, не перезаписывайте их.
+    // Это предотвращает потерю пользовательского ввода при переключении языков.
+    // Если хотите принудительную загрузку, добавьте другой метод или параметр.
+    if (!this.sightInfo[language].left.loaded) {
+      // <--- Только если еще не загружено
+      this.sightInfo[language].left.heading = heading;
+      this.sightInfo[language].left.body = body;
+      this.sightInfo[language].left.media = media;
+      this.sightInfo[language].left.loaded = true; // <--- Устанавливаем loaded только при загрузке
+    }
+  };
+
+  // updateSightInfo: Используется для сохранения ЛЮБЫХ пользовательских изменений.
+  // Она НЕ должна влиять на флаг 'loaded', который управляется 'loadSightInfo'.
+  updateSightInfo = (
+    language: Language,
+    heading: string,
+    body: string,
+    media: MediaObject | null
+  ) => {
+    this.sightInfo[language].left.heading = heading;
+    this.sightInfo[language].left.body = body;
+    this.sightInfo[language].left.media = media;
+    // this.sightInfo[language].left.loaded = true; // <-- УДАЛИТЕ эту строку
+  };
+
+  clearSightInfo = () => {
+    this.sightInfo = {
+      id: 0,
+      city_id: 0,
+      city: "",
+      latitude: 0,
+      longitude: 0,
+      thumbnail: "",
+      watermark_lu: "",
+      watermark_rd: "",
+      left_article: 0,
+      preview_media: "",
+      video_preview: "",
+      ru: {
+        info: { name: "", address: "" },
+        left: { loaded: false, heading: "", body: "", media: null },
+        right: [],
+      },
+      en: {
+        info: { name: "", address: "" },
+        left: { loaded: false, heading: "", body: "", media: null },
+        right: [],
+      },
+      zh: {
+        info: { name: "", address: "" },
+        left: { loaded: false, heading: "", body: "", media: null },
+        right: [],
+      },
+    };
+  };
+}
+
+export const editSightStore = new EditSightStore();
diff --git a/src/shared/store/MediaStore/index.tsx b/src/shared/store/MediaStore/index.tsx
new file mode 100644
index 0000000..c06f8b6
--- /dev/null
+++ b/src/shared/store/MediaStore/index.tsx
@@ -0,0 +1,27 @@
+import { makeAutoObservable, runInAction } from "mobx";
+import { authInstance } from "@shared";
+
+type Media = {
+  id: string;
+  filename: string;
+  media_name: string;
+  media_type: number;
+};
+
+class MediaStore {
+  media: Media[] = [];
+
+  constructor() {
+    makeAutoObservable(this);
+  }
+
+  getMedia = async () => {
+    const response = await authInstance.get("/media");
+
+    runInAction(() => {
+      this.media = [...response.data];
+    });
+  };
+}
+
+export const mediaStore = new MediaStore();
diff --git a/src/shared/store/SightsStore/index.tsx b/src/shared/store/SightsStore/index.tsx
index 7acd0d6..5df9823 100644
--- a/src/shared/store/SightsStore/index.tsx
+++ b/src/shared/store/SightsStore/index.tsx
@@ -1,12 +1,17 @@
-import { authInstance, languageInstance, languageStore } from "@shared";
-import { makeAutoObservable, runInAction } from "mobx";
+import {
+  articlesStore,
+  authInstance,
+  languageInstance,
+  languageStore,
+  editSightStore,
+} from "@shared";
+import { computed, makeAutoObservable, runInAction } from "mobx";
 
 export type Language = "ru" | "en" | "zh";
 
 export type MultilingualContent = {
   [key in Language]: {
     name: string;
-    description: string;
     address: string;
   };
 };
@@ -30,7 +35,6 @@ export type Sight = {
 export type CreateSight = {
   [key in Language]: {
     name: string;
-    description: string;
     address: string;
   };
 };
@@ -39,9 +43,9 @@ class SightsStore {
   sights: Sight[] = [];
   sight: Sight | null = null;
   createSight: CreateSight = {
-    ru: { name: "", description: "", address: "" },
-    en: { name: "", description: "", address: "" },
-    zh: { name: "", description: "", address: "" },
+    ru: { name: "", address: "" },
+    en: { name: "", address: "" },
+    zh: { name: "", address: "" },
   };
 
   constructor() {
@@ -60,6 +64,41 @@ class SightsStore {
 
     runInAction(() => {
       this.sight = response.data;
+      editSightStore.sightInfo = {
+        ...editSightStore.sightInfo,
+        id: response.data.id,
+        city_id: response.data.city_id,
+        city: response.data.city,
+        latitude: response.data.latitude,
+        longitude: response.data.longitude,
+        thumbnail: response.data.thumbnail,
+        watermark_lu: response.data.watermark_lu,
+        watermark_rd: response.data.watermark_rd,
+        left_article: response.data.left_article,
+        preview_media: response.data.preview_media,
+        video_preview: response.data.video_preview,
+        [languageStore.language]: {
+          info: {
+            name: response.data.name,
+            address: response.data.address,
+            description: response.data.description,
+          },
+          left: {
+            heading: editSightStore.sightInfo[languageStore.language].left
+              .loaded
+              ? editSightStore.sightInfo[languageStore.language].left.heading
+              : articlesStore.articles[languageStore.language].find(
+                  (article) => article.id === response.data.left_article
+                )?.heading,
+            body: editSightStore.sightInfo[languageStore.language].left.loaded
+              ? editSightStore.sightInfo[languageStore.language].left.body
+              : articlesStore.articles[languageStore.language].find(
+                  (article) => article.id === response.data.left_article
+                )?.body,
+          },
+        },
+      };
+      console.log(editSightStore.sightInfo);
     });
   };
 
@@ -70,7 +109,6 @@ class SightsStore {
     const id = (
       await authInstance.post("/sight", {
         name: this.createSight[languageStore.language].name,
-        description: this.createSight[languageStore.language].description,
         address: this.createSight[languageStore.language].address,
         city_id: city,
         latitude: coordinates.latitude,
@@ -86,8 +124,6 @@ class SightsStore {
       `/sight/${id}`,
       {
         name: this.createSight[anotherLanguages[0] as Language].name,
-        description:
-          this.createSight[anotherLanguages[0] as Language].description,
         address: this.createSight[anotherLanguages[0] as Language].address,
         city_id: city,
         latitude: coordinates.latitude,
@@ -99,8 +135,6 @@ class SightsStore {
       `/sight/${id}`,
       {
         name: this.createSight[anotherLanguages[1] as Language].name,
-        description:
-          this.createSight[anotherLanguages[1] as Language].description,
         address: this.createSight[anotherLanguages[1] as Language].address,
         city_id: city,
         latitude: coordinates.latitude,
@@ -110,9 +144,9 @@ class SightsStore {
 
     runInAction(() => {
       this.createSight = {
-        ru: { name: "", description: "", address: "" },
-        en: { name: "", description: "", address: "" },
-        zh: { name: "", description: "", address: "" },
+        ru: { name: "", address: "" },
+        en: { name: "", address: "" },
+        zh: { name: "", address: "" },
       };
     });
   };
@@ -139,22 +173,41 @@ class SightsStore {
       this.createSight = {
         ru: {
           name: "",
-          description: "",
           address: "",
         },
         en: {
           name: "",
-          description: "",
           address: "",
         },
         zh: {
           name: "",
-          description: "",
           address: "",
         },
       };
     });
   };
+
+  sightData = computed(() => {
+    return {
+      name: this.sight?.name,
+      address: this.sight?.address,
+      city_id: this.sight?.city_id,
+      latitude: this.sight?.latitude,
+      longitude: this.sight?.longitude,
+      thumbnail: this.sight?.thumbnail,
+      watermark_lu: this.sight?.watermark_lu,
+      watermark_rd: this.sight?.watermark_rd,
+      left_article: this.sight?.left_article,
+      preview_media: this.sight?.preview_media,
+      video_preview: this.sight?.video_preview,
+      [languageStore.language]: {
+        info: {
+          name: this.sight?.name,
+          address: this.sight?.address,
+        },
+      },
+    };
+  });
 }
 
 export const sightsStore = new SightsStore();
diff --git a/src/shared/store/index.ts b/src/shared/store/index.ts
index 9be9b82..cb82e5c 100644
--- a/src/shared/store/index.ts
+++ b/src/shared/store/index.ts
@@ -6,3 +6,5 @@ export * from "./SnapshotStore";
 export * from "./SightsStore";
 export * from "./CityStore";
 export * from "./ArticlesStore";
+export * from "./EditSightStore";
+export * from "./MediaStore";
diff --git a/src/shared/ui/CoordinatesInput/index.tsx b/src/shared/ui/CoordinatesInput/index.tsx
index 60918af..b34361b 100644
--- a/src/shared/ui/CoordinatesInput/index.tsx
+++ b/src/shared/ui/CoordinatesInput/index.tsx
@@ -2,11 +2,15 @@ import { Box, TextField } from "@mui/material";
 import { useEffect, useState } from "react";
 
 export const CoordinatesInput = ({
+  initialValue,
   setValue,
 }: {
+  initialValue: { latitude: number; longitude: number };
   setValue: (value: { latitude: number; longitude: number }) => void;
 }) => {
-  const [inputValue, setInputValue] = useState<string>("");
+  const [inputValue, setInputValue] = useState<string>(
+    `${initialValue.latitude} ${initialValue.longitude}`
+  );
 
   useEffect(() => {
     setValue({
diff --git a/src/widgets/LanguageSwitcher/index.tsx b/src/widgets/LanguageSwitcher/index.tsx
index 0d34162..b6e1650 100644
--- a/src/widgets/LanguageSwitcher/index.tsx
+++ b/src/widgets/LanguageSwitcher/index.tsx
@@ -45,7 +45,7 @@ export const LanguageSwitcher = observer(() => {
   };
 
   return (
-    <div className="fixed top-1/2  -translate-y-1/2 right-0 flex flex-col gap-2 p-4  ">
+    <div className="fixed top-1/2  -translate-y-1/2 right-0 flex flex-col gap-2 p-4 z-10  ">
       {/* Added some styling for better visualization */}
       {LANGUAGES.map((lang) => (
         <Button
diff --git a/src/widgets/MediaViewer/ThreeView.tsx b/src/widgets/MediaViewer/ThreeView.tsx
new file mode 100644
index 0000000..12ab158
--- /dev/null
+++ b/src/widgets/MediaViewer/ThreeView.tsx
@@ -0,0 +1,22 @@
+import { Canvas } from "@react-three/fiber";
+import { OrbitControls, Stage, useGLTF } from "@react-three/drei";
+
+type ModelViewerProps = {
+  fileUrl: string;
+  height?: string;
+};
+
+export const ThreeView = ({ fileUrl, height = "100%" }: ModelViewerProps) => {
+  const { scene } = useGLTF(fileUrl);
+
+  return (
+    <Canvas style={{ width: "100%", height: height }}>
+      <ambientLight />
+      <directionalLight />
+      <Stage environment="city" intensity={0.6}>
+        <primitive object={scene} />
+      </Stage>
+      <OrbitControls />
+    </Canvas>
+  );
+};
diff --git a/src/widgets/MediaViewer/index.tsx b/src/widgets/MediaViewer/index.tsx
new file mode 100644
index 0000000..0a4a9ed
--- /dev/null
+++ b/src/widgets/MediaViewer/index.tsx
@@ -0,0 +1,107 @@
+import { Box } from "@mui/material";
+
+import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer";
+import { ThreeView } from "./ThreeView";
+
+export interface MediaData {
+  id: string | number;
+  media_type: number;
+  filename?: string;
+}
+
+export function MediaViewer({ media }: Readonly<{ media?: MediaData }>) {
+  const token = localStorage.getItem("token");
+  return (
+    <Box
+      sx={{
+        width: "80%",
+        height: "100%",
+        maxWidth: "600px",
+        display: "flex",
+        flexGrow: 1,
+        justifyContent: "center",
+        margin: "0 auto",
+      }}
+    >
+      {media?.media_type === 1 && (
+        <img
+          src={`${import.meta.env.VITE_KRBL_MEDIA}${
+            media?.id
+          }/download?token=${token}`}
+          alt={media?.filename}
+          style={{
+            maxWidth: "100%",
+            height: "auto",
+            objectFit: "contain",
+            borderRadius: 8,
+          }}
+        />
+      )}
+
+      {media?.media_type === 2 && (
+        <video
+          src={`${import.meta.env.VITE_KRBL_MEDIA}${
+            media?.id
+          }/download?token=${token}`}
+          style={{
+            margin: "auto 0",
+            height: "fit-content",
+            width: "fit-content",
+            objectFit: "contain",
+            borderRadius: 30,
+          }}
+          controls
+          autoPlay
+          muted
+        />
+      )}
+      {media?.media_type === 3 && (
+        <img
+          src={`${import.meta.env.VITE_KRBL_MEDIA}${
+            media?.id
+          }/download?token=${token}`}
+          alt={media?.filename}
+          style={{
+            maxWidth: "100%",
+            height: "100%",
+            objectFit: "contain",
+            borderRadius: 8,
+          }}
+        />
+      )}
+      {media?.media_type === 4 && (
+        <img
+          src={`${import.meta.env.VITE_KRBL_MEDIA}${
+            media?.id
+          }/download?token=${token}`}
+          alt={media?.filename}
+          style={{
+            maxWidth: "100%",
+            height: "100%",
+            objectFit: "contain",
+            borderRadius: 8,
+          }}
+        />
+      )}
+
+      {media?.media_type === 5 && (
+        <ReactPhotoSphereViewer
+          src={`${import.meta.env.VITE_KRBL_MEDIA}${
+            media?.id
+          }/download?token=${token}`}
+          width={"100%"}
+          height={"100%"}
+        />
+      )}
+
+      {media?.media_type === 6 && (
+        <ThreeView
+          fileUrl={`${import.meta.env.VITE_KRBL_MEDIA}${
+            media?.id
+          }/download?token=${token}`}
+          height="100%"
+        />
+      )}
+    </Box>
+  );
+}
diff --git a/src/widgets/ReactMarkdownEditor/index.tsx b/src/widgets/ReactMarkdownEditor/index.tsx
index 45efa01..e4c498e 100644
--- a/src/widgets/ReactMarkdownEditor/index.tsx
+++ b/src/widgets/ReactMarkdownEditor/index.tsx
@@ -2,58 +2,58 @@ import { styled } from "@mui/material/styles";
 import SimpleMDE, { SimpleMDEReactProps } from "react-simplemde-editor";
 import "easymde/dist/easymde.min.css";
 
-const StyledMarkdownEditor = styled("div")(() => ({
+const StyledMarkdownEditor = styled("div")(({ theme }) => ({
   "& .editor-toolbar": {
-    backgroundColor: "inherit",
-    borderColor: "inherit",
+    backgroundColor: theme.palette.background.paper,
+    borderColor: theme.palette.divider,
   },
   "& .editor-toolbar button": {
-    color: "inherit",
+    color: theme.palette.text.primary,
   },
   "& .editor-toolbar button:hover": {
-    backgroundColor: "inherit",
+    backgroundColor: theme.palette.action.hover,
   },
   "& .editor-toolbar button:active, & .editor-toolbar button.active": {
-    backgroundColor: "inherit",
+    backgroundColor: theme.palette.action.selected,
   },
   "& .editor-statusbar": {
     display: "none",
   },
   // Стили для самого редактора
   "& .CodeMirror": {
-    backgroundColor: "inherit",
-    color: "inherit",
-    borderColor: "inherit",
+    backgroundColor: theme.palette.background.paper,
+    color: theme.palette.text.primary,
+    borderColor: theme.palette.divider,
   },
   // Стили для текста в редакторе
   "& .CodeMirror-selected": {
-    backgroundColor: "inherit !important",
+    backgroundColor: `${theme.palette.action.selected} !important`,
   },
   "& .CodeMirror-cursor": {
-    borderLeftColor: "inherit",
+    borderLeftColor: theme.palette.text.primary,
   },
   // Стили для markdown разметки
   "& .cm-header": {
-    color: "inherit",
+    color: theme.palette.primary.main,
   },
   "& .cm-quote": {
-    color: "inherit",
+    color: theme.palette.text.secondary,
     fontStyle: "italic",
   },
   "& .cm-link": {
-    color: "inherit",
+    color: theme.palette.primary.main,
   },
   "& .cm-url": {
-    color: "inherit",
+    color: theme.palette.secondary.main,
   },
   "& .cm-formatting": {
-    color: "inherit",
+    color: theme.palette.text.secondary,
   },
 
   "& .CodeMirror .editor-preview-full": {
-    backgroundColor: "inherit",
-    color: "inherit",
-    borderColor: "inherit",
+    backgroundColor: theme.palette.background.paper,
+    color: theme.palette.text.primary,
+    borderColor: theme.palette.divider,
   },
 
   "& .EasyMDEContainer": {
@@ -100,8 +100,7 @@ export const ReactMarkdownEditor = (props: SimpleMDEReactProps) => {
     ];
   return (
     <StyledMarkdownEditor
-      autoFocus={false}
-      spellCheck={false}
+      className="my-markdown-editor"
       sx={{ marginTop: 1.5, marginBottom: 3 }}
     >
       <SimpleMDE {...props} />
diff --git a/src/widgets/SightTabs/InformationTab/index.tsx b/src/widgets/SightTabs/InformationTab/index.tsx
index b6215f0..2d044ee 100644
--- a/src/widgets/SightTabs/InformationTab/index.tsx
+++ b/src/widgets/SightTabs/InformationTab/index.tsx
@@ -4,6 +4,12 @@ import {
   Box,
   Autocomplete,
   Typography,
+  Paper,
+  Tooltip,
+  Dialog,
+  DialogTitle,
+  MenuItem,
+  Menu as MuiMenu,
 } from "@mui/material";
 import {
   BackButton,
@@ -14,24 +20,70 @@ import {
   Language,
   cityStore,
   CoordinatesInput,
+  editSightStore,
+  SelectMediaDialog,
+  PreviewMediaDialog,
 } from "@shared";
 import { LanguageSwitcher } from "@widgets";
+import { Info, ImagePlus } from "lucide-react";
 
 import { observer } from "mobx-react-lite";
-import { useState } from "react";
+import { useRef, useState } from "react";
 
 // Мокап для всплывающей подсказки
 
 export const InformationTab = observer(
   ({ value, index }: { value: number; index: number }) => {
     const { cities } = cityStore;
+    const [isMediaModalOpen, setIsMediaModalOpen] = useState(false);
+    const [mediaId, setMediaId] = useState<string>("");
+    const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
     const { createSight, updateCreateSight, createSightAction } = sightsStore;
-    const [city, setCity] = useState<number>(0);
+    const { sightInfo } = editSightStore;
+    const [city, setCity] = useState<number>(sightInfo.city_id ?? 0);
     const [coordinates, setCoordinates] = useState({
-      latitude: 0,
-      longitude: 0,
+      latitude: sightInfo.latitude ?? 0,
+      longitude: sightInfo.longitude ?? 0,
     });
     const { language } = languageStore;
+    const token = localStorage.getItem("token");
+
+    // Menu state for each media button
+    const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
+    const [activeMenuType, setActiveMenuType] = useState<
+      "thumbnail" | "watermark_lu" | "watermark_rd" | null
+    >(null);
+    const [isAddMediaOpen, setIsAddMediaOpen] = useState(false);
+
+    const handleMenuOpen = (
+      event: React.MouseEvent<HTMLElement>,
+      type: "thumbnail" | "watermark_lu" | "watermark_rd"
+    ) => {
+      setMenuAnchorEl(event.currentTarget);
+      setActiveMenuType(type);
+    };
+
+    const handleMenuClose = () => {
+      setMenuAnchorEl(null);
+      setActiveMenuType(null);
+    };
+
+    const handleCreateNew = () => {
+      handleMenuClose();
+    };
+
+    const handleAddMedia = () => {
+      setIsAddMediaOpen(true);
+      handleMenuClose();
+    };
+
+    const handleMediaSelect = (selectedMediaId: string) => {
+      if (!activeMenuType) return;
+
+      // Close the dialog
+      setIsAddMediaOpen(false);
+      setActiveMenuType(null);
+    };
 
     const handleChange = (
       language: Language,
@@ -42,325 +94,326 @@ export const InformationTab = observer(
 
     const handleSave = async () => {
       try {
-        await createSightAction(createSight[language], city, coordinates);
+        await createSightAction(city, coordinates);
       } catch (error) {
         console.error(error);
       }
     };
 
     return (
-      <TabPanel value={value} index={index}>
-        <Box
-          sx={{
-            display: "flex",
-            flexDirection: "column",
-            gap: 3,
-            position: "relative",
-            paddingBottom: "70px" /* Space for save button */,
-          }}
-        >
-          <BackButton />
-
+      <>
+        <TabPanel value={value} index={index}>
           <Box
             sx={{
               display: "flex",
-
-              gap: 4, // Added gap between the two main columns
-              width: "100%",
               flexDirection: "column",
+              gap: 3,
+              position: "relative",
+              paddingBottom: "70px" /* Space for save button */,
             }}
           >
-            {/* Left column with main fields */}
+            <BackButton />
+
             <Box
               sx={{
-                flexGrow: 1,
                 display: "flex",
-                width: "80%",
+
+                gap: 4, // Added gap between the two main columns
+                width: "100%",
                 flexDirection: "column",
-                gap: 2.5,
               }}
             >
-              <TextField
-                label={`Название (${language.toUpperCase()})`}
-                value={createSight[language]?.name ?? ""}
-                onChange={(e) => {
-                  handleChange(language as Language, {
-                    name: e.target.value,
-                  });
+              {/* Left column with main fields */}
+              <Box
+                sx={{
+                  flexGrow: 1,
+                  display: "flex",
+                  width: "80%",
+                  flexDirection: "column",
+                  gap: 2.5,
                 }}
-                fullWidth
-                variant="outlined"
-              />
-              <TextField
-                label={`Описание (${language.toUpperCase()})`}
-                value={createSight?.[language]?.description ?? ""}
-                onChange={(e) => {
-                  handleChange(language as Language, {
-                    description: e.target.value,
-                  });
-                }}
-                fullWidth
-                variant="outlined"
-                multiline
-                rows={4}
-              />
-              <TextField
-                label="Адрес"
-                value={createSight?.[language]?.address ?? ""}
-                onChange={(e) => {
-                  handleChange(language as Language, {
-                    address: e.target.value,
-                  });
-                }}
-                fullWidth
-                variant="outlined"
-              />
-              <Autocomplete
-                options={cities}
-                getOptionLabel={(option) => option.name}
-                onChange={(_, value) => {
-                  setCity(value?.id ?? 0);
-                }}
-                renderInput={(params) => (
-                  <TextField {...params} label="Город" />
-                )}
-              />
+              >
+                <TextField
+                  label={`Название (${language.toUpperCase()})`}
+                  value={sightInfo[language]?.info?.name ?? ""}
+                  onChange={(e) => {
+                    handleChange(language as Language, {
+                      name: e.target.value,
+                    });
+                  }}
+                  fullWidth
+                  variant="outlined"
+                />
 
-              <CoordinatesInput setValue={setCoordinates} />
-            </Box>
+                <TextField
+                  label="Адрес"
+                  value={sightInfo[language]?.info?.address ?? ""}
+                  onChange={(e) => {
+                    handleChange(language as Language, {
+                      address: e.target.value,
+                    });
+                  }}
+                  fullWidth
+                  variant="outlined"
+                />
 
-            {/* Правая колонка для логотипа и водяных знаков
-            <Box
-              sx={{
-                display: "flex",
+                <Autocomplete
+                  options={cities}
+                  value={cities.find((city) => city.id === sightInfo.city_id)}
+                  getOptionLabel={(option) => option.name}
+                  onChange={(_, value) => {
+                    setCity(value?.id ?? 0);
+                  }}
+                  renderInput={(params) => (
+                    <TextField {...params} label="Город" />
+                  )}
+                />
 
-                gap: 4,
-              }}
-            >
+                <CoordinatesInput
+                  initialValue={coordinates}
+                  setValue={setCoordinates}
+                />
+              </Box>
 
               <Box
                 sx={{
                   display: "flex",
-                  justifyContent: "space-around",
-                  width: "80%",
-                  gap: 2,
-                  flexDirection: { xs: "column", sm: "row" }, // Stack on extra small, side-by-side on small and up
+
+                  gap: 4,
                 }}
               >
-                <Paper
-                  elevation={2}
+                <Box
                   sx={{
-                    padding: 2,
                     display: "flex",
-                    flexDirection: "column",
-                    alignItems: "center",
-                    gap: 1,
-                    flex: 1,
-                    minWidth: 150, // Ensure a minimum width
+                    justifyContent: "space-around",
+                    width: "80%",
+                    gap: 2,
+                    flexDirection: { xs: "column", sm: "row" }, // Stack on extra small, side-by-side on small and up
                   }}
                 >
-                  <Box sx={{ display: "flex", alignItems: "center" }}>
-                    <Typography
-                      variant="subtitle2"
-                      gutterBottom
-                      sx={{ mb: 0, mr: 0.5 }}
-                    >
-                      Логотип
-                    </Typography>
-                    <Tooltip title={watermarkTooltipText}>
-                      <Info
-                        size={16}
-                        color="gray"
-                        style={{ cursor: "pointer" }}
-                      />
-                    </Tooltip>
-                  </Box>
-                  <Box
+                  <Paper
+                    elevation={2}
                     sx={{
-                      width: 80,
-                      height: 80,
-                      backgroundColor: "grey.200",
+                      padding: 2,
                       display: "flex",
+                      flexDirection: "column",
                       alignItems: "center",
-                      justifyContent: "center",
-                      borderRadius: 1,
-                      mb: 1,
-                      cursor: mockSightData.watermark_lu
-                        ? "pointer"
-                        : "default", // Only clickable if there's an image
-                      "&:hover": {
-                        backgroundColor: mockSightData.watermark_lu
-                          ? "grey.300"
-                          : "grey.200",
-                      },
+                      gap: 1,
+                      flex: 1,
+                      minWidth: 150, // Ensure a minimum width
                     }}
-                    onClick={() =>
-                      mockSightData.watermark_lu &&
-                      handleSelectMedia("watermark_lu")
-                    }
                   >
-                    {mockSightData.watermark_lu ? (
-                      <img
-                        src={mockSightData.watermark_lu}
-                        alt="Знак л.в"
-                        style={{ maxWidth: "100%", maxHeight: "100%" }}
-                      />
-                    ) : (
-                      <ImagePlus size={24} color="grey" />
-                    )}
-                  </Box>
-                  <Button
-                    variant="outlined"
-                    size="small"
-                    onClick={() => handleSelectMedia("watermark_lu")}
-                  >
-                    Выбрать
-                  </Button>
-                </Paper>
-                <Paper
-                  elevation={2}
-                  sx={{
-                    padding: 2,
-                    display: "flex",
-                    flexDirection: "column",
-                    alignItems: "center",
-                    gap: 1,
-                    flex: 1,
-                    minWidth: 150, // Ensure a minimum width
-                  }}
-                >
-                  <Box sx={{ display: "flex", alignItems: "center" }}>
-                    <Typography
-                      variant="subtitle2"
-                      gutterBottom
-                      sx={{ mb: 0, mr: 0.5 }}
+                    <Box sx={{ display: "flex", alignItems: "center" }}>
+                      <Typography
+                        variant="subtitle2"
+                        gutterBottom
+                        sx={{ mb: 0, mr: 0.5 }}
+                      >
+                        Логотип
+                      </Typography>
+                    </Box>
+                    <Box
+                      sx={{
+                        width: 80,
+                        height: 80,
+                        backgroundColor: "grey.200",
+                        display: "flex",
+                        alignItems: "center",
+                        justifyContent: "center",
+                        borderRadius: 1,
+                        mb: 1,
+                        cursor: editSightStore.sightInfo?.thumbnail
+                          ? "pointer"
+                          : "default", // Only clickable if there's an image
+                        "&:hover": {
+                          backgroundColor: editSightStore.sightInfo?.thumbnail
+                            ? "red.300"
+                            : "grey.200",
+                        },
+                      }}
+                      onClick={() => {
+                        setIsMediaModalOpen(true);
+                      }}
                     >
-                      Водяной знак (л.в)
-                    </Typography>
-                    <Tooltip title={watermarkTooltipText}>
-                      <Info
-                        size={16}
-                        color="gray"
-                        style={{ cursor: "pointer" }}
-                      />
-                    </Tooltip>
-                  </Box>
-                  <Box
+                      {editSightStore.sightInfo?.thumbnail ? (
+                        <img
+                          src={`${import.meta.env.VITE_KRBL_MEDIA}${
+                            editSightStore.sightInfo?.thumbnail
+                          }/download?token=${token}`}
+                          alt="Логотип"
+                          style={{ maxWidth: "100%", maxHeight: "100%" }}
+                          onClick={() => {
+                            setIsPreviewMediaOpen(true);
+                            setMediaId(editSightStore.sightInfo?.thumbnail);
+                          }}
+                        />
+                      ) : (
+                        <ImagePlus size={24} color="grey" />
+                      )}
+                    </Box>
+                    <Button
+                      variant="outlined"
+                      size="small"
+                      onClick={(e) => handleMenuOpen(e, "thumbnail")}
+                    >
+                      Выбрать
+                    </Button>
+                  </Paper>
+                  <Paper
+                    elevation={2}
                     sx={{
-                      width: 80,
-                      height: 80,
-                      backgroundColor: "grey.200",
+                      padding: 2,
                       display: "flex",
+                      flexDirection: "column",
                       alignItems: "center",
-                      justifyContent: "center",
-                      borderRadius: 1,
-                      mb: 1,
-                      cursor: mockSightData.watermark_lu
-                        ? "pointer"
-                        : "default", // Only clickable if there's an image
-                      "&:hover": {
-                        backgroundColor: mockSightData.watermark_lu
-                          ? "grey.300"
-                          : "grey.200",
-                      },
+                      gap: 1,
+                      flex: 1,
+                      minWidth: 150, // Ensure a minimum width
                     }}
-                    onClick={() =>
-                      mockSightData.watermark_lu &&
-                      handleSelectMedia("watermark_lu")
-                    }
                   >
-                    {mockSightData.watermark_lu ? (
-                      <img
-                        src={mockSightData.watermark_lu}
-                        alt="Знак л.в"
-                        style={{ maxWidth: "100%", maxHeight: "100%" }}
-                      />
-                    ) : (
-                      <ImagePlus size={24} color="grey" />
-                    )}
-                  </Box>
-                  <Button
-                    variant="outlined"
-                    size="small"
-                    onClick={() => handleSelectMedia("watermark_lu")}
-                  >
-                    Выбрать
-                  </Button>
-                </Paper>
+                    <Box sx={{ display: "flex", alignItems: "center" }}>
+                      <Typography
+                        variant="subtitle2"
+                        gutterBottom
+                        sx={{ mb: 0, mr: 0.5 }}
+                      >
+                        Водяной знак (л.в)
+                      </Typography>
+                      <Tooltip title={"asf"}>
+                        <Info
+                          size={16}
+                          color="gray"
+                          style={{ cursor: "pointer" }}
+                        />
+                      </Tooltip>
+                    </Box>
+                    <Box
+                      sx={{
+                        width: 80,
+                        height: 80,
+                        backgroundColor: "grey.200",
+                        display: "flex",
+                        alignItems: "center",
+                        justifyContent: "center",
+                        borderRadius: 1,
+                        mb: 1,
+                        cursor: editSightStore.sightInfo?.watermark_lu
+                          ? "pointer"
+                          : "default", // Only clickable if there's an image
+                        "&:hover": {
+                          backgroundColor: editSightStore.sightInfo
+                            ?.watermark_lu
+                            ? "grey.300"
+                            : "grey.200",
+                        },
+                      }}
+                      onClick={() => {
+                        setIsPreviewMediaOpen(true);
+                        setMediaId(editSightStore.sightInfo?.watermark_lu);
+                      }}
+                    >
+                      {editSightStore.sightInfo?.watermark_lu ? (
+                        <img
+                          src={`${import.meta.env.VITE_KRBL_MEDIA}${
+                            editSightStore.sightInfo?.watermark_lu
+                          }/download?token=${token}`}
+                          alt="Знак л.в"
+                          style={{ maxWidth: "100%", maxHeight: "100%" }}
+                          onClick={() => {
+                            setIsMediaModalOpen(true);
+                            setMediaId(editSightStore.sightInfo?.watermark_lu);
+                          }}
+                        />
+                      ) : (
+                        <ImagePlus size={24} color="grey" />
+                      )}
+                    </Box>
+                    <Button
+                      variant="outlined"
+                      size="small"
+                      onClick={(e) => handleMenuOpen(e, "watermark_lu")}
+                    >
+                      Выбрать
+                    </Button>
+                  </Paper>
 
-                <Paper
-                  elevation={2}
-                  sx={{
-                    padding: 2,
-                    display: "flex",
-                    flexDirection: "column",
-                    alignItems: "center",
-                    gap: 1,
-                    flex: 1,
-                    minWidth: 150, // Ensure a minimum width
-                  }}
-                >
-                  <Box sx={{ display: "flex", alignItems: "center" }}>
-                    <Typography
-                      variant="subtitle2"
-                      gutterBottom
-                      sx={{ mb: 0, mr: 0.5 }}
-                    >
-                      Водяной знак (п.в)
-                    </Typography>
-                    <Tooltip title={watermarkTooltipText}>
-                      <Info
-                        size={16}
-                        color="gray"
-                        style={{ cursor: "pointer" }}
-                      />
-                    </Tooltip>
-                  </Box>
-                  <Box
+                  <Paper
+                    elevation={2}
                     sx={{
-                      width: 80,
-                      height: 80,
-                      backgroundColor: "grey.200",
+                      padding: 2,
                       display: "flex",
+                      flexDirection: "column",
                       alignItems: "center",
-                      justifyContent: "center",
-                      borderRadius: 1,
-                      mb: 1,
-                      cursor: mockSightData.watermark_rd
-                        ? "pointer"
-                        : "default", // Only clickable if there's an image
-                      "&:hover": {
-                        backgroundColor: mockSightData.watermark_rd
-                          ? "grey.300"
-                          : "grey.200",
-                      },
+                      gap: 1,
+                      flex: 1,
+                      minWidth: 150, // Ensure a minimum width
                     }}
-                    onClick={() =>
-                      mockSightData.watermark_rd &&
-                      handleSelectMedia("watermark_rd")
-                    }
                   >
-                    {mockSightData.watermark_rd ? (
-                      <img
-                        src={mockSightData.watermark_rd}
-                        alt="Знак п.в"
-                        style={{ maxWidth: "100%", maxHeight: "100%" }}
-                      />
-                    ) : (
-                      <ImagePlus size={24} color="grey" />
-                    )}
-                  </Box>
-                  <Button
-                    variant="outlined"
-                    size="small"
-                    onClick={() => handleSelectMedia("watermark_rd")}
-                  >
-                    Выбрать
-                  </Button>
-                </Paper>
+                    <Box sx={{ display: "flex", alignItems: "center" }}>
+                      <Typography
+                        variant="subtitle2"
+                        gutterBottom
+                        sx={{ mb: 0, mr: 0.5 }}
+                      >
+                        Водяной знак (п.в)
+                      </Typography>
+                      <Tooltip title={"asfaf"}>
+                        <Info
+                          size={16}
+                          color="gray"
+                          style={{ cursor: "pointer" }}
+                        />
+                      </Tooltip>
+                    </Box>
+                    <Box
+                      sx={{
+                        width: 80,
+                        height: 80,
+                        backgroundColor: "grey.200",
+                        display: "flex",
+                        alignItems: "center",
+                        justifyContent: "center",
+                        borderRadius: 1,
+                        mb: 1,
+                        cursor: editSightStore.sightInfo?.watermark_rd
+                          ? "pointer"
+                          : "default", // Only clickable if there's an image
+                        "&:hover": {
+                          backgroundColor: editSightStore.sightInfo
+                            ?.watermark_rd
+                            ? "grey.300"
+                            : "grey.200",
+                        },
+                      }}
+                      onClick={() => editSightStore.sightInfo?.watermark_rd}
+                    >
+                      {editSightStore.sightInfo?.watermark_rd ? (
+                        <img
+                          src={`${import.meta.env.VITE_KRBL_MEDIA}${
+                            editSightStore.sightInfo?.watermark_rd
+                          }/download?token=${token}`}
+                          alt="Знак п.в"
+                          style={{ maxWidth: "100%", maxHeight: "100%" }}
+                          onClick={() => {
+                            setIsPreviewMediaOpen(true);
+                            setMediaId(editSightStore.sightInfo?.watermark_rd);
+                          }}
+                        />
+                      ) : (
+                        <ImagePlus size={24} color="grey" />
+                      )}
+                    </Box>
+                    <Button
+                      variant="outlined"
+                      size="small"
+                      onClick={(e) => handleMenuOpen(e, "watermark_rd")}
+                    >
+                      Выбрать
+                    </Button>
+                  </Paper>
+                </Box>
               </Box>
             </Box>
-          </Box> */}
 
             {/* LanguageSwitcher positioned at the top right */}
 
@@ -384,8 +437,41 @@ export const InformationTab = observer(
               </Button>
             </Box>
           </Box>
-        </Box>
-      </TabPanel>
+        </TabPanel>
+
+        {/* Media Menu */}
+        <MuiMenu
+          anchorEl={menuAnchorEl}
+          open={Boolean(menuAnchorEl)}
+          onClose={handleMenuClose}
+          anchorOrigin={{
+            vertical: "top",
+            horizontal: "right",
+          }}
+          transformOrigin={{
+            vertical: "bottom",
+            horizontal: "right",
+          }}
+        >
+          <MenuItem onClick={handleCreateNew}>Создать новую</MenuItem>
+          <MenuItem onClick={handleAddMedia}>Выбрать существующую</MenuItem>
+        </MuiMenu>
+
+        <SelectMediaDialog
+          open={isAddMediaOpen}
+          onClose={() => {
+            setIsAddMediaOpen(false);
+            setActiveMenuType(null);
+          }}
+          onSelectArticle={handleMediaSelect}
+        />
+
+        <PreviewMediaDialog
+          open={isPreviewMediaOpen}
+          onClose={() => setIsPreviewMediaOpen(false)}
+          mediaId={mediaId}
+        />
+      </>
     );
   }
 );
diff --git a/src/widgets/SightTabs/LeftWidgetTab/index.tsx b/src/widgets/SightTabs/LeftWidgetTab/index.tsx
index 98d1742..ca6c673 100644
--- a/src/widgets/SightTabs/LeftWidgetTab/index.tsx
+++ b/src/widgets/SightTabs/LeftWidgetTab/index.tsx
@@ -1,242 +1,348 @@
+// @widgets/LeftWidgetTab.tsx
 import { Box, Button, TextField, Paper, Typography } from "@mui/material";
-import { BackButton, Sight, TabPanel } from "@shared";
-import { ReactMarkdownComponent, ReactMarkdownEditor } from "@widgets";
+import {
+  articlesStore,
+  BackButton,
+  TabPanel,
+  languageStore,
+  SelectMediaDialog,
+  editSightStore,
+} from "@shared";
+import {
+  LanguageSwitcher,
+  ReactMarkdownComponent,
+  ReactMarkdownEditor,
+} from "@widgets";
 import { Unlink, Trash2, ImagePlus } from "lucide-react";
-import { useState } from "react";
+import { useState, useEffect, useCallback } from "react";
+import { observer } from "mobx-react-lite";
 
-export const LeftWidgetTab = ({
-  value,
-  index,
-  data,
-}: {
-  value: number;
-  index: number;
-  data?: Sight;
-}) => {
-  const [articleTitle, setArticleTitle] = useState("");
-  const [markdownContent, setMarkdownContent] = useState("");
-  const [articleMedia, setArticleMedia] = useState<string | null>(null); // Для превью медиа
+export const LeftWidgetTab = observer(
+  ({ value, index }: { value: number; index: number }) => {
+    const { sightInfo, updateSightInfo, loadSightInfo } = editSightStore;
+    const { articleLoading, getArticleByArticleId } = articlesStore;
+    const { language } = languageStore;
+    const linkedArticle = getArticleByArticleId.get(); // Получаем связанную статью
+    const data = sightInfo[languageStore.language]; // Получаем данные для текущего языка
 
-  const handleSelectMediaForArticle = () => {
-    // Логика открытия модального окна для выбора медиа для статьи
-    console.log("Select media fo  r left article");
-    // Для примера, установим моковое изображение
-    // setArticleMedia("https://via.placeholder.com/350x200.png?text=Article+Media");
-  };
+    useEffect(() => {
+      // Этот useEffect должен загружать данные ИЗ СВЯЗАННОЙ СТАТЬИ
+      // ТОЛЬКО ЕСЛИ данные для ТЕКУЩЕГО ЯЗЫКА еще не были загружены
+      // или если sightInfo.left_article изменился (т.е. привязали новую статью).
 
-  const handleUnlinkArticle = () => {
-    console.log("Unlink left article");
-  };
+      // Мы также должны учитывать, что linkedArticle может измениться (т.е. новую статью привязали)
+      // или language изменился.
+      // Если для текущего языка данные еще не "загружены" (`loaded: false`),
+      // тогда мы берем их из `linkedArticle` и инициализируем.
+      console.log("data.left.loaded", data.left.loaded);
+      if (!data.left.loaded) {
+        // <--- КЛЮЧЕВОЕ УСЛОВИЕ
+        if (linkedArticle && !articleLoading) {
+          console.log("loadSightInfo", linkedArticle, language);
+          loadSightInfo(
+            languageStore.language,
+            linkedArticle.heading,
+            linkedArticle.body || "",
+            null
+          );
+        }
+      }
+      // Зависимости: linkedArticle (для реакции на изменение привязанной статьи),
+      // languageStore.language (для реакции на изменение языка),
+      // loadSightInfo (чтобы useEffect знал об изменениях в функции),
+      // data.left.loaded (чтобы useEffect перепроверил условие, когда этот флаг изменится).
+      // Важно: если data.left.loaded становится true, то этот эффект не будет
+      // перезапускаться для того же языка.
+    }, [
+      linkedArticle?.heading,
+      language,
+      loadSightInfo,
+      data.left.loaded,
+      articleLoading,
+    ]);
 
-  const handleDeleteArticle = () => {
-    console.log("Delete left article");
-  };
+    const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] =
+      useState(false);
 
-  const handleSave = () => {
-    console.log("Saving left widget...");
-  };
+    const handleOpenMediaDialog = useCallback(() => {
+      setIsSelectMediaDialogOpen(true);
+    }, []);
 
-  return (
-    <TabPanel value={value} index={index}>
-      <Box
-        sx={{
-          display: "flex",
-          flexDirection: "column",
-          gap: 3,
-          paddingBottom: "70px",
-          position: "relative",
-        }}
-      >
-        <BackButton />
+    const handleMediaSelected = useCallback(
+      (selectedMedia: any) => {
+        // При выборе медиа, обновляем данные для ТЕКУЩЕГО ЯЗЫКА
+        // сохраняя текущие heading и body.
+        updateSightInfo(
+          languageStore.language,
+          data.left.heading,
+          data.left.body,
+          selectedMedia
+        );
+        setIsSelectMediaDialogOpen(false);
+      },
+      [
+        languageStore.language,
+        data.left.heading,
+        data.left.body,
+        updateSightInfo,
+      ]
+    );
 
-        <Paper
-          elevation={2}
+    const handleCloseMediaDialog = useCallback(() => {
+      setIsSelectMediaDialogOpen(false);
+    }, []);
+
+    // ... (остальной JSX код остался почти без изменений)
+    return (
+      <TabPanel value={value} index={index}>
+        <LanguageSwitcher />
+        <Box
           sx={{
             display: "flex",
-            alignItems: "center",
-            justifyContent: "space-between",
-            paddingX: 2.5,
-            paddingY: 1.5,
-            borderRadius: 2,
-            border: "1px solid",
-            borderColor: "divider",
+            flexDirection: "column",
+            gap: 3,
+            paddingBottom: "70px",
+            position: "relative",
           }}
         >
-          <Typography variant="h6">Левая статья</Typography>
-          <Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
-            <Button
-              variant="outlined"
-              color="primary"
-              startIcon={<Unlink size={18} />}
-              onClick={handleUnlinkArticle}
-              size="small"
-            >
-              Открепить
-            </Button>
-            <Button
-              variant="outlined"
-              color="error"
-              startIcon={<Trash2 size={18} />}
-              onClick={handleDeleteArticle}
-              size="small"
-            >
-              Удалить
-            </Button>
-          </Box>
-        </Paper>
+          <BackButton />
 
-        <Box sx={{ display: "flex", gap: 3, flexGrow: 1 }}>
-          {/* Левая колонка: Редактирование */}
-          <Box
-            sx={{ flex: 2, display: "flex", flexDirection: "column", gap: 2 }}
+          <Paper
+            elevation={2}
+            sx={{
+              display: "flex",
+              alignItems: "center",
+              justifyContent: "space-between",
+              paddingX: 2.5,
+              paddingY: 1.5,
+              borderRadius: 2,
+              border: "1px solid",
+              borderColor: "divider",
+            }}
           >
-            <TextField
-              label="Название информации" // На макете "Название" для статьи, потом "Информация"
-              value={articleTitle}
-              onChange={(e) => setArticleTitle(e.target.value)}
-              variant="outlined"
-              sx={{ width: "100%" }} // Примерная ширина как на макете
-            />
-
-            <ReactMarkdownEditor
-              value={markdownContent}
-              onChange={setMarkdownContent}
-            />
-
-            {/* Блок МЕДИА для статьи */}
-            <Paper elevation={1} sx={{ padding: 2, mt: 1 }}>
-              <Typography variant="h6" gutterBottom>
-                МЕДИА
-              </Typography>
-              {/* Здесь будет UI для управления медиа статьи */}
-              {articleMedia ? (
-                <Box sx={{ mb: 1 }}>
-                  <img
-                    src={articleMedia}
-                    alt="Article media"
-                    style={{
-                      maxWidth: "100%",
-                      maxHeight: "150px",
-                      borderRadius: "4px",
-                    }}
-                  />
-                </Box>
-              ) : (
-                <Box
-                  sx={{
-                    width: "100%",
-                    height: 100,
-                    backgroundColor: "grey.100",
-                    display: "flex",
-                    alignItems: "center",
-                    justifyContent: "center",
-                    borderRadius: 1,
-                    mb: 1,
-                    border: "2px dashed",
-                  }}
+            <Typography variant="h6">Левая статья</Typography>
+            <Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
+              {linkedArticle && (
+                <Button
+                  variant="outlined"
+                  color="primary"
+                  startIcon={<Unlink size={18} />}
+                  size="small"
                 >
-                  <Typography color="text.secondary">Нет медиа</Typography>
-                </Box>
+                  Открепить
+                </Button>
               )}
-              <Button variant="contained" onClick={handleSelectMediaForArticle}>
-                Выбрать/Загрузить медиа
+              <Button
+                variant="outlined"
+                color="error"
+                startIcon={<Trash2 size={18} />}
+                size="small"
+              >
+                Удалить
               </Button>
-            </Paper>
-          </Box>
+            </Box>
+          </Paper>
 
-          {/* Правая колонка: Предпросмотр */}
-          <Box sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
-            <Typography variant="h6">Предпросмотр</Typography>
-            <Paper
-              elevation={3}
+          <Box sx={{ display: "flex", gap: 3, flexGrow: 1 }}>
+            {/* Левая колонка: Редактирование */}
+            <Box
+              sx={{ flex: 2, display: "flex", flexDirection: "column", gap: 2 }}
+            >
+              <TextField
+                label="Название информации"
+                value={data.left.heading}
+                onChange={(e) =>
+                  updateSightInfo(
+                    languageStore.language,
+                    e.target.value,
+                    data.left.body,
+                    data.left.media
+                  )
+                }
+                variant="outlined"
+                fullWidth
+              />
+
+              <ReactMarkdownEditor
+                value={data.left.body}
+                onChange={(value) =>
+                  updateSightInfo(
+                    languageStore.language,
+                    data.left.heading,
+                    value,
+                    data.left.media
+                  )
+                }
+              />
+
+              {/* Блок МЕДИА для статьи */}
+              <Paper elevation={1} sx={{ padding: 2, mt: 1 }}>
+                <Typography variant="h6" gutterBottom>
+                  МЕДИА
+                </Typography>
+                {data.left.media ? (
+                  <Box sx={{ mb: 1 }}>
+                    <img
+                      src={data.left.media.filename}
+                      alt="Selected media"
+                      style={{
+                        maxWidth: "100%",
+                        maxHeight: "150px",
+                        objectFit: "contain",
+                      }}
+                    />
+                  </Box>
+                ) : (
+                  <Box
+                    sx={{
+                      width: "100%",
+                      height: 100,
+                      backgroundColor: "grey.100",
+                      display: "flex",
+                      alignItems: "center",
+                      justifyContent: "center",
+                      borderRadius: 1,
+                      mb: 1,
+                      border: "2px dashed",
+                      borderColor: "grey.300",
+                    }}
+                  >
+                    <Typography color="text.secondary">Нет медиа</Typography>
+                  </Box>
+                )}
+                <Button
+                  variant="contained"
+                  startIcon={<ImagePlus size={18} />}
+                  onClick={handleOpenMediaDialog}
+                >
+                  Выбрать/Загрузить медиа
+                </Button>
+                {data.left.media && (
+                  <Button
+                    variant="outlined"
+                    color="error"
+                    size="small"
+                    sx={{ ml: 1 }}
+                    onClick={() =>
+                      updateSightInfo(
+                        languageStore.language,
+                        data.left.heading,
+                        data.left.body,
+                        null
+                      )
+                    }
+                  >
+                    Удалить медиа
+                  </Button>
+                )}
+              </Paper>
+            </Box>
+
+            {/* Правая колонка: Предпросмотр */}
+            <Box
               sx={{
-                width: "100%", // Ширина как на макете ~350px
-                minWidth: 320,
-                maxWidth: 400,
-                height: "auto", // Автоматическая высота или можно задать minHeight
-                minHeight: 500,
-                backgroundColor: "#877361", // Желтоватый фон
-                overflowY: "auto",
-                padding: 0,
+                flex: 1,
                 display: "flex",
                 flexDirection: "column",
+                gap: 1.5,
               }}
             >
-              {/* Медиа в превью (если есть) */}
-              {articleMedia && (
-                <Box
-                  sx={{
-                    width: "100%",
-                    height: 200,
-                    backgroundColor: "grey.300",
-                    display: "flex",
-                    alignItems: "center",
-                    justifyContent: "center",
-                  }}
-                >
-                  <img
-                    src={articleMedia}
-                    alt="Превью медиа"
-                    style={{
-                      objectFit: "cover",
-                      width: "100%",
-                      height: "100%",
-                    }}
-                  />
-                </Box>
-              )}
-              {!articleMedia && (
-                <Box
-                  sx={{
-                    width: "100%",
-                    height: 200,
-                    backgroundColor: "grey.300",
-                    display: "flex",
-                    alignItems: "center",
-                    justifyContent: "center",
-                  }}
-                >
-                  <ImagePlus size={48} color="grey" />
-                </Box>
-              )}
-
-              {/* Заголовок в превью */}
-              <Box
+              <Typography variant="h6">Предпросмотр</Typography>
+              <Paper
+                elevation={3}
                 sx={{
+                  width: "100%",
+                  minWidth: 320,
+                  maxWidth: 400,
+                  height: "auto",
+                  minHeight: 500,
                   backgroundColor: "#877361",
-                  color: "white",
-                  padding: 1.5,
+                  overflowY: "auto",
+                  padding: 0,
+                  display: "flex",
+                  flexDirection: "column",
                 }}
               >
-                <Typography
-                  variant="h5"
-                  component="h2"
-                  sx={{ wordBreak: "break-word" }}
+                {/* Медиа в превью (если есть) */}
+                {data.left.media ? (
+                  <Box
+                    sx={{
+                      width: "100%",
+                      height: 200,
+                      backgroundColor: "grey.300",
+                      display: "flex",
+                      alignItems: "center",
+                      justifyContent: "center",
+                    }}
+                  >
+                    <img
+                      src={data.left.media.filename}
+                      alt="Превью медиа"
+                      style={{
+                        objectFit: "cover",
+                        width: "100%",
+                        height: "100%",
+                      }}
+                    />
+                  </Box>
+                ) : (
+                  <Box
+                    sx={{
+                      width: "100%",
+                      height: 200,
+                      backgroundColor: "grey.300",
+                      display: "flex",
+                      alignItems: "center",
+                      justifyContent: "center",
+                    }}
+                  >
+                    <ImagePlus size={48} color="grey" />
+                  </Box>
+                )}
+
+                {/* Заголовок в превью */}
+                <Box
+                  sx={{
+                    backgroundColor: "#877361",
+                    color: "white",
+                    padding: 1.5,
+                  }}
                 >
-                  {articleTitle || "Название информации"}
-                </Typography>
-              </Box>
+                  <Typography
+                    variant="h5"
+                    component="h2"
+                    sx={{ wordBreak: "break-word" }}
+                  >
+                    {data.left.heading || "Название информации"}
+                  </Typography>
+                </Box>
 
-              {/* Текст статьи в превью */}
-              <Box
-                sx={{
-                  padding: 2,
+                {/* Текст статьи в превью */}
+                <Box
+                  sx={{
+                    padding: 2,
+                    flexGrow: 1,
+                  }}
+                >
+                  <ReactMarkdownComponent value={data.left.body} />
+                </Box>
+              </Paper>
+            </Box>
+          </Box>
 
-                  flexGrow: 1,
-                }}
-              >
-                <ReactMarkdownComponent value={markdownContent} />
-              </Box>
-            </Paper>
+          <Box sx={{ position: "absolute", bottom: 0, right: 0, padding: 2 }}>
+            <Button variant="contained" color="success">
+              Сохранить
+            </Button>
           </Box>
         </Box>
 
-        <Box sx={{ position: "absolute", bottom: 0, right: 0, padding: 2 }}>
-          <Button variant="contained" color="success" onClick={handleSave}>
-            Сохранить
-          </Button>
-        </Box>
-      </Box>
-    </TabPanel>
-  );
-};
+        <SelectMediaDialog
+          open={isSelectMediaDialogOpen}
+          onClose={handleCloseMediaDialog}
+          onSelectMedia={handleMediaSelected}
+        />
+      </TabPanel>
+    );
+  }
+);
diff --git a/src/widgets/SightTabs/RightWidgetTab/index.tsx b/src/widgets/SightTabs/RightWidgetTab/index.tsx
index c8be167..53c80af 100644
--- a/src/widgets/SightTabs/RightWidgetTab/index.tsx
+++ b/src/widgets/SightTabs/RightWidgetTab/index.tsx
@@ -1,405 +1,404 @@
+// RightWidgetTab.tsx
+import { Box, Button, Paper, TextField, Typography } from "@mui/material";
 import {
-  Box,
-  Button,
-  List,
-  ListItemButton,
-  ListItemText,
-  Paper,
-  Typography,
-  Menu,
-  MenuItem,
-  Dialog,
-  DialogTitle,
-  DialogContent,
-  DialogActions,
-  TextField,
-  InputAdornment,
-} from "@mui/material";
-import {
-  articlesStore,
-  BackButton,
-  SelectArticleModal,
-  Sight,
   TabPanel,
+  BackButton,
+  languageStore, // Предполагаем, что он есть в @shared
+  Language, // Предполагаем, что он есть в @shared
+  // SelectArticleModal, // Добавим позже
+  // articlesStore,      // Добавим позже
 } from "@shared";
-import { SightEdit } from "@widgets";
-import { ImagePlus, Plus, Search } from "lucide-react";
+import { LanguageSwitcher } from "@widgets"; // Предполагаем, что LanguageSwitcher у вас есть
 import { observer } from "mobx-react-lite";
-import { useState, useEffect, useRef } from "react";
+import { useState, useMemo, useEffect } from "react";
+import { editSightStore, BlockItem } from "@shared"; // Путь к вашему стору
 
-// --- Mock Data (can be moved to a separate file or fetched from an API) ---
-const mockRightWidgetBlocks = [
-  { id: "preview_media", name: "Превью-медиа", type: "special" },
-  { id: "article_1", name: "1. История", type: "article" },
-  { id: "article_2", name: "2. Факты", type: "article" },
-  {
-    id: "article_3",
-    name: "3. Блокада (Пример длинного названия)",
-    type: "article",
-  },
+// Импортируем сюда же определения BlockItem, если не выносим в types.ts
+// export interface BlockItem { id: string; type: 'media' | 'article'; nameForSidebar: string; linkedArticleStoreId?: string; }
+
+// --- Начальные данные для структуры блоков (позже это может загружаться) ---
+// ID здесь должны быть уникальными для списка.
+const initialBlockStructures: Omit<BlockItem, "nameForSidebar">[] = [
+  { id: "preview_media_main", type: "media" },
+  { id: "article_1_local", type: "article" }, // Эти статьи будут редактироваться локально
+  { id: "article_2_local", type: "article" },
 ];
 
-const mockSelectedBlockData = {
-  id: "article_1",
-  heading: "История основания Санкт-Петербурга",
-  body: "## Начало\nГород был основан 27 мая 1703 года Петром I...",
-  media: [],
-};
-
-const mockExistingArticles = [
-  { id: "existing_1", title: "История Эрмитажа", type: "article" },
-  { id: "existing_2", title: "Петропавловская крепость", type: "article" },
-  { id: "existing_3", title: "Исаакиевский собор", type: "article" },
-  { id: "existing_4", title: "Кунсткамера", type: "article" },
-];
-
-// --- ArticleListSidebar Component ---
-interface ArticleBlock {
-  id: string;
-  name: string;
-  type: string;
-  linkedArticleId?: string; // Added for linked articles
+interface RightWidgetTabProps {
+  value: number;
+  index: number;
 }
 
-interface ArticleListSidebarProps {
-  blocks: ArticleBlock[];
-  selectedBlockId: string | null;
-  onSelectBlock: (blockId: string) => void;
-  onCreateNew: () => void;
-  onSelectExisting: () => void;
-}
-
-const ArticleListSidebar = ({
-  blocks,
-  selectedBlockId,
-  onSelectBlock,
-  onCreateNew,
-  onSelectExisting,
-}: ArticleListSidebarProps) => {
-  const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
-
-  const handleMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
-    setMenuAnchorEl(event.currentTarget);
-  };
-
-  const handleMenuClose = () => {
-    setMenuAnchorEl(null);
-  };
-
-  return (
-    <Paper
-      elevation={2}
-      sx={{
-        width: 260,
-        minWidth: 240,
-        display: "flex",
-        flexDirection: "column",
-        justifyContent: "space-between",
-        padding: 1.5,
-        borderRadius: 2,
-        border: "1px solid",
-        borderColor: "divider",
-      }}
-    >
-      <List
-        dense
-        sx={{
-          overflowY: "auto",
-          flexGrow: 1,
-          maxHeight: "calc(100% - 60px)",
-        }}
-      >
-        {blocks.map((block) => (
-          <ListItemButton
-            key={block.id}
-            selected={selectedBlockId === block.id}
-            onClick={() => onSelectBlock(block.id)}
-            sx={{
-              borderRadius: 1,
-              mb: 0.5,
-              backgroundColor:
-                selectedBlockId === block.id ? "primary.light" : "transparent",
-              "&.Mui-selected": {
-                backgroundColor: "primary.main",
-                color: "primary.contrastText",
-                "&:hover": {
-                  backgroundColor: "primary.dark",
-                },
-              },
-              "&:hover": {
-                backgroundColor:
-                  selectedBlockId !== block.id ? "action.hover" : undefined,
-              },
-            }}
-          >
-            <ListItemText
-              primary={block.name}
-              primaryTypographyProps={{
-                fontWeight: selectedBlockId === block.id ? "bold" : "normal",
-                overflow: "hidden",
-                textOverflow: "ellipsis",
-                whiteSpace: "nowrap",
-              }}
-            />
-          </ListItemButton>
-        ))}
-      </List>
-
-      <button
-        className="w-8 h-8 rounded-full bg-blue-500 text-white flex items-center justify-center hover:bg-blue-600 transition-colors"
-        onClick={handleMenuOpen}
-      >
-        <Plus color="white" />
-      </button>
-      <Menu
-        anchorEl={menuAnchorEl}
-        open={Boolean(menuAnchorEl)}
-        onClose={handleMenuClose}
-        anchorOrigin={{
-          vertical: "top",
-          horizontal: "right",
-        }}
-        transformOrigin={{
-          vertical: "bottom",
-          horizontal: "right",
-        }}
-      >
-        <MenuItem onClick={onCreateNew}>Создать новую</MenuItem>
-        <MenuItem onClick={onSelectExisting}>Выбрать существующую</MenuItem>
-      </Menu>
-    </Paper>
-  );
-};
-
-// --- ArticleEditorPane Component ---
-interface ArticleData {
-  id: string;
-  heading: string;
-  body: string;
-  media: any[]; // Define a proper type for media if available
-}
-
-interface ArticleEditorPaneProps {
-  articleData: ArticleData | null;
-  onDelete: (blockId: string) => void;
-}
-
-const ArticleEditorPane = ({
-  articleData,
-  onDelete,
-}: ArticleEditorPaneProps) => {
-  if (!articleData) {
-    return (
-      <Paper
-        elevation={2}
-        sx={{
-          flexGrow: 1,
-          padding: 2.5,
-          borderRadius: 2,
-          border: "1px solid",
-          borderColor: "divider",
-          display: "flex",
-          alignItems: "center",
-          justifyContent: "center",
-        }}
-      >
-        <Typography variant="h6" color="text.secondary">
-          Выберите блок для редактирования
-        </Typography>
-      </Paper>
-    );
-  }
-
-  return (
-    <Paper
-      elevation={2}
-      sx={{
-        flexGrow: 1,
-        padding: 2.5,
-        borderRadius: 2,
-        border: "1px solid",
-        borderColor: "divider",
-        overflowY: "auto",
-      }}
-    >
-      <SightEdit />
-      <Paper elevation={1} sx={{ padding: 2, mt: 1, width: "75%" }}>
-        <Typography variant="h6" gutterBottom>
-          МЕДИА
-        </Typography>
-        <Box
-          sx={{
-            width: "100%",
-            height: 100,
-            backgroundColor: "grey.100",
-            display: "flex",
-            alignItems: "center",
-            justifyContent: "center",
-            borderRadius: 1,
-            mb: 1,
-            border: "2px dashed",
-            borderColor: "grey.300",
-          }}
-        >
-          <Typography color="text.secondary">Нет медиа</Typography>
-        </Box>
-        <Button variant="contained">Выбрать/Загрузить медиа</Button>
-      </Paper>
-    </Paper>
-  );
-};
-
-// --- RightWidgetTab (Parent) Component ---
 export const RightWidgetTab = observer(
-  ({ value, index, data }: { value: number; index: number; data?: Sight }) => {
-    const [rightWidgetBlocks, setRightWidgetBlocks] = useState<ArticleBlock[]>(
-      mockRightWidgetBlocks
-    );
-    const [selectedBlockId, setSelectedBlockId] = useState<string | null>(
-      mockRightWidgetBlocks[1]?.id || null
-    );
-    const [isSelectModalOpen, setIsSelectModalOpen] = useState(false);
+  ({ value, index }: RightWidgetTabProps) => {
+    const { language } = languageStore; // Текущий язык
+    const { sightInfo } = editSightStore; // Данные достопримечательности
 
+    // 1. Структура блоков: порядок, тип, связи (не сам контент)
+    // Имена nameForSidebar будут динамически браться из sightInfo или articlesStore
+    const [blockItemsStructure, setBlockItemsStructure] = useState<
+      Omit<BlockItem, "nameForSidebar">[]
+    >(initialBlockStructures);
+
+    // 2. ID выбранного блока для редактирования
+    const [selectedBlockId, setSelectedBlockId] = useState<string | null>(
+      () => {
+        // По умолчанию выбираем первый блок, если он есть
+        return initialBlockStructures.length > 0
+          ? initialBlockStructures[0].id
+          : null;
+      }
+    );
+
+    // 3. Состояние для модального окна выбора существующей статьи (добавим позже)
+    // const [isSelectModalOpen, setIsSelectModalOpen] = useState(false);
+
+    // --- Производные данные (Derived State) ---
+
+    // Блоки для отображения в сайдбаре (с локализованными именами)
+    const blocksForSidebar: BlockItem[] = useMemo(() => {
+      return blockItemsStructure.map((struct) => {
+        let name = `Блок ${struct.id}`; // Имя по умолчанию
+
+        if (struct.type === "media" && struct.id === "preview_media_main") {
+          name = "Превью-медиа"; // Фиксированное имя для этого блока
+        } else if (struct.type === "article") {
+          if (struct.linkedArticleStoreId) {
+            // TODO: Найти имя в articlesStore по struct.linkedArticleStoreId
+            name = `Связанная: ${struct.linkedArticleStoreId}`;
+          } else {
+            // Это локальная статья, берем заголовок из editSightStore
+            const articleContent = sightInfo[language]?.right?.find(
+              (a) => a.id === struct.id
+            );
+            name =
+              articleContent?.heading ||
+              `Статья ${struct.id.slice(-4)} (${language.toUpperCase()})`;
+          }
+        }
+        return { ...struct, nameForSidebar: name };
+      });
+    }, [blockItemsStructure, language, sightInfo]);
+
+    // Данные выбранного блока (структура + контент)
+    const selectedBlockData = useMemo(() => {
+      if (!selectedBlockId) return null;
+      const structure = blockItemsStructure.find(
+        (b) => b.id === selectedBlockId
+      );
+      if (!structure) return null;
+
+      if (structure.type === "article" && !structure.linkedArticleStoreId) {
+        const content = sightInfo[language]?.right?.find(
+          (a) => a.id === selectedBlockId
+        );
+        return {
+          structure,
+          content: content || { id: selectedBlockId, heading: "", body: "" }, // Заглушка, если нет контента
+        };
+      }
+      // Для media или связанных статей пока просто структура
+      return { structure, content: null };
+    }, [selectedBlockId, blockItemsStructure, language, sightInfo]);
+
+    // --- Обработчики событий ---
     const handleSelectBlock = (blockId: string) => {
       setSelectedBlockId(blockId);
-      console.log("Selected block:", blockId);
     };
 
-    const handleCreateNew = () => {
-      const newBlockId = `article_${Date.now()}`;
-      setRightWidgetBlocks((prevBlocks) => [
-        ...prevBlocks,
-        {
-          id: newBlockId,
-          name: `${
-            prevBlocks.filter((b) => b.type === "article").length + 1
-          }. Новый блок`,
-          type: "article",
-        },
-      ]);
+    const handleCreateNewArticle = () => {
+      const newBlockId = `article_local_${Date.now()}`;
+      const newBlockStructure: Omit<BlockItem, "nameForSidebar"> = {
+        id: newBlockId,
+        type: "article",
+      };
+      setBlockItemsStructure((prev) => [...prev, newBlockStructure]);
+
+      // Добавляем пустой контент для этой статьи во все языки в editSightStore
+      const baseName = `Новая статья ${
+        blockItemsStructure.filter((b) => b.type === "article").length + 1
+      }`;
+      ["ru", "en", "zh"].forEach((lang) => {
+        const currentLang = lang as Language;
+        if (
+          editSightStore.sightInfo[currentLang] &&
+          !editSightStore.sightInfo[currentLang].right?.find(
+            (r) => r.id === newBlockId
+          )
+        ) {
+          editSightStore.sightInfo[currentLang].right.push({
+            id: newBlockId,
+            heading: `${baseName} (${currentLang.toUpperCase()})`,
+            body: `Содержимое для ${baseName} (${currentLang.toUpperCase()})...`,
+          });
+        }
+      });
       setSelectedBlockId(newBlockId);
     };
 
-    const handleSelectExisting = () => {
-      setIsSelectModalOpen(true);
-    };
-
-    const handleCloseSelectModal = () => {
-      setIsSelectModalOpen(false);
-    };
-
-    const handleSelectArticle = (articleId: string) => {
-      const article = articlesStore.articles.find((a) => a.id === articleId);
-      if (article) {
-        const newBlockId = `article_linked_${article.id}_${Date.now()}`;
-        setRightWidgetBlocks((prevBlocks) => [
-          ...prevBlocks,
-          {
-            id: newBlockId,
-            name: `${
-              prevBlocks.filter((b) => b.type === "article").length + 1
-            }. ${article.service_name}`,
-            type: "article",
-            linkedArticleId: article.id,
-          },
-        ]);
-        setSelectedBlockId(newBlockId);
+    const handleHeadingChange = (newHeading: string) => {
+      if (
+        selectedBlockData &&
+        selectedBlockData.structure.type === "article" &&
+        !selectedBlockData.structure.linkedArticleStoreId
+      ) {
+        const blockId = selectedBlockData.structure.id;
+        const langData = editSightStore.sightInfo[language];
+        const article = langData?.right?.find((a) => a.id === blockId);
+        if (article) {
+          article.heading = newHeading;
+        } else if (langData) {
+          // Если статьи еще нет, добавляем
+          langData.right.push({ id: blockId, heading: newHeading, body: "" });
+        }
+        // Обновить имя в сайдбаре (т.к. blocksForSidebar пересчитается)
+        // Для этого достаточно, чтобы sightInfo был observable и blocksForSidebar от него зависел
       }
-      handleCloseSelectModal();
     };
 
-    const handleUnlinkBlock = (blockId: string) => {
-      console.log("Unlink block:", blockId);
-      // Example: If a block is linked to an existing article, this might "unlink" it
-      // For now, it simply removes it, you might want to convert it to a new editable block.
-      setRightWidgetBlocks((blocks) => blocks.filter((b) => b.id !== blockId));
-      setSelectedBlockId(null);
+    const handleBodyChange = (newBody: string) => {
+      if (
+        selectedBlockData &&
+        selectedBlockData.structure.type === "article" &&
+        !selectedBlockData.structure.linkedArticleStoreId
+      ) {
+        const blockId = selectedBlockData.structure.id;
+        const langData = editSightStore.sightInfo[language];
+        const article = langData?.right?.find((a) => a.id === blockId);
+        if (article) {
+          article.body = newBody;
+        } else if (langData) {
+          // Если статьи еще нет, добавляем
+          langData.right.push({ id: blockId, heading: "", body: newBody });
+        }
+      }
     };
 
-    const handleDeleteBlock = (blockId: string) => {
-      console.log("Delete block:", blockId);
-      setRightWidgetBlocks((blocks) => blocks.filter((b) => b.id !== blockId));
-      setSelectedBlockId(null);
+    const handleDeleteBlock = (blockIdToDelete: string) => {
+      setBlockItemsStructure((prev) =>
+        prev.filter((b) => b.id !== blockIdToDelete)
+      );
+      // Удаляем контент из editSightStore для всех языков
+      ["ru", "en", "zh"].forEach((lang) => {
+        const currentLang = lang as Language;
+        if (editSightStore.sightInfo[currentLang]) {
+          editSightStore.sightInfo[currentLang].right =
+            editSightStore.sightInfo[currentLang].right?.filter(
+              (r) => r.id !== blockIdToDelete
+            );
+        }
+      });
+
+      if (selectedBlockId === blockIdToDelete) {
+        setSelectedBlockId(
+          blockItemsStructure.length > 1
+            ? blockItemsStructure.filter((b) => b.id !== blockIdToDelete)[0]?.id
+            : null
+        );
+      }
     };
 
     const handleSave = () => {
-      console.log("Saving right widget...");
-      // Implement save logic here, e.g., send data to an API
+      console.log(
+        "Сохранение Right Widget:",
+        JSON.stringify(editSightStore.sightInfo, null, 2)
+      );
+      // Здесь будет логика отправки editSightStore.sightInfo на сервер
+      alert("Данные для сохранения (см. консоль)");
     };
 
-    // Determine the current block data to pass to the editor pane
-    const currentBlockToEdit = selectedBlockId
-      ? selectedBlockId === mockSelectedBlockData.id
-        ? mockSelectedBlockData
-        : {
-            id: selectedBlockId,
-            heading:
-              rightWidgetBlocks.find((b) => b.id === selectedBlockId)?.name ||
-              "Заголовок...",
-            body: "Содержимое...",
-            media: [],
-          }
-      : null;
-
-    // Get list of already linked article IDs
-    const linkedArticleIds = rightWidgetBlocks
-      .filter((block) => block.linkedArticleId)
-      .map((block) => block.linkedArticleId as string);
+    // --- Инициализация контента в сторе для initialBlockStructures (если его там нет) ---
+    useEffect(() => {
+      initialBlockStructures.forEach((struct) => {
+        if (struct.type === "article" && !struct.linkedArticleStoreId) {
+          const baseName = `Статья ${struct.id.split("_")[1]}`; // Пример "История" или "Факты"
+          ["ru", "en", "zh"].forEach((lang) => {
+            const currentLang = lang as Language;
+            if (
+              editSightStore.sightInfo[currentLang] &&
+              !editSightStore.sightInfo[currentLang].right?.find(
+                (r) => r.id === struct.id
+              )
+            ) {
+              editSightStore.sightInfo[currentLang].right?.push({
+                id: struct.id,
+                heading: `${baseName} (${currentLang.toUpperCase()})`, // Например: "История (RU)"
+                body: `Начальное содержимое для ${baseName} на ${currentLang.toUpperCase()}.`,
+              });
+            }
+          });
+        }
+      });
+    }, []); // Запускается один раз при монтировании
 
     return (
       <TabPanel value={value} index={index}>
+        <LanguageSwitcher />
         <Box
           sx={{
             display: "flex",
             flexDirection: "column",
             height: "100%",
-            minHeight: "calc(100vh - 200px)", // Adjust as needed
+            minHeight: "calc(100vh - 200px)",
             gap: 2,
-            paddingBottom: "70px", // Space for the save button
+            paddingBottom: "70px",
             position: "relative",
           }}
         >
           <BackButton />
 
-          <Box sx={{ display: "flex", flexGrow: 1, gap: 2.5 }}>
-            <ArticleListSidebar
-              blocks={rightWidgetBlocks}
-              selectedBlockId={selectedBlockId}
-              onSelectBlock={handleSelectBlock}
-              onCreateNew={handleCreateNew}
-              onSelectExisting={handleSelectExisting}
-            />
+          <Box sx={{ display: "flex", flexGrow: 1, gap: 2.5, minHeight: 0 }}>
+            {/* Компонент сайдбара списка блоков */}
+            <Paper
+              elevation={1}
+              sx={{
+                width: 280,
+                padding: 1.5,
+                display: "flex",
+                flexDirection: "column",
+              }}
+            >
+              <Typography variant="h6" gutterBottom>
+                Блоки
+              </Typography>
+              <Box sx={{ flexGrow: 1, overflowY: "auto" }}>
+                {blocksForSidebar.map((block) => (
+                  <Button
+                    key={block.id}
+                    fullWidth
+                    variant={
+                      selectedBlockId === block.id ? "contained" : "outlined"
+                    }
+                    onClick={() => handleSelectBlock(block.id)}
+                    sx={{
+                      justifyContent: "flex-start",
+                      mb: 0.5,
+                      textTransform: "none",
+                    }}
+                  >
+                    {block.nameForSidebar}
+                  </Button>
+                ))}
+              </Box>
+              <Button
+                variant="contained"
+                onClick={handleCreateNewArticle}
+                sx={{ mt: 1 }}
+              >
+                + Новая статья
+              </Button>
+              {/* TODO: Кнопка "Выбрать существующую" */}
+            </Paper>
 
-            <ArticleEditorPane
-              articleData={currentBlockToEdit}
-              onDelete={handleDeleteBlock}
-            />
+            {/* Компонент редактора выбранного блока */}
+            <Paper
+              elevation={1}
+              sx={{ flexGrow: 1, padding: 2.5, overflowY: "auto" }}
+            >
+              <Typography variant="h6" gutterBottom>
+                Редактор блока ({language.toUpperCase()})
+              </Typography>
+              {selectedBlockData ? (
+                <Box>
+                  <Typography variant="subtitle1">
+                    ID: {selectedBlockData.structure.id}
+                  </Typography>
+                  <Typography variant="subtitle1">
+                    Тип: {selectedBlockData.structure.type}
+                  </Typography>
+                  {selectedBlockData.structure.type === "media" && (
+                    <Box
+                      my={2}
+                      p={2}
+                      border="1px dashed grey"
+                      height={150}
+                      display="flex"
+                      alignItems="center"
+                      justifyContent="center"
+                    >
+                      <Typography color="textSecondary">
+                        Загрузчик медиа для "{selectedBlockData.structure.id}"
+                      </Typography>
+                    </Box>
+                  )}
+                  {selectedBlockData.structure.type === "article" &&
+                    !selectedBlockData.structure.linkedArticleStoreId &&
+                    selectedBlockData.content && (
+                      <Box mt={2}>
+                        <TextField
+                          fullWidth
+                          label="Заголовок статьи"
+                          value={selectedBlockData.content.heading}
+                          onChange={(e) => handleHeadingChange(e.target.value)}
+                          sx={{ mb: 2 }}
+                        />
+                        <TextField
+                          fullWidth
+                          multiline
+                          rows={8}
+                          label="Текст статьи"
+                          value={selectedBlockData.content.body}
+                          onChange={(e) => handleBodyChange(e.target.value)}
+                          sx={{ mb: 2 }}
+                          // Здесь позже можно будет вставить SightEdit
+                        />
+                        {/* TODO: Секция медиа для статьи */}
+                        <Button
+                          color="error"
+                          variant="outlined"
+                          onClick={() =>
+                            handleDeleteBlock(selectedBlockData.structure.id)
+                          }
+                        >
+                          Удалить эту статью
+                        </Button>
+                      </Box>
+                    )}
+                  {selectedBlockData.structure.type === "article" &&
+                    selectedBlockData.structure.linkedArticleStoreId && (
+                      <Box mt={2}>
+                        <Typography>
+                          Это связанная статья:{" "}
+                          {selectedBlockData.structure.linkedArticleStoreId}
+                        </Typography>
+                        {/* TODO: Кнопки "Открепить", "Удалить из списка" */}
+                      </Box>
+                    )}
+                </Box>
+              ) : (
+                <Typography color="textSecondary">
+                  Выберите блок для редактирования
+                </Typography>
+              )}
+            </Paper>
           </Box>
 
           <Box
             sx={{
               position: "absolute",
               bottom: 0,
+              left: 0,
               right: 0,
               padding: 2,
-              backgroundColor: "background.paper", // Ensure button is visible
-              width: "100%", // Cover the full width to make it a sticky footer
+              backgroundColor: "background.paper",
+              borderTop: "1px solid",
+              borderColor: "divider",
+              zIndex: 10,
               display: "flex",
               justifyContent: "flex-end",
             }}
           >
-            <Button variant="contained" color="success" onClick={handleSave}>
+            <Button
+              variant="contained"
+              color="success"
+              onClick={handleSave}
+              size="large"
+            >
               Сохранить изменения
             </Button>
           </Box>
         </Box>
-
-        <SelectArticleModal
-          open={isSelectModalOpen}
-          onClose={handleCloseSelectModal}
-          onSelectArticle={handleSelectArticle}
-          linkedArticleIds={linkedArticleIds}
-        />
+        {/* <SelectArticleModal open={isSelectModalOpen} ... /> */}
       </TabPanel>
     );
   }
diff --git a/src/widgets/index.ts b/src/widgets/index.ts
index c21b92f..5db03b3 100644
--- a/src/widgets/index.ts
+++ b/src/widgets/index.ts
@@ -7,3 +7,4 @@ export * from "./SightEdit";
 export * from "./LanguageSwitcher";
 export * from "./DevicesTable";
 export * from "./SightsTable";
+export * from "./MediaViewer";
diff --git a/src/widgets/modals/SelectArticleDialog/index.tsx b/src/widgets/modals/SelectArticleDialog/index.tsx
new file mode 100644
index 0000000..e59b718
--- /dev/null
+++ b/src/widgets/modals/SelectArticleDialog/index.tsx
@@ -0,0 +1,188 @@
+import { articlesStore } from "@shared";
+import { observer } from "mobx-react-lite";
+import { useEffect, useRef, useState } from "react";
+import {
+  Dialog,
+  DialogTitle,
+  DialogContent,
+  DialogActions,
+  Button,
+  TextField,
+  List,
+  ListItemButton,
+  ListItemText,
+  Paper,
+  Box,
+  Typography,
+  InputAdornment,
+} from "@mui/material";
+import { ImagePlus, Search } from "lucide-react";
+import { ReactMarkdownComponent } from "@widgets";
+
+interface SelectArticleModalProps {
+  open: boolean;
+  onClose: () => void;
+  onSelectArticle: (articleId: string) => void;
+  linkedArticleIds?: string[]; // Add optional prop for linked articles
+}
+
+export const SelectArticleModal = observer(
+  ({
+    open,
+    onClose,
+    onSelectArticle,
+
+    linkedArticleIds = [], // Default to empty array if not provided
+  }: SelectArticleModalProps) => {
+    const { articles, getArticle } = articlesStore; // articles here refers to fetched articles from store
+    const [searchQuery, setSearchQuery] = useState("");
+    const [hoveredArticleId, setHoveredArticleId] = useState<string | null>(
+      null
+    );
+    const hoverTimerRef = useRef<NodeJS.Timeout | null>(null);
+
+    useEffect(() => {
+      if (hoveredArticleId) {
+        hoverTimerRef.current = setTimeout(() => {
+          getArticle(hoveredArticleId);
+        }, 200);
+      }
+
+      return () => {
+        if (hoverTimerRef.current) {
+          clearTimeout(hoverTimerRef.current);
+        }
+      };
+    }, [hoveredArticleId, getArticle]);
+
+    const handleArticleHover = (articleId: string) => {
+      setHoveredArticleId(articleId);
+    };
+
+    const handleArticleLeave = () => {
+      setHoveredArticleId(null);
+      if (hoverTimerRef.current) {
+        clearTimeout(hoverTimerRef.current);
+      }
+    };
+
+    const filteredArticles = articles
+      .filter((article) => !linkedArticleIds.includes(article.id))
+      .filter((article) =>
+        article.service_name.toLowerCase().includes(searchQuery.toLowerCase())
+      );
+
+    return (
+      <Dialog open={open} onClose={onClose} maxWidth="lg" fullWidth>
+        <DialogTitle>Выберите существующую статью</DialogTitle>
+        <DialogContent
+          className="flex gap-4"
+          dividers // Adds a divider below the title and above the actions
+          sx={{ height: "600px", display: "flex", flexDirection: "row" }} // Fixed height for DialogContent
+        >
+          <Paper className="w-[66%] flex flex-col">
+            <TextField
+              fullWidth
+              placeholder="Поиск статей..."
+              value={searchQuery}
+              onChange={(e) => setSearchQuery(e.target.value)}
+              sx={{ mb: 2, mt: 1 }}
+              InputProps={{
+                startAdornment: (
+                  <InputAdornment position="start">
+                    <Search size={20} />
+                  </InputAdornment>
+                ),
+              }}
+            />
+            <List sx={{ flexGrow: 1, overflowY: "auto" }}>
+              {filteredArticles.map((article) => (
+                <ListItemButton
+                  key={article.id}
+                  onClick={() => onSelectArticle(article.id)}
+                  onMouseEnter={() => handleArticleHover(article.id)}
+                  onMouseLeave={handleArticleLeave}
+                  sx={{
+                    borderRadius: 1,
+                    mb: 0.5,
+                    "&:hover": {
+                      backgroundColor: "action.hover",
+                    },
+                  }}
+                >
+                  <ListItemText primary={article.service_name} />
+                </ListItemButton>
+              ))}
+            </List>
+          </Paper>
+          <Paper className="flex-1 flex flex-col">
+            <Box
+              className="rounded-2xl overflow-hidden"
+              sx={{
+                width: "100%",
+                height: "100%",
+                background: "#877361",
+                borderColor: "grey.300",
+                display: "flex",
+                flexDirection: "column",
+              }}
+            >
+              {/* Media Preview Area */}
+              <Box
+                sx={{
+                  width: "100%",
+                  height: 200,
+                  flexShrink: 0,
+                  backgroundColor: "grey.300",
+                  display: "flex",
+                  alignItems: "center",
+                  justifyContent: "center",
+                }}
+              >
+                <ImagePlus size={48} color="grey" />
+              </Box>
+
+              {/* Title Area */}
+              <Box
+                sx={{
+                  width: "100%",
+                  height: "70px",
+                  background: "#877361",
+                  display: "flex",
+                  flexShrink: 0,
+                  alignItems: "center",
+                  borderBottom: "1px solid",
+                  px: 2,
+                }}
+              >
+                <Typography variant="h6" color="white">
+                  {articlesStore.articleData?.heading ||
+                    "Нет данных для предпросмотра"}
+                </Typography>
+              </Box>
+
+              {/* Body Preview Area */}
+              <Box
+                sx={{
+                  px: 2,
+                  flexGrow: 1,
+                  overflowY: "auto",
+                  backgroundColor: "#877361", // To make markdown readable
+                  color: "white",
+                  py: 1,
+                }}
+              >
+                <ReactMarkdownComponent
+                  value={articlesStore.articleData?.body || ""}
+                />
+              </Box>
+            </Box>
+          </Paper>
+        </DialogContent>
+        <DialogActions>
+          <Button onClick={onClose}>Отмена</Button>
+        </DialogActions>
+      </Dialog>
+    );
+  }
+);
diff --git a/src/widgets/modals/index.ts b/src/widgets/modals/index.ts
new file mode 100644
index 0000000..e714367
--- /dev/null
+++ b/src/widgets/modals/index.ts
@@ -0,0 +1 @@
+export * from "./SelectArticleDialog";
diff --git a/yarn.lock b/yarn.lock
index f7ee7ec..84388b2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -138,6 +138,11 @@
   resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.3.tgz"
   integrity sha512-7EYtGezsdiDMyY80+65EzwiGmcJqpmcZCojSXaRgdrBaGtWTgDZKq69cPIVped6MkIM78cTQ2GOiEYjwOlG4xw==
 
+"@babel/runtime@^7.17.8", "@babel/runtime@^7.26.0":
+  version "7.27.4"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.4.tgz#a91ec580e6c00c67118127777c316dfd5a5a6abf"
+  integrity sha512-t3yaEOuGu9NlIZ+hIeGbBjFtZT7j2cb2tg0fuaJKeGotchRjjLfrBA9Kwf8quhpP1EUuxModQg04q/mBwyg8uA==
+
 "@babel/template@^7.27.2":
   version "7.27.2"
   resolved "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz"
@@ -168,6 +173,11 @@
     "@babel/helper-string-parser" "^7.27.1"
     "@babel/helper-validator-identifier" "^7.27.1"
 
+"@dimforge/rapier3d-compat@^0.12.0":
+  version "0.12.0"
+  resolved "https://registry.yarnpkg.com/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz#7b3365e1dfdc5cd957b45afe920b4ac06c7cd389"
+  integrity sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==
+
 "@emnapi/core@^1.4.3":
   version "1.4.3"
   resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.4.3.tgz#9ac52d2d5aea958f67e52c40a065f51de59b77d6"
@@ -555,6 +565,18 @@
     "@jridgewell/resolve-uri" "^3.1.0"
     "@jridgewell/sourcemap-codec" "^1.4.14"
 
+"@mediapipe/tasks-vision@0.10.17":
+  version "0.10.17"
+  resolved "https://registry.yarnpkg.com/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz#2c1c73ed81902b21d37336a587b96183bb6882d5"
+  integrity sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==
+
+"@monogrid/gainmap-js@^3.0.6":
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/@monogrid/gainmap-js/-/gainmap-js-3.1.0.tgz#4ac1f88abd6affdf0b51d79318109183b499c502"
+  integrity sha512-Obb0/gEd/HReTlg8ttaYk+0m62gQJmCblMOjHSMHRrBP2zdfKMHLCRbh/6ex9fSUJMKdjjIEiohwkbGD3wj2Nw==
+  dependencies:
+    promise-worker-transferable "^1.0.4"
+
 "@mui/core-downloads-tracker@^7.1.0":
   version "7.1.0"
   resolved "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.1.0.tgz"
@@ -662,11 +684,63 @@
     "@nodelib/fs.scandir" "2.1.5"
     fastq "^1.6.0"
 
+"@photo-sphere-viewer/core@^5.13.2":
+  version "5.13.2"
+  resolved "https://registry.yarnpkg.com/@photo-sphere-viewer/core/-/core-5.13.2.tgz#518f27a2b7ca5a80068d8922183a9999a1b33ad1"
+  integrity sha512-rL4Ey39Prx4Iyxt1f2tAqlXvqu4/ovXfUvIpLt540OpZJiFjWccs6qLywof9vuhBJ7PXHudHWCjRPce0W8kx8w==
+  dependencies:
+    three "^0.175.0"
+
 "@popperjs/core@^2.11.8":
   version "2.11.8"
   resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz"
   integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
 
+"@react-three/drei@^10.1.2":
+  version "10.1.2"
+  resolved "https://registry.yarnpkg.com/@react-three/drei/-/drei-10.1.2.tgz#3c41a0b19460aee7604067309cebe737147cf85a"
+  integrity sha512-CCcLAqZEvYiUErOcJgGzovY3RH6KgdrqD4ubeAx1nyGbSPLnKR9a8ynYbPdtZhIyiWqGc07z+RYQzpaOfN4ZIA==
+  dependencies:
+    "@babel/runtime" "^7.26.0"
+    "@mediapipe/tasks-vision" "0.10.17"
+    "@monogrid/gainmap-js" "^3.0.6"
+    "@use-gesture/react" "^10.3.1"
+    camera-controls "^2.9.0"
+    cross-env "^7.0.3"
+    detect-gpu "^5.0.56"
+    glsl-noise "^0.0.0"
+    hls.js "^1.5.17"
+    maath "^0.10.8"
+    meshline "^3.3.1"
+    stats-gl "^2.2.8"
+    stats.js "^0.17.0"
+    suspend-react "^0.1.3"
+    three-mesh-bvh "^0.8.3"
+    three-stdlib "^2.35.6"
+    troika-three-text "^0.52.4"
+    tunnel-rat "^0.1.2"
+    use-sync-external-store "^1.4.0"
+    utility-types "^3.11.0"
+    zustand "^5.0.1"
+
+"@react-three/fiber@^9.1.2":
+  version "9.1.2"
+  resolved "https://registry.yarnpkg.com/@react-three/fiber/-/fiber-9.1.2.tgz#c988f3aa916f64771483784ca3bb6ba4b116395e"
+  integrity sha512-k8FR9yVHV9kIF3iuOD0ds5hVymXYXfgdKklqziBVod9ZEJ8uk05Zjw29J/omU3IKeUfLNAIHfxneN3TUYM4I2w==
+  dependencies:
+    "@babel/runtime" "^7.17.8"
+    "@types/react-reconciler" "^0.28.9"
+    "@types/webxr" "*"
+    base64-js "^1.5.1"
+    buffer "^6.0.3"
+    its-fine "^2.0.0"
+    react-reconciler "^0.31.0"
+    react-use-measure "^2.1.7"
+    scheduler "^0.25.0"
+    suspend-react "^0.1.3"
+    use-sync-external-store "^1.4.0"
+    zustand "^5.0.3"
+
 "@rolldown/pluginutils@1.0.0-beta.9":
   version "1.0.0-beta.9"
   resolved "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz"
@@ -892,6 +966,11 @@
     "@tailwindcss/oxide" "4.1.8"
     tailwindcss "4.1.8"
 
+"@tweenjs/tween.js@~23.1.3":
+  version "23.1.3"
+  resolved "https://registry.yarnpkg.com/@tweenjs/tween.js/-/tween.js-23.1.3.tgz#eff0245735c04a928bb19c026b58c2a56460539d"
+  integrity sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==
+
 "@tybys/wasm-util@^0.9.0":
   version "0.9.0"
   resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.9.0.tgz#3e75eb00604c8d6db470bf18c37b7d984a0e3355"
@@ -946,6 +1025,11 @@
   dependencies:
     "@types/ms" "*"
 
+"@types/draco3d@^1.4.0":
+  version "1.4.10"
+  resolved "https://registry.yarnpkg.com/@types/draco3d/-/draco3d-1.4.10.tgz#63ec0ba78b30bd58203ec031f4e4f0198c596dca"
+  integrity sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==
+
 "@types/estree-jsx@^1.0.0":
   version "1.0.5"
   resolved "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz"
@@ -994,6 +1078,11 @@
   dependencies:
     undici-types "~6.21.0"
 
+"@types/offscreencanvas@^2019.6.4":
+  version "2019.7.3"
+  resolved "https://registry.yarnpkg.com/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz#90267db13f64d6e9ccb5ae3eac92786a7c77a516"
+  integrity sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==
+
 "@types/parse-json@^4.0.0":
   version "4.0.2"
   resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz"
@@ -1009,6 +1098,11 @@
   resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.5.tgz"
   integrity sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==
 
+"@types/react-reconciler@^0.28.9":
+  version "0.28.9"
+  resolved "https://registry.yarnpkg.com/@types/react-reconciler/-/react-reconciler-0.28.9.tgz#d24b4864c384e770c83275b3fe73fba00269c83b"
+  integrity sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==
+
 "@types/react-transition-group@^4.4.12":
   version "4.4.12"
   resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz"
@@ -1021,6 +1115,11 @@
   dependencies:
     csstype "^3.0.2"
 
+"@types/stats.js@*":
+  version "0.17.4"
+  resolved "https://registry.yarnpkg.com/@types/stats.js/-/stats.js-0.17.4.tgz#1933e5ff153a23c7664487833198d685c22e791e"
+  integrity sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==
+
 "@types/tern@*":
   version "0.23.9"
   resolved "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz"
@@ -1028,6 +1127,19 @@
   dependencies:
     "@types/estree" "*"
 
+"@types/three@*":
+  version "0.176.0"
+  resolved "https://registry.yarnpkg.com/@types/three/-/three-0.176.0.tgz#b6eced2b05e839395a6171e066c4631bc5b0a1e0"
+  integrity sha512-FwfPXxCqOtP7EdYMagCFePNKoG1AGBDUEVKtluv2BTVRpSt7b+X27xNsirPCTCqY1pGYsPUzaM3jgWP7dXSxlw==
+  dependencies:
+    "@dimforge/rapier3d-compat" "^0.12.0"
+    "@tweenjs/tween.js" "~23.1.3"
+    "@types/stats.js" "*"
+    "@types/webxr" "*"
+    "@webgpu/types" "*"
+    fflate "~0.8.2"
+    meshoptimizer "~0.18.1"
+
 "@types/unist@*", "@types/unist@^3.0.0":
   version "3.0.3"
   resolved "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz"
@@ -1038,6 +1150,11 @@
   resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz"
   integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==
 
+"@types/webxr@*", "@types/webxr@^0.5.2":
+  version "0.5.22"
+  resolved "https://registry.yarnpkg.com/@types/webxr/-/webxr-0.5.22.tgz#d8a14c12bbfaaa4a13de21ec2d4a8197b3e1b532"
+  integrity sha512-Vr6Stjv5jPRqH690f5I5GLjVk8GSsoQSYJ2FVd/3jJF7KaqfwPi3ehfBS96mlQ2kPCwZaX6U0rG2+NGHBKkA/A==
+
 "@typescript-eslint/eslint-plugin@8.33.0":
   version "8.33.0"
   resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz"
@@ -1140,6 +1257,18 @@
   resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz"
   integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==
 
+"@use-gesture/core@10.3.1":
+  version "10.3.1"
+  resolved "https://registry.yarnpkg.com/@use-gesture/core/-/core-10.3.1.tgz#976c9421e905f0079d49822cfd5c2e56b808fc56"
+  integrity sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==
+
+"@use-gesture/react@^10.3.1":
+  version "10.3.1"
+  resolved "https://registry.yarnpkg.com/@use-gesture/react/-/react-10.3.1.tgz#17a743a894d9bd9a0d1980c618f37f0164469867"
+  integrity sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==
+  dependencies:
+    "@use-gesture/core" "10.3.1"
+
 "@vitejs/plugin-react@^4.4.1":
   version "4.5.0"
   resolved "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.0.tgz"
@@ -1152,6 +1281,11 @@
     "@types/babel__core" "^7.20.5"
     react-refresh "^0.17.0"
 
+"@webgpu/types@*":
+  version "0.1.61"
+  resolved "https://registry.yarnpkg.com/@webgpu/types/-/types-0.1.61.tgz#60ac1756bbeeae778b5357a94d4e6e160592d6f1"
+  integrity sha512-w2HbBvH+qO19SB5pJOJFKs533CdZqxl3fcGonqL321VHkW7W/iBo6H8bjDy6pr/+pbMwIu5dnuaAxH7NxBqUrQ==
+
 acorn-jsx@^5.3.2:
   version "5.3.2"
   resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
@@ -1217,6 +1351,18 @@ balanced-match@^1.0.0:
   resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
   integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
 
+base64-js@^1.3.1, base64-js@^1.5.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+  integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+
+bidi-js@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/bidi-js/-/bidi-js-1.0.3.tgz#6f8bcf3c877c4d9220ddf49b9bb6930c88f877d2"
+  integrity sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==
+  dependencies:
+    require-from-string "^2.0.2"
+
 brace-expansion@^1.1.7:
   version "1.1.11"
   resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz"
@@ -1249,6 +1395,14 @@ browserslist@^4.24.0:
     node-releases "^2.0.19"
     update-browserslist-db "^1.1.3"
 
+buffer@^6.0.3:
+  version "6.0.3"
+  resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
+  integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
+  dependencies:
+    base64-js "^1.3.1"
+    ieee754 "^1.2.1"
+
 call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
@@ -1262,6 +1416,11 @@ callsites@^3.0.0:
   resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz"
   integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
 
+camera-controls@^2.9.0:
+  version "2.10.1"
+  resolved "https://registry.yarnpkg.com/camera-controls/-/camera-controls-2.10.1.tgz#78bc58001a2d5925c29312154ce77d16967dec56"
+  integrity sha512-KnaKdcvkBJ1Irbrzl8XD6WtZltkRjp869Jx8c0ujs9K+9WD+1D7ryBsCiVqJYUqt6i/HR5FxT7RLASieUD+Q5w==
+
 caniuse-lite@^1.0.30001716:
   version "1.0.30001718"
   resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz"
@@ -1377,7 +1536,14 @@ cosmiconfig@^7.0.0:
     path-type "^4.0.0"
     yaml "^1.10.0"
 
-cross-spawn@^7.0.6:
+cross-env@^7.0.3:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"
+  integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==
+  dependencies:
+    cross-spawn "^7.0.1"
+
+cross-spawn@^7.0.1, cross-spawn@^7.0.6:
   version "7.0.6"
   resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz"
   integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
@@ -1425,6 +1591,13 @@ dequal@^2.0.0:
   resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz"
   integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
 
+detect-gpu@^5.0.56:
+  version "5.0.70"
+  resolved "https://registry.yarnpkg.com/detect-gpu/-/detect-gpu-5.0.70.tgz#db2202d3cd440714ba6e789ff8b62d1b584eabf7"
+  integrity sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==
+  dependencies:
+    webgl-constants "^1.1.1"
+
 detect-libc@^2.0.3, detect-libc@^2.0.4:
   version "2.0.4"
   resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz"
@@ -1445,6 +1618,11 @@ dom-helpers@^5.0.1:
     "@babel/runtime" "^7.8.7"
     csstype "^3.0.2"
 
+draco3d@^1.4.1:
+  version "1.5.7"
+  resolved "https://registry.yarnpkg.com/draco3d/-/draco3d-1.5.7.tgz#94f9bce293eb8920c159dc91a4ce9124a9e899e0"
+  integrity sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==
+
 dunder-proto@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
@@ -1665,6 +1843,11 @@ esutils@^2.0.2:
   resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz"
   integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
 
+eventemitter3@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4"
+  integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==
+
 extend@^3.0.0:
   version "3.0.2"
   resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz"
@@ -1708,6 +1891,16 @@ fdir@^6.4.4:
   resolved "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz"
   integrity sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==
 
+fflate@^0.6.9:
+  version "0.6.10"
+  resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.6.10.tgz#5f40f9659205936a2d18abf88b2e7781662b6d43"
+  integrity sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==
+
+fflate@~0.8.2:
+  version "0.8.2"
+  resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea"
+  integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==
+
 file-entry-cache@^8.0.0:
   version "8.0.0"
   resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz"
@@ -1831,6 +2024,11 @@ globals@^16.0.0:
   resolved "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz"
   integrity sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==
 
+glsl-noise@^0.0.0:
+  version "0.0.0"
+  resolved "https://registry.yarnpkg.com/glsl-noise/-/glsl-noise-0.0.0.tgz#367745f3a33382c0eeec4cb54b7e99cfc1d7670b"
+  integrity sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==
+
 gopd@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
@@ -1962,6 +2160,11 @@ hastscript@^9.0.0:
     property-information "^7.0.0"
     space-separated-tokens "^2.0.0"
 
+hls.js@^1.5.17:
+  version "1.6.4"
+  resolved "https://registry.yarnpkg.com/hls.js/-/hls.js-1.6.4.tgz#0c4070f5f719eda2687e2ab13b061dbb01967dd8"
+  integrity sha512-sxFS61suCMJBvpOhmi4WLnarOZ8S8JAxK5J1icvrkopE8aRMc1pRB9WZWMX5Obh9nieVEML6uLLeyGksapyX5A==
+
 hoist-non-react-statics@^3.3.1:
   version "3.3.2"
   resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz"
@@ -1979,6 +2182,11 @@ html-void-elements@^3.0.0:
   resolved "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz"
   integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==
 
+ieee754@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
+  integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
+
 ignore@^5.2.0:
   version "5.3.2"
   resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz"
@@ -1989,6 +2197,11 @@ ignore@^7.0.0:
   resolved "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz"
   integrity sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==
 
+immediate@~3.0.5:
+  version "3.0.6"
+  resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
+  integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
+
 import-fresh@^3.2.1:
   version "3.3.1"
   resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz"
@@ -2069,11 +2282,23 @@ is-plain-obj@^4.0.0:
   resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz"
   integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==
 
+is-promise@^2.1.0:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1"
+  integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
+
 isexe@^2.0.0:
   version "2.0.0"
   resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz"
   integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
 
+its-fine@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/its-fine/-/its-fine-2.0.0.tgz#a90b18a3ee4c211a1fb6faac2abcc2b682ce1f21"
+  integrity sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==
+  dependencies:
+    "@types/react-reconciler" "^0.28.9"
+
 jiti@^2.4.2:
   version "2.4.2"
   resolved "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz"
@@ -2136,6 +2361,13 @@ levn@^0.4.1:
     prelude-ls "^1.2.1"
     type-check "~0.4.0"
 
+lie@^3.0.2:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a"
+  integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==
+  dependencies:
+    immediate "~3.0.5"
+
 lightningcss-darwin-arm64@1.30.1:
   version "1.30.1"
   resolved "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz"
@@ -2255,6 +2487,11 @@ lucide-react@^0.511.0:
   resolved "https://registry.npmjs.org/lucide-react/-/lucide-react-0.511.0.tgz"
   integrity sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w==
 
+maath@^0.10.8:
+  version "0.10.8"
+  resolved "https://registry.yarnpkg.com/maath/-/maath-0.10.8.tgz#cf647544430141bf6982da6e878abb6c4b804e24"
+  integrity sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==
+
 magic-string@^0.30.17:
   version "0.30.17"
   resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz"
@@ -2382,6 +2619,16 @@ merge2@^1.3.0:
   resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz"
   integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
 
+meshline@^3.3.1:
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/meshline/-/meshline-3.3.1.tgz#20decfd5cdd25c8469e862ddf0ab1ad167759734"
+  integrity sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==
+
+meshoptimizer@~0.18.1:
+  version "0.18.1"
+  resolved "https://registry.yarnpkg.com/meshoptimizer/-/meshoptimizer-0.18.1.tgz#cdb90907f30a7b5b1190facd3b7ee6b7087797d8"
+  integrity sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==
+
 micromark-core-commonmark@^2.0.0:
   version "2.0.3"
   resolved "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz"
@@ -2787,6 +3034,11 @@ postcss@^8.5.3:
     picocolors "^1.1.1"
     source-map-js "^1.2.1"
 
+potpack@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/potpack/-/potpack-1.0.2.tgz#23b99e64eb74f5741ffe7656b5b5c4ddce8dfc14"
+  integrity sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==
+
 prelude-ls@^1.2.1:
   version "1.2.1"
   resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz"
@@ -2797,6 +3049,14 @@ process@^0.11.1:
   resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz"
   integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
 
+promise-worker-transferable@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz#2c72861ba053e5ae42b487b4a83b1ed3ae3786e8"
+  integrity sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==
+  dependencies:
+    is-promise "^2.1.0"
+    lie "^3.0.2"
+
 prop-types@^15.6.2, prop-types@^15.8.1:
   version "15.8.1"
   resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
@@ -2865,6 +3125,20 @@ react-markdown@^10.1.0:
     unist-util-visit "^5.0.0"
     vfile "^6.0.0"
 
+react-photo-sphere-viewer@^6.2.3:
+  version "6.2.3"
+  resolved "https://registry.yarnpkg.com/react-photo-sphere-viewer/-/react-photo-sphere-viewer-6.2.3.tgz#bdbe33a03315077b1d49f2d4690f0dc72563ac9a"
+  integrity sha512-VzG0aY9CI8OIQjdIoJCjYF1QlnLFpN2pM+zKm1JrpAKQrBZ6B+Uxy94vpVQkGDERgn8FWE0+LIntTgAr60pLyQ==
+  dependencies:
+    eventemitter3 "^5.0.1"
+
+react-reconciler@^0.31.0:
+  version "0.31.0"
+  resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.31.0.tgz#6b7390fe8fab59210daf523d7400943973de1458"
+  integrity sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ==
+  dependencies:
+    scheduler "^0.25.0"
+
 react-refresh@^0.17.0:
   version "0.17.0"
   resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz"
@@ -2909,6 +3183,11 @@ react-transition-group@^4.4.5:
     loose-envify "^1.4.0"
     prop-types "^15.6.2"
 
+react-use-measure@^2.1.7:
+  version "2.1.7"
+  resolved "https://registry.yarnpkg.com/react-use-measure/-/react-use-measure-2.1.7.tgz#36b8a2e7fd2fa58109ab851b3addcb0aad66ad1d"
+  integrity sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==
+
 react@^19.1.0:
   version "19.1.0"
   resolved "https://registry.npmjs.org/react/-/react-19.1.0.tgz"
@@ -2944,6 +3223,11 @@ remark-rehype@^11.0.0:
     unified "^11.0.0"
     vfile "^6.0.0"
 
+require-from-string@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
+  integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
+
 resolve-from@^4.0.0:
   version "4.0.0"
   resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz"
@@ -2999,6 +3283,11 @@ run-parallel@^1.1.9:
   dependencies:
     queue-microtask "^1.2.2"
 
+scheduler@^0.25.0:
+  version "0.25.0"
+  resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.25.0.tgz#336cd9768e8cceebf52d3c80e3dcf5de23e7e015"
+  integrity sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==
+
 scheduler@^0.26.0:
   version "0.26.0"
   resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz"
@@ -3046,6 +3335,19 @@ space-separated-tokens@^2.0.0:
   resolved "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz"
   integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==
 
+stats-gl@^2.2.8:
+  version "2.4.2"
+  resolved "https://registry.yarnpkg.com/stats-gl/-/stats-gl-2.4.2.tgz#28a6c869fc3a36a8be608ef21df63c0aad99d1ba"
+  integrity sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==
+  dependencies:
+    "@types/three" "*"
+    three "^0.170.0"
+
+stats.js@^0.17.0:
+  version "0.17.0"
+  resolved "https://registry.yarnpkg.com/stats.js/-/stats.js-0.17.0.tgz#b1c3dc46d94498b578b7fd3985b81ace7131cc7d"
+  integrity sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==
+
 stringify-entities@^4.0.0:
   version "4.0.4"
   resolved "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz"
@@ -3090,6 +3392,11 @@ supports-preserve-symlinks-flag@^1.0.0:
   resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
   integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
 
+suspend-react@^0.1.3:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/suspend-react/-/suspend-react-0.1.3.tgz#a52f49d21cfae9a2fb70bd0c68413d3f9d90768e"
+  integrity sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==
+
 tailwindcss@4.1.8, tailwindcss@^4.1.8:
   version "4.1.8"
   resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.8.tgz"
@@ -3112,6 +3419,38 @@ tar@^7.4.3:
     mkdirp "^3.0.1"
     yallist "^5.0.0"
 
+three-mesh-bvh@^0.8.3:
+  version "0.8.3"
+  resolved "https://registry.yarnpkg.com/three-mesh-bvh/-/three-mesh-bvh-0.8.3.tgz#c5e72472e7f062ff79084157a25c122d73184163"
+  integrity sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==
+
+three-stdlib@^2.35.6:
+  version "2.36.0"
+  resolved "https://registry.yarnpkg.com/three-stdlib/-/three-stdlib-2.36.0.tgz#1d806b8db9156a6c87ed10f61f56f8a3ab634b42"
+  integrity sha512-kv0Byb++AXztEGsULgMAs8U2jgUdz6HPpAB/wDJnLiLlaWQX2APHhiTJIN7rqW+Of0eRgcp7jn05U1BsCP3xBA==
+  dependencies:
+    "@types/draco3d" "^1.4.0"
+    "@types/offscreencanvas" "^2019.6.4"
+    "@types/webxr" "^0.5.2"
+    draco3d "^1.4.1"
+    fflate "^0.6.9"
+    potpack "^1.0.1"
+
+three@^0.170.0:
+  version "0.170.0"
+  resolved "https://registry.yarnpkg.com/three/-/three-0.170.0.tgz#6087f97aab79e9e9312f9c89fcef6808642dfbb7"
+  integrity sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==
+
+three@^0.175.0:
+  version "0.175.0"
+  resolved "https://registry.yarnpkg.com/three/-/three-0.175.0.tgz#67b357b0b1ee8ef0445b9a768f59363ab1fa7921"
+  integrity sha512-nNE3pnTHxXN/Phw768u0Grr7W4+rumGg/H6PgeseNJojkJtmeHJfZWi41Gp2mpXl1pg1pf1zjwR4McM1jTqkpg==
+
+three@^0.177.0:
+  version "0.177.0"
+  resolved "https://registry.yarnpkg.com/three/-/three-0.177.0.tgz#e51f2eb2b921fbab535bdfa81c403f9993b9dd83"
+  integrity sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg==
+
 tinyglobby@^0.2.13:
   version "0.2.14"
   resolved "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz"
@@ -3132,6 +3471,26 @@ trim-lines@^3.0.0:
   resolved "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz"
   integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==
 
+troika-three-text@^0.52.4:
+  version "0.52.4"
+  resolved "https://registry.yarnpkg.com/troika-three-text/-/troika-three-text-0.52.4.tgz#f7b2389a2067d9506a5757457771cf4f6356e738"
+  integrity sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==
+  dependencies:
+    bidi-js "^1.0.2"
+    troika-three-utils "^0.52.4"
+    troika-worker-utils "^0.52.0"
+    webgl-sdf-generator "1.1.1"
+
+troika-three-utils@^0.52.4:
+  version "0.52.4"
+  resolved "https://registry.yarnpkg.com/troika-three-utils/-/troika-three-utils-0.52.4.tgz#9292019e93cab97582af1cf491c4c895e5c03b66"
+  integrity sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==
+
+troika-worker-utils@^0.52.0:
+  version "0.52.0"
+  resolved "https://registry.yarnpkg.com/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz#ba5525fc444345006ebab0bc9cabdd66f1561e66"
+  integrity sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==
+
 trough@^2.0.0:
   version "2.2.0"
   resolved "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz"
@@ -3147,6 +3506,13 @@ tslib@^2.4.0, tslib@^2.8.0:
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
   integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
 
+tunnel-rat@^0.1.2:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/tunnel-rat/-/tunnel-rat-0.1.2.tgz#1717efbc474ea2d8aa05a91622457a6e201c0aeb"
+  integrity sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==
+  dependencies:
+    zustand "^4.3.2"
+
 type-check@^0.4.0, type-check@~0.4.0:
   version "0.4.0"
   resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz"
@@ -3244,7 +3610,7 @@ uri-js@^4.2.2:
   dependencies:
     punycode "^2.1.0"
 
-use-sync-external-store@^1.4.0:
+use-sync-external-store@^1.2.2, use-sync-external-store@^1.4.0:
   version "1.5.0"
   resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz"
   integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==
@@ -3261,6 +3627,11 @@ util@^0.10.3:
   dependencies:
     inherits "2.0.3"
 
+utility-types@^3.11.0:
+  version "3.11.0"
+  resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.11.0.tgz#607c40edb4f258915e901ea7995607fdf319424c"
+  integrity sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==
+
 vfile-location@^5.0.0:
   version "5.0.3"
   resolved "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz"
@@ -3304,6 +3675,16 @@ web-namespaces@^2.0.0:
   resolved "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz"
   integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==
 
+webgl-constants@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/webgl-constants/-/webgl-constants-1.1.1.tgz#f9633ee87fea56647a60b9ce735cbdfb891c6855"
+  integrity sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==
+
+webgl-sdf-generator@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz#3e1b422b3d87cd3cc77f2602c9db63bc0f6accbd"
+  integrity sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==
+
 which@^2.0.1:
   version "2.0.2"
   resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz"
@@ -3336,6 +3717,18 @@ yocto-queue@^0.1.0:
   resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"
   integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
 
+zustand@^4.3.2:
+  version "4.5.7"
+  resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.7.tgz#7d6bb2026a142415dd8be8891d7870e6dbe65f55"
+  integrity sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==
+  dependencies:
+    use-sync-external-store "^1.2.2"
+
+zustand@^5.0.1, zustand@^5.0.3:
+  version "5.0.5"
+  resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.5.tgz#3e236f6a953142d975336d179bc735d97db17e84"
+  integrity sha512-mILtRfKW9xM47hqxGIxCv12gXusoY/xTSHBYApXozR0HmQv299whhBeeAcRy+KrPPybzosvJBCOmVjq6x12fCg==
+
 zwitch@^2.0.0:
   version "2.0.4"
   resolved "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz"