From 3b4876793efea1d1566b648997ad5d036b5ecd5a Mon Sep 17 00:00:00 2001 From: Kevin Carter Date: Fri, 14 Nov 2025 14:47:26 +0000 Subject: [PATCH] latest --- .env | 30 +- .env.development | 15 - .env.example | 0 .env.production | 15 - .gitignore | 3 + Dockerfile.dev | 41 - Dockerfile.storybook.macos.dev | 28 - Dockerfile.storybook.macos.prod | 51 - README.md | 107 - dist/.vite/manifest.json | 59 + dist/assets/index-CmYeIoD0.js | 68827 ++++++++++++++++ dist/assets/index-CmYeIoD0.js.map | 1 + dist/assets/index.css | 11178 +++ dist/assets/pdf.js | 21232 +++++ dist/assets/pdf.js.map | 1 + dist/assets/pdf.worker.min.mjs | 21 + dist/assets/vendor-mui.js | 19850 +++++ dist/assets/vendor-mui.js.map | 1 + dist/assets/vendor-react.js | 1901 + dist/assets/vendor-react.js.map | 1 + dist/assets/vendor-tldraw.js | 68796 +++++++++++++++ dist/assets/vendor-tldraw.js.map | 1 + dist/assets/vendor-utils.js | 12264 +++ dist/assets/vendor-utils.js.map | 1 + dist/audioWorklet.js | 12 + dist/favicon.ico | Bin 0 -> 15406 bytes dist/icons/icon-192x192-maskable.png | Bin 0 -> 54525 bytes dist/icons/icon-192x192.png | Bin 0 -> 54525 bytes dist/icons/icon-512x512-maskable.png | Bin 0 -> 265495 bytes dist/icons/icon-512x512.png | Bin 0 -> 265495 bytes dist/icons/sticker-tool.svg | 21 + dist/index.html | 18 + dist/manifest.webmanifest | 1 + dist/offline.html | 70 + dist/registerSW.js | 1 + dist/sw.js | 3889 + dist/sw.js.map | 1 + package-lock.json | 16525 ++++ package.json | 1 + src/AppRoutes.tsx | 105 +- src/axiosConfig.ts | 9 +- src/components/QueueStatusIndicator.tsx | 1 + src/contexts/AuthContext.tsx | 264 +- src/contexts/NeoInstituteContext.tsx | 23 +- src/contexts/NeoUserContext.tsx | 155 +- src/contexts/UserContext.tsx | 439 +- src/debugConfig.ts | 2 + src/pages/Header.tsx | 14 +- src/pages/auth/adminPage.tsx | 6 +- src/pages/auth/loginPage.tsx | 6 +- src/pages/auth/signupPage.tsx | 5 +- src/pages/dev/SimpleUploadTest.tsx | 876 + .../CCDocumentIntelligence/CCBundleViewer.tsx | 471 + .../CCDoclingViewer.tsx | 403 + .../CCDocumentIntelligence.tsx | 605 + .../CCEnhancedFilePanel.tsx | 570 + .../CCFileDetailPanel.tsx | 363 + src/pages/tldraw/singlePlayerPage.tsx | 232 +- src/pages/user/calendarPage.tsx | 16 +- src/pages/user/dashboardPage.tsx | 96 + src/services/auth/authService.ts | 16 +- src/services/auth/localStorageService.ts | 3 + src/services/auth/registrationService.ts | 69 +- src/services/graph/databaseNameService.ts | 71 +- src/services/graph/graphNeoDBService.ts | 43 +- src/services/graph/neoDBService.ts | 2 +- src/services/graph/neoRegistrationService.ts | 20 +- src/services/graph/neoShapeService.ts | 18 +- src/services/graph/timetableNeoDBService.ts | 20 +- src/services/graph/userNeoDBService.ts | 112 +- src/services/initService.ts | 3 +- src/services/provisioningService.ts | 89 + src/services/tldraw/snapshotService.ts | 196 +- src/stores/navigationStore.ts | 8 +- src/supabaseClient.ts | 7 +- src/types/api.ts | 6 +- src/types/graph-shape.ts | 4 +- src/types/navigation.ts | 6 +- src/utils/flow/graph/NodeTypes.tsx | 6 +- src/utils/folderPicker.ts | 216 + .../cc-base/cc-calendar/CalendarComponent.tsx | 2 +- .../cc-base/cc-calendar/CalendarDialogs.tsx | 6 +- .../cc-graph/CCSchoolNodeShapeUtil.tsx | 6 +- .../cc-graph/CCTeacherNodeShapeUtil.tsx | 4 +- .../cc-base/cc-graph/CCUserNodeShapeUtil.tsx | 4 +- .../CCUserTimetableLessonNodeShapeUtil.tsx | 18 +- .../tldraw/cc-base/cc-graph/cc-graph-props.ts | 31 +- .../cc-base/cc-graph/cc-graph-shapes.ts | 8 +- .../cc-base/cc-graph/cc-graph-shared.tsx | 10 +- .../tldraw/cc-base/cc-graph/cc-graph-types.ts | 27 +- .../cc-base/cc-graph/graphStateUtil.tsx | 12 +- src/utils/tldraw/cc-base/cc-migrations.ts | 281 +- src/utils/tldraw/cc-base/cc-props.ts | 2 +- .../cc-base/shape-helpers/graph-helpers.ts | 4 +- src/utils/tldraw/schemas.ts | 51 +- src/utils/tldraw/shapes.ts | 2 - .../components/shared/BasePanel.tsx | 18 +- .../components/shared/CCCabinetsPanel.tsx | 129 + .../components/shared/CCFilesPanel.tsx | 863 + .../shared/CCFilesPanelEnhanced.tsx | 505 + .../components/shared/panel-styles.ts | 10 + .../ui-overrides/toolbars/toolsToolbar.tsx | 4 +- src/{vite-env.d.ts => vite-env.d.old.ts} | 0 vite.config.ts | 14 +- 104 files changed, 231519 insertions(+), 1031 deletions(-) delete mode 100644 .env.development delete mode 100644 .env.example delete mode 100644 .env.production create mode 100644 .gitignore delete mode 100644 Dockerfile.dev delete mode 100644 Dockerfile.storybook.macos.dev delete mode 100644 Dockerfile.storybook.macos.prod delete mode 100644 README.md create mode 100644 dist/.vite/manifest.json create mode 100644 dist/assets/index-CmYeIoD0.js create mode 100644 dist/assets/index-CmYeIoD0.js.map create mode 100644 dist/assets/index.css create mode 100644 dist/assets/pdf.js create mode 100644 dist/assets/pdf.js.map create mode 100644 dist/assets/pdf.worker.min.mjs create mode 100644 dist/assets/vendor-mui.js create mode 100644 dist/assets/vendor-mui.js.map create mode 100644 dist/assets/vendor-react.js create mode 100644 dist/assets/vendor-react.js.map create mode 100644 dist/assets/vendor-tldraw.js create mode 100644 dist/assets/vendor-tldraw.js.map create mode 100644 dist/assets/vendor-utils.js create mode 100644 dist/assets/vendor-utils.js.map create mode 100644 dist/audioWorklet.js create mode 100644 dist/favicon.ico create mode 100644 dist/icons/icon-192x192-maskable.png create mode 100644 dist/icons/icon-192x192.png create mode 100644 dist/icons/icon-512x512-maskable.png create mode 100644 dist/icons/icon-512x512.png create mode 100644 dist/icons/sticker-tool.svg create mode 100644 dist/index.html create mode 100644 dist/manifest.webmanifest create mode 100644 dist/offline.html create mode 100644 dist/registerSW.js create mode 100644 dist/sw.js create mode 100644 dist/sw.js.map create mode 100644 package-lock.json create mode 100644 src/components/QueueStatusIndicator.tsx create mode 100644 src/pages/dev/SimpleUploadTest.tsx create mode 100644 src/pages/tldraw/CCDocumentIntelligence/CCBundleViewer.tsx create mode 100644 src/pages/tldraw/CCDocumentIntelligence/CCDoclingViewer.tsx create mode 100644 src/pages/tldraw/CCDocumentIntelligence/CCDocumentIntelligence.tsx create mode 100644 src/pages/tldraw/CCDocumentIntelligence/CCEnhancedFilePanel.tsx create mode 100644 src/pages/tldraw/CCDocumentIntelligence/CCFileDetailPanel.tsx create mode 100644 src/pages/user/dashboardPage.tsx create mode 100644 src/services/provisioningService.ts create mode 100644 src/utils/folderPicker.ts create mode 100644 src/utils/tldraw/ui-overrides/components/shared/CCCabinetsPanel.tsx create mode 100644 src/utils/tldraw/ui-overrides/components/shared/CCFilesPanel.tsx create mode 100644 src/utils/tldraw/ui-overrides/components/shared/CCFilesPanelEnhanced.tsx rename src/{vite-env.d.ts => vite-env.d.old.ts} (100%) diff --git a/.env b/.env index 0657ca7..cf50cf4 100644 --- a/.env +++ b/.env @@ -1,14 +1,18 @@ -VITE_APP_URL=app.classroomcopilot.ai -VITE_FRONTEND_SITE_URL=classroomcopilot.ai -VITE_APP_PROTOCOL=https -VITE_APP_NAME=Classroom Copilot -VITE_SUPER_ADMIN_EMAIL=kcar@kevlarai.com -VITE_DEV=false -VITE_SUPABASE_URL=https://supa.classroomcopilot.ai -VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiaWF0IjoxNzM0OTg4MzkxLCJpc3MiOiJzdXBhYmFzZSIsImV4cCI6MTc2NjUyNDM5MSwicm9sZSI6ImFub24ifQ.utdDZzVlhYIc-cSXuC2kyZz7HN59YfyMH4eaOw1hRlk -VITE_APP_API_URL=https://api.classroomcopilot.ai -VITE_STRICT_MODE=false +PORT_FRONTEND=5173 +PORT_FRONTEND_HMR=3002 +PORT_API=800 +PORT_SUPABASE=8000 -APP_PROTOCOL=https -APP_URL=app.classroomcopilot.ai -PORT_FRONTEND=3000 \ No newline at end of file +HOST_FRONTEND=localhost:5173 +VITE_PORT_FRONTEND=5173 +VITE_PORT_FRONTEND_HMR=5173 + +VITE_APP_NAME=Classroom Copilot +VITE_SUPER_ADMIN_EMAIL=admin@classroomcopilot.ai +VITE_DEV=true +VITE_FRONTEND_SITE_URL=http://localhost:5173 +VITE_APP_HMR_URL=http://localhost:5173 +VITE_SUPABASE_URL=http://localhost:8000 +VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0 +VITE_API_URL=http://localhost:8080 +VITE_API_BASE=http://localhost:8080 \ No newline at end of file diff --git a/.env.development b/.env.development deleted file mode 100644 index ae29536..0000000 --- a/.env.development +++ /dev/null @@ -1,15 +0,0 @@ -# Production environment configuration -# These values override .env for production mode - -# Disable development features -VITE_DEV=true -VITE_STRICT_MODE=true - -# App environment -VITE_SUPER_ADMIN_EMAIL=kcar@kevlarai.com -VITE_FRONTEND_SITE_URL=classroomcopilot.test -VITE_APP_PROTOCOL=http -VITE_SUPABASE_URL=supa.classroomcopilot.test -VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiaWF0IjoxNzM0OTg4MzkxLCJpc3MiOiJzdXBhYmFzZSIsImV4cCI6MTc2NjUyNDM5MSwicm9sZSI6ImFub24ifQ.utdDZzVlhYIc-cSXuC2kyZz7HN59YfyMH4eaOw1hRlk -VITE_WHISPERLIVE_URL=whisperlive.classroomcopilot.test -VITE_APP_API_URL=api.classroomcopilot.test \ No newline at end of file diff --git a/.env.example b/.env.example deleted file mode 100644 index e69de29..0000000 diff --git a/.env.production b/.env.production deleted file mode 100644 index 7a0f1a5..0000000 --- a/.env.production +++ /dev/null @@ -1,15 +0,0 @@ -# Production environment configuration -# These values override .env for production mode - -# Disable development features -VITE_DEV=false -VITE_STRICT_MODE=false - -# App environment -VITE_SUPER_ADMIN_EMAIL=kcar@kevlarai.com -VITE_FRONTEND_SITE_URL=classroomcopilot.ai -VITE_APP_PROTOCOL=https -VITE_SUPABASE_URL=supa.classroomcopilot.ai -VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiaWF0IjoxNzM0OTg4MzkxLCJpc3MiOiJzdXBhYmFzZSIsImV4cCI6MTc2NjUyNDM5MSwicm9sZSI6ImFub24ifQ.utdDZzVlhYIc-cSXuC2kyZz7HN59YfyMH4eaOw1hRlk -VITE_WHISPERLIVE_URL=whisperlive.classroomcopilot.ai -VITE_APP_API_URL=api.classroomcopilot.ai \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a2e04e4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules + +.env \ No newline at end of file diff --git a/Dockerfile.dev b/Dockerfile.dev deleted file mode 100644 index 50c4a93..0000000 --- a/Dockerfile.dev +++ /dev/null @@ -1,41 +0,0 @@ -FROM node:20 as builder -WORKDIR /app -COPY package*.json ./ -COPY .env.development .env.development - -# First generate package-lock.json if it doesn't exist, then do clean install -RUN if [ ! -f package-lock.json ]; then npm install --package-lock-only; fi && npm ci -COPY . . -# Run build with development mode -RUN npm run build -- --mode development - -FROM nginx:alpine -# Copy built files -COPY --from=builder /app/dist /usr/share/nginx/html - -# Create a simple nginx configuration -RUN echo 'server { \ - listen 3003; \ - root /usr/share/nginx/html; \ - index index.html; \ - location / { \ - try_files $uri $uri/ /index.html; \ - expires 30d; \ - add_header Cache-Control "public, no-transform"; \ - } \ - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { \ - expires 30d; \ - add_header Cache-Control "public, no-transform"; \ - } \ - location ~ /\. { \ - deny all; \ - } \ - error_page 404 /index.html; \ -}' > /etc/nginx/conf.d/default.conf - -# Set up permissions -RUN chown -R nginx:nginx /usr/share/nginx/html \ - && chown -R nginx:nginx /var/log/nginx - -# Expose HTTP port (NPM will handle HTTPS) -EXPOSE 3003 \ No newline at end of file diff --git a/Dockerfile.storybook.macos.dev b/Dockerfile.storybook.macos.dev deleted file mode 100644 index 3a9f10d..0000000 --- a/Dockerfile.storybook.macos.dev +++ /dev/null @@ -1,28 +0,0 @@ -# Dockerfile.storybook -FROM node:20-slim - -WORKDIR /app - -# Install basic dependencies -RUN apt-get update && apt-get install -y \ - xdg-utils \ - && rm -rf /var/lib/apt/lists/* - -# Copy package files -COPY package*.json ./ - -# Copy yarn.lock if it exists -COPY yarn.lock* ./ - -# Install dependencies -RUN yarn install - -# Copy the rest of the application -COPY . . - -# Expose port Storybook runs on -EXPOSE 6006 - -# Start Storybook in development mode with host configuration -ENV BROWSER=none -CMD ["yarn", "storybook", "dev", "-p", "6006", "--host", "0.0.0.0"] \ No newline at end of file diff --git a/Dockerfile.storybook.macos.prod b/Dockerfile.storybook.macos.prod deleted file mode 100644 index f8a2ca9..0000000 --- a/Dockerfile.storybook.macos.prod +++ /dev/null @@ -1,51 +0,0 @@ -# Build stage -FROM node:20 as builder -WORKDIR /app - -# Copy package files -COPY package*.json ./ - -# Copy yarn.lock if it exists -COPY yarn.lock* ./ - -# Install dependencies -RUN yarn install - -# Copy the rest of the application -COPY . . - -# Build Storybook -RUN yarn build-storybook - -# Production stage -FROM nginx:alpine -WORKDIR /usr/share/nginx/html - -# Copy built Storybook files -COPY --from=builder /app/storybook-static . - -# Create nginx configuration -RUN echo 'server { \ - listen 6006; \ - root /usr/share/nginx/html; \ - index index.html; \ - location / { \ - try_files $uri $uri/ /index.html; \ - expires 30d; \ - add_header Cache-Control "public, no-transform"; \ - } \ - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { \ - expires 30d; \ - add_header Cache-Control "public, no-transform"; \ - } \ - location ~ /\. { \ - deny all; \ - } \ - error_page 404 /index.html; \ -}' > /etc/nginx/conf.d/default.conf - -# Set up permissions -RUN chown -R nginx:nginx /usr/share/nginx/html \ - && chown -R nginx:nginx /var/log/nginx - -EXPOSE 6006 \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index a30d7f1..0000000 --- a/README.md +++ /dev/null @@ -1,107 +0,0 @@ -# Frontend Service - -This directory contains the frontend service for ClassroomCopilot, including the web application and static file serving configuration. - -## Directory Structure - -``` -frontend/ -├── src/ # Frontend application source code -└── Dockerfile.dev # Development container configuration -└── Dockerfile.prod # Production container configuration -``` - -## Configuration - -### Environment Variables - -The frontend service uses the following environment variables: - -- `VITE_FRONTEND_SITE_URL`: The base URL of the frontend application -- `VITE_APP_NAME`: The name of the application -- `VITE_SUPER_ADMIN_EMAIL`: Email address of the super admin -- `VITE_DEV`: Development mode flag -- `VITE_SUPABASE_URL`: Supabase API URL -- `VITE_SUPABASE_ANON_KEY`: Supabase anonymous key -- `VITE_STRICT_MODE`: Strict mode flag -- Other environment variables are defined in the root `.env` file - -### Server Configuration - -The frontend container uses a simple nginx configuration that: -- Serves static files on port 80 -- Handles SPA routing -- Manages caching headers -- Denies access to hidden files - -SSL termination and domain routing are handled by Nginx Proxy Manager (NPM). - -## Usage - -### Development - -1. Start the development environment: - ```bash - NGINX_MODE=dev ./init_macos_dev.sh up - ``` - -2. Configure NPM: - - Create a new proxy host for app.localhost - - Forward to http://frontend:80 - - Enable SSL with a self-signed certificate - - Add custom locations for SPA routing - -3. Access the application: - - HTTPS: https://app.localhost - -### Production - -1. Set environment variables: - ```bash - NGINX_MODE=prod - ``` - -2. Start the production environment: - ```bash - ./init_macos_dev.sh up - ``` - -3. Configure NPM: - - Create a new proxy host for app.classroomcopilot.ai - - Forward to http://frontend:80 - - Enable SSL with Cloudflare certificates - - Add custom locations for SPA routing - -## Security - -- SSL termination handled by NPM -- Static file serving with proper caching headers -- Hidden file access denied -- SPA routing with fallback to index.html - -## Troubleshooting - -### Connection Issues -- Check NPM logs in the admin interface -- Verify frontend container is running -- Ensure NPM proxy host is properly configured -- Check network connectivity between NPM and frontend - -### SPA Routing Issues -- Verify NPM custom locations are properly configured -- Check frontend container logs -- Ensure all routes fall back to index.html - -## Maintenance - -### Log Files -Located in `/var/log/nginx/`: -- `access.log`: General access logs -- `error.log`: Error logs - -### Configuration Updates -1. Modify Dockerfile.dev or Dockerfile.prod as needed -2. Rebuild and restart the container: - ```bash - docker compose up -d --build frontend - ``` diff --git a/dist/.vite/manifest.json b/dist/.vite/manifest.json new file mode 100644 index 0000000..9c6d253 --- /dev/null +++ b/dist/.vite/manifest.json @@ -0,0 +1,59 @@ +{ + "_vendor-mui.js": { + "file": "assets/vendor-mui.js", + "name": "vendor-mui", + "imports": [ + "_vendor-react.js" + ] + }, + "_vendor-react.js": { + "file": "assets/vendor-react.js", + "name": "vendor-react" + }, + "_vendor-tldraw.js": { + "file": "assets/vendor-tldraw.js", + "name": "vendor-tldraw", + "imports": [ + "_vendor-react.js", + "_vendor-mui.js" + ] + }, + "_vendor-utils.js": { + "file": "assets/vendor-utils.js", + "name": "vendor-utils", + "imports": [ + "_vendor-react.js" + ] + }, + "index.html": { + "file": "assets/index-CmYeIoD0.js", + "name": "index", + "src": "index.html", + "isEntry": true, + "imports": [ + "_vendor-mui.js", + "_vendor-react.js", + "_vendor-tldraw.js", + "_vendor-utils.js" + ], + "dynamicImports": [ + "node_modules/pdfjs-dist/build/pdf.mjs" + ], + "css": [ + "assets/index.css" + ], + "assets": [ + "assets/pdf.worker.min.mjs" + ] + }, + "node_modules/pdfjs-dist/build/pdf.mjs": { + "file": "assets/pdf.js", + "name": "pdf", + "src": "node_modules/pdfjs-dist/build/pdf.mjs", + "isDynamicEntry": true + }, + "node_modules/pdfjs-dist/build/pdf.worker.min.mjs": { + "file": "assets/pdf.worker.min.mjs", + "src": "node_modules/pdfjs-dist/build/pdf.worker.min.mjs" + } +} \ No newline at end of file diff --git a/dist/assets/index-CmYeIoD0.js b/dist/assets/index-CmYeIoD0.js new file mode 100644 index 0000000..ae369ee --- /dev/null +++ b/dist/assets/index-CmYeIoD0.js @@ -0,0 +1,68827 @@ +import { p as propTypesExports, d as createTheme, j as jsxRuntimeExports, e as utils$7, g as styled, B as Box, i as Button, A as AccountCircleIcon, C as CalendarIcon, T as TeacherIcon, k as BusinessIcon, G as GraphIcon, m as ClassIcon, n as Tooltip, I as IconButton, q as ArrowBackIcon, H as HistoryIcon, r as ArrowForwardIcon, M as Menu$1, t as MenuItem, L as ListItemIcon, v as ListItemText, S as StudentIcon, E as ExpandMoreIcon, w as useTheme, x as AppBar, y as Toolbar$1, z as Typography, D as TLDrawDevIcon, F as DevToolsIcon, J as Divider, K as MultiplayerIcon, N as ExamIcon, O as ExamMarkerIcon, P as SettingsIcon, Q as SearchIcon, R as AdminIcon, U as LogoutIcon, V as LoginIcon, W as Alert, X as TextField, Y as Container, Z as Stack, _ as Paper, $ as CircularProgress, a0 as ButtonGroup, a1 as List, a2 as ListItem, a3 as Tabs, a4 as useMediaQuery, a5 as ThemeProvider, a6 as Tab, a7 as PushPinIcon, a8 as PushPinOutlinedIcon, a9 as ShapesIcon, aa as NodeIcon, ab as NavigationIcon, ac as YouTubeIcon, ad as SlidesIcon, ae as Snackbar } from './vendor-mui.js'; +import { r as reactExports, e as getAugmentedNamespace, a as reactDomExports, g as getDefaultExportFromCjs, u as useNavigate, b as React$2, f as useLocation, h as useSearchParams, i as Routes, j as Route, B as BrowserRouter } from './vendor-react.js'; +import { c as createShapeId, g as getSnapshot, l as loadSnapshot, B as BaseBoxShapeUtil, H as HTMLContainer, t as toDomPrecision, a as arrayOf, s as string, o as object$1, n as number, b as boolean, D as DefaultSizeStyle, d as DefaultDashStyle, e as DefaultColorStyle, f as clamp$1, h as getIndexBetween, T as TldrawUiDialogHeader, i as TldrawUiDialogTitle, j as TldrawUiDialogCloseButton, k as TldrawUiDialogBody, m as TldrawUiDialogFooter, p as TldrawUiButton, q as TldrawUiButtonLabel, u as useEditor, r as useDialogs, R as Rectangle2d, v as optional, w as DEFAULT_EMBED_DEFINITIONS, x as BindingUtil, V as Vec, y as createShapePropsMigrationSequence, z as createShapePropsMigrationIds, A as createTLSchema, C as defaultShapeSchemas, E as defaultBindingSchemas, F as createTLSchemaFromUtils, G as defaultBindingUtils, I as defaultShapeUtils, J as createTLStore, K as DefaultToolbar, L as TldrawUiMenuItem, M as DefaultToolbarContent, N as DefaultHelperButtons, O as DefaultHelperButtonsContent, P as DefaultNavigationPanel, Q as DefaultStylePanel, S as DefaultStylePanelContent, U as useRelevantStyles, W as DefaultZoomMenu, X as DefaultZoomMenuContent, Y as useTools, Z as DefaultKeyboardShortcutsDialog, _ as DefaultKeyboardShortcutsDialogContent, $ as DefaultContextMenu, a0 as DefaultContextMenuContent, a1 as DefaultDebugMenu, a2 as DefaultDebugMenuContent, a3 as useToasts, a4 as DefaultActionsMenu, a5 as DefaultActionsMenuContent, a6 as createBindingId, a7 as atom, a8 as useValue, a9 as exportToBlob, aa as Box$1, ab as computed, ac as BaseBoxShapeTool, ad as StateNode, ae as useTldrawUser, af as Tldraw, ag as DEFAULT_SUPPORT_VIDEO_TYPES, ah as DEFAULT_SUPPORTED_IMAGE_TYPES, ai as objectMapEntries, aj as objectMapValues, ak as isEqual, al as react, am as uniqueId, an as transact, ao as reverseRecordsDiff, ap as exhaustiveSwitchError, aq as fpsThrottle, ar as squashRecordDiffs, as as warnOnce, at as assert, au as registerTldrawLibraryVersion, av as useRefState, aw as useTLSchemaFromUtils, ax as useShallowObjectIdentity, ay as useAtom, az as isSignal, aA as getUserPreferences, aB as defaultUserPreferences, aC as TAB_ID, aD as createPresenceStateDerivation, aE as AssetRecordType, aF as getHashForString, aG as client } from './vendor-tldraw.js'; +import { c as createClient, a as axios$1, b as create$1, A as AxiosError, _ as __vitePreload } from './vendor-utils.js'; + +true&&(function polyfill() { + const relList = document.createElement("link").relList; + if (relList && relList.supports && relList.supports("modulepreload")) { + return; + } + for (const link of document.querySelectorAll('link[rel="modulepreload"]')) { + processPreload(link); + } + new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.type !== "childList") { + continue; + } + for (const node of mutation.addedNodes) { + if (node.tagName === "LINK" && node.rel === "modulepreload") + processPreload(node); + } + } + }).observe(document, { childList: true, subtree: true }); + function getFetchOpts(link) { + const fetchOpts = {}; + if (link.integrity) fetchOpts.integrity = link.integrity; + if (link.referrerPolicy) fetchOpts.referrerPolicy = link.referrerPolicy; + if (link.crossOrigin === "use-credentials") + fetchOpts.credentials = "include"; + else if (link.crossOrigin === "anonymous") fetchOpts.credentials = "omit"; + else fetchOpts.credentials = "same-origin"; + return fetchOpts; + } + function processPreload(link) { + if (link.ep) + return; + link.ep = true; + const fetchOpts = getFetchOpts(link); + fetch(link.href, fetchOpts); + } +}()); + +const LOG_LEVELS = { + error: 0, + // Always shown if enabled + warn: 1, + // Shows warns and errors + info: 2, + // Shows info, warns, and errors + debug: 3, + // Shows debug and above + trace: 4 + // Shows everything +}; +class DebugLogger { + config = { + enabled: true, + level: "debug", + categories: [ + "system", + "navigation", + "presentation", + "selection", + "camera", + "binding", + "shape", + "tldraw-service" + ] + }; + setConfig(config) { + this.config = { ...this.config, ...config }; + } + shouldLog(level, category) { + return this.config.enabled && LOG_LEVELS[level] <= LOG_LEVELS[this.config.level] && this.config.categories.includes(category); + } + log(level, category, message, data) { + if (!this.shouldLog(level, category)) { + return; + } + const levelEmojis = { + error: "🔴", + // Red circle for errors + warn: "⚠️", + // Warning symbol + info: "ℹ️", + // Information symbol + debug: "🔧", + // Wrench for debug + trace: "🔍" + // Magnifying glass for trace + }; + const prefix = `${levelEmojis[level]} [${category}]`; + switch (level) { + case "error": + if (data) { + console.error(`${prefix} ${message}`, data); + } else { + console.error(`${prefix} ${message}`); + } + break; + case "warn": + if (data) { + console.warn(`${prefix} ${message}`, data); + } else { + console.warn(`${prefix} ${message}`); + } + break; + case "info": + if (data) { + console.info(`${prefix} ${message}`, data); + } else { + console.info(`${prefix} ${message}`); + } + break; + case "debug": + if (data) { + console.debug(`${prefix} ${message}`, data); + } else { + console.debug(`${prefix} ${message}`); + } + break; + case "trace": + if (data) { + console.trace(`${prefix} ${message}`, data); + } else { + console.trace(`${prefix} ${message}`); + } + break; + } + } + // Convenience methods + error(category, message, data) { + this.log("error", category, message, data); + } + warn(category, message, data) { + this.log("warn", category, message, data); + } + info(category, message, data) { + this.log("info", category, message, data); + } + debug(category, message, data) { + this.log("debug", category, message, data); + } + trace(category, message, data) { + this.log("trace", category, message, data); + } +} +const logger = new DebugLogger(); +logger.setConfig({ + enabled: true, + level: "debug", + categories: [ + "app", + "header", + "routing", + "neo4j-context", + "auth-context", + "auth-service", + "state-management", + "local-storage", + "axios", + "system", + "navigation", + "calendar", + "presentation", + "selection", + "camera", + "binding", + "shape", + "tldraw-service", + "tldraw-events", + "signup-page", + "timetable-service", + "dev-page", + "super-admin-auth-route", + "admin-page", + "storage-service", + "user-context", + "login-form", + "super-admin-section", + "routes", + "neo4j-service", + "supabase-client", + "user-page", + "site-page", + "auth-page", + "email-signup-form", + "supabase-profile-service", + "multiplayer-page", + "snapshot-service", + "sync-service", + "slides-panel", + "local-store-service", + "shared-store-service", + "single-player-page", + "user-toolbar", + "registration-service", + "graph-service", + "graph-shape", + "calendar-shape", + "snapshot-toolbar", + "graphStateUtil", + "baseNodeShapeUtil", + "school-service", + "microphone-state-tool", + "store-service", + "morphic-page", + "not-found", + "share-handler", + "transcription-service", + "slideshow-helpers", + "slide-shape", + "graph-panel", + "cc-user-node-shape-util", + "cc-base-shape-util", + "node-canvas", + "navigation-service", + "autosave", + "cc-exam-marker", + "cc-search", + "cc-web-browser", + "neo-user-context", + "neo-institute-context", + "cc-node-snapshot-panel", + "user-neo-db", + "navigation-queue-service", + "editor-state", + "neo-shape-service", + // Add new navigation categories + "navigation-context", + "navigation-history", + "navigation-ui", + "navigation-store", + "navigation-queue", + "navigation-state", + "context-switch", + "history-management", + "node-navigation", + "navigation-panel", + "auth", + "school-context", + "database-name-service", + "tldraw", + "websocket", + "app", + "auth-service", + "storage-service", + "routing", + "auth-service", + "user-context", + "neo-user-context", + "neo-institute-context" + ] +}); + +var lib = {exports: {}}; + +var Modal$2 = {}; + +var ModalPortal = {exports: {}}; + +var focusManager = {}; + +var tabbable = {exports: {}}; + +(function (module, exports) { + + Object.defineProperty(exports, "__esModule", { + value: true + }); + exports.default = findTabbableDescendants; + /*! + * Adapted from jQuery UI core + * + * http://jqueryui.com + * + * Copyright 2014 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/category/ui-core/ + */ + + var DISPLAY_NONE = "none"; + var DISPLAY_CONTENTS = "contents"; + + // match the whole word to prevent fuzzy searching + var tabbableNode = /^(input|select|textarea|button|object|iframe)$/; + + function isNotOverflowing(element, style) { + return style.getPropertyValue("overflow") !== "visible" || + // if 'overflow: visible' set, check if there is actually any overflow + element.scrollWidth <= 0 && element.scrollHeight <= 0; + } + + function hidesContents(element) { + var zeroSize = element.offsetWidth <= 0 && element.offsetHeight <= 0; + + // If the node is empty, this is good enough + if (zeroSize && !element.innerHTML) return true; + + try { + // Otherwise we need to check some styles + var style = window.getComputedStyle(element); + var displayValue = style.getPropertyValue("display"); + return zeroSize ? displayValue !== DISPLAY_CONTENTS && isNotOverflowing(element, style) : displayValue === DISPLAY_NONE; + } catch (exception) { + // eslint-disable-next-line no-console + console.warn("Failed to inspect element style"); + return false; + } + } + + function visible(element) { + var parentElement = element; + var rootNode = element.getRootNode && element.getRootNode(); + while (parentElement) { + if (parentElement === document.body) break; + + // if we are not hidden yet, skip to checking outside the Web Component + if (rootNode && parentElement === rootNode) parentElement = rootNode.host.parentNode; + + if (hidesContents(parentElement)) return false; + parentElement = parentElement.parentNode; + } + return true; + } + + function focusable(element, isTabIndexNotNaN) { + var nodeName = element.nodeName.toLowerCase(); + var res = tabbableNode.test(nodeName) && !element.disabled || (nodeName === "a" ? element.href || isTabIndexNotNaN : isTabIndexNotNaN); + return res && visible(element); + } + + function tabbable(element) { + var tabIndex = element.getAttribute("tabindex"); + if (tabIndex === null) tabIndex = undefined; + var isTabIndexNaN = isNaN(tabIndex); + return (isTabIndexNaN || tabIndex >= 0) && focusable(element, !isTabIndexNaN); + } + + function findTabbableDescendants(element) { + var descendants = [].slice.call(element.querySelectorAll("*"), 0).reduce(function (finished, el) { + return finished.concat(!el.shadowRoot ? [el] : findTabbableDescendants(el.shadowRoot)); + }, []); + return descendants.filter(tabbable); + } + module.exports = exports["default"]; +} (tabbable, tabbable.exports)); + +var tabbableExports = tabbable.exports; + +Object.defineProperty(focusManager, "__esModule", { + value: true +}); +focusManager.resetState = resetState$4; +focusManager.log = log$4; +focusManager.handleBlur = handleBlur; +focusManager.handleFocus = handleFocus; +focusManager.markForFocusLater = markForFocusLater; +focusManager.returnFocus = returnFocus; +focusManager.popWithoutFocus = popWithoutFocus; +focusManager.setupScopedFocus = setupScopedFocus; +focusManager.teardownScopedFocus = teardownScopedFocus; +var _tabbable = tabbableExports; +var _tabbable2 = _interopRequireDefault$i(_tabbable); +function _interopRequireDefault$i(obj) { + return obj && obj.__esModule ? obj : { default: obj }; +} +var focusLaterElements = []; +var modalElement = null; +var needToFocus = false; +function resetState$4() { + focusLaterElements = []; +} +function log$4() { +} +function handleBlur() { + needToFocus = true; +} +function handleFocus() { + if (needToFocus) { + needToFocus = false; + if (!modalElement) { + return; + } + setTimeout(function() { + if (modalElement.contains(document.activeElement)) { + return; + } + var el = (0, _tabbable2.default)(modalElement)[0] || modalElement; + el.focus(); + }, 0); + } +} +function markForFocusLater() { + focusLaterElements.push(document.activeElement); +} +function returnFocus() { + var preventScroll = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : false; + var toFocus = null; + try { + if (focusLaterElements.length !== 0) { + toFocus = focusLaterElements.pop(); + toFocus.focus({ preventScroll }); + } + return; + } catch (e) { + console.warn(["You tried to return focus to", toFocus, "but it is not in the DOM anymore"].join(" ")); + } +} +function popWithoutFocus() { + focusLaterElements.length > 0 && focusLaterElements.pop(); +} +function setupScopedFocus(element) { + modalElement = element; + if (window.addEventListener) { + window.addEventListener("blur", handleBlur, false); + document.addEventListener("focus", handleFocus, true); + } else { + window.attachEvent("onBlur", handleBlur); + document.attachEvent("onFocus", handleFocus); + } +} +function teardownScopedFocus() { + modalElement = null; + if (window.addEventListener) { + window.removeEventListener("blur", handleBlur); + document.removeEventListener("focus", handleFocus); + } else { + window.detachEvent("onBlur", handleBlur); + document.detachEvent("onFocus", handleFocus); + } +} + +var scopeTab = {exports: {}}; + +(function (module, exports) { + + Object.defineProperty(exports, "__esModule", { + value: true + }); + exports.default = scopeTab; + + var _tabbable = tabbableExports; + + var _tabbable2 = _interopRequireDefault(_tabbable); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + function getActiveElement() { + var el = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document; + + return el.activeElement.shadowRoot ? getActiveElement(el.activeElement.shadowRoot) : el.activeElement; + } + + function scopeTab(node, event) { + var tabbable = (0, _tabbable2.default)(node); + + if (!tabbable.length) { + // Do nothing, since there are no elements that can receive focus. + event.preventDefault(); + return; + } + + var target = void 0; + + var shiftKey = event.shiftKey; + var head = tabbable[0]; + var tail = tabbable[tabbable.length - 1]; + var activeElement = getActiveElement(); + + // proceed with default browser behavior on tab. + // Focus on last element on shift + tab. + if (node === activeElement) { + if (!shiftKey) return; + target = tail; + } + + if (tail === activeElement && !shiftKey) { + target = head; + } + + if (head === activeElement && shiftKey) { + target = tail; + } + + if (target) { + event.preventDefault(); + target.focus(); + return; + } + + // Safari radio issue. + // + // Safari does not move the focus to the radio button, + // so we need to force it to really walk through all elements. + // + // This is very error prone, since we are trying to guess + // if it is a safari browser from the first occurence between + // chrome or safari. + // + // The chrome user agent contains the first ocurrence + // as the 'chrome/version' and later the 'safari/version'. + var checkSafari = /(\bChrome\b|\bSafari\b)\//.exec(navigator.userAgent); + var isSafariDesktop = checkSafari != null && checkSafari[1] != "Chrome" && /\biPod\b|\biPad\b/g.exec(navigator.userAgent) == null; + + // If we are not in safari desktop, let the browser control + // the focus + if (!isSafariDesktop) return; + + var x = tabbable.indexOf(activeElement); + + if (x > -1) { + x += shiftKey ? -1 : 1; + } + + target = tabbable[x]; + + // If the tabbable element does not exist, + // focus head/tail based on shiftKey + if (typeof target === "undefined") { + event.preventDefault(); + target = shiftKey ? tail : head; + target.focus(); + return; + } + + event.preventDefault(); + + target.focus(); + } + module.exports = exports["default"]; +} (scopeTab, scopeTab.exports)); + +var scopeTabExports = scopeTab.exports; + +var ariaAppHider$1 = {}; + +var warning = function() { +}; +var warning_1 = warning; + +var safeHTMLElement = {}; + +var exenv = {exports: {}}; + +/*! + Copyright (c) 2015 Jed Watson. + Based on code that is Copyright 2013-2015, Facebook, Inc. + All rights reserved. +*/ + +(function (module) { + /* global define */ + + (function () { + + var canUseDOM = !!( + typeof window !== 'undefined' && + window.document && + window.document.createElement + ); + + var ExecutionEnvironment = { + + canUseDOM: canUseDOM, + + canUseWorkers: typeof Worker !== 'undefined', + + canUseEventListeners: + canUseDOM && !!(window.addEventListener || window.attachEvent), + + canUseViewport: canUseDOM && !!window.screen + + }; + + if (module.exports) { + module.exports = ExecutionEnvironment; + } else { + window.ExecutionEnvironment = ExecutionEnvironment; + } + + }()); +} (exenv)); + +var exenvExports = exenv.exports; + +Object.defineProperty(safeHTMLElement, "__esModule", { + value: true +}); +safeHTMLElement.canUseDOM = safeHTMLElement.SafeNodeList = safeHTMLElement.SafeHTMLCollection = undefined; + +var _exenv = exenvExports; + +var _exenv2 = _interopRequireDefault$h(_exenv); + +function _interopRequireDefault$h(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var EE = _exenv2.default; + +var SafeHTMLElement = EE.canUseDOM ? window.HTMLElement : {}; + +safeHTMLElement.SafeHTMLCollection = EE.canUseDOM ? window.HTMLCollection : {}; + +safeHTMLElement.SafeNodeList = EE.canUseDOM ? window.NodeList : {}; + +safeHTMLElement.canUseDOM = EE.canUseDOM; + +safeHTMLElement.default = SafeHTMLElement; + +Object.defineProperty(ariaAppHider$1, "__esModule", { + value: true +}); +ariaAppHider$1.resetState = resetState$3; +ariaAppHider$1.log = log$3; +ariaAppHider$1.assertNodeList = assertNodeList; +ariaAppHider$1.setElement = setElement; +ariaAppHider$1.validateElement = validateElement; +ariaAppHider$1.hide = hide; +ariaAppHider$1.show = show; +ariaAppHider$1.documentNotReadyOrSSRTesting = documentNotReadyOrSSRTesting; +var _warning = warning_1; +var _warning2 = _interopRequireDefault$g(_warning); +var _safeHTMLElement$1 = safeHTMLElement; +function _interopRequireDefault$g(obj) { + return obj && obj.__esModule ? obj : { default: obj }; +} +var globalElement = null; +function resetState$3() { + if (globalElement) { + if (globalElement.removeAttribute) { + globalElement.removeAttribute("aria-hidden"); + } else if (globalElement.length != null) { + globalElement.forEach(function(element) { + return element.removeAttribute("aria-hidden"); + }); + } else { + document.querySelectorAll(globalElement).forEach(function(element) { + return element.removeAttribute("aria-hidden"); + }); + } + } + globalElement = null; +} +function log$3() { +} +function assertNodeList(nodeList, selector) { + if (!nodeList || !nodeList.length) { + throw new Error("react-modal: No elements were found for selector " + selector + "."); + } +} +function setElement(element) { + var useElement = element; + if (typeof useElement === "string" && _safeHTMLElement$1.canUseDOM) { + var el = document.querySelectorAll(useElement); + assertNodeList(el, useElement); + useElement = el; + } + globalElement = useElement || globalElement; + return globalElement; +} +function validateElement(appElement) { + var el = appElement || globalElement; + if (el) { + return Array.isArray(el) || el instanceof HTMLCollection || el instanceof NodeList ? el : [el]; + } else { + (0, _warning2.default)(false, ["react-modal: App element is not defined.", "Please use `Modal.setAppElement(el)` or set `appElement={el}`.", "This is needed so screen readers don't see main content", "when modal is opened. It is not recommended, but you can opt-out", "by setting `ariaHideApp={false}`."].join(" ")); + return []; + } +} +function hide(appElement) { + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = void 0; + try { + for (var _iterator = validateElement(appElement)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var el = _step.value; + el.setAttribute("aria-hidden", "true"); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } +} +function show(appElement) { + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = void 0; + try { + for (var _iterator2 = validateElement(appElement)[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var el = _step2.value; + el.removeAttribute("aria-hidden"); + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } +} +function documentNotReadyOrSSRTesting() { + globalElement = null; +} + +var classList$1 = {}; + +Object.defineProperty(classList$1, "__esModule", { + value: true +}); +classList$1.resetState = resetState$2; +classList$1.log = log$2; +var htmlClassList = {}; +var docBodyClassList = {}; +function removeClass(at, cls) { + at.classList.remove(cls); +} +function resetState$2() { + var htmlElement = document.getElementsByTagName("html")[0]; + for (var cls in htmlClassList) { + removeClass(htmlElement, htmlClassList[cls]); + } + var body = document.body; + for (var _cls in docBodyClassList) { + removeClass(body, docBodyClassList[_cls]); + } + htmlClassList = {}; + docBodyClassList = {}; +} +function log$2() { +} +var incrementReference = function incrementReference2(poll, className) { + if (!poll[className]) { + poll[className] = 0; + } + poll[className] += 1; + return className; +}; +var decrementReference = function decrementReference2(poll, className) { + if (poll[className]) { + poll[className] -= 1; + } + return className; +}; +var trackClass = function trackClass2(classListRef, poll, classes) { + classes.forEach(function(className) { + incrementReference(poll, className); + classListRef.add(className); + }); +}; +var untrackClass = function untrackClass2(classListRef, poll, classes) { + classes.forEach(function(className) { + decrementReference(poll, className); + poll[className] === 0 && classListRef.remove(className); + }); +}; +classList$1.add = function add2(element, classString) { + return trackClass(element.classList, element.nodeName.toLowerCase() == "html" ? htmlClassList : docBodyClassList, classString.split(" ")); +}; +classList$1.remove = function remove2(element, classString) { + return untrackClass(element.classList, element.nodeName.toLowerCase() == "html" ? htmlClassList : docBodyClassList, classString.split(" ")); +}; + +var portalOpenInstances$1 = {}; + +Object.defineProperty(portalOpenInstances$1, "__esModule", { + value: true +}); +portalOpenInstances$1.log = log$1; +portalOpenInstances$1.resetState = resetState$1; +function _classCallCheck$1(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } +} +var PortalOpenInstances = function PortalOpenInstances2() { + var _this = this; + _classCallCheck$1(this, PortalOpenInstances2); + this.register = function(openInstance) { + if (_this.openInstances.indexOf(openInstance) !== -1) { + return; + } + _this.openInstances.push(openInstance); + _this.emit("register"); + }; + this.deregister = function(openInstance) { + var index = _this.openInstances.indexOf(openInstance); + if (index === -1) { + return; + } + _this.openInstances.splice(index, 1); + _this.emit("deregister"); + }; + this.subscribe = function(callback) { + _this.subscribers.push(callback); + }; + this.emit = function(eventType) { + _this.subscribers.forEach(function(subscriber) { + return subscriber( + eventType, + // shallow copy to avoid accidental mutation + _this.openInstances.slice() + ); + }); + }; + this.openInstances = []; + this.subscribers = []; +}; +var portalOpenInstances = new PortalOpenInstances(); +function log$1() { + console.log("portalOpenInstances ----------"); + console.log(portalOpenInstances.openInstances.length); + portalOpenInstances.openInstances.forEach(function(p) { + return console.log(p); + }); + console.log("end portalOpenInstances ----------"); +} +function resetState$1() { + portalOpenInstances = new PortalOpenInstances(); +} +portalOpenInstances$1.default = portalOpenInstances; + +var bodyTrap$1 = {}; + +Object.defineProperty(bodyTrap$1, "__esModule", { + value: true +}); +bodyTrap$1.resetState = resetState; +bodyTrap$1.log = log; +var _portalOpenInstances = portalOpenInstances$1; +var _portalOpenInstances2 = _interopRequireDefault$f(_portalOpenInstances); +function _interopRequireDefault$f(obj) { + return obj && obj.__esModule ? obj : { default: obj }; +} +var before = void 0, after = void 0, instances = []; +function resetState() { + var _arr = [before, after]; + for (var _i = 0; _i < _arr.length; _i++) { + var item = _arr[_i]; + if (!item) continue; + item.parentNode && item.parentNode.removeChild(item); + } + before = after = null; + instances = []; +} +function log() { + console.log("bodyTrap ----------"); + console.log(instances.length); + var _arr2 = [before, after]; + for (var _i2 = 0; _i2 < _arr2.length; _i2++) { + var item = _arr2[_i2]; + var check = item || {}; + console.log(check.nodeName, check.className, check.id); + } + console.log("edn bodyTrap ----------"); +} +function focusContent() { + if (instances.length === 0) { + return; + } + instances[instances.length - 1].focusContent(); +} +function bodyTrap(eventType, openInstances) { + if (!before && !after) { + before = document.createElement("div"); + before.setAttribute("data-react-modal-body-trap", ""); + before.style.position = "absolute"; + before.style.opacity = "0"; + before.setAttribute("tabindex", "0"); + before.addEventListener("focus", focusContent); + after = before.cloneNode(); + after.addEventListener("focus", focusContent); + } + instances = openInstances; + if (instances.length > 0) { + if (document.body.firstChild !== before) { + document.body.insertBefore(before, document.body.firstChild); + } + if (document.body.lastChild !== after) { + document.body.appendChild(after); + } + } else { + if (before.parentElement) { + before.parentElement.removeChild(before); + } + if (after.parentElement) { + after.parentElement.removeChild(after); + } + } +} +_portalOpenInstances2.default.subscribe(bodyTrap); + +(function (module, exports) { + Object.defineProperty(exports, "__esModule", { + value: true + }); + var _extends = Object.assign || function(target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + for (var key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + return target; + }; + var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function(obj) { + return typeof obj; + } : function(obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; + }; + var _createClass = /* @__PURE__ */ function() { + function defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } + } + return function(Constructor, protoProps, staticProps) { + if (protoProps) defineProperties(Constructor.prototype, protoProps); + if (staticProps) defineProperties(Constructor, staticProps); + return Constructor; + }; + }(); + var _react = reactExports; + var _propTypes = propTypesExports; + var _propTypes2 = _interopRequireDefault(_propTypes); + var _focusManager = focusManager; + var focusManager$1 = _interopRequireWildcard(_focusManager); + var _scopeTab = scopeTabExports; + var _scopeTab2 = _interopRequireDefault(_scopeTab); + var _ariaAppHider = ariaAppHider$1; + var ariaAppHider = _interopRequireWildcard(_ariaAppHider); + var _classList = classList$1; + var classList = _interopRequireWildcard(_classList); + var _safeHTMLElement = safeHTMLElement; + var _safeHTMLElement2 = _interopRequireDefault(_safeHTMLElement); + var _portalOpenInstances = portalOpenInstances$1; + var _portalOpenInstances2 = _interopRequireDefault(_portalOpenInstances); + + function _interopRequireWildcard(obj) { + if (obj && obj.__esModule) { + return obj; + } else { + var newObj = {}; + if (obj != null) { + for (var key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; + } + } + newObj.default = obj; + return newObj; + } + } + function _interopRequireDefault(obj) { + return obj && obj.__esModule ? obj : { default: obj }; + } + function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } + } + function _possibleConstructorReturn(self, call) { + if (!self) { + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + } + return call && (typeof call === "object" || typeof call === "function") ? call : self; + } + function _inherits(subClass, superClass) { + if (typeof superClass !== "function" && superClass !== null) { + throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); + } + subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); + if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; + } + var CLASS_NAMES = { + overlay: "ReactModal__Overlay", + content: "ReactModal__Content" + }; + var isTabKey = function isTabKey2(event) { + return event.code === "Tab" || event.keyCode === 9; + }; + var isEscKey = function isEscKey2(event) { + return event.code === "Escape" || event.keyCode === 27; + }; + var ariaHiddenInstances = 0; + var ModalPortal = function(_Component) { + _inherits(ModalPortal2, _Component); + function ModalPortal2(props) { + _classCallCheck(this, ModalPortal2); + var _this = _possibleConstructorReturn(this, (ModalPortal2.__proto__ || Object.getPrototypeOf(ModalPortal2)).call(this, props)); + _this.setOverlayRef = function(overlay) { + _this.overlay = overlay; + _this.props.overlayRef && _this.props.overlayRef(overlay); + }; + _this.setContentRef = function(content) { + _this.content = content; + _this.props.contentRef && _this.props.contentRef(content); + }; + _this.afterClose = function() { + var _this$props = _this.props, appElement = _this$props.appElement, ariaHideApp = _this$props.ariaHideApp, htmlOpenClassName = _this$props.htmlOpenClassName, bodyOpenClassName = _this$props.bodyOpenClassName, parentSelector = _this$props.parentSelector; + var parentDocument = parentSelector && parentSelector().ownerDocument || document; + bodyOpenClassName && classList.remove(parentDocument.body, bodyOpenClassName); + htmlOpenClassName && classList.remove(parentDocument.getElementsByTagName("html")[0], htmlOpenClassName); + if (ariaHideApp && ariaHiddenInstances > 0) { + ariaHiddenInstances -= 1; + if (ariaHiddenInstances === 0) { + ariaAppHider.show(appElement); + } + } + if (_this.props.shouldFocusAfterRender) { + if (_this.props.shouldReturnFocusAfterClose) { + focusManager$1.returnFocus(_this.props.preventScroll); + focusManager$1.teardownScopedFocus(); + } else { + focusManager$1.popWithoutFocus(); + } + } + if (_this.props.onAfterClose) { + _this.props.onAfterClose(); + } + _portalOpenInstances2.default.deregister(_this); + }; + _this.open = function() { + _this.beforeOpen(); + if (_this.state.afterOpen && _this.state.beforeClose) { + clearTimeout(_this.closeTimer); + _this.setState({ beforeClose: false }); + } else { + if (_this.props.shouldFocusAfterRender) { + focusManager$1.setupScopedFocus(_this.node); + focusManager$1.markForFocusLater(); + } + _this.setState({ isOpen: true }, function() { + _this.openAnimationFrame = requestAnimationFrame(function() { + _this.setState({ afterOpen: true }); + if (_this.props.isOpen && _this.props.onAfterOpen) { + _this.props.onAfterOpen({ + overlayEl: _this.overlay, + contentEl: _this.content + }); + } + }); + }); + } + }; + _this.close = function() { + if (_this.props.closeTimeoutMS > 0) { + _this.closeWithTimeout(); + } else { + _this.closeWithoutTimeout(); + } + }; + _this.focusContent = function() { + return _this.content && !_this.contentHasFocus() && _this.content.focus({ preventScroll: true }); + }; + _this.closeWithTimeout = function() { + var closesAt = Date.now() + _this.props.closeTimeoutMS; + _this.setState({ beforeClose: true, closesAt }, function() { + _this.closeTimer = setTimeout(_this.closeWithoutTimeout, _this.state.closesAt - Date.now()); + }); + }; + _this.closeWithoutTimeout = function() { + _this.setState({ + beforeClose: false, + isOpen: false, + afterOpen: false, + closesAt: null + }, _this.afterClose); + }; + _this.handleKeyDown = function(event) { + if (isTabKey(event)) { + (0, _scopeTab2.default)(_this.content, event); + } + if (_this.props.shouldCloseOnEsc && isEscKey(event)) { + event.stopPropagation(); + _this.requestClose(event); + } + }; + _this.handleOverlayOnClick = function(event) { + if (_this.shouldClose === null) { + _this.shouldClose = true; + } + if (_this.shouldClose && _this.props.shouldCloseOnOverlayClick) { + if (_this.ownerHandlesClose()) { + _this.requestClose(event); + } else { + _this.focusContent(); + } + } + _this.shouldClose = null; + }; + _this.handleContentOnMouseUp = function() { + _this.shouldClose = false; + }; + _this.handleOverlayOnMouseDown = function(event) { + if (!_this.props.shouldCloseOnOverlayClick && event.target == _this.overlay) { + event.preventDefault(); + } + }; + _this.handleContentOnClick = function() { + _this.shouldClose = false; + }; + _this.handleContentOnMouseDown = function() { + _this.shouldClose = false; + }; + _this.requestClose = function(event) { + return _this.ownerHandlesClose() && _this.props.onRequestClose(event); + }; + _this.ownerHandlesClose = function() { + return _this.props.onRequestClose; + }; + _this.shouldBeClosed = function() { + return !_this.state.isOpen && !_this.state.beforeClose; + }; + _this.contentHasFocus = function() { + return document.activeElement === _this.content || _this.content.contains(document.activeElement); + }; + _this.buildClassName = function(which, additional) { + var classNames = (typeof additional === "undefined" ? "undefined" : _typeof(additional)) === "object" ? additional : { + base: CLASS_NAMES[which], + afterOpen: CLASS_NAMES[which] + "--after-open", + beforeClose: CLASS_NAMES[which] + "--before-close" + }; + var className = classNames.base; + if (_this.state.afterOpen) { + className = className + " " + classNames.afterOpen; + } + if (_this.state.beforeClose) { + className = className + " " + classNames.beforeClose; + } + return typeof additional === "string" && additional ? className + " " + additional : className; + }; + _this.attributesFromObject = function(prefix, items) { + return Object.keys(items).reduce(function(acc, name) { + acc[prefix + "-" + name] = items[name]; + return acc; + }, {}); + }; + _this.state = { + afterOpen: false, + beforeClose: false + }; + _this.shouldClose = null; + _this.moveFromContentToOverlay = null; + return _this; + } + _createClass(ModalPortal2, [{ + key: "componentDidMount", + value: function componentDidMount() { + if (this.props.isOpen) { + this.open(); + } + } + }, { + key: "componentDidUpdate", + value: function componentDidUpdate(prevProps, prevState) { + if (this.props.isOpen && !prevProps.isOpen) { + this.open(); + } else if (!this.props.isOpen && prevProps.isOpen) { + this.close(); + } + if (this.props.shouldFocusAfterRender && this.state.isOpen && !prevState.isOpen) { + this.focusContent(); + } + } + }, { + key: "componentWillUnmount", + value: function componentWillUnmount() { + if (this.state.isOpen) { + this.afterClose(); + } + clearTimeout(this.closeTimer); + cancelAnimationFrame(this.openAnimationFrame); + } + }, { + key: "beforeOpen", + value: function beforeOpen() { + var _props = this.props, appElement = _props.appElement, ariaHideApp = _props.ariaHideApp, htmlOpenClassName = _props.htmlOpenClassName, bodyOpenClassName = _props.bodyOpenClassName, parentSelector = _props.parentSelector; + var parentDocument = parentSelector && parentSelector().ownerDocument || document; + bodyOpenClassName && classList.add(parentDocument.body, bodyOpenClassName); + htmlOpenClassName && classList.add(parentDocument.getElementsByTagName("html")[0], htmlOpenClassName); + if (ariaHideApp) { + ariaHiddenInstances += 1; + ariaAppHider.hide(appElement); + } + _portalOpenInstances2.default.register(this); + } + // Don't steal focus from inner elements + }, { + key: "render", + value: function render() { + var _props2 = this.props, id = _props2.id, className = _props2.className, overlayClassName = _props2.overlayClassName, defaultStyles = _props2.defaultStyles, children = _props2.children; + var contentStyles = className ? {} : defaultStyles.content; + var overlayStyles = overlayClassName ? {} : defaultStyles.overlay; + if (this.shouldBeClosed()) { + return null; + } + var overlayProps = { + ref: this.setOverlayRef, + className: this.buildClassName("overlay", overlayClassName), + style: _extends({}, overlayStyles, this.props.style.overlay), + onClick: this.handleOverlayOnClick, + onMouseDown: this.handleOverlayOnMouseDown + }; + var contentProps = _extends({ + id, + ref: this.setContentRef, + style: _extends({}, contentStyles, this.props.style.content), + className: this.buildClassName("content", className), + tabIndex: "-1", + onKeyDown: this.handleKeyDown, + onMouseDown: this.handleContentOnMouseDown, + onMouseUp: this.handleContentOnMouseUp, + onClick: this.handleContentOnClick, + role: this.props.role, + "aria-label": this.props.contentLabel + }, this.attributesFromObject("aria", _extends({ modal: true }, this.props.aria)), this.attributesFromObject("data", this.props.data || {}), { + "data-testid": this.props.testId + }); + var contentElement = this.props.contentElement(contentProps, children); + return this.props.overlayElement(overlayProps, contentElement); + } + }]); + return ModalPortal2; + }(_react.Component); + ModalPortal.defaultProps = { + style: { + overlay: {}, + content: {} + }, + defaultStyles: {} + }; + ModalPortal.propTypes = { + isOpen: _propTypes2.default.bool.isRequired, + defaultStyles: _propTypes2.default.shape({ + content: _propTypes2.default.object, + overlay: _propTypes2.default.object + }), + style: _propTypes2.default.shape({ + content: _propTypes2.default.object, + overlay: _propTypes2.default.object + }), + className: _propTypes2.default.oneOfType([_propTypes2.default.string, _propTypes2.default.object]), + overlayClassName: _propTypes2.default.oneOfType([_propTypes2.default.string, _propTypes2.default.object]), + parentSelector: _propTypes2.default.func, + bodyOpenClassName: _propTypes2.default.string, + htmlOpenClassName: _propTypes2.default.string, + ariaHideApp: _propTypes2.default.bool, + appElement: _propTypes2.default.oneOfType([_propTypes2.default.instanceOf(_safeHTMLElement2.default), _propTypes2.default.instanceOf(_safeHTMLElement.SafeHTMLCollection), _propTypes2.default.instanceOf(_safeHTMLElement.SafeNodeList), _propTypes2.default.arrayOf(_propTypes2.default.instanceOf(_safeHTMLElement2.default))]), + onAfterOpen: _propTypes2.default.func, + onAfterClose: _propTypes2.default.func, + onRequestClose: _propTypes2.default.func, + closeTimeoutMS: _propTypes2.default.number, + shouldFocusAfterRender: _propTypes2.default.bool, + shouldCloseOnOverlayClick: _propTypes2.default.bool, + shouldReturnFocusAfterClose: _propTypes2.default.bool, + preventScroll: _propTypes2.default.bool, + role: _propTypes2.default.string, + contentLabel: _propTypes2.default.string, + aria: _propTypes2.default.object, + data: _propTypes2.default.object, + children: _propTypes2.default.node, + shouldCloseOnEsc: _propTypes2.default.bool, + overlayRef: _propTypes2.default.func, + contentRef: _propTypes2.default.func, + id: _propTypes2.default.string, + overlayElement: _propTypes2.default.func, + contentElement: _propTypes2.default.func, + testId: _propTypes2.default.string + }; + exports.default = ModalPortal; + module.exports = exports["default"]; +} (ModalPortal, ModalPortal.exports)); + +var ModalPortalExports = ModalPortal.exports; + +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +function componentWillMount() { + // Call this.constructor.gDSFP to support sub-classes. + var state = this.constructor.getDerivedStateFromProps(this.props, this.state); + if (state !== null && state !== undefined) { + this.setState(state); + } +} + +function componentWillReceiveProps(nextProps) { + // Call this.constructor.gDSFP to support sub-classes. + // Use the setState() updater to ensure state isn't stale in certain edge cases. + function updater(prevState) { + var state = this.constructor.getDerivedStateFromProps(nextProps, prevState); + return state !== null && state !== undefined ? state : null; + } + // Binding "this" is important for shallow renderer support. + this.setState(updater.bind(this)); +} + +function componentWillUpdate(nextProps, nextState) { + try { + var prevProps = this.props; + var prevState = this.state; + this.props = nextProps; + this.state = nextState; + this.__reactInternalSnapshotFlag = true; + this.__reactInternalSnapshot = this.getSnapshotBeforeUpdate( + prevProps, + prevState + ); + } finally { + this.props = prevProps; + this.state = prevState; + } +} + +// React may warn about cWM/cWRP/cWU methods being deprecated. +// Add a flag to suppress these warnings for this special case. +componentWillMount.__suppressDeprecationWarning = true; +componentWillReceiveProps.__suppressDeprecationWarning = true; +componentWillUpdate.__suppressDeprecationWarning = true; + +function polyfill(Component) { + var prototype = Component.prototype; + + if (!prototype || !prototype.isReactComponent) { + throw new Error('Can only polyfill class components'); + } + + if ( + typeof Component.getDerivedStateFromProps !== 'function' && + typeof prototype.getSnapshotBeforeUpdate !== 'function' + ) { + return Component; + } + + // If new component APIs are defined, "unsafe" lifecycles won't be called. + // Error if any of these lifecycles are present, + // Because they would work differently between older and newer (16.3+) versions of React. + var foundWillMountName = null; + var foundWillReceivePropsName = null; + var foundWillUpdateName = null; + if (typeof prototype.componentWillMount === 'function') { + foundWillMountName = 'componentWillMount'; + } else if (typeof prototype.UNSAFE_componentWillMount === 'function') { + foundWillMountName = 'UNSAFE_componentWillMount'; + } + if (typeof prototype.componentWillReceiveProps === 'function') { + foundWillReceivePropsName = 'componentWillReceiveProps'; + } else if (typeof prototype.UNSAFE_componentWillReceiveProps === 'function') { + foundWillReceivePropsName = 'UNSAFE_componentWillReceiveProps'; + } + if (typeof prototype.componentWillUpdate === 'function') { + foundWillUpdateName = 'componentWillUpdate'; + } else if (typeof prototype.UNSAFE_componentWillUpdate === 'function') { + foundWillUpdateName = 'UNSAFE_componentWillUpdate'; + } + if ( + foundWillMountName !== null || + foundWillReceivePropsName !== null || + foundWillUpdateName !== null + ) { + var componentName = Component.displayName || Component.name; + var newApiName = + typeof Component.getDerivedStateFromProps === 'function' + ? 'getDerivedStateFromProps()' + : 'getSnapshotBeforeUpdate()'; + + throw Error( + 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + + componentName + + ' uses ' + + newApiName + + ' but also contains the following legacy lifecycles:' + + (foundWillMountName !== null ? '\n ' + foundWillMountName : '') + + (foundWillReceivePropsName !== null + ? '\n ' + foundWillReceivePropsName + : '') + + (foundWillUpdateName !== null ? '\n ' + foundWillUpdateName : '') + + '\n\nThe above lifecycles should be removed. Learn more about this warning here:\n' + + 'https://fb.me/react-async-component-lifecycle-hooks' + ); + } + + // React <= 16.2 does not support static getDerivedStateFromProps. + // As a workaround, use cWM and cWRP to invoke the new static lifecycle. + // Newer versions of React will ignore these lifecycles if gDSFP exists. + if (typeof Component.getDerivedStateFromProps === 'function') { + prototype.componentWillMount = componentWillMount; + prototype.componentWillReceiveProps = componentWillReceiveProps; + } + + // React <= 16.2 does not support getSnapshotBeforeUpdate. + // As a workaround, use cWU to invoke the new lifecycle. + // Newer versions of React will ignore that lifecycle if gSBU exists. + if (typeof prototype.getSnapshotBeforeUpdate === 'function') { + if (typeof prototype.componentDidUpdate !== 'function') { + throw new Error( + 'Cannot polyfill getSnapshotBeforeUpdate() for components that do not define componentDidUpdate() on the prototype' + ); + } + + prototype.componentWillUpdate = componentWillUpdate; + + var componentDidUpdate = prototype.componentDidUpdate; + + prototype.componentDidUpdate = function componentDidUpdatePolyfill( + prevProps, + prevState, + maybeSnapshot + ) { + // 16.3+ will not execute our will-update method; + // It will pass a snapshot value to did-update though. + // Older versions will require our polyfilled will-update value. + // We need to handle both cases, but can't just check for the presence of "maybeSnapshot", + // Because for <= 15.x versions this might be a "prevContext" object. + // We also can't just check "__reactInternalSnapshot", + // Because get-snapshot might return a falsy value. + // So check for the explicit __reactInternalSnapshotFlag flag to determine behavior. + var snapshot = this.__reactInternalSnapshotFlag + ? this.__reactInternalSnapshot + : maybeSnapshot; + + componentDidUpdate.call(this, prevProps, prevState, snapshot); + }; + } + + return Component; +} + +const reactLifecyclesCompat_es = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({ + __proto__: null, + polyfill +}, Symbol.toStringTag, { value: 'Module' })); + +const require$$6 = /*@__PURE__*/getAugmentedNamespace(reactLifecyclesCompat_es); + +Object.defineProperty(Modal$2, "__esModule", { + value: true +}); +Modal$2.bodyOpenClassName = Modal$2.portalClassName = void 0; +var _extends$1 = Object.assign || function(target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + for (var key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + return target; +}; +var _createClass = /* @__PURE__ */ function() { + function defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } + } + return function(Constructor, protoProps, staticProps) { + if (protoProps) defineProperties(Constructor.prototype, protoProps); + if (staticProps) defineProperties(Constructor, staticProps); + return Constructor; + }; +}(); +var _react = reactExports; +var _react2 = _interopRequireDefault$e(_react); +var _reactDom = reactDomExports; +var _reactDom2 = _interopRequireDefault$e(_reactDom); +var _propTypes = propTypesExports; +var _propTypes2 = _interopRequireDefault$e(_propTypes); +var _ModalPortal = ModalPortalExports; +var _ModalPortal2 = _interopRequireDefault$e(_ModalPortal); +var _ariaAppHider = ariaAppHider$1; +var ariaAppHider = _interopRequireWildcard$1(_ariaAppHider); +var _safeHTMLElement = safeHTMLElement; +var _safeHTMLElement2 = _interopRequireDefault$e(_safeHTMLElement); +var _reactLifecyclesCompat = require$$6; +function _interopRequireWildcard$1(obj) { + if (obj && obj.__esModule) { + return obj; + } else { + var newObj = {}; + if (obj != null) { + for (var key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; + } + } + newObj.default = obj; + return newObj; + } +} +function _interopRequireDefault$e(obj) { + return obj && obj.__esModule ? obj : { default: obj }; +} +function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } +} +function _possibleConstructorReturn(self, call) { + if (!self) { + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + } + return call && (typeof call === "object" || typeof call === "function") ? call : self; +} +function _inherits(subClass, superClass) { + if (typeof superClass !== "function" && superClass !== null) { + throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); + } + subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); + if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; +} +var portalClassName = Modal$2.portalClassName = "ReactModalPortal"; +var bodyOpenClassName = Modal$2.bodyOpenClassName = "ReactModal__Body--open"; +var isReact16 = _safeHTMLElement.canUseDOM && _reactDom2.default.createPortal !== void 0; +var createHTMLElement = function createHTMLElement2(name) { + return document.createElement(name); +}; +var getCreatePortal = function getCreatePortal2() { + return isReact16 ? _reactDom2.default.createPortal : _reactDom2.default.unstable_renderSubtreeIntoContainer; +}; +function getParentElement(parentSelector2) { + return parentSelector2(); +} +var Modal$1 = function(_Component) { + _inherits(Modal2, _Component); + function Modal2() { + var _ref; + var _temp, _this, _ret; + _classCallCheck(this, Modal2); + for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref = Modal2.__proto__ || Object.getPrototypeOf(Modal2)).call.apply(_ref, [this].concat(args))), _this), _this.removePortal = function() { + !isReact16 && _reactDom2.default.unmountComponentAtNode(_this.node); + var parent = getParentElement(_this.props.parentSelector); + if (parent && parent.contains(_this.node)) { + parent.removeChild(_this.node); + } else { + console.warn('React-Modal: "parentSelector" prop did not returned any DOM element. Make sure that the parent element is unmounted to avoid any memory leaks.'); + } + }, _this.portalRef = function(ref) { + _this.portal = ref; + }, _this.renderPortal = function(props) { + var createPortal = getCreatePortal(); + var portal = createPortal(_this, _react2.default.createElement(_ModalPortal2.default, _extends$1({ defaultStyles: Modal2.defaultStyles }, props)), _this.node); + _this.portalRef(portal); + }, _temp), _possibleConstructorReturn(_this, _ret); + } + _createClass(Modal2, [{ + key: "componentDidMount", + value: function componentDidMount() { + if (!_safeHTMLElement.canUseDOM) return; + if (!isReact16) { + this.node = createHTMLElement("div"); + } + this.node.className = this.props.portalClassName; + var parent = getParentElement(this.props.parentSelector); + parent.appendChild(this.node); + !isReact16 && this.renderPortal(this.props); + } + }, { + key: "getSnapshotBeforeUpdate", + value: function getSnapshotBeforeUpdate(prevProps) { + var prevParent = getParentElement(prevProps.parentSelector); + var nextParent = getParentElement(this.props.parentSelector); + return { prevParent, nextParent }; + } + }, { + key: "componentDidUpdate", + value: function componentDidUpdate(prevProps, _, snapshot) { + if (!_safeHTMLElement.canUseDOM) return; + var _props = this.props, isOpen = _props.isOpen, portalClassName2 = _props.portalClassName; + if (prevProps.portalClassName !== portalClassName2) { + this.node.className = portalClassName2; + } + var prevParent = snapshot.prevParent, nextParent = snapshot.nextParent; + if (nextParent !== prevParent) { + prevParent.removeChild(this.node); + nextParent.appendChild(this.node); + } + if (!prevProps.isOpen && !isOpen) return; + !isReact16 && this.renderPortal(this.props); + } + }, { + key: "componentWillUnmount", + value: function componentWillUnmount() { + if (!_safeHTMLElement.canUseDOM || !this.node || !this.portal) return; + var state = this.portal.state; + var now = Date.now(); + var closesAt = state.isOpen && this.props.closeTimeoutMS && (state.closesAt || now + this.props.closeTimeoutMS); + if (closesAt) { + if (!state.beforeClose) { + this.portal.closeWithTimeout(); + } + setTimeout(this.removePortal, closesAt - now); + } else { + this.removePortal(); + } + } + }, { + key: "render", + value: function render() { + if (!_safeHTMLElement.canUseDOM || !isReact16) { + return null; + } + if (!this.node && isReact16) { + this.node = createHTMLElement("div"); + } + var createPortal = getCreatePortal(); + return createPortal(_react2.default.createElement(_ModalPortal2.default, _extends$1({ + ref: this.portalRef, + defaultStyles: Modal2.defaultStyles + }, this.props)), this.node); + } + }], [{ + key: "setAppElement", + value: function setAppElement(element) { + ariaAppHider.setElement(element); + } + /* eslint-disable react/no-unused-prop-types */ + /* eslint-enable react/no-unused-prop-types */ + }]); + return Modal2; +}(_react.Component); +Modal$1.propTypes = { + isOpen: _propTypes2.default.bool.isRequired, + style: _propTypes2.default.shape({ + content: _propTypes2.default.object, + overlay: _propTypes2.default.object + }), + portalClassName: _propTypes2.default.string, + bodyOpenClassName: _propTypes2.default.string, + htmlOpenClassName: _propTypes2.default.string, + className: _propTypes2.default.oneOfType([_propTypes2.default.string, _propTypes2.default.shape({ + base: _propTypes2.default.string.isRequired, + afterOpen: _propTypes2.default.string.isRequired, + beforeClose: _propTypes2.default.string.isRequired + })]), + overlayClassName: _propTypes2.default.oneOfType([_propTypes2.default.string, _propTypes2.default.shape({ + base: _propTypes2.default.string.isRequired, + afterOpen: _propTypes2.default.string.isRequired, + beforeClose: _propTypes2.default.string.isRequired + })]), + appElement: _propTypes2.default.oneOfType([_propTypes2.default.instanceOf(_safeHTMLElement2.default), _propTypes2.default.instanceOf(_safeHTMLElement.SafeHTMLCollection), _propTypes2.default.instanceOf(_safeHTMLElement.SafeNodeList), _propTypes2.default.arrayOf(_propTypes2.default.instanceOf(_safeHTMLElement2.default))]), + onAfterOpen: _propTypes2.default.func, + onRequestClose: _propTypes2.default.func, + closeTimeoutMS: _propTypes2.default.number, + ariaHideApp: _propTypes2.default.bool, + shouldFocusAfterRender: _propTypes2.default.bool, + shouldCloseOnOverlayClick: _propTypes2.default.bool, + shouldReturnFocusAfterClose: _propTypes2.default.bool, + preventScroll: _propTypes2.default.bool, + parentSelector: _propTypes2.default.func, + aria: _propTypes2.default.object, + data: _propTypes2.default.object, + role: _propTypes2.default.string, + contentLabel: _propTypes2.default.string, + shouldCloseOnEsc: _propTypes2.default.bool, + overlayRef: _propTypes2.default.func, + contentRef: _propTypes2.default.func, + id: _propTypes2.default.string, + overlayElement: _propTypes2.default.func, + contentElement: _propTypes2.default.func +}; +Modal$1.defaultProps = { + isOpen: false, + portalClassName, + bodyOpenClassName, + role: "dialog", + ariaHideApp: true, + closeTimeoutMS: 0, + shouldFocusAfterRender: true, + shouldCloseOnEsc: true, + shouldCloseOnOverlayClick: true, + shouldReturnFocusAfterClose: true, + preventScroll: false, + parentSelector: function parentSelector() { + return document.body; + }, + overlayElement: function overlayElement(props, contentEl) { + return _react2.default.createElement( + "div", + props, + contentEl + ); + }, + contentElement: function contentElement(props, children) { + return _react2.default.createElement( + "div", + props, + children + ); + } +}; +Modal$1.defaultStyles = { + overlay: { + position: "fixed", + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: "rgba(255, 255, 255, 0.75)" + }, + content: { + position: "absolute", + top: "40px", + left: "40px", + right: "40px", + bottom: "40px", + border: "1px solid #ccc", + background: "#fff", + overflow: "auto", + WebkitOverflowScrolling: "touch", + borderRadius: "4px", + outline: "none", + padding: "20px" + } +}; +(0, _reactLifecyclesCompat.polyfill)(Modal$1); +Modal$2.default = Modal$1; + +(function (module, exports) { + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _Modal = Modal$2; + + var _Modal2 = _interopRequireDefault(_Modal); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + exports.default = _Modal2.default; + module.exports = exports["default"]; +} (lib, lib.exports)); + +var libExports = lib.exports; +const Modal = /*@__PURE__*/getDefaultExportFromCjs(libExports); + +let isInitialized = false; +const initializeApp = () => { + if (isInitialized) { + return; + } + logger.debug("app", "🚀 App initializing", { + isDevMode: true === "true", + environment: "production" + }); + Modal.setAppElement("#root"); + isInitialized = true; +}; + +const themeOptions = { + palette: { + primary: { + main: "#1976d2", + light: "#42a5f5", + dark: "#1565c0", + contrastText: "#ffffff" + }, + secondary: { + main: "#dc004e", + light: "#ff4081", + dark: "#c51162", + contrastText: "#ffffff" + }, + error: { + main: "#f44336", + light: "#e57373", + dark: "#d32f2f" + }, + warning: { + main: "#ff9800", + light: "#ffb74d", + dark: "#f57c00" + }, + info: { + main: "#2196f3", + light: "#64b5f6", + dark: "#1976d2" + }, + success: { + main: "#4caf50", + light: "#81c784", + dark: "#388e3c" + }, + background: { + default: "#f5f5f5", + paper: "#ffffff" + }, + text: { + primary: "rgba(0, 0, 0, 0.87)", + secondary: "rgba(0, 0, 0, 0.6)", + disabled: "rgba(0, 0, 0, 0.38)" + } + }, + typography: { + fontFamily: [ + "-apple-system", + "BlinkMacSystemFont", + '"Segoe UI"', + "Roboto", + '"Helvetica Neue"', + "Arial", + "sans-serif" + ].join(","), + h1: { + fontSize: "2.5rem", + fontWeight: 500 + }, + h2: { + fontSize: "2rem", + fontWeight: 500 + }, + h3: { + fontSize: "1.75rem", + fontWeight: 500 + }, + h4: { + fontSize: "1.5rem", + fontWeight: 500 + }, + h5: { + fontSize: "1.25rem", + fontWeight: 500 + }, + h6: { + fontSize: "1rem", + fontWeight: 500 + }, + body1: { + fontSize: "1rem", + lineHeight: 1.5 + }, + body2: { + fontSize: "0.875rem", + lineHeight: 1.43 + } + }, + shape: { + borderRadius: 4 + }, + components: { + MuiButton: { + styleOverrides: { + root: { + textTransform: "none", + borderRadius: "4px", + padding: "6px 16px" + }, + contained: { + boxShadow: "none", + "&:hover": { + boxShadow: "0px 2px 4px -1px rgba(0,0,0,0.2)" + } + } + } + }, + MuiTextField: { + styleOverrides: { + root: { + "& .MuiOutlinedInput-root": { + borderRadius: "4px" + } + } + } + }, + MuiCard: { + styleOverrides: { + root: { + borderRadius: "8px", + boxShadow: "0px 2px 4px -1px rgba(0,0,0,0.1)" + } + } + }, + MuiAppBar: { + styleOverrides: { + root: { + boxShadow: "0px 1px 3px rgba(0,0,0,0.12)" + } + } + } + }, + breakpoints: { + values: { + xs: 0, + sm: 600, + md: 960, + lg: 1280, + xl: 1920 + } + } +}; +const theme = createTheme(themeOptions); + +const supabaseUrl = "http://localhost:8000"; +const supabaseAnonKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0"; +logger.info("supabase-client", "🔄 Supabase configuration", { + url: supabaseUrl, + key: supabaseAnonKey +}); +let supabaseInstance = null; +const getSupabaseClient = () => { + if (!supabaseInstance) { + logger.info("supabase-client", "🔄 Initializing Supabase client"); + supabaseInstance = createClient( + supabaseUrl, + supabaseAnonKey, + { + auth: { + flowType: "pkce", + autoRefreshToken: true, + persistSession: true, + detectSessionInUrl: false, + storage: window.localStorage, + storageKey: "supabase.auth.token", + debug: true + }, + global: { + headers: { + "X-Client-Info": "classroom-copilot" + } + } + } + ); + logger.info("supabase-client", "🔄 Supabase client configuration loaded", { + url: supabaseUrl, + hasKey: true, + storageKey: "supabase.auth.token" + }); + } + return supabaseInstance; +}; +const supabase = new Proxy({}, { + get: (target, prop) => { + const client = getSupabaseClient(); + return client[prop]; + } +}); + +var StorageKeys = /* @__PURE__ */ ((StorageKeys2) => { + StorageKeys2["USER"] = "user"; + StorageKeys2["USER_ROLE"] = "user_role"; + StorageKeys2["SUPABASE_TOKEN"] = "supabase_token"; + StorageKeys2["MS_TOKEN"] = "msAccessToken"; + StorageKeys2["NEO4J_USER_DB"] = "neo4jUserDbName"; + StorageKeys2["NEO4J_WORKER_DB"] = "neo4jWorkerDbName"; + StorageKeys2["USER_NODES"] = "userNodes"; + StorageKeys2["CALENDAR_DATA"] = "calendarData"; + StorageKeys2["IS_NEW_REGISTRATION"] = "isNewRegistration"; + StorageKeys2["TLDRAW_PREFERENCES"] = "tldrawUserPreferences"; + StorageKeys2["TLDRAW_FILE_PATH"] = "tldrawUserFilePath"; + StorageKeys2["LOCAL_SNAPSHOT"] = "localSnapshot"; + StorageKeys2["NODE_FILE_PATH"] = "nodeFilePath"; + StorageKeys2["ONENOTE_NOTEBOOK"] = "oneNoteNotebook"; + StorageKeys2["PRESENTATION_MODE"] = "presentationMode"; + StorageKeys2["TLDRAW_USER"] = "tldrawUser"; + return StorageKeys2; +})(StorageKeys || {}); +class StorageService { + static instance; + constructor() { + } + static getInstance() { + if (!StorageService.instance) { + StorageService.instance = new StorageService(); + } + return StorageService.instance; + } + get(key) { + try { + const item = localStorage.getItem(key); + return item ? JSON.parse(item) : null; + } catch (error) { + logger.error("storage-service", `Error retrieving ${key}:`, error); + return null; + } + } + set(key, value) { + try { + const serializedValue = JSON.stringify(value); + localStorage.setItem(key, serializedValue); + logger.debug("storage-service", `Stored ${key} in localStorage`); + } catch (error) { + logger.error("storage-service", `Error storing ${key}:`, error); + } + } + remove(key) { + try { + localStorage.removeItem(key); + logger.debug("storage-service", `Removed ${key} from localStorage`); + } catch (error) { + logger.error("storage-service", `Error removing ${key}:`, error); + } + } + clearAll() { + try { + Object.values(StorageKeys).forEach((key) => { + localStorage.removeItem(key); + }); + logger.debug("storage-service", "Cleared all app items from localStorage"); + } catch (error) { + logger.error("storage-service", "Error clearing storage:", error); + } + } + // Helper method to update state and storage together + setStateAndStorage(setter, key, value) { + setter(value); + this.set(key, value); + } +} +const storageService = StorageService.getInstance(); + +class DatabaseNameService { + static CC_USERS = "cc.users"; + static CC_SCHOOLS = "cc.institutes"; + static getUserPrivateDB(userType, username) { + const dbName = `${this.CC_USERS}.${userType}.${username}`; + logger.debug("database-name-service", "📥 Generating user private DB name", { + userType, + username, + dbName + }); + return dbName; + } + static getSchoolPrivateDB(schoolId) { + const dbName = `${this.CC_SCHOOLS}.${schoolId}`; + logger.debug("database-name-service", "📥 Generating school private DB name", { + schoolId, + dbName + }); + return dbName; + } + static getDevelopmentSchoolDB() { + const dbName = `${this.CC_SCHOOLS}.development.default`; + logger.debug("database-name-service", "📥 Getting default school DB name", { + dbName + }); + return dbName; + } + static getContextDatabase(context, userType, username) { + logger.debug("database-name-service", "📥 Resolving context database", { + context, + userType, + username + }); + if (["school", "department", "class"].includes(context)) { + logger.debug("database-name-service", "✅ Using schools database for context", { + context, + dbName: this.CC_SCHOOLS + }); + return this.CC_SCHOOLS; + } + const userDb = this.getUserPrivateDB(userType, username); + logger.debug("database-name-service", "✅ Using user private database for context", { + context, + dbName: userDb + }); + return userDb; + } +} + +function convertToCCUser(user, metadata) { + const username = metadata.username || metadata.preferred_username || metadata.email?.split("@")[0] || user.email?.split("@")[0] || "user"; + const displayName = metadata.display_name || metadata.name || metadata.preferred_username || username; + const userType = metadata.user_type || "student"; + const userDbName = DatabaseNameService.getUserPrivateDB( + userType, + username + ); + const schoolDbName = DatabaseNameService.getDevelopmentSchoolDB(); + return { + id: user.id, + email: user.email, + user_type: userType, + username, + display_name: displayName, + user_db_name: userDbName, + school_db_name: schoolDbName, + created_at: user.created_at, + updated_at: user.updated_at + }; +} +class AuthService { + static instance; + constructor() { + } + onAuthStateChange(callback) { + return supabase.auth.onAuthStateChange((event, session) => { + logger.info("auth-service", "🔄 Auth state changed", { + event, + hasSession: !!session, + userId: session?.user?.id, + eventType: event + }); + if (event === "SIGNED_OUT") { + storageService.clearAll(); + } + callback(event, session); + }); + } + static getInstance() { + if (!AuthService.instance) { + AuthService.instance = new AuthService(); + } + return AuthService.instance; + } + async getCurrentSession() { + try { + const { + data: { session }, + error + } = await supabase.auth.getSession(); + if (error) { + throw error; + } + if (!session) { + return { user: null, accessToken: null, message: "No active session" }; + } + return { + user: convertToCCUser( + session.user, + session.user.user_metadata + ), + accessToken: session.access_token, + message: "Session retrieved" + }; + } catch (error) { + logger.error("auth-service", "Failed to get current session:", error); + throw error; + } + } + async getCurrentUser() { + try { + const { + data: { user }, + error + } = await supabase.auth.getUser(); + if (error || !user) { + return null; + } + return convertToCCUser(user, user.user_metadata); + } catch (error) { + logger.error("auth-service", "Failed to get current user:", error); + return null; + } + } + async login({ + email, + password, + role + }) { + try { + logger.info("auth-service", "🔄 Attempting login", { + email, + role + }); + const { data, error } = await supabase.auth.signInWithPassword({ + email, + password + }); + if (error) { + logger.error("auth-service", "❌ Supabase auth error", { + error: error.message, + status: error.status + }); + throw error; + } + if (!data.session) { + logger.error("auth-service", "❌ No session after login"); + throw new Error("No session after login"); + } + const ccUser = convertToCCUser( + data.user, + data.user.user_metadata + ); + storageService.set(StorageKeys.USER_ROLE, ccUser.user_type); + storageService.set(StorageKeys.USER, ccUser); + storageService.set(StorageKeys.SUPABASE_TOKEN, data.session.access_token); + logger.info("auth-service", "✅ Login successful", { + userId: ccUser.id, + role: ccUser.user_type, + username: ccUser.username + }); + return { + user: ccUser, + accessToken: data.session.access_token, + userRole: ccUser.user_type, + message: "Login successful" + }; + } catch (error) { + logger.error("auth-service", "❌ Login failed:", error); + throw error; + } + } + async logout() { + try { + logger.debug("auth-service", "🔄 Attempting logout"); + const { error } = await supabase.auth.signOut({ scope: "local" }); + if (error) { + logger.error("auth-service", "❌ Logout failed:", error); + throw error; + } + storageService.clearAll(); + await supabase.auth.refreshSession(); + logger.debug("auth-service", "✅ Logout successful"); + } catch (error) { + logger.error("auth-service", "❌ Logout failed:", error); + throw error; + } + } +} +const authService = AuthService.getInstance(); + +const AuthContext = reactExports.createContext({ + user: null, + user_role: null, + loading: true, + error: null, + signIn: async () => { + }, + signOut: async () => { + }, + clearError: () => { + } +}); +function AuthProvider({ children }) { + const navigate = useNavigate(); + const [user, setUser] = reactExports.useState(null); + const [user_role, setUserRole] = reactExports.useState(null); + const [loading, setLoading] = reactExports.useState(true); + const [error, setError] = reactExports.useState(null); + reactExports.useEffect(() => { + const loadUser = async () => { + try { + const { data: { user: user2 } } = await supabase.auth.getUser(); + if (user2) { + const metadata = user2.user_metadata; + setUser({ + id: user2.id, + email: user2.email, + user_type: metadata.user_type || "", + username: metadata.username || "", + display_name: metadata.display_name || "", + user_db_name: `cc.users.${metadata.user_type}.${metadata.username}`, + school_db_name: "cc.institutes.development.default", + created_at: user2.created_at, + updated_at: user2.updated_at + }); + setUserRole(metadata.user_role || null); + } else { + setUser(null); + } + } catch (error2) { + logger.error("auth-context", "❌ Failed to load user", { error: error2 }); + setError(error2 instanceof Error ? error2 : new Error("Failed to load user")); + } finally { + setLoading(false); + } + }; + loadUser(); + const { data: { subscription } } = supabase.auth.onAuthStateChange(async (event, session) => { + if (event === "SIGNED_IN" && session?.user) { + const metadata = session.user.user_metadata; + setUser({ + id: session.user.id, + email: session.user.email, + user_type: metadata.user_type || "", + username: metadata.username || "", + display_name: metadata.display_name || "", + user_db_name: `cc.users.${metadata.user_type}.${metadata.username}`, + school_db_name: "cc.institutes.development.default", + created_at: session.user.created_at, + updated_at: session.user.updated_at + }); + } else if (event === "SIGNED_OUT") { + setUser(null); + } + }); + return () => { + subscription.unsubscribe(); + }; + }, []); + const signIn = async (email, password) => { + try { + setLoading(true); + const { data, error: signInError } = await supabase.auth.signInWithPassword({ + email, + password + }); + if (signInError) throw signInError; + if (data.user) { + const metadata = data.user.user_metadata; + setUser({ + id: data.user.id, + email: data.user.email, + user_type: metadata.user_type || "", + username: metadata.username || "", + display_name: metadata.display_name || "", + user_db_name: `cc.users.${metadata.user_type}.${metadata.username}`, + school_db_name: "cc.institutes.development.default", + created_at: data.user.created_at, + updated_at: data.user.updated_at + }); + } + } catch (error2) { + logger.error("auth-context", "❌ Sign in failed", { error: error2 }); + setError(error2 instanceof Error ? error2 : new Error("Sign in failed")); + throw error2; + } finally { + setLoading(false); + } + }; + const signOut = async () => { + try { + setLoading(true); + await authService.logout(); + setUser(null); + navigate("/"); + } catch (error2) { + logger.error("auth-context", "❌ Sign out failed", { error: error2 }); + setError(error2 instanceof Error ? error2 : new Error("Sign out failed")); + throw error2; + } finally { + setLoading(false); + } + }; + const clearError = () => setError(null); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + AuthContext.Provider, + { + value: { + user, + user_role, + loading, + error, + signIn, + signOut, + clearError + }, + children + } + ); +} +const useAuth = () => reactExports.useContext(AuthContext); + +class PresentationService { + editor; + initialSlideshow = null; + cameraProxyId = createShapeId("camera-proxy"); + lastUserInteractionTime = 0; + USER_INTERACTION_DEBOUNCE = 1e3; + // 1 second + zoomLevels = /* @__PURE__ */ new Map(); + // Track zoom levels by shape dimensions + isMoving = false; + constructor(editor) { + this.editor = editor; + logger.debug("system", "🎥 PresentationService initialized"); + const style = document.createElement("style"); + style.setAttribute("data-camera-proxy", this.cameraProxyId); + style.textContent = ` + [data-shape-id="${this.cameraProxyId}"] { + opacity: 0 !important; + pointer-events: none !important; + } + `; + document.head.appendChild(style); + } + getShapeDimensionKey(width, height) { + return `${Math.round(width)}_${Math.round(height)}`; + } + async moveToShape(shape) { + if (this.isMoving) { + logger.debug("presentation", "⏳ Movement in progress, queueing next movement"); + await new Promise((resolve) => setTimeout(resolve, 100)); + return this.moveToShape(shape); + } + this.isMoving = true; + const bounds = this.editor.getShapePageBounds(shape.id); + if (!bounds) { + logger.warn("presentation", "⚠️ Could not get bounds for shape"); + this.isMoving = false; + return; + } + try { + this.editor.updateShape({ + id: this.cameraProxyId, + type: "frame", + x: bounds.minX, + y: bounds.minY, + props: { + w: bounds.width, + h: bounds.height, + name: "camera-proxy" + } + }); + await new Promise((resolve) => requestAnimationFrame(resolve)); + const viewport = this.editor.getViewportPageBounds(); + const padding = 32; + const dimensionKey = this.getShapeDimensionKey(bounds.width, bounds.height); + let targetZoom = this.zoomLevels.get(dimensionKey); + if (!targetZoom) { + targetZoom = Math.min( + (viewport.width - padding * 2) / bounds.width, + (viewport.height - padding * 2) / bounds.height + ); + this.zoomLevels.set(dimensionKey, targetZoom); + logger.debug("presentation", "📏 New zoom level calculated", { + dimensions: dimensionKey, + zoom: targetZoom + }); + } + this.editor.stopCameraAnimation(); + this.editor.zoomToBounds(bounds, { + animation: { + duration: 500, + easing: (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2 + }, + targetZoom, + inset: padding + }); + await new Promise((resolve) => setTimeout(resolve, 500)); + } catch (error) { + logger.error("presentation", "❌ Error during shape transition", { error }); + } finally { + this.isMoving = false; + } + } + startPresentationMode() { + logger.info("presentation", "🎥 Starting presentation mode"); + this.zoomLevels.clear(); + const slideshows = this.editor.getSortedChildIdsForParent(this.editor.getCurrentPageId()).map((id) => this.editor.getShape(id)).filter((shape) => shape?.type === "cc-slideshow"); + if (slideshows.length === 0) { + logger.warn("presentation", "⚠️ No slideshows found"); + return () => { + }; + } + this.initialSlideshow = slideshows[0]; + if (!this.editor.getShape(this.cameraProxyId)) { + this.editor.createShape({ + id: this.cameraProxyId, + type: "frame", + x: 0, + y: 0, + props: { + w: 1, + h: 1, + name: "camera-proxy" + } + }); + } + const handleStoreChange = (event) => { + if (event.source === "user") { + const now = Date.now(); + if (now - this.lastUserInteractionTime > this.USER_INTERACTION_DEBOUNCE) { + logger.debug("presentation", "📝 User interaction received"); + this.lastUserInteractionTime = now; + } + } + if (!event.changes.updated) return; + const shapeUpdates = Object.entries(event.changes.updated).filter( + ([, [from, to]]) => from.typeName === "shape" && to.typeName === "shape" && from.type === "cc-slideshow" && to.type === "cc-slideshow" + ); + if (shapeUpdates.length === 0) return; + for (const [, [from, to]] of shapeUpdates) { + const fromShape = from; + const toShape = to; + if (!this.initialSlideshow || fromShape.id !== this.initialSlideshow.id) continue; + const fromShow = fromShape; + const toShow = toShape; + if (fromShow.props.currentSlideIndex === toShow.props.currentSlideIndex) continue; + logger.info("presentation", "🔄 Moving to new slide", { + from: fromShow.props.currentSlideIndex, + to: toShow.props.currentSlideIndex + }); + const bindings = this.editor.getBindingsFromShape(toShow, "cc-slide-layout").filter((b) => b.type === "cc-slide-layout").filter((b) => !b.props.placeholder).sort((a, b) => a.props.index > b.props.index ? 1 : -1); + const currentBinding = bindings[toShow.props.currentSlideIndex]; + if (!currentBinding) { + logger.warn("presentation", "⚠️ Could not find binding for target slide"); + continue; + } + const currentSlide = this.editor.getShape(currentBinding.toId); + if (!currentSlide) { + logger.warn("presentation", "⚠️ Could not find target slide"); + continue; + } + void this.moveToShape(currentSlide); + } + }; + const storeCleanup = this.editor.store.listen(handleStoreChange); + return () => { + logger.info("presentation", "🧹 Running presentation mode cleanup"); + storeCleanup(); + this.stopPresentationMode(); + }; + } + stopPresentationMode() { + this.zoomLevels.clear(); + this.isMoving = false; + if (this.editor.getShape(this.cameraProxyId)) { + this.editor.deleteShape(this.cameraProxyId); + } + const style = document.querySelector(`style[data-camera-proxy="${this.cameraProxyId}"]`); + if (style) { + style.remove(); + } + } + // Public method to move to any shape (slide or slideshow) + zoomToShape(shape) { + void this.moveToShape(shape); + } +} + +const TLDrawContext = reactExports.createContext({ + tldrawPreferences: null, + tldrawUserFilePath: null, + localSnapshot: null, + presentationMode: false, + sharedStore: null, + connectionStatus: "online", + presentationService: null, + setTldrawPreferences: () => { + }, + setTldrawUserFilePath: () => { + }, + handleLocalSnapshot: async () => { + }, + togglePresentationMode: () => { + }, + initializePreferences: () => { + }, + setSharedStore: () => { + }, + setConnectionStatus: () => { + } +}); +const TLDrawProvider = ({ children }) => { + const [tldrawPreferences, setTldrawPreferencesState] = reactExports.useState( + storageService.get(StorageKeys.TLDRAW_PREFERENCES) + ); + const [tldrawUserFilePath, setTldrawUserFilePathState] = reactExports.useState( + storageService.get(StorageKeys.TLDRAW_FILE_PATH) + ); + const [localSnapshot, setLocalSnapshot] = reactExports.useState( + storageService.get(StorageKeys.LOCAL_SNAPSHOT) + ); + const [presentationMode, setPresentationMode] = reactExports.useState( + storageService.get(StorageKeys.PRESENTATION_MODE) || false + ); + const [sharedStore, setSharedStore] = reactExports.useState(null); + const [connectionStatus, setConnectionStatus] = reactExports.useState("online"); + const [presentationService, setPresentationService] = reactExports.useState(null); + const initializePreferences = reactExports.useCallback((userId) => { + logger.debug("tldraw-context", "🔄 Initializing TLDraw preferences"); + const storedPrefs = storageService.get(StorageKeys.TLDRAW_PREFERENCES); + if (storedPrefs) { + logger.debug("tldraw-context", "📥 Found stored preferences"); + setTldrawPreferencesState(storedPrefs); + return; + } + const defaultPrefs = { + id: userId, + name: "User", + color: `hsl(${Math.random() * 360}, 70%, 50%)`, + locale: "en", + colorScheme: "system", + isSnapMode: false, + isWrapMode: false, + isDynamicSizeMode: false, + isPasteAtCursorMode: false, + animationSpeed: 1, + edgeScrollSpeed: 1 + }; + logger.debug("tldraw-context", "📝 Creating default preferences"); + storageService.set(StorageKeys.TLDRAW_PREFERENCES, defaultPrefs); + setTldrawPreferencesState(defaultPrefs); + }, []); + const setTldrawPreferences = reactExports.useCallback((preferences) => { + logger.debug("tldraw-context", "🔄 Setting TLDraw preferences", { preferences }); + if (preferences) { + storageService.set(StorageKeys.TLDRAW_PREFERENCES, preferences); + } else { + storageService.remove(StorageKeys.TLDRAW_PREFERENCES); + } + setTldrawPreferencesState(preferences); + }, []); + const setTldrawUserFilePath = (path) => { + logger.debug("tldraw-context", "🔄 Setting TLDraw user file path"); + if (path) { + storageService.set(StorageKeys.TLDRAW_FILE_PATH, path); + } else { + storageService.remove(StorageKeys.TLDRAW_FILE_PATH); + } + setTldrawUserFilePathState(path); + }; + const handleLocalSnapshot = reactExports.useCallback(async (action, store, setLoadingState) => { + if (!store) { + setLoadingState({ status: "error", error: "Store not initialized" }); + return; + } + try { + if (sharedStore) { + if (action === "put") { + const snapshot = getSnapshot(store); + await sharedStore.saveSnapshot(snapshot, setLoadingState); + } else if (action === "get") { + const savedSnapshot = storageService.get(StorageKeys.LOCAL_SNAPSHOT); + if (savedSnapshot) { + await sharedStore.loadSnapshot(savedSnapshot, setLoadingState); + } + } + } else if (action === "put") { + logger.debug("tldraw-context", "💾 Putting snapshot into local storage"); + const snapshot = getSnapshot(store); + logger.debug("tldraw-context", "📦 Snapshot:", snapshot); + setLocalSnapshot(snapshot); + storageService.set(StorageKeys.LOCAL_SNAPSHOT, snapshot); + setLoadingState({ status: "ready", error: "" }); + } else if (action === "get") { + logger.debug("tldraw-context", "📂 Getting snapshot from local storage"); + setLoadingState({ status: "loading", error: "" }); + const savedSnapshot = storageService.get(StorageKeys.LOCAL_SNAPSHOT); + if (savedSnapshot && savedSnapshot.document && savedSnapshot.session) { + try { + logger.debug("tldraw-context", "📥 Loading snapshot into editor"); + loadSnapshot(store, savedSnapshot); + setLoadingState({ status: "ready", error: "" }); + } catch (error) { + logger.error("tldraw-context", "❌ Failed to load snapshot:", error); + store.clear(); + setLoadingState({ status: "error", error: "Failed to load snapshot" }); + } + } else { + logger.debug("tldraw-context", "⚠️ No valid snapshot found in local storage"); + setLoadingState({ status: "ready", error: "" }); + } + } + } catch (error) { + logger.error("tldraw-context", "❌ Error handling local snapshot:", error); + setLoadingState({ + status: "error", + error: error instanceof Error ? error.message : "Unknown error" + }); + } + }, [sharedStore]); + const togglePresentationMode = reactExports.useCallback((editor) => { + logger.debug("tldraw-context", "🔄 Toggling presentation mode"); + setPresentationMode((prev) => { + const newValue = !prev; + storageService.set(StorageKeys.PRESENTATION_MODE, newValue); + if (newValue && editor) { + logger.info("presentation", "🎥 Initializing presentation service"); + const service = new PresentationService(editor); + setPresentationService(service); + service.startPresentationMode(); + } else if (!newValue && presentationService) { + logger.info("presentation", "🛑 Stopping presentation service"); + presentationService.stopPresentationMode(); + setPresentationService(null); + } + return newValue; + }); + }, [presentationService]); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + TLDrawContext.Provider, + { + value: { + tldrawPreferences, + tldrawUserFilePath, + localSnapshot, + presentationMode, + sharedStore, + connectionStatus, + presentationService, + setTldrawPreferences, + setTldrawUserFilePath, + handleLocalSnapshot, + togglePresentationMode, + initializePreferences, + setSharedStore, + setConnectionStatus + }, + children + } + ); +}; +const useTLDraw = () => reactExports.useContext(TLDrawContext); + +const UserContext = reactExports.createContext({ + user: null, + loading: true, + error: null, + profile: null, + preferences: {}, + isMobile: false, + isInitialized: false, + updateProfile: async () => { + }, + updatePreferences: async () => { + }, + clearError: () => { + } +}); +function UserProvider({ children }) { + const [user] = reactExports.useState(null); + const [profile, setProfile] = reactExports.useState(null); + const [preferences, setPreferences] = reactExports.useState({}); + const [loading, setLoading] = reactExports.useState(true); + const [isInitialized, setIsInitialized] = reactExports.useState(false); + const [error, setError] = reactExports.useState(null); + const [isMobile] = reactExports.useState(window.innerWidth <= 768); + reactExports.useEffect(() => { + const loadUserProfile = async () => { + try { + const { data: { user: user2 } } = await supabase.auth.getUser(); + if (!user2) { + setProfile(null); + setLoading(false); + setIsInitialized(true); + return; + } + const { data, error: error2 } = await supabase.from("profiles").select("*").eq("id", user2.id).single(); + if (error2) { + throw error2; + } + const metadata = user2.user_metadata; + const userDbName = DatabaseNameService.getUserPrivateDB(metadata.user_type || "", metadata.username || ""); + const schoolDbName = DatabaseNameService.getDevelopmentSchoolDB(); + const userProfile = { + id: user2.id, + email: user2.email, + user_type: metadata.user_type || "", + username: metadata.username || "", + display_name: metadata.display_name || "", + user_db_name: userDbName, + school_db_name: schoolDbName, + created_at: user2.created_at, + updated_at: user2.updated_at + }; + setProfile(userProfile); + logger.debug("user-context", "✅ User profile loaded", { + userId: userProfile.id, + userType: userProfile.user_type, + username: userProfile.username, + userDbName: userProfile.user_db_name, + schoolDbName: userProfile.school_db_name + }); + setPreferences({ + theme: data.theme || "system", + notifications: data.notifications_enabled || false + }); + } catch (error2) { + logger.error("user-context", "❌ Failed to load user profile", { error: error2 }); + setError(error2 instanceof Error ? error2 : new Error("Failed to load user profile")); + } finally { + setLoading(false); + setIsInitialized(true); + } + }; + loadUserProfile(); + }, []); + const updateProfile = async (updates) => { + if (!user?.id || !profile) { + return; + } + setLoading(true); + try { + const { error: error2 } = await supabase.from("profiles").update({ + ...updates, + updated_at: (/* @__PURE__ */ new Date()).toISOString() + }).eq("id", user.id); + if (error2) { + throw error2; + } + setProfile((prev) => prev ? { ...prev, ...updates } : null); + logger.info("user-context", "✅ Profile updated successfully"); + } catch (error2) { + logger.error("user-context", "❌ Failed to update profile", { error: error2 }); + setError(error2 instanceof Error ? error2 : new Error("Failed to update profile")); + throw error2; + } finally { + setLoading(false); + } + }; + const updatePreferences = async (updates) => { + if (!user?.id) { + return; + } + setLoading(true); + try { + const newPreferences = { ...preferences, ...updates }; + setPreferences(newPreferences); + const { error: error2 } = await supabase.from("profiles").update({ + preferences: newPreferences, + updated_at: (/* @__PURE__ */ new Date()).toISOString() + }).eq("id", user.id); + if (error2) { + throw error2; + } + logger.info("user-context", "✅ Preferences updated successfully"); + } catch (error2) { + logger.error("user-context", "❌ Failed to update preferences", { error: error2 }); + setError(error2 instanceof Error ? error2 : new Error("Failed to update preferences")); + throw error2; + } finally { + setLoading(false); + } + }; + return /* @__PURE__ */ jsxRuntimeExports.jsx( + UserContext.Provider, + { + value: { + user: profile, + loading, + error, + profile, + preferences, + isMobile, + isInitialized, + updateProfile, + updatePreferences, + clearError: () => setError(null) + }, + children + } + ); +} +const useUser = () => reactExports.useContext(UserContext); + +const baseURL = "http://localhost:8001"; +const instance = axios$1.create({ + baseURL, + timeout: 12e4, + // Increase timeout to 120 seconds for large files + headers: { + "Content-Type": "application/json" + } +}); +instance.interceptors.request.use( + (config) => { + if (config.headers["Content-Type"] === "application/json" && config.data instanceof FormData) { + delete config.headers["Content-Type"]; + } + logger.debug("axios", "🔄 Outgoing request", { + method: config.method, + url: config.url, + baseURL: config.baseURL + }); + return config; + }, + (error) => { + logger.error("axios", "❌ Request error", error); + return Promise.reject(error); + } +); +instance.interceptors.response.use( + (response) => { + logger.debug("axios", "✅ Response received", { + status: response.status, + url: response.config.url + }); + return response; + }, + (error) => { + if (error.response) { + logger.error("axios", "❌ Response error", { + status: error.response.status, + url: error.config.url, + data: error.response.data + }); + } else if (error.request) { + logger.error("axios", "❌ No response received", { + url: error.config.url + }); + } else { + logger.error("axios", "❌ Request setup error", error.message); + } + return Promise.reject(error); + } +); +const { isAxiosError } = axios$1; +const axios = Object.assign(instance, { isAxiosError }); + +function formatEmailForDatabase(email) { + const sanitized = email.toLowerCase().replace("@", "at").replace(/\./g, "dot").replace(/_/g, "underscore").replace(/-/g, "dash"); + return `${sanitized}`; +} + +const DEV_SCHOOL_NAME = "default"; +const DEV_SCHOOL_GROUP = "development"; +const ADMIN_USER_NAME = "kcar"; +const ADMIN_USER_GROUP = "admin"; +class UserNeoDBService { + static async fetchUserNodesData(email, userDbName, workerDbName) { + try { + if (!userDbName) { + logger.error("neo4j-service", "❌ Attempted to fetch nodes without database name"); + return null; + } + const formattedEmail = formatEmailForDatabase(email); + const uniqueId = `User_${formattedEmail}`; + logger.debug("neo4j-service", "🔄 Fetching user nodes data", { + email, + formattedEmail, + userDbName, + workerDbName, + uniqueId + }); + const userNode = await this.getDefaultNode("profile", userDbName); + if (!userNode || !userNode.data) { + throw new Error("Failed to fetch user node or node data missing"); + } + logger.debug("neo4j-service", "✅ Found user node", { + nodeId: userNode.id, + type: userNode.type, + hasData: !!userNode.data, + userDbName, + workerDbName + }); + const processedNodes = { + privateUserNode: { + ...userNode.data, + __primarylabel__: "User", + title: userNode.data.user_email || "User", + w: 200, + h: 200, + headerColor: "#3e6589", + backgroundColor: "#f0f0f0", + isLocked: false + }, + connectedNodes: {} + }; + try { + const calendarNode = await this.getDefaultNode("calendar", userDbName); + if (calendarNode?.data) { + processedNodes.connectedNodes.calendar = { + ...calendarNode.data, + __primarylabel__: "Calendar", + title: calendarNode.data.calendar_name || "Calendar", + w: 200, + h: 200, + headerColor: "#3e6589", + backgroundColor: "#f0f0f0", + isLocked: false + }; + logger.debug("neo4j-service", "✅ Found calendar node", { + nodeId: calendarNode.id, + node_storage_path: calendarNode.data.node_storage_path + }); + } else { + logger.debug("neo4j-service", "ℹ️ No calendar node found"); + } + } catch (error) { + logger.warn("neo4j-service", "⚠️ Failed to fetch calendar node:", error); + } + if (workerDbName) { + try { + const teacherNode = await this.getDefaultNode("teaching", userDbName); + if (teacherNode?.data) { + processedNodes.connectedNodes.teacher = { + ...teacherNode.data, + __primarylabel__: "Teacher", + title: teacherNode.data.teacher_name_formal || "Teacher", + w: 200, + h: 200, + headerColor: "#3e6589", + backgroundColor: "#f0f0f0", + isLocked: false, + user_db_name: userDbName, + school_db_name: workerDbName + }; + logger.debug("neo4j-service", "✅ Found teacher node", { + nodeId: teacherNode.id, + node_storage_path: teacherNode.data.node_storage_path, + userDbName, + workerDbName + }); + } else { + logger.debug("neo4j-service", "ℹ️ No teacher node found"); + } + } catch (error) { + logger.warn("neo4j-service", "⚠️ Failed to fetch teacher node:", error); + } + } + logger.debug("neo4j-service", "✅ Processed all user nodes", { + hasUserNode: !!processedNodes.privateUserNode, + hasCalendar: !!processedNodes.connectedNodes.calendar, + hasTeacher: !!processedNodes.connectedNodes.teacher, + teacherData: processedNodes.connectedNodes.teacher ? { + uuid_string: processedNodes.connectedNodes.teacher.uuid_string, + school_db_name: processedNodes.connectedNodes.teacher.school_db_name, + node_storage_path: processedNodes.connectedNodes.teacher.node_storage_path + } : null + }); + return processedNodes; + } catch (error) { + if (error instanceof Error) { + logger.error("neo4j-service", "❌ Failed to fetch user nodes:", error.message); + } else { + logger.error("neo4j-service", "❌ Failed to fetch user nodes:", String(error)); + } + throw error; + } + } + static getUserDatabaseName(userType, username) { + return DatabaseNameService.getUserPrivateDB(userType, username); + } + static getSchoolDatabaseName(schoolId) { + return DatabaseNameService.getSchoolPrivateDB(schoolId); + } + static getDefaultSchoolDatabaseName() { + return DatabaseNameService.getDevelopmentSchoolDB(); + } + static async fetchNodeData(nodeId, dbName) { + try { + logger.debug("neo4j-service", "🔄 Fetching node data", { nodeId, dbName }); + const response = await axios.get("/database/tools/get-node", { + params: { + uuid_string: nodeId, + db_name: dbName + } + }); + if (response.data?.status === "success" && response.data.node) { + return response.data.node; + } + return null; + } catch (error) { + logger.error("neo4j-service", "❌ Failed to fetch node data:", error); + throw error; + } + } + static getNodeDatabaseName(node) { + if (!node || !node.node_storage_path) { + logger.error("neo4j-service", "❌ Invalid node or missing node_storage_path", { + node: node ? { id: node.id, type: node.type, label: node.label } : null, + hasStoragePath: !!node?.node_storage_path + }); + throw new Error("Node is missing required storage path information"); + } + if (node.node_storage_path.startsWith("users/")) { + const parts2 = node.node_storage_path.split("/"); + if (parts2.length >= 4) { + return parts2[3]; + } + logger.warn("neo4j-service", "⚠️ Unexpected user path format", { path: node.node_storage_path }); + return `cc.users.${ADMIN_USER_GROUP}.${ADMIN_USER_NAME}`; + } + if (node.node_storage_path.startsWith("schools/")) { + return `cc.institutes.${DEV_SCHOOL_GROUP}.${DEV_SCHOOL_NAME}`; + } + const parts = node.node_storage_path.split("/"); + if (parts.length >= 4) { + return parts[3]; + } + logger.warn("neo4j-service", "⚠️ Using fallback database name", { + path: node.node_storage_path, + nodeType: node.type + }); + return `cc.users.kcar`; + } + static async getDefaultNode(context, dbName) { + try { + logger.debug("neo4j-service", "🔄 Fetching default node", { context, dbName }); + const params = { db_name: dbName }; + if (context === "overview") { + const navigationStore = useNavigationStore.getState(); + params.base_context = navigationStore.context.base; + } + const response = await axios.get( + `/database/tools/get-default-node/${context}`, + { params } + ); + if (response.data?.status === "success" && response.data.node) { + return { + id: response.data.node.id, + node_storage_path: response.data.node.node_storage_path, + type: response.data.node.type, + label: response.data.node.label, + data: response.data.node.data + }; + } + return null; + } catch (error) { + logger.error("neo4j-service", "❌ Failed to fetch default node:", error); + throw error; + } + } + static async fetchCalendarStructure(dbName) { + try { + logger.debug("navigation", "🔄 Fetching calendar structure", { dbName }); + const response = await axios.get( + `/database/calendar-structure/get-calendar-structure?db_name=${dbName}` + ); + if (response.data.status === "success") { + logger.info("navigation", "✅ Calendar structure fetched successfully"); + return response.data.data; + } + throw new Error("Failed to fetch calendar structure"); + } catch (error) { + logger.error("navigation", "❌ Failed to fetch calendar structure:", error); + throw error; + } + } + static async fetchWorkerStructure(dbName) { + try { + logger.debug("navigation", "🔄 Fetching worker structure", { dbName }); + const response = await axios.get( + `/database/worker-structure/get-worker-structure?db_name=${dbName}` + ); + if (response.data.status === "success") { + logger.info("navigation", "✅ Worker structure fetched successfully"); + return response.data.data; + } + throw new Error("Failed to fetch worker structure"); + } catch (error) { + logger.error("navigation", "❌ Failed to fetch worker structure:", error); + throw error; + } + } +} + +const getShapeType = (nodeType) => { + return `cc-${nodeType.replace(/([A-Z])/g, "-$1").toLowerCase().substring(1)}-node`; +}; +const isValidNodeType = (type) => { + return type in { + User: true, + Developer: true, + Teacher: true, + Student: true, + Calendar: true, + TeacherTimetable: true, + TimetableLesson: true, + PlannedLesson: true, + School: true, + CalendarYear: true, + CalendarMonth: true, + CalendarWeek: true, + CalendarDay: true, + CalendarTimeChunk: true, + ScienceLab: true, + KeyStageSyllabus: true, + YearGroupSyllabus: true, + CurriculumStructure: true, + Topic: true, + TopicLesson: true, + LearningStatement: true, + SchoolTimetable: true, + AcademicYear: true, + AcademicTerm: true, + AcademicWeek: true, + AcademicDay: true, + AcademicPeriod: true, + RegistrationPeriod: true, + PastoralStructure: true, + KeyStage: true, + Department: true, + Room: true, + SubjectClass: true, + DepartmentStructure: true, + UserTeacherTimetable: true, + UserTimetableLesson: true + }; +}; + +const getCurrentHistoryNode = (history) => { + const node = history.currentIndex === -1 || !history.nodes.length ? null : history.nodes[history.currentIndex]; + logger.debug("history-management", "📍 Getting current history node", { + currentIndex: history.currentIndex, + totalNodes: history.nodes.length, + node + }); + return node; +}; +const addToHistory = (history, node) => { + logger.debug("history-management", "➕ Adding node to history", { + currentIndex: history.currentIndex, + newNode: node, + existingNodes: history.nodes.length + }); + const newNodes = [...history.nodes.slice(0, history.currentIndex + 1), node]; + const newHistory = { + nodes: newNodes, + currentIndex: newNodes.length - 1 + }; + logger.debug("history-management", "✅ History updated", { + previousState: history, + newState: newHistory + }); + return newHistory; +}; +const navigateHistory = (history, index) => { + logger.debug("history-management", "🔄 Navigating history", { + currentIndex: history.currentIndex, + targetIndex: index, + totalNodes: history.nodes.length + }); + if (index < 0 || index >= history.nodes.length) { + logger.warn("history-management", "⚠️ Invalid history navigation index", { + requestedIndex: index, + historyLength: history.nodes.length + }); + return history; + } + const newHistory = { + nodes: history.nodes, + currentIndex: index + }; + logger.debug("history-management", "✅ History navigation complete", { + from: history.currentIndex, + to: index, + node: history.nodes[index] + }); + return newHistory; +}; +const isProfileContext = (context) => { + return ["profile", "calendar", "teaching"].includes(context); +}; +const isInstituteContext = (context) => { + return ["school", "department", "class"].includes(context); +}; +const getContextDatabase = (context, userDbName, workerDbName) => { + logger.debug("navigation-context", "🔄 Getting context database", { + mainContext: context.main, + baseContext: context.base, + userDbName, + workerDbName + }); + if (context.main === "profile") { + if (!userDbName) { + logger.error("navigation-context", "❌ Missing user database name for profile context"); + throw new Error("User database name is required for profile context"); + } + logger.debug("navigation-context", "✅ Using user database", { dbName: userDbName }); + return userDbName; + } else { + if (!workerDbName) { + logger.error("navigation-context", "❌ Missing worker database name for institute context"); + throw new Error("Worker database name is required for institute context"); + } + logger.debug("navigation-context", "✅ Using worker database", { dbName: workerDbName }); + return workerDbName; + } +}; + +const initialState = { + main: "profile", + base: "profile", + node: null, + history: { + nodes: [], + currentIndex: -1 + } +}; +function getDefaultBaseForMain(main) { + return main === "profile" ? "profile" : "school"; +} +function validateContextTransition(current, updates) { + const newState = { ...current, ...updates }; + if (updates.main) { + newState.base = getDefaultBaseForMain(updates.main); + } + if (updates.base) { + const isValid = newState.main === "profile" ? isProfileContext(updates.base) : isInstituteContext(updates.base); + if (!isValid) { + newState.base = getDefaultBaseForMain(newState.main); + } + } + return newState; +} +const useNavigationStore = create$1((set, get) => ({ + context: initialState, + isLoading: false, + error: null, + switchContext: async (contextSwitch, userDbName, workerDbName) => { + try { + if (contextSwitch.main === "profile" && !userDbName) { + logger.error("navigation-context", "❌ User database connection not initialized"); + set({ + error: "User database connection not initialized", + isLoading: false + }); + return; + } + if (contextSwitch.main === "institute" && !workerDbName) { + logger.error("navigation-context", "❌ Worker database connection not initialized"); + set({ + error: "Worker database connection not initialized", + isLoading: false + }); + return; + } + logger.debug("navigation-context", "🔄 Starting context switch", { + from: { + main: get().context.main, + base: get().context.base, + extended: contextSwitch.extended, + nodeId: get().context.node?.id + }, + to: { + main: contextSwitch.main, + base: contextSwitch.base, + extended: contextSwitch.extended + }, + skipBaseContextLoad: contextSwitch.skipBaseContextLoad + }); + set({ isLoading: true, error: null }); + const currentState = get().context; + const clearedState = { + ...currentState, + node: null + }; + set({ + context: clearedState, + isLoading: true + }); + let newState = { + ...currentState, + node: null + }; + if (contextSwitch.main) { + newState = validateContextTransition(newState, { main: contextSwitch.main }); + if (!contextSwitch.skipBaseContextLoad) { + newState.base = getDefaultBaseForMain(contextSwitch.main); + } + logger.debug("navigation-state", "✅ Main context updated", { + previous: currentState.main, + new: newState.main, + defaultBase: newState.base + }); + } + if (contextSwitch.base) { + newState = validateContextTransition(newState, { base: contextSwitch.base }); + logger.debug("navigation-state", "✅ Base context updated", { + previous: currentState.base, + new: newState.base + }); + } + logger.debug("navigation-state", "✅ Context validation complete", { + validatedState: newState, + originalState: currentState + }); + const targetContext = contextSwitch.base || contextSwitch.extended || (contextSwitch.main ? getDefaultBaseForMain(contextSwitch.main) : newState.base); + const dbName = getContextDatabase(newState, userDbName, workerDbName); + logger.debug("context-switch", "🔍 Fetching default node for context", { + targetContext, + dbName, + currentState: newState + }); + const defaultNode = await UserNeoDBService.getDefaultNode(targetContext, dbName); + if (!defaultNode) { + const errorMsg = `No default node found for context: ${targetContext}`; + logger.error("context-switch", "❌ Default node fetch failed", { targetContext }); + set({ + error: errorMsg, + isLoading: false + }); + return; + } + logger.debug("context-switch", "✨ Default node fetched", { + nodeId: defaultNode.id, + node_storage_path: defaultNode.node_storage_path, + type: defaultNode.type + }); + const newHistory = addToHistory(currentState.history, defaultNode); + logger.debug("history-management", "📚 History updated", { + previousState: currentState.history, + newState: newHistory, + addedNode: defaultNode + }); + set({ + context: { + ...newState, + node: defaultNode, + history: newHistory + }, + isLoading: false, + error: null + }); + logger.debug("navigation-context", "✅ Context switch completed", { + finalState: { + main: newState.main, + base: newState.base, + nodeId: defaultNode.id + } + }); + } catch (error) { + logger.error("navigation-context", "❌ Failed to switch context:", error); + set({ + error: error instanceof Error ? error.message : "Failed to switch context", + isLoading: false + }); + } + }, + goBack: () => { + const currentState = get().context; + if (currentState.history.currentIndex > 0) { + const newHistory = navigateHistory(currentState.history, currentState.history.currentIndex - 1); + const node = getCurrentHistoryNode(newHistory); + set({ + context: { + ...currentState, + node, + history: newHistory + } + }); + } + }, + goForward: () => { + const currentState = get().context; + if (currentState.history.currentIndex < currentState.history.nodes.length - 1) { + const newHistory = navigateHistory(currentState.history, currentState.history.currentIndex + 1); + const node = getCurrentHistoryNode(newHistory); + set({ + context: { + ...currentState, + node, + history: newHistory + } + }); + } + }, + setMainContext: async (main, userDbName, workerDbName) => { + try { + await get().switchContext({ main }, userDbName, workerDbName); + } catch (error) { + logger.error("navigation", "❌ Failed to set main context:", error); + set({ + error: error instanceof Error ? error.message : "Failed to set main context", + isLoading: false + }); + } + }, + setBaseContext: async (base, userDbName, workerDbName) => { + try { + await get().switchContext({ base }, userDbName, workerDbName); + } catch (error) { + logger.error("navigation", "❌ Failed to set base context:", error); + set({ + error: error instanceof Error ? error.message : "Failed to set base context", + isLoading: false + }); + } + }, + setExtendedContext: async (extended, userDbName, workerDbName) => { + try { + await get().switchContext({ extended }, userDbName, workerDbName); + } catch (error) { + logger.error("navigation", "❌ Failed to set extended context:", error); + set({ + error: error instanceof Error ? error.message : "Failed to set extended context", + isLoading: false + }); + } + }, + navigate: async (nodeId, dbName) => { + try { + set({ isLoading: true, error: null }); + const currentState = get().context; + const existingNodeIndex = currentState.history.nodes.findIndex((n) => n.id === nodeId); + if (existingNodeIndex !== -1) { + logger.debug("navigation", "📍 Navigating to existing node in history", { + nodeId, + historyIndex: existingNodeIndex, + currentIndex: currentState.history.currentIndex + }); + const newHistory2 = navigateHistory(currentState.history, existingNodeIndex); + const node2 = getCurrentHistoryNode(newHistory2); + set({ + context: { + ...currentState, + node: node2, + history: newHistory2 + }, + isLoading: false, + error: null + }); + return; + } + const nodeData = await UserNeoDBService.fetchNodeData(nodeId, dbName); + if (!nodeData) { + throw new Error(`Node not found: ${nodeId}`); + } + const node = { + id: nodeId, + node_storage_path: nodeData.node_data.node_storage_path || "", + label: nodeData.node_data.title || nodeData.node_data.user_name || nodeId, + type: nodeData.node_type + }; + logger.debug("navigation", "📍 Adding new node to history", { + nodeId: node.id, + type: node.type, + node_storage_path: node.node_storage_path + }); + const newHistory = addToHistory(currentState.history, node); + set({ + context: { + ...currentState, + node, + history: newHistory + }, + isLoading: false, + error: null + }); + } catch (error) { + logger.error("navigation", "❌ Failed to navigate:", error); + set({ + error: error instanceof Error ? error.message : "Failed to navigate", + isLoading: false + }); + } + }, + navigateToNode: async (node, userDbName, workerDbName) => { + try { + set({ isLoading: true, error: null }); + if (!isValidNodeType(node.type)) { + throw new Error(`Invalid node type: ${node.type}`); + } + const dbName = getContextDatabase(get().context, userDbName, workerDbName); + await get().navigate(node.id, dbName); + } catch (error) { + logger.error("navigation", "❌ Failed to navigate to node:", error); + set({ + error: error instanceof Error ? error.message : "Failed to navigate to node", + isLoading: false + }); + } + }, + refreshNavigationState: async (userDbName, workerDbName) => { + try { + set({ isLoading: true, error: null }); + const currentState = get().context; + if (currentState.node) { + const dbName = getContextDatabase(currentState, userDbName, workerDbName); + const nodeData = await UserNeoDBService.fetchNodeData(currentState.node.id, dbName); + if (nodeData) { + const node = { + id: currentState.node.id, + node_storage_path: nodeData.node_data.node_storage_path || "", + label: nodeData.node_data.title || nodeData.node_data.user_name || currentState.node.id, + type: nodeData.node_type + }; + set({ + context: { + ...currentState, + node + } + }); + } + } + set({ isLoading: false }); + } catch (error) { + logger.error("navigation", "❌ Failed to refresh navigation state:", error); + set({ + error: error instanceof Error ? error.message : "Failed to refresh navigation state", + isLoading: false + }); + } + } +})); + +const NeoUserContext = reactExports.createContext({ + userNode: null, + calendarNode: null, + workerNode: null, + userDbName: null, + workerDbName: null, + isLoading: false, + isInitialized: false, + error: null, + navigateToDay: async () => { + }, + navigateToWeek: async () => { + }, + navigateToMonth: async () => { + }, + navigateToYear: async () => { + }, + navigateToTimetable: async () => { + }, + navigateToJournal: async () => { + }, + navigateToPlanner: async () => { + }, + navigateToClass: async () => { + }, + navigateToLesson: async () => { + }, + currentCalendarNode: null, + currentWorkerNode: null, + calendarStructure: null, + workerStructure: null +}); +const NeoUserProvider = ({ children }) => { + const { user } = useAuth(); + const { profile, isInitialized: isUserInitialized } = useUser(); + const navigationStore = useNavigationStore(); + const [userNode, setUserNode] = reactExports.useState(null); + const [calendarNode] = reactExports.useState(null); + const [workerNode] = reactExports.useState(null); + const [currentCalendarNode, setCurrentCalendarNode] = reactExports.useState(null); + const [currentWorkerNode, setCurrentWorkerNode] = reactExports.useState(null); + const [calendarStructure] = reactExports.useState(null); + const [workerStructure] = reactExports.useState(null); + const [userDbName, setUserDbName] = reactExports.useState(null); + const [workerDbName, setWorkerDbName] = reactExports.useState(null); + const [isLoading, setIsLoading] = reactExports.useState(true); + const [isInitialized, setIsInitialized] = reactExports.useState(false); + const [error, setError] = reactExports.useState(null); + const initializationRef = React$2.useRef({ + hasStarted: false, + isComplete: false + }); + const getBaseNodeProps = () => ({ + title: "", + w: 200, + h: 200, + headerColor: "#000000", + backgroundColor: "#ffffff", + isLocked: false, + __primarylabel__: "UserTeacherTimetable", + uuid_string: "", + node_storage_path: "", + created: (/* @__PURE__ */ new Date()).toISOString(), + merged: (/* @__PURE__ */ new Date()).toISOString(), + state: { + parentId: null, + isPageChild: false, + hasChildren: false, + bindings: [] + }, + defaultComponent: true + }); + reactExports.useEffect(() => { + if (!isUserInitialized || !profile || isInitialized || initializationRef.current.hasStarted) { + return; + } + const initializeContext = async () => { + try { + initializationRef.current.hasStarted = true; + setIsLoading(true); + setError(null); + const userDb = profile.user_db_name || (user?.email ? `cc.users.${user.email.replace("@", "at").replace(/\./g, "dot")}` : null); + if (!userDb) { + throw new Error("No user database name available"); + } + logger.debug("neo-user-context", "🔄 Starting context initialization"); + await navigationStore.switchContext({ + main: "profile", + base: "profile", + extended: "overview" + }, userDb, profile.school_db_name); + const userNavigationNode = navigationStore.context.node; + if (userNavigationNode?.data) { + const userNodeData = { + ...getBaseNodeProps(), + __primarylabel__: "User", + uuid_string: userNavigationNode.id, + node_storage_path: userNavigationNode.node_storage_path || "", + title: String(userNavigationNode.data?.user_name || "User"), + user_name: String(userNavigationNode.data?.user_name || "User"), + user_email: user?.email || "", + user_type: "User", + user_id: userNavigationNode.id, + worker_node_data: JSON.stringify(userNavigationNode.data || {}) + }; + setUserNode(userNodeData); + } + setUserDbName(userDb); + setWorkerDbName(profile.school_db_name); + setIsInitialized(true); + setIsLoading(false); + initializationRef.current.isComplete = true; + logger.debug("neo-user-context", "✅ Context initialization complete"); + } catch (error2) { + const errorMessage = error2 instanceof Error ? error2.message : "Failed to initialize user context"; + logger.error("neo-user-context", "❌ Failed to initialize context", { error: errorMessage }); + setError(errorMessage); + setIsLoading(false); + setIsInitialized(true); + initializationRef.current.isComplete = true; + } + }; + initializeContext(); + }, [user?.email, profile, isUserInitialized, navigationStore, isInitialized]); + const navigateToDay = async (id) => { + if (!userDbName) return; + setIsLoading(true); + try { + await navigationStore.switchContext({ + base: "calendar", + extended: "day" + }, userDbName, workerDbName); + const node = navigationStore.context.node; + if (node?.data) { + const nodeData = { + ...getBaseNodeProps(), + __primarylabel__: "CalendarDay", + uuid_string: id || node.id, + node_storage_path: node.node_storage_path || "", + title: node.label, + name: node.label, + calendar_type: "day", + calendar_name: node.label, + start_date: (/* @__PURE__ */ new Date()).toISOString(), + end_date: (/* @__PURE__ */ new Date()).toISOString() + }; + setCurrentCalendarNode({ + id: id || node.id, + label: node.label, + title: node.label, + node_storage_path: node.node_storage_path || "", + type: "CalendarDay", + nodeData + }); + } + } catch (error2) { + setError(error2 instanceof Error ? error2.message : "Failed to navigate to day"); + } finally { + setIsLoading(false); + } + }; + const navigateToWeek = async (id) => { + if (!userDbName) return; + setIsLoading(true); + try { + await navigationStore.switchContext({ + base: "calendar", + extended: "week" + }, userDbName, workerDbName); + const node = navigationStore.context.node; + if (node?.data) { + const nodeData = { + ...getBaseNodeProps(), + __primarylabel__: "CalendarWeek", + uuid_string: id || node.id, + node_storage_path: node.node_storage_path || "", + title: node.label, + name: node.label, + calendar_type: "week", + calendar_name: node.label, + start_date: (/* @__PURE__ */ new Date()).toISOString(), + end_date: (/* @__PURE__ */ new Date()).toISOString() + }; + setCurrentCalendarNode({ + id: id || node.id, + label: node.label, + title: node.label, + node_storage_path: node.node_storage_path || "", + type: "CalendarWeek", + nodeData + }); + } + } catch (error2) { + setError(error2 instanceof Error ? error2.message : "Failed to navigate to week"); + } finally { + setIsLoading(false); + } + }; + const navigateToMonth = async (id) => { + if (!userDbName) return; + setIsLoading(true); + try { + await navigationStore.switchContext({ + base: "calendar", + extended: "month" + }, userDbName, workerDbName); + const node = navigationStore.context.node; + if (node?.data) { + const nodeData = { + ...getBaseNodeProps(), + __primarylabel__: "CalendarMonth", + uuid_string: id || node.id, + node_storage_path: node.node_storage_path || "", + title: node.label, + name: node.label, + calendar_type: "month", + calendar_name: node.label, + start_date: (/* @__PURE__ */ new Date()).toISOString(), + end_date: (/* @__PURE__ */ new Date()).toISOString() + }; + setCurrentCalendarNode({ + id: id || node.id, + label: node.label, + title: node.label, + node_storage_path: node.node_storage_path || "", + type: "CalendarMonth", + nodeData + }); + } + } catch (error2) { + setError(error2 instanceof Error ? error2.message : "Failed to navigate to month"); + } finally { + setIsLoading(false); + } + }; + const navigateToYear = async (id) => { + if (!userDbName) return; + setIsLoading(true); + try { + await navigationStore.switchContext({ + base: "calendar", + extended: "year" + }, userDbName, workerDbName); + const node = navigationStore.context.node; + if (node?.data) { + const nodeData = { + ...getBaseNodeProps(), + __primarylabel__: "CalendarYear", + uuid_string: id || node.id, + node_storage_path: node.node_storage_path || "", + title: node.label, + name: node.label, + calendar_type: "year", + calendar_name: node.label, + start_date: (/* @__PURE__ */ new Date()).toISOString(), + end_date: (/* @__PURE__ */ new Date()).toISOString() + }; + setCurrentCalendarNode({ + id: id || node.id, + label: node.label, + title: node.label, + node_storage_path: node.node_storage_path || "", + type: "CalendarYear", + nodeData + }); + } + } catch (error2) { + setError(error2 instanceof Error ? error2.message : "Failed to navigate to year"); + } finally { + setIsLoading(false); + } + }; + const navigateToTimetable = async (id) => { + if (!userDbName) return; + setIsLoading(true); + try { + await navigationStore.switchContext({ + base: "teaching", + extended: "timetable" + }, userDbName, workerDbName); + const node = navigationStore.context.node; + if (node?.data) { + const nodeData = { + ...getBaseNodeProps(), + __primarylabel__: "UserTeacherTimetable", + uuid_string: id || node.id, + node_storage_path: node.node_storage_path || "", + title: node.label, + school_db_name: workerDbName || "", + school_timetable_id: id || node.id + }; + setCurrentWorkerNode({ + id: id || node.id, + label: node.label, + title: node.label, + node_storage_path: node.node_storage_path || "", + type: "UserTeacherTimetable", + nodeData + }); + } + } catch (error2) { + setError(error2 instanceof Error ? error2.message : "Failed to navigate to timetable"); + } finally { + setIsLoading(false); + } + }; + const navigateToJournal = async (id) => { + if (!userDbName) return; + setIsLoading(true); + try { + await navigationStore.switchContext({ + base: "teaching", + extended: "journal" + }, userDbName, workerDbName); + const node = navigationStore.context.node; + if (node?.data) { + const nodeData = { + ...getBaseNodeProps(), + __primarylabel__: "UserTeacherTimetable", + uuid_string: id || node.id, + node_storage_path: node.node_storage_path || "", + title: node.label, + school_db_name: workerDbName || "", + school_timetable_id: id || node.id + }; + setCurrentWorkerNode({ + id: id || node.id, + label: node.label, + title: node.label, + node_storage_path: node.node_storage_path || "", + type: "UserTeacherTimetable", + nodeData + }); + } + } catch (error2) { + setError(error2 instanceof Error ? error2.message : "Failed to navigate to journal"); + } finally { + setIsLoading(false); + } + }; + const navigateToPlanner = async (id) => { + if (!userDbName) return; + setIsLoading(true); + try { + await navigationStore.switchContext({ + base: "teaching", + extended: "planner" + }, userDbName, workerDbName); + const node = navigationStore.context.node; + if (node?.data) { + const nodeData = { + ...getBaseNodeProps(), + __primarylabel__: "UserTeacherTimetable", + uuid_string: id || node.id, + node_storage_path: node.node_storage_path || "", + title: node.label, + school_db_name: workerDbName || "", + school_timetable_id: id || node.id + }; + setCurrentWorkerNode({ + id: id || node.id, + label: node.label, + title: node.label, + node_storage_path: node.node_storage_path || "", + type: "UserTeacherTimetable", + nodeData + }); + } + } catch (error2) { + setError(error2 instanceof Error ? error2.message : "Failed to navigate to planner"); + } finally { + setIsLoading(false); + } + }; + const navigateToClass = async (id) => { + if (!userDbName) return; + setIsLoading(true); + try { + await navigationStore.switchContext({ + base: "teaching", + extended: "classes" + }, userDbName, workerDbName); + await navigationStore.navigate(id, userDbName); + const node = navigationStore.context.node; + if (node?.data) { + const nodeData = { + ...getBaseNodeProps(), + __primarylabel__: "UserTeacherTimetable", + uuid_string: node.id, + node_storage_path: node.node_storage_path || "", + title: node.label, + school_db_name: workerDbName || "", + school_timetable_id: node.id + }; + setCurrentWorkerNode({ + id: node.id, + label: node.label, + title: node.label, + node_storage_path: node.node_storage_path || "", + type: "UserTeacherTimetable", + nodeData + }); + } + } catch (error2) { + setError(error2 instanceof Error ? error2.message : "Failed to navigate to class"); + } finally { + setIsLoading(false); + } + }; + const navigateToLesson = async (id) => { + if (!userDbName) return; + setIsLoading(true); + try { + await navigationStore.switchContext({ + base: "teaching", + extended: "lessons" + }, userDbName, workerDbName); + await navigationStore.navigate(id, userDbName); + const node = navigationStore.context.node; + if (node?.data) { + const nodeData = { + ...getBaseNodeProps(), + __primarylabel__: "UserTeacherTimetable", + uuid_string: node.id, + node_storage_path: node.node_storage_path || "", + title: node.label, + school_db_name: workerDbName || "", + school_timetable_id: node.id + }; + setCurrentWorkerNode({ + id: node.id, + label: node.label, + title: node.label, + node_storage_path: node.node_storage_path || "", + type: "UserTeacherTimetable", + nodeData + }); + } + } catch (error2) { + setError(error2 instanceof Error ? error2.message : "Failed to navigate to lesson"); + } finally { + setIsLoading(false); + } + }; + return /* @__PURE__ */ jsxRuntimeExports.jsx(NeoUserContext.Provider, { value: { + userNode, + calendarNode, + workerNode, + userDbName, + workerDbName, + isLoading, + isInitialized, + error, + navigateToDay, + navigateToWeek, + navigateToMonth, + navigateToYear, + navigateToTimetable, + navigateToJournal, + navigateToPlanner, + navigateToClass, + navigateToLesson, + currentCalendarNode, + currentWorkerNode, + calendarStructure, + workerStructure + }, children }); +}; +const useNeoUser = () => reactExports.useContext(NeoUserContext); + +class SchoolNeoDBService { + static async createSchools() { + logger.warn("school-service", "📤 Creating schools using default config.yaml"); + try { + const response = await axios.post( + "/database/entity/create-schools", + {}, + { + headers: { + "Content-Type": "application/json" + } + } + ); + if (response.data.status === "success" || response.data.status === "Accepted") { + logger.info("school-service", "✅ Schools successfully"); + return { + status: "success", + message: "Schools created successfully" + }; + } + throw new Error(response.data.message || "Creation failed"); + } catch (err) { + const error = err; + logger.error("school-service", "❌ Failed to create school", { + error: error.message, + details: error.response?.data + }); + throw error; + } + } + static async getSchoolNode(schoolDbName) { + logger.debug("school-service", "🔄 Fetching school node", { schoolDbName }); + try { + const response = await axios.get(`/database/tools/get-default-node/school?db_name=${schoolDbName}`); + if (response.data?.status === "success" && response.data.node) { + logger.info("school-service", "✅ School node fetched successfully"); + return response.data.node; + } + logger.warn("school-service", "⚠️ No school node found"); + return null; + } catch (error) { + if (error instanceof AxiosError && error.response?.status === 404) { + logger.warn("school-service", "⚠️ School node not found (404)", { schoolDbName }); + return null; + } + logger.error("school-service", "❌ Failed to fetch school node:", error); + throw error; + } + } +} + +const NeoInstituteContext = reactExports.createContext({ + schoolNode: null, + isLoading: true, + isInitialized: false, + error: null +}); +const NeoInstituteProvider = ({ children }) => { + const { user } = useAuth(); + const { profile, isInitialized: isUserInitialized } = useUser(); + const [schoolNode, setSchoolNode] = reactExports.useState(null); + const [isLoading, setIsLoading] = reactExports.useState(true); + const [isInitialized, setIsInitialized] = reactExports.useState(false); + const [error, setError] = reactExports.useState(null); + reactExports.useEffect(() => { + if (!isUserInitialized) { + logger.debug("neo-institute-context", "⏳ Waiting for user initialization..."); + return; + } + if (!profile || !profile.school_db_name) { + setIsLoading(false); + setIsInitialized(true); + return; + } + const loadSchoolNode = async () => { + try { + setIsLoading(true); + logger.debug("neo-institute-context", "🔄 Loading school node", { + schoolDbName: profile.school_db_name, + userEmail: user?.email + }); + const node = await SchoolNeoDBService.getSchoolNode(profile.school_db_name); + if (node) { + setSchoolNode(node); + logger.debug("neo-institute-context", "✅ School node loaded", { + schoolId: node.uuid_string, + dbName: profile.school_db_name + }); + } else { + logger.warn("neo-institute-context", "⚠️ No school node found"); + } + } catch (error2) { + const errorMessage = error2 instanceof Error ? error2.message : "Failed to load school node"; + logger.error("neo-institute-context", "❌ Failed to load school node", { + error: errorMessage, + schoolDbName: profile.school_db_name + }); + setError(errorMessage); + } finally { + setIsLoading(false); + setIsInitialized(true); + } + }; + loadSchoolNode(); + }, [user?.email, profile, isUserInitialized]); + return /* @__PURE__ */ jsxRuntimeExports.jsx(NeoInstituteContext.Provider, { value: { + schoolNode, + isLoading, + isInitialized, + error + }, children }); +}; +const useNeoInstitute = () => reactExports.useContext(NeoInstituteContext); + +var Menu = {}; + +var interopRequireDefault = {exports: {}}; + +(function (module) { + function _interopRequireDefault(e) { + return e && e.__esModule ? e : { + "default": e + }; + } + module.exports = _interopRequireDefault, module.exports.__esModule = true, module.exports["default"] = module.exports; +} (interopRequireDefault)); + +var interopRequireDefaultExports = interopRequireDefault.exports; + +var createSvgIcon = {}; + +const require$$0 = /*@__PURE__*/getAugmentedNamespace(utils$7); + +var hasRequiredCreateSvgIcon; + +function requireCreateSvgIcon () { + if (hasRequiredCreateSvgIcon) return createSvgIcon; + hasRequiredCreateSvgIcon = 1; + (function (exports) { + 'use client'; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + Object.defineProperty(exports, "default", { + enumerable: true, + get: function () { + return _utils.createSvgIcon; + } + }); + var _utils = require$$0; + } (createSvgIcon)); + return createSvgIcon; +} + +var _interopRequireDefault$d = interopRequireDefaultExports; +Object.defineProperty(Menu, "__esModule", { + value: true +}); +var default_1$d = Menu.default = void 0; +var _createSvgIcon$d = _interopRequireDefault$d(requireCreateSvgIcon()); +var _jsxRuntime$d = jsxRuntimeExports; +var _default$d = (0, _createSvgIcon$d.default)( /*#__PURE__*/(0, _jsxRuntime$d.jsx)("path", { + d: "M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z" +}), 'Menu'); +default_1$d = Menu.default = _default$d; + +const NAVIGATION_CONTEXTS = { + // Personal Contexts + profile: { + id: "profile", + icon: "Person", + label: "User Profile", + description: "Personal workspace and settings", + defaultNodeId: "user-root", + views: [ + { + id: "overview", + icon: "Dashboard", + label: "Overview", + description: "View your profile overview" + }, + { + id: "settings", + icon: "Settings", + label: "Settings", + description: "Manage your preferences" + }, + { + id: "history", + icon: "History", + label: "History", + description: "View your activity history" + }, + { + id: "journal", + icon: "Book", + label: "Journal", + description: "Your personal journal" + }, + { + id: "planner", + icon: "Event", + label: "Planner", + description: "Plan your activities" + } + ] + }, + calendar: { + id: "calendar", + icon: "CalendarToday", + label: "Calendar", + description: "Calendar navigation and events", + defaultNodeId: "calendar-root", + views: [ + { + id: "overview", + icon: "Dashboard", + label: "Overview", + description: "Calendar overview" + }, + { + id: "day", + icon: "Today", + label: "Day View", + description: "Navigate by day" + }, + { + id: "week", + icon: "ViewWeek", + label: "Week View", + description: "Navigate by week" + }, + { + id: "month", + icon: "DateRange", + label: "Month View", + description: "Navigate by month" + }, + { + id: "year", + icon: "Event", + label: "Year View", + description: "Navigate by year" + } + ] + }, + teaching: { + id: "teaching", + icon: "School", + label: "Teaching", + description: "Teaching workspace", + defaultNodeId: "teacher-root", + views: [ + { + id: "overview", + icon: "Dashboard", + label: "Overview", + description: "Teaching overview" + }, + { + id: "timetable", + icon: "Schedule", + label: "Timetable", + description: "View your teaching schedule" + }, + { + id: "classes", + icon: "Class", + label: "Classes", + description: "Manage your classes" + }, + { + id: "lessons", + icon: "Book", + label: "Lessons", + description: "Plan and view lessons" + }, + { + id: "journal", + icon: "Book", + label: "Journal", + description: "Your teaching journal" + }, + { + id: "planner", + icon: "Event", + label: "Planner", + description: "Plan your teaching activities" + } + ] + }, + // Institutional Contexts + school: { + id: "school", + icon: "Business", + label: "School", + description: "School management", + defaultNodeId: "school-root", + views: [ + { + id: "overview", + icon: "Dashboard", + label: "Overview", + description: "School overview" + }, + { + id: "departments", + icon: "AccountTree", + label: "Departments", + description: "View departments" + }, + { + id: "staff", + icon: "People", + label: "Staff", + description: "Staff directory" + } + ] + }, + department: { + id: "department", + icon: "AccountTree", + label: "Department", + description: "Department management", + defaultNodeId: "department-root", + views: [ + { + id: "overview", + icon: "Dashboard", + label: "Overview", + description: "Department overview" + }, + { + id: "teachers", + icon: "People", + label: "Teachers", + description: "Department teachers" + }, + { + id: "subjects", + icon: "Subject", + label: "Subjects", + description: "Department subjects" + } + ] + }, + class: { + id: "class", + icon: "Class", + label: "Class", + description: "Class management", + defaultNodeId: "class-root", + views: [ + { + id: "overview", + icon: "Dashboard", + label: "Overview", + description: "Class overview" + }, + { + id: "students", + icon: "People", + label: "Students", + description: "Class students" + }, + { + id: "timetable", + icon: "Schedule", + label: "Timetable", + description: "Class schedule" + } + ] + } +}; + +const NavigationRoot = styled(Box)` + display: flex; + align-items: center; + gap: 8px; + height: 100%; + overflow: hidden; +`; +const NavigationControls = styled(Box)` + display: flex; + align-items: center; + gap: 4px; +`; +const ContextToggleContainer = styled(Box)(({ theme }) => ({ + display: "flex", + alignItems: "center", + backgroundColor: theme.palette.action.hover, + borderRadius: theme.shape.borderRadius, + padding: theme.spacing(0.5), + gap: theme.spacing(0.5), + "& .button-label": { + "@media (max-width: 500px)": { + display: "none" + } + } +})); +const ContextToggleButton = styled(Button, { + shouldForwardProp: (prop) => prop !== "active" +})(({ theme, active }) => ({ + minWidth: 0, + padding: theme.spacing(0.5, 1.5), + borderRadius: theme.shape.borderRadius, + backgroundColor: active ? theme.palette.primary.main : "transparent", + color: active ? theme.palette.primary.contrastText : theme.palette.text.primary, + textTransform: "none", + transition: theme.transitions.create(["background-color", "color"], { + duration: theme.transitions.duration.shorter + }), + "&:hover": { + backgroundColor: active ? theme.palette.primary.dark : theme.palette.action.hover + }, + "@media (max-width: 500px)": { + padding: theme.spacing(0.5) + } +})); +const GraphNavigator = () => { + const { + context, + switchContext, + goBack, + goForward, + isLoading + } = useNavigationStore(); + const { userDbName, workerDbName, isInitialized: isNeoUserInitialized } = useNeoUser(); + const [contextMenuAnchor, setContextMenuAnchor] = reactExports.useState(null); + const [historyMenuAnchor, setHistoryMenuAnchor] = reactExports.useState(null); + const rootRef = reactExports.useRef(null); + const [availableWidth, setAvailableWidth] = reactExports.useState(0); + reactExports.useEffect(() => { + const calculateAvailableSpace = () => { + if (!rootRef.current) return; + const header = rootRef.current.closest(".MuiToolbar-root"); + if (!header) return; + const title = header.querySelector(".app-title"); + const menu = header.querySelector(".menu-button"); + if (!title || !menu) return; + const headerWidth = header.clientWidth; + const titleWidth = title.clientWidth; + const menuWidth = menu.clientWidth; + const padding = 48; + const newAvailableWidth = headerWidth - titleWidth - menuWidth - padding; + console.log("Available width:", newAvailableWidth); + setAvailableWidth(newAvailableWidth); + }; + const resizeObserver = new ResizeObserver(() => { + window.requestAnimationFrame(calculateAvailableSpace); + }); + if (rootRef.current) { + const header = rootRef.current.closest(".MuiToolbar-root"); + if (header) { + resizeObserver.observe(header); + resizeObserver.observe(rootRef.current); + } + } + calculateAvailableSpace(); + return () => { + resizeObserver.disconnect(); + }; + }, []); + const getVisibility = () => { + if (availableWidth < 300) { + return { + navigation: false, + contextLabel: true, + // Keep context label visible longer + toggleLabels: false + }; + } else if (availableWidth < 450) { + return { + navigation: false, + contextLabel: true, + // Keep context label visible + toggleLabels: true + }; + } else if (availableWidth < 600) { + return { + navigation: true, + contextLabel: true, + toggleLabels: true + }; + } + return { + navigation: true, + contextLabel: true, + toggleLabels: true + }; + }; + const visibility = getVisibility(); + const handleHistoryClick = (event) => { + setHistoryMenuAnchor(event.currentTarget); + }; + const handleHistoryClose = () => { + setHistoryMenuAnchor(null); + }; + const handleHistoryItemClick = (index) => { + const { currentIndex } = context.history; + const steps = index - currentIndex; + if (steps < 0) { + for (let i = 0; i < -steps; i++) { + goBack(); + } + } else if (steps > 0) { + for (let i = 0; i < steps; i++) { + goForward(); + } + } + handleHistoryClose(); + }; + const handleContextChange = reactExports.useCallback(async (newContext) => { + try { + if (["school", "department", "class"].includes(newContext) && !workerDbName) { + logger.error("navigation", "❌ Cannot switch to institute context: missing worker database"); + return; + } + if (["profile", "calendar", "teaching"].includes(newContext) && !userDbName) { + logger.error("navigation", "❌ Cannot switch to profile context: missing user database"); + return; + } + logger.debug("navigation", "🔄 Changing main context", { + from: context.main, + to: newContext, + userDbName, + workerDbName + }); + const defaultView = getDefaultViewForContext(newContext); + await switchContext({ + main: ["profile", "calendar", "teaching"].includes(newContext) ? "profile" : "institute", + base: newContext, + extended: defaultView, + skipBaseContextLoad: false + }, userDbName, workerDbName); + } catch (error) { + logger.error("navigation", "❌ Failed to change context:", error); + } + }, [context.main, switchContext, userDbName, workerDbName]); + const getDefaultViewForContext = (context2) => { + switch (context2) { + case "calendar": + return "overview"; + case "teaching": + return "overview"; + case "school": + return "overview"; + case "department": + return "overview"; + case "class": + return "overview"; + default: + return "overview"; + } + }; + const handleContextMenu = (event) => { + setContextMenuAnchor(event.currentTarget); + }; + const handleContextSelect = reactExports.useCallback(async (context2) => { + setContextMenuAnchor(null); + try { + const contextDef = NAVIGATION_CONTEXTS[context2]; + const defaultExtended = contextDef?.views[0]?.id; + await switchContext({ + base: context2, + extended: defaultExtended + }, userDbName, workerDbName); + } catch (error) { + logger.error("navigation", "❌ Failed to select context:", error); + } + }, [switchContext, userDbName, workerDbName]); + const getContextItems = reactExports.useCallback(() => { + if (context.main === "profile") { + return [ + { id: "profile", label: "Profile", icon: AccountCircleIcon }, + { id: "calendar", label: "Calendar", icon: CalendarIcon }, + { id: "teaching", label: "Teaching", icon: TeacherIcon } + ]; + } else { + return [ + { id: "school", label: "School", icon: BusinessIcon }, + { id: "department", label: "Department", icon: GraphIcon }, + { id: "class", label: "Class", icon: ClassIcon } + ]; + } + }, [context.main]); + const getContextIcon = reactExports.useCallback((contextType) => { + switch (contextType) { + case "profile": + return /* @__PURE__ */ jsxRuntimeExports.jsx(AccountCircleIcon, {}); + case "calendar": + return /* @__PURE__ */ jsxRuntimeExports.jsx(CalendarIcon, {}); + case "teaching": + return /* @__PURE__ */ jsxRuntimeExports.jsx(TeacherIcon, {}); + case "school": + return /* @__PURE__ */ jsxRuntimeExports.jsx(BusinessIcon, {}); + case "department": + return /* @__PURE__ */ jsxRuntimeExports.jsx(GraphIcon, {}); + case "class": + return /* @__PURE__ */ jsxRuntimeExports.jsx(ClassIcon, {}); + default: + return /* @__PURE__ */ jsxRuntimeExports.jsx(AccountCircleIcon, {}); + } + }, []); + const isDisabled = !isNeoUserInitialized || isLoading; + const { history } = context; + const canGoBack = history.currentIndex > 0; + const canGoForward = history.currentIndex < history.nodes.length - 1; + return /* @__PURE__ */ jsxRuntimeExports.jsxs(NavigationRoot, { ref: rootRef, children: [ + /* @__PURE__ */ jsxRuntimeExports.jsxs(NavigationControls, { sx: { display: visibility.navigation ? "flex" : "none" }, children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(Tooltip, { title: "Back", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: /* @__PURE__ */ jsxRuntimeExports.jsx( + IconButton, + { + onClick: goBack, + disabled: !canGoBack || isDisabled, + size: "small", + children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowBackIcon, { fontSize: "small" }) + } + ) }) }), + /* @__PURE__ */ jsxRuntimeExports.jsx(Tooltip, { title: "History", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: /* @__PURE__ */ jsxRuntimeExports.jsx( + IconButton, + { + onClick: handleHistoryClick, + disabled: !history.nodes.length || isDisabled, + size: "small", + children: /* @__PURE__ */ jsxRuntimeExports.jsx(HistoryIcon, { fontSize: "small" }) + } + ) }) }), + /* @__PURE__ */ jsxRuntimeExports.jsx(Tooltip, { title: "Forward", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: /* @__PURE__ */ jsxRuntimeExports.jsx( + IconButton, + { + onClick: goForward, + disabled: !canGoForward || isDisabled, + size: "small", + children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowForwardIcon, { fontSize: "small" }) + } + ) }) }) + ] }), + /* @__PURE__ */ jsxRuntimeExports.jsx( + Menu$1, + { + anchorEl: historyMenuAnchor, + open: Boolean(historyMenuAnchor), + onClose: handleHistoryClose, + anchorOrigin: { + vertical: "bottom", + horizontal: "center" + }, + transformOrigin: { + vertical: "top", + horizontal: "center" + }, + children: history.nodes.map((node, index) => /* @__PURE__ */ jsxRuntimeExports.jsxs( + MenuItem, + { + onClick: () => handleHistoryItemClick(index), + selected: index === history.currentIndex, + children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ListItemIcon, { children: getContextIcon(node.type) }), + /* @__PURE__ */ jsxRuntimeExports.jsx( + ListItemText, + { + primary: node.label || node.id, + secondary: node.type + } + ) + ] + }, + `${node.id}-${index}` + )) + } + ), + /* @__PURE__ */ jsxRuntimeExports.jsxs(ContextToggleContainer, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx( + ContextToggleButton, + { + active: context.main === "profile", + onClick: () => handleContextChange("profile"), + startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(StudentIcon, {}), + disabled: isDisabled || !userDbName, + children: visibility.toggleLabels && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "button-label", children: "Profile" }) + } + ), + /* @__PURE__ */ jsxRuntimeExports.jsx( + ContextToggleButton, + { + active: context.main === "institute", + onClick: () => handleContextChange("school"), + startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(TeacherIcon, {}), + disabled: isDisabled || !workerDbName, + children: visibility.toggleLabels && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "button-label", children: "Institute" }) + } + ) + ] }), + /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(Tooltip, { title: context.base, children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: /* @__PURE__ */ jsxRuntimeExports.jsxs( + Button, + { + onClick: handleContextMenu, + disabled: isDisabled, + sx: { + minWidth: 0, + p: 0.5, + color: "text.primary", + "&:hover": { + bgcolor: "action.hover" + } + }, + children: [ + getContextIcon(context.base), + visibility.contextLabel && /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { sx: { ml: 1 }, children: context.base }), + /* @__PURE__ */ jsxRuntimeExports.jsx(ExpandMoreIcon, { sx: { ml: visibility.contextLabel ? 0.5 : 0 } }) + ] + } + ) }) }) }), + /* @__PURE__ */ jsxRuntimeExports.jsx( + Menu$1, + { + anchorEl: contextMenuAnchor, + open: Boolean(contextMenuAnchor), + onClose: () => setContextMenuAnchor(null), + children: getContextItems().map((item) => /* @__PURE__ */ jsxRuntimeExports.jsxs( + MenuItem, + { + onClick: () => handleContextSelect(item.id), + disabled: isDisabled, + children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ListItemIcon, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(item.icon, {}) }), + /* @__PURE__ */ jsxRuntimeExports.jsx(ListItemText, { primary: item.label }) + ] + }, + item.id + )) + } + ) + ] }); +}; + +const Header = () => { + const theme = useTheme(); + const navigate = useNavigate(); + const location = useLocation(); + const { user, signOut } = useAuth(); + const [anchorEl, setAnchorEl] = reactExports.useState(null); + const [isAuthenticated, setIsAuthenticated] = reactExports.useState(!!user); + const isAdmin = user?.email === "admin@classroomcopilot.ai"; + const showGraphNavigation = location.pathname === "/single-player"; + reactExports.useEffect(() => { + const newAuthState = !!user; + setIsAuthenticated(newAuthState); + logger.debug("user-context", "🔄 User state changed in header", { + hasUser: newAuthState, + userId: user?.id, + userEmail: user?.email, + userState: newAuthState ? "logged-in" : "logged-out", + isAdmin + }); + }, [user, isAdmin]); + const handleMenuOpen = (event) => { + setAnchorEl(event.currentTarget); + }; + const handleMenuClose = () => { + setAnchorEl(null); + }; + const handleNavigation = (path) => { + navigate(path); + handleMenuClose(); + }; + const handleSignupNavigation = (role) => { + navigate("/signup", { state: { role } }); + handleMenuClose(); + }; + const handleSignOut = async () => { + try { + logger.debug("auth-service", "🔄 Signing out user", { userId: user?.id }); + await signOut(); + setIsAuthenticated(false); + setAnchorEl(null); + logger.debug("auth-service", "✅ User signed out"); + } catch (error) { + logger.error("auth-service", "❌ Error signing out:", error); + console.error("Error signing out:", error); + } + }; + return /* @__PURE__ */ jsxRuntimeExports.jsx( + AppBar, + { + position: "fixed", + sx: { + height: `${HEADER_HEIGHT}px`, + bgcolor: theme.palette.background.paper, + color: theme.palette.text.primary, + boxShadow: 1 + }, + children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Toolbar$1, { sx: { + display: "flex", + justifyContent: "space-between", + minHeight: `${HEADER_HEIGHT}px !important`, + height: `${HEADER_HEIGHT}px`, + gap: 2, + px: { xs: 1, sm: 2 } + }, children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { sx: { + display: "flex", + alignItems: "center", + gap: 2, + minWidth: { xs: "auto", sm: "200px" } + }, children: /* @__PURE__ */ jsxRuntimeExports.jsx( + Typography, + { + variant: "h6", + component: "div", + className: "app-title", + sx: { + cursor: "pointer", + color: theme.palette.text.primary, + "&:hover": { + color: theme.palette.primary.main + }, + fontSize: { xs: "1rem", sm: "1.25rem" } + }, + onClick: () => navigate(isAuthenticated ? "/single-player" : "/"), + children: "ClassroomCopilot" + } + ) }), + /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { sx: { + position: "absolute", + left: "50%", + transform: "translateX(-50%)", + display: "flex", + justifyContent: "center", + visibility: showGraphNavigation ? "visible" : "hidden", + width: { + xs: "calc(100% - 160px)", + // More space for menu and title + sm: "calc(100% - 200px)", + // Standard spacing + md: "auto" + // Full width on medium and up + }, + maxWidth: "800px", + "& .navigation-controls": { + display: { xs: "none", sm: "flex" } + }, + "& .context-section": { + display: { xs: "none", md: "flex" } + }, + "& .context-toggle": { + display: "flex" + // Always show the profile/institute toggle + } + }, children: /* @__PURE__ */ jsxRuntimeExports.jsx(GraphNavigator, {}) }), + /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { + display: "flex", + justifyContent: "flex-end", + minWidth: { xs: "auto", sm: "200px" }, + ml: "auto" + }, children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx( + IconButton, + { + className: "menu-button", + color: "inherit", + onClick: handleMenuOpen, + edge: "end", + sx: { + "&:hover": { + bgcolor: theme.palette.action.hover + } + }, + children: /* @__PURE__ */ jsxRuntimeExports.jsx(default_1$d, {}) + } + ), + /* @__PURE__ */ jsxRuntimeExports.jsx( + Menu$1, + { + anchorEl, + open: Boolean(anchorEl), + onClose: handleMenuClose, + slotProps: { + paper: { + elevation: 3, + sx: { + bgcolor: theme.palette.background.paper, + color: theme.palette.text.primary, + minWidth: "240px" + } + } + }, + children: isAuthenticated ? [ + // Development Tools Section + /* @__PURE__ */ jsxRuntimeExports.jsxs(MenuItem, { onClick: () => handleNavigation("/tldraw-dev"), children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ListItemIcon, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(TLDrawDevIcon, {}) }), + /* @__PURE__ */ jsxRuntimeExports.jsx(ListItemText, { primary: "TLDraw Dev" }) + ] }, "tldraw"), + /* @__PURE__ */ jsxRuntimeExports.jsxs(MenuItem, { onClick: () => handleNavigation("/dev"), children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ListItemIcon, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(DevToolsIcon, {}) }), + /* @__PURE__ */ jsxRuntimeExports.jsx(ListItemText, { primary: "Dev Tools" }) + ] }, "dev"), + /* @__PURE__ */ jsxRuntimeExports.jsx(Divider, {}, "dev-divider"), + // Main Features Section + /* @__PURE__ */ jsxRuntimeExports.jsxs(MenuItem, { onClick: () => handleNavigation("/multiplayer"), children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ListItemIcon, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(MultiplayerIcon, {}) }), + /* @__PURE__ */ jsxRuntimeExports.jsx(ListItemText, { primary: "Multiplayer" }) + ] }, "multiplayer"), + /* @__PURE__ */ jsxRuntimeExports.jsxs(MenuItem, { onClick: () => handleNavigation("/calendar"), children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ListItemIcon, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(CalendarIcon, {}) }), + /* @__PURE__ */ jsxRuntimeExports.jsx(ListItemText, { primary: "Calendar" }) + ] }, "calendar"), + /* @__PURE__ */ jsxRuntimeExports.jsxs(MenuItem, { onClick: () => handleNavigation("/teacher-planner"), children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ListItemIcon, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(ExamIcon, {}) }), + /* @__PURE__ */ jsxRuntimeExports.jsx(ListItemText, { primary: "Teacher Planner" }) + ] }, "planner"), + /* @__PURE__ */ jsxRuntimeExports.jsxs(MenuItem, { onClick: () => handleNavigation("/exam-marker"), children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ListItemIcon, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(ExamMarkerIcon, {}) }), + /* @__PURE__ */ jsxRuntimeExports.jsx(ListItemText, { primary: "Exam Marker" }) + ] }, "exam"), + /* @__PURE__ */ jsxRuntimeExports.jsx(Divider, {}, "features-divider"), + // Utilities Section + /* @__PURE__ */ jsxRuntimeExports.jsxs(MenuItem, { onClick: () => handleNavigation("/settings"), children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ListItemIcon, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(SettingsIcon, {}) }), + /* @__PURE__ */ jsxRuntimeExports.jsx(ListItemText, { primary: "Settings" }) + ] }, "settings"), + /* @__PURE__ */ jsxRuntimeExports.jsxs(MenuItem, { onClick: () => handleNavigation("/search"), children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ListItemIcon, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(SearchIcon, {}) }), + /* @__PURE__ */ jsxRuntimeExports.jsx(ListItemText, { primary: "Search" }) + ] }, "search"), + // Admin Section + ...isAdmin ? [ + /* @__PURE__ */ jsxRuntimeExports.jsx(Divider, {}, "admin-divider"), + /* @__PURE__ */ jsxRuntimeExports.jsxs(MenuItem, { onClick: () => handleNavigation("/admin"), children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ListItemIcon, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(AdminIcon, {}) }), + /* @__PURE__ */ jsxRuntimeExports.jsx(ListItemText, { primary: "Admin Dashboard" }) + ] }, "admin") + ] : [], + // Authentication Section + /* @__PURE__ */ jsxRuntimeExports.jsx(Divider, {}, "auth-divider"), + /* @__PURE__ */ jsxRuntimeExports.jsxs(MenuItem, { onClick: handleSignOut, children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ListItemIcon, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(LogoutIcon, {}) }), + /* @__PURE__ */ jsxRuntimeExports.jsx(ListItemText, { primary: "Sign Out" }) + ] }, "signout") + ] : [ + // Authentication Section for Non-authenticated Users + /* @__PURE__ */ jsxRuntimeExports.jsxs(MenuItem, { onClick: () => handleNavigation("/login"), children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ListItemIcon, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(LoginIcon, {}) }), + /* @__PURE__ */ jsxRuntimeExports.jsx(ListItemText, { primary: "Sign In" }) + ] }, "signin"), + /* @__PURE__ */ jsxRuntimeExports.jsx(Divider, {}, "signup-divider"), + /* @__PURE__ */ jsxRuntimeExports.jsxs(MenuItem, { onClick: () => handleSignupNavigation("teacher"), children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ListItemIcon, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(TeacherIcon, {}) }), + /* @__PURE__ */ jsxRuntimeExports.jsx( + ListItemText, + { + primary: "Sign up as Teacher", + secondary: "Create a teacher account" + } + ) + ] }, "teacher-signup"), + /* @__PURE__ */ jsxRuntimeExports.jsxs(MenuItem, { onClick: () => handleSignupNavigation("student"), children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ListItemIcon, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(StudentIcon, {}) }), + /* @__PURE__ */ jsxRuntimeExports.jsx( + ListItemText, + { + primary: "Sign up as Student", + secondary: "Create a student account" + } + ) + ] }, "student-signup") + ] + } + ) + ] }) + ] }) + } + ); +}; + +const HEADER_HEIGHT = 40; +const Layout = ({ children }) => { + return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(Header, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx("main", { className: "main-content", style: { + paddingTop: `${HEADER_HEIGHT}px`, + height: "100vh", + width: "100%" + }, children }) + ] }); +}; + +const EmailLoginForm = ({ role, onSubmit }) => { + const [email, setEmail] = reactExports.useState(""); + const [password, setPassword] = reactExports.useState(""); + const [error, setError] = reactExports.useState(null); + const [isLoading, setIsLoading] = reactExports.useState(false); + const handleSubmit = async (e) => { + e.preventDefault(); + setError(null); + setIsLoading(true); + try { + await onSubmit({ email, password, role }); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to login"); + } finally { + setIsLoading(false); + } + }; + return /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { component: "form", onSubmit: handleSubmit, sx: { width: "100%" }, children: [ + error && /* @__PURE__ */ jsxRuntimeExports.jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error }), + /* @__PURE__ */ jsxRuntimeExports.jsx( + TextField, + { + fullWidth: true, + label: "Email", + type: "email", + value: email, + onChange: (e) => setEmail(e.target.value), + margin: "normal", + required: true + } + ), + /* @__PURE__ */ jsxRuntimeExports.jsx( + TextField, + { + fullWidth: true, + label: "Password", + type: "password", + value: password, + onChange: (e) => setPassword(e.target.value), + margin: "normal", + required: true + } + ), + /* @__PURE__ */ jsxRuntimeExports.jsx( + Button, + { + type: "submit", + fullWidth: true, + variant: "contained", + color: "primary", + disabled: isLoading, + sx: { mt: 3 }, + children: isLoading ? "Logging in..." : "Login" + } + ) + ] }); +}; + +const LoginPage = () => { + const navigate = useNavigate(); + const { user, signIn } = useAuth(); + const [error, setError] = reactExports.useState(null); + logger.debug("login-page", "🔍 Login page loaded", { + hasUser: !!user + }); + reactExports.useEffect(() => { + if (user) { + navigate("/single-player"); + } + }, [user, navigate]); + const handleLogin = async (credentials) => { + try { + setError(null); + await signIn(credentials.email, credentials.password); + navigate("/single-player"); + } catch (error2) { + logger.error("login-page", "❌ Login failed", error2); + setError(error2 instanceof Error ? error2.message : "Login failed"); + throw error2; + } + }; + if (user) { + return null; + } + return /* @__PURE__ */ jsxRuntimeExports.jsxs( + Container, + { + sx: { + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + minHeight: "100vh", + gap: 4 + }, + children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "h2", component: "h1", gutterBottom: true, children: "ClassroomCopilot.ai" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "h4", gutterBottom: true, children: "Login" }), + error && /* @__PURE__ */ jsxRuntimeExports.jsx(Alert, { severity: "error", sx: { width: "100%", maxWidth: 400 }, children: error }), + /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { sx: { width: "100%", maxWidth: 400 }, children: /* @__PURE__ */ jsxRuntimeExports.jsx( + EmailLoginForm, + { + role: "email_teacher", + onSubmit: handleLogin + } + ) }) + ] + } + ); +}; + +const EmailSignupForm = ({ + role, + onSubmit +}) => { + const [email, setEmail] = reactExports.useState(""); + const [password, setPassword] = reactExports.useState(""); + const [confirmPassword, setConfirmPassword] = reactExports.useState(""); + const [displayName, setDisplayName] = reactExports.useState(""); + const [error, setError] = reactExports.useState(null); + const [isLoading, setIsLoading] = reactExports.useState(false); + const validateForm = () => { + if (!email || !password || !confirmPassword || !displayName) { + return "All fields are required"; + } + if (password !== confirmPassword) { + return "Passwords do not match"; + } + if (password.length < 6) { + return "Password must be at least 6 characters"; + } + if (!email.includes("@")) { + return "Please enter a valid email address"; + } + return null; + }; + const handleSubmit = async (e) => { + e.preventDefault(); + const validationError = validateForm(); + if (validationError) { + setError(validationError); + return; + } + setError(null); + setIsLoading(true); + try { + logger.debug("email-signup-form", "🔄 Submitting signup form", { + email, + role, + hasDisplayName: !!displayName + }); + await onSubmit({ email, password, role }, displayName); + } catch (err) { + setError( + err instanceof Error ? err.message : "An error occurred during signup" + ); + logger.error( + "email-signup-form", + "❌ Signup form submission failed", + err + ); + } finally { + setIsLoading(false); + } + }; + return /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { component: "form", onSubmit: handleSubmit, noValidate: true, children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Stack, { spacing: 2, children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx( + TextField, + { + required: true, + fullWidth: true, + id: "displayName", + label: "Display Name", + name: "displayName", + autoComplete: "name", + value: displayName, + onChange: (e) => setDisplayName(e.target.value), + autoFocus: true + } + ), + /* @__PURE__ */ jsxRuntimeExports.jsx( + TextField, + { + required: true, + fullWidth: true, + id: "email", + label: "Email Address", + name: "email", + autoComplete: "email", + value: email, + onChange: (e) => setEmail(e.target.value) + } + ), + /* @__PURE__ */ jsxRuntimeExports.jsx( + TextField, + { + required: true, + fullWidth: true, + name: "password", + label: "Password", + type: "password", + id: "password", + autoComplete: "new-password", + value: password, + onChange: (e) => setPassword(e.target.value) + } + ), + /* @__PURE__ */ jsxRuntimeExports.jsx( + TextField, + { + required: true, + fullWidth: true, + name: "confirmPassword", + label: "Confirm Password", + type: "password", + id: "confirmPassword", + autoComplete: "new-password", + value: confirmPassword, + onChange: (e) => setConfirmPassword(e.target.value) + } + ), + error && /* @__PURE__ */ jsxRuntimeExports.jsx(Alert, { severity: "error", children: error }), + /* @__PURE__ */ jsxRuntimeExports.jsx( + Button, + { + type: "submit", + fullWidth: true, + variant: "contained", + disabled: isLoading || !email || !password || !confirmPassword || !displayName, + children: isLoading ? "Signing up..." : "Sign Up" + } + ) + ] }) }); +}; + +const DEV_SCHOOL_UUID = "kevlarai"; +class NeoRegistrationService { + static instance; + constructor() { + } + static getInstance() { + if (!NeoRegistrationService.instance) { + NeoRegistrationService.instance = new NeoRegistrationService(); + } + return NeoRegistrationService.instance; + } + async registerNeo4JUser(user, username, role) { + try { + let schoolNode = null; + if (role.includes("teacher") || role.includes("student")) { + schoolNode = await this.fetchSchoolNode(DEV_SCHOOL_UUID); + if (!schoolNode) { + throw new Error("Failed to fetch required school node"); + } + } + const formData = new FormData(); + formData.append("user_id", user.id); + formData.append("user_type", role); + formData.append("user_name", username); + formData.append("user_email", user.email || ""); + if (schoolNode) { + formData.append("school_uuid_string", schoolNode.uuid_string); + formData.append("school_name", schoolNode.name); + formData.append("school_website", schoolNode.website); + formData.append("school_node_storage_path", schoolNode.node_storage_path); + const workerData = role.includes("teacher") ? { + teacher_code: username, + teacher_name_formal: username, + teacher_email: user.email + } : { + student_code: username, + student_name_formal: username, + student_email: user.email + }; + formData.append("worker_data", JSON.stringify(workerData)); + } + logger.debug("neo4j-service", "🔄 Sending form data", { + userId: user.id, + userType: role, + userName: username, + userEmail: user.email, + schoolNode: schoolNode ? { + uuid_string: schoolNode.uuid_string, + name: schoolNode.name + } : null + }); + const response = await axios.post("/database/entity/create-user", formData, { + headers: { + "Content-Type": "multipart/form-data" + } + }); + if (response.data.status !== "success") { + throw new Error(`Failed to create user: ${JSON.stringify(response.data)}`); + } + const userNode = response.data.data.user_node; + const workerNode = response.data.data.worker_node; + if (response.data.data.calendar_nodes) { + logger.debug("neo4j-service", "🔄 Storing calendar data", { + calendarNodes: response.data.data.calendar_nodes + }); + storageService.set(StorageKeys.CALENDAR_DATA, response.data.data.calendar_nodes); + } + userNode.worker_node_data = JSON.stringify(workerNode); + await this.updateUserNeo4jDetails(user.id, userNode); + logger.info("neo4j-service", "✅ Neo4j user registration successful", { + userId: user.id, + nodeId: userNode.uuid_string, + hasCalendar: !!response.data.data.calendar_nodes + }); + return userNode; + } catch (error) { + logger.error("neo4j-service", "❌ Neo4j user registration failed", error); + throw error; + } + } + async updateUserNeo4jDetails(userId, userNode) { + const { error } = await supabase.from("profiles").update({ + metadata: { + ...userNode + }, + updated_at: (/* @__PURE__ */ new Date()).toISOString() + }).eq("id", userId); + if (error) { + logger.error("neo4j-service", "❌ Failed to update Neo4j details:", error); + throw error; + } + } + async fetchSchoolNode(schoolUrn) { + logger.debug("neo4j-service", "🔄 Fetching school node", { schoolUrn }); + try { + const response = await axios.get(`/database/tools/get-school-node?school_urn=${schoolUrn}`); + if (response.data?.status === "success" && response.data.school_node) { + logger.info("neo4j-service", "✅ School node fetched successfully"); + return response.data.school_node; + } + throw new Error("Failed to fetch school node: " + JSON.stringify(response.data)); + } catch (error) { + logger.error("neo4j-service", "❌ Failed to fetch school node:", error); + throw error; + } + } +} +const neoRegistrationService = NeoRegistrationService.getInstance(); + +const REGISTRATION_SERVICE = "registration-service"; +class RegistrationService { + static instance; + constructor() { + } + static getInstance() { + if (!RegistrationService.instance) { + RegistrationService.instance = new RegistrationService(); + } + return RegistrationService.instance; + } + async register(credentials, displayName) { + try { + logger.debug(REGISTRATION_SERVICE, "🔄 Starting registration", { + email: credentials.email, + role: credentials.role, + hasDisplayName: !!displayName + }); + const username = formatEmailForDatabase(credentials.email); + const { data: authData, error: signUpError } = await supabase.auth.signUp({ + email: credentials.email, + password: credentials.password, + options: { + data: { + user_type: credentials.role, + username, + display_name: displayName + } + } + }); + if (signUpError) { + logger.error(REGISTRATION_SERVICE, "❌ Supabase signup error", { error: signUpError }); + throw signUpError; + } + if (!authData.user) { + logger.error(REGISTRATION_SERVICE, "❌ No user data after registration"); + throw new Error("No user data after registration"); + } + const ccUser = convertToCCUser(authData.user, authData.user.user_metadata); + const { error: updateError } = await supabase.from("profiles").update({ + user_type: credentials.role, + username, + display_name: displayName + }).eq("id", authData.user.id).select().single(); + if (updateError) { + logger.error(REGISTRATION_SERVICE, "❌ Failed to update profile", updateError); + throw updateError; + } + storageService.set(StorageKeys.IS_NEW_REGISTRATION, true); + try { + const userNode = await neoRegistrationService.registerNeo4JUser( + ccUser, + username, + // Pass username for database operations + credentials.role + ); + logger.info(REGISTRATION_SERVICE, "✅ Registration successful with Neo4j setup", { + userId: ccUser.id, + hasUserNode: !!userNode + }); + return { + user: ccUser, + accessToken: authData.session?.access_token || null, + userRole: credentials.role, + message: "Registration successful" + }; + } catch (neo4jError) { + logger.warn(REGISTRATION_SERVICE, "⚠️ Neo4j setup problem", { + userId: ccUser.id, + error: neo4jError + }); + return { + user: ccUser, + accessToken: authData.session?.access_token || null, + userRole: credentials.role, + message: "Registration successful - Neo4j setup pending" + }; + } + } catch (error) { + logger.error(REGISTRATION_SERVICE, "❌ Registration failed:", error); + throw error; + } + } +} +RegistrationService.getInstance(); + +var Microsoft = {}; + +var _interopRequireDefault$c = interopRequireDefaultExports; +Object.defineProperty(Microsoft, "__esModule", { + value: true +}); +var default_1$c = Microsoft.default = void 0; +_interopRequireWildcard(reactExports); +var _createSvgIcon$c = _interopRequireDefault$c(requireCreateSvgIcon()); +var _jsxRuntime$c = jsxRuntimeExports; +function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } +function _interopRequireWildcard(obj, nodeInterop) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } +var _default$c = (0, _createSvgIcon$c.default)( /*#__PURE__*/(0, _jsxRuntime$c.jsx)("path", { + d: "M2 3h9v9H2V3m9 19H2v-9h9v9M21 3v9h-9V3h9m0 19h-9v-9h9v9Z" +}), 'Microsoft'); +default_1$c = Microsoft.default = _default$c; + +const SignupPage = () => { + const location = useLocation(); + const navigate = useNavigate(); + const { user } = useAuth(); + const registrationService = RegistrationService.getInstance(); + const { role = "teacher" } = location.state || {}; + const roleDisplay = role === "teacher" ? "Teacher" : "Student"; + logger.debug("signup-page", "🔍 Signup page loaded", { + role, + hasUser: !!user + }); + reactExports.useEffect(() => { + if (user) { + navigate("/single-player"); + } + }, [user, navigate]); + const handleSignup = async (credentials, displayName) => { + try { + const result = await registrationService.register( + credentials, + displayName + ); + if (result.user) { + navigate("/single-player"); + } + } catch (error) { + logger.error("signup-page", "❌ Registration failed", error); + throw error; + } + }; + const switchRole = () => { + navigate("/signup", { + state: { role: role === "teacher" ? "student" : "teacher" } + }); + }; + if (user) { + return null; + } + return /* @__PURE__ */ jsxRuntimeExports.jsxs( + Container, + { + sx: { + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + minHeight: "100vh", + gap: 4 + }, + children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "h2", component: "h1", gutterBottom: true, children: "ClassroomCopilot.ai" }), + /* @__PURE__ */ jsxRuntimeExports.jsxs(Typography, { variant: "h4", gutterBottom: true, children: [ + roleDisplay, + " Sign Up" + ] }), + /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { width: "100%", maxWidth: 400 }, children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx( + Button, + { + fullWidth: true, + variant: "outlined", + startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(default_1$c, {}), + onClick: () => { + }, + sx: { mb: 3 }, + children: "Sign up with Microsoft" + } + ), + /* @__PURE__ */ jsxRuntimeExports.jsx(Divider, { sx: { my: 2 }, children: "OR" }), + /* @__PURE__ */ jsxRuntimeExports.jsx( + EmailSignupForm, + { + role: `email_${role}`, + onSubmit: handleSignup + } + ), + /* @__PURE__ */ jsxRuntimeExports.jsx( + Stack, + { + direction: "row", + spacing: 2, + justifyContent: "center", + sx: { mt: 3 }, + children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Button, { variant: "text", onClick: switchRole, children: [ + "Switch to ", + role === "teacher" ? "Student" : "Teacher", + " Sign Up" + ] }) + } + ) + ] }) + ] + } + ); +}; + +const CC_BASE_STYLE_CONSTANTS = { + // Container styles + CONTAINER: { + borderRadius: "4px", + borderWidth: "2px", + boxShadow: "0 2px 4px var(--color-muted-1)" + }, + HEADER: { + height: 32, + padding: 8}, + CONTENT: { + padding: 8, + backgroundColor: "white" + }, + COLORS: { + primary: "#3e6589", + border: "#e2e8f0"}, + // Minimum dimensions + MIN_DIMENSIONS: { + width: 100, + height: 100 + } +}; +const CC_CALENDAR_STYLE_CONSTANTS = { + // Calendar event styles + EVENT: { + mainFrame: { + backgroundColor: "transparent", + padding: "0px", + display: "flex", + alignItems: "center", + justifyContent: "center", + minHeight: "100%", + borderRadius: "4px" + }, + title: { + fontSize: "1.1em", + fontWeight: "normal", + textAlign: "center", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + opacity: 1, + padding: "0px 0px", + width: "100%", + letterSpacing: "0.02em", + margin: "0px 0px" + } + } +}; +const CC_SLIDESHOW_STYLE_CONSTANTS = { + DEFAULT_SLIDE_WIDTH: 1280, + DEFAULT_SLIDE_HEIGHT: 720, + SLIDE_HEADER_HEIGHT: 32, + SLIDE_HEADER_PADDING: 8, + SLIDE_CONTENT_PADDING: 0, + SLIDE_BORDER_RADIUS: 4, + SLIDE_BORDER_WIDTH: 1, + SLIDE_SPACING: 16, + SLIDE_COLORS: { + background: "#ffffff", + border: "#e2e8f0", + text: "#ffffff", + secondary: "#718096" + } +}; + +class CCBaseShapeUtil extends BaseBoxShapeUtil { + indicator(shape) { + return /* @__PURE__ */ jsxRuntimeExports.jsx( + "rect", + { + width: shape.props.w, + height: shape.props.h, + fill: "none", + rx: CC_BASE_STYLE_CONSTANTS.CONTAINER.borderRadius, + stroke: CC_BASE_STYLE_CONSTANTS.COLORS.border, + strokeWidth: CC_BASE_STYLE_CONSTANTS.CONTAINER.borderWidth + } + ); + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getToolbarItems(shape) { + return []; + } + onAfterCreate(shape) { + logger.info("cc-base-shape-util", "onAfterCreate", shape); + return shape; + } + component(shape) { + const { + props: { w, h, isLocked } + } = shape; + const toolbarItems = this.getToolbarItems(shape); + return /* @__PURE__ */ jsxRuntimeExports.jsxs( + HTMLContainer, + { + id: shape.id, + style: { + width: toDomPrecision(w), + height: toDomPrecision(h), + backgroundColor: shape.props.headerColor, + borderRadius: CC_BASE_STYLE_CONSTANTS.CONTAINER.borderRadius, + boxShadow: CC_BASE_STYLE_CONSTANTS.CONTAINER.boxShadow, + overflow: "hidden", + position: "relative" + }, + children: [ + /* @__PURE__ */ jsxRuntimeExports.jsxs( + "div", + { + style: { + backgroundColor: shape.props.headerColor, + padding: CC_BASE_STYLE_CONSTANTS.HEADER.padding, + height: CC_BASE_STYLE_CONSTANTS.HEADER.height, + display: "flex", + justifyContent: "space-between", + alignItems: "center", + cursor: isLocked ? "not-allowed" : "move", + pointerEvents: "all", + position: "relative", + zIndex: 1 + }, + children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: "white", fontWeight: "bold" }, children: shape.props.title }), + /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { style: { display: "flex", gap: "4px", alignItems: "center", pointerEvents: "all" }, children: [ + toolbarItems.map((item) => /* @__PURE__ */ jsxRuntimeExports.jsx( + "button", + { + title: item.label, + onClick: (e) => { + logger.info("cc-base-shape-util", "toolbar item clicked", item.id); + e.preventDefault(); + e.stopPropagation(); + item.onClick(e, shape); + }, + onPointerDown: (e) => { + logger.info("cc-base-shape-util", "toolbar item pointer down", item.id); + e.preventDefault(); + e.stopPropagation(); + }, + style: { + background: "transparent", + border: "none", + padding: "4px", + cursor: "pointer", + color: "white", + opacity: item.isActive ? 1 : 0.7, + display: "flex", + alignItems: "center", + justifyContent: "center", + pointerEvents: "all", + fontSize: "16px", + width: "24px", + height: "24px", + zIndex: 100, + userSelect: "none", + position: "relative", + touchAction: "none" + }, + children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { pointerEvents: "none" }, children: item.icon }) + }, + item.id + )), + isLocked && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: "white" }, children: "🔒" }) + ] }) + ] + } + ), + /* @__PURE__ */ jsxRuntimeExports.jsx( + "div", + { + style: { + position: "absolute", + top: CC_BASE_STYLE_CONSTANTS.HEADER.height, + left: 0, + right: 0, + bottom: 0, + overflow: "auto", + padding: CC_BASE_STYLE_CONSTANTS.CONTENT.padding, + backgroundColor: shape.props.backgroundColor + }, + children: this.renderContent(shape) + } + ) + ] + } + ); + } +} + +const baseShapeProps = { + title: string, + w: number, + h: number, + headerColor: string, + backgroundColor: string, + isLocked: boolean +}; +const ccShapeProps = { + calendar: { + ...baseShapeProps, + date: string, + selectedDate: string, + view: string, + events: arrayOf(object$1({ + id: string, + title: string, + start: string, + end: string, + groupId: string.optional(), + extendedProps: object$1({ + subjectClass: string, + color: string, + periodCode: string, + node_storage_path: string.optional() + }) + })) + }, + liveTranscription: { + ...baseShapeProps, + isRecording: boolean, + segments: arrayOf(object$1({ + id: string, + text: string, + completed: boolean, + start: string, + end: string + })), + currentSegment: object$1({ + id: string, + text: string, + completed: boolean, + start: string, + end: string + }).optional(), + lastProcessedSegment: string.optional() + }, + settings: { + ...baseShapeProps, + userEmail: string, + user_role: string, + isTeacher: boolean + }, + slideshow: { + ...baseShapeProps, + currentSlideIndex: number, + slidePattern: string, + numSlides: number, + slides: arrayOf(object$1({ + imageData: string, + meta: object$1({ + text: string, + format: string + }) + })).optional() + }, + slide: { + ...baseShapeProps, + imageData: string, + meta: object$1({ + text: string, + format: string + }) + }, + "cc-youtube-embed": { + ...baseShapeProps, + video_url: string, + transcript: arrayOf(object$1({ + start: number, + duration: number, + text: string + })), + transcriptVisible: boolean + }, + search: { + ...baseShapeProps, + query: string, + results: arrayOf(object$1({ + title: string, + url: string, + content: string + })), + isSearching: boolean + }, + webBrowser: { + ...baseShapeProps, + url: string, + history: arrayOf(string), + currentHistoryIndex: number, + isLoading: boolean + } +}; +({ + "cc-slide-layout": { + isMovingWithParent: boolean.optional(), + placeholder: boolean.optional()} +}); +const getDefaultCCBaseProps = () => ({ + title: "Base Shape", + w: 100, + h: 100, + headerColor: "#3e6589", + backgroundColor: "#ffffff", + isLocked: false +}); +const getDefaultCCCalendarProps = () => ({ + ...getDefaultCCBaseProps(), + date: (/* @__PURE__ */ new Date()).toISOString(), + selectedDate: (/* @__PURE__ */ new Date()).toISOString(), + view: "timeGridWeek", + events: [] +}); +const getDefaultCCLiveTranscriptionProps = () => ({ + ...getDefaultCCBaseProps(), + isRecording: false, + segments: [], + currentSegment: void 0, + lastProcessedSegment: void 0 +}); +const getDefaultCCSettingsProps = () => ({ + ...getDefaultCCBaseProps(), + userEmail: "", + user_role: "", + isTeacher: false +}); +function getDefaultCCSlideShowProps() { + const baseWidth = 1280; + const baseHeight = 720; + const totalHeight = baseHeight + CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_HEADER_HEIGHT + // Slideshow's own header + CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_SPACING * 2 + // Top and bottom spacing + CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_CONTENT_PADDING; + return { + title: "Slideshow", + w: baseWidth, + h: totalHeight, + headerColor: "#3e6589", + backgroundColor: "#0f0f0f", + isLocked: false, + currentSlideIndex: 0, + slidePattern: "horizontal", + numSlides: 3, + slides: [] + }; +} +function getDefaultCCSlideProps() { + const baseWidth = 1280; + const baseHeight = 720; + const totalHeight = baseHeight + CC_BASE_STYLE_CONSTANTS.HEADER.height; + return { + title: "Slide", + w: baseWidth, + h: totalHeight, + headerColor: "#3e6589", + backgroundColor: "#0f0f0f", + isLocked: false, + imageData: "", + meta: { + text: "", + format: "markdown" + } + }; +} +function getDefaultCCYoutubeEmbedProps() { + const videoHeight = 450; + const totalHeight = videoHeight + CC_BASE_STYLE_CONSTANTS.HEADER.height + CC_BASE_STYLE_CONSTANTS.CONTENT.padding * 2; + return { + ...getDefaultCCBaseProps(), + title: "YouTube Video", + w: 800, + h: totalHeight, + headerColor: "#ff0000", + backgroundColor: "#0f0f0f", + isLocked: false, + video_url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + transcript: [], + transcriptVisible: false + }; +} +const getDefaultCCSearchProps = () => ({ + ...getDefaultCCBaseProps(), + w: 400, + h: 500, + title: "Search", + headerColor: "#1a73e8", + backgroundColor: "#ffffff", + query: "", + results: [], + isSearching: false +}); +const getDefaultCCWebBrowserProps = () => ({ + ...getDefaultCCBaseProps(), + title: "Web Browser", + w: 800, + h: 600, + headerColor: "#1a73e8", + backgroundColor: "#ffffff", + url: "", + history: [], + currentHistoryIndex: -1, + isLoading: false +}); + +const ccShapeMigrations = { + calendar: { + firstVersion: 1, + currentVersion: 1, + migrators: { + 1: { + up: (record) => { + if (record.typeName !== "shape") return record; + const shape = record; + if (shape.type !== "cc-calendar") return record; + return { + ...shape, + props: { + ...getDefaultCCCalendarProps(), + ...shape.props + } + }; + }, + down: (record) => { + return record; + } + } + } + }, + liveTranscription: { + firstVersion: 1, + currentVersion: 1, + migrators: { + 1: { + up: (record) => { + if (record.typeName !== "shape") return record; + const shape = record; + if (shape.type !== "cc-live-transcription") return record; + return { + ...shape, + props: { + ...getDefaultCCLiveTranscriptionProps(), + ...shape.props + } + }; + }, + down: (record) => { + return record; + } + } + } + }, + settings: { + firstVersion: 1, + currentVersion: 1, + migrators: { + 1: { + up: (record) => { + if (record.typeName !== "shape") return record; + const shape = record; + if (shape.type !== "cc-settings") return record; + return { + ...shape, + props: { + ...getDefaultCCSettingsProps(), + ...shape.props + } + }; + }, + down: (record) => { + return record; + } + } + } + }, + slideshow: { + firstVersion: 1, + currentVersion: 1, + migrators: { + 1: { + up: (record) => { + if (record.typeName !== "shape") return record; + const shape = record; + if (shape.type !== "cc-slideshow") return record; + return { + ...shape, + props: { + ...getDefaultCCSlideShowProps(), + ...shape.props + } + }; + }, + down: (record) => { + return record; + } + } + } + }, + slide: { + firstVersion: 1, + currentVersion: 1, + migrators: { + 1: { + up: (record) => { + if (record.typeName !== "shape") return record; + const shape = record; + if (shape.type !== "cc-slide") return record; + return { + ...shape, + props: { + ...getDefaultCCSlideProps(), + ...shape.props + } + }; + }, + down: (record) => { + return record; + } + } + } + }, + search: { + firstVersion: 1, + currentVersion: 1, + migrators: { + 1: { + up: (record) => { + if (record.typeName !== "shape") return record; + const shape = record; + if (shape.type !== "cc-search") return record; + return { + ...shape, + props: { + ...getDefaultCCSearchProps(), + ...shape.props + } + }; + }, + down: (record) => { + return record; + } + } + } + }, + webBrowser: { + firstVersion: 1, + currentVersion: 1, + migrators: { + 1: { + up: (record) => { + if (record.typeName !== "shape") return record; + const shape = record; + if (shape.type !== "cc-web-browser") return record; + return { + ...shape, + props: { + ...getDefaultCCWebBrowserProps(), + ...shape.props + } + }; + }, + down: (record) => { + return record; + } + } + } + } +}; + +class CCSlideShowShapeUtil extends CCBaseShapeUtil { + static type = "cc-slideshow"; + static props = ccShapeProps.slideshow; + static migrations = ccShapeMigrations.slideshow; + static styles = { + color: DefaultColorStyle, + dash: DefaultDashStyle, + size: DefaultSizeStyle + }; + getDefaultProps() { + return getDefaultCCSlideShowProps(); + } + canResize = () => false; + isAspectRatioLocked = () => true; + hideResizeHandles = () => false; + hideRotateHandle = () => false; + canEdit = () => false; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + canBind(args) { + return true; + } + onBeforeCreate(shape) { + return shape; + } + renderContent = () => { + return /* @__PURE__ */ jsxRuntimeExports.jsx("div", {}); + }; +} + +class CCSlideShapeUtil extends CCBaseShapeUtil { + static type = "cc-slide"; + static props = ccShapeProps.slide; + static migrations = ccShapeMigrations.slide; + static styles = { + color: DefaultColorStyle, + dash: DefaultDashStyle, + size: DefaultSizeStyle + }; + getDefaultProps() { + return getDefaultCCSlideProps(); + } + canResize = () => false; + isAspectRatioLocked = () => true; + hideResizeHandles = () => true; + hideRotateHandle = () => true; + canEdit = () => false; + canBind(args) { + return args.fromShapeType === "cc-slideshow" && args.toShapeType === "cc-slide" && args.bindingType === "cc-slide-layout"; + } + getTargetSlideshow(shape, pageAnchor) { + return this.editor.getShapeAtPoint(pageAnchor, { + hitInside: true, + filter: (otherShape) => this.editor.canBindShapes({ fromShape: otherShape, toShape: shape, binding: "cc-slide-layout" }) + }); + } + getBindingIndexForPosition(shape, slideshow, pageAnchor) { + const allBindings = this.editor.getBindingsFromShape(slideshow, "cc-slide-layout").filter((b) => !b.props.placeholder || b.toId === shape.id).sort((a, b) => a.props.index > b.props.index ? 1 : -1); + const spacing = CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_SPACING; + const headerHeight = CC_BASE_STYLE_CONSTANTS.HEADER.height; + const contentPadding = CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_CONTENT_PADDING; + let order; + if (slideshow.props.slidePattern === "horizontal") { + order = clamp$1( + Math.round((pageAnchor.x - slideshow.x - spacing) / (shape.props.w + spacing)), + 0, + allBindings.length + ); + } else if (slideshow.props.slidePattern === "vertical") { + order = clamp$1( + Math.round((pageAnchor.y - slideshow.y - headerHeight - contentPadding - spacing) / (shape.props.h + spacing)), + 0, + allBindings.length + ); + } else if (slideshow.props.slidePattern === "grid") { + const cols = Math.ceil(Math.sqrt(allBindings.length)); + const col = clamp$1( + Math.round((pageAnchor.x - slideshow.x - spacing) / (shape.props.w + spacing)), + 0, + cols + ); + const row = clamp$1( + Math.round((pageAnchor.y - slideshow.y - headerHeight - contentPadding - spacing) / (shape.props.h + spacing)), + 0, + Math.ceil(allBindings.length / cols) + ); + order = clamp$1(row * cols + col, 0, allBindings.length); + } else { + order = 0; + } + const belowSib = allBindings[order - 1]; + const aboveSib = allBindings[order]; + if (belowSib?.toId === shape.id) { + return belowSib.props.index; + } else if (aboveSib?.toId === shape.id) { + return aboveSib.props.index; + } + return getIndexBetween(belowSib?.props.index, aboveSib?.props.index); + } + onTranslateStart = (shape) => { + const bindings = this.editor.getBindingsToShape(shape.id, "cc-slide-layout"); + logger.debug("shape", "✅ onTranslateStart", { + shape, + bindings, + hasBindings: bindings.length > 0, + bindingTypes: bindings.map((b) => ({ + id: b.id, + fromId: b.fromId, + placeholder: b.props.placeholder, + isMovingWithParent: b.props.isMovingWithParent + })) + }); + this.editor.updateBindings( + bindings.map((binding) => ({ + ...binding, + props: { ...binding.props, placeholder: true } + })) + ); + }; + onTranslate = (initial, current) => { + const pageAnchor = this.editor.getShapePageTransform(current).applyToPoint({ x: current.props.w / 2, y: current.props.h / 2 }); + const targetSlideshow = this.getTargetSlideshow(current, pageAnchor); + const currentBindings = this.editor.getBindingsToShape(current.id, "cc-slide-layout"); + const currentBinding = currentBindings[0]; + const currentSlideshow = currentBinding ? this.editor.getShape(currentBinding.fromId) : void 0; + logger.debug("shape", "✅ onTranslate", { + initial, + current, + hasTargetSlideshow: !!targetSlideshow, + targetSlideshowId: targetSlideshow?.id, + currentBindings: currentBindings.map((b) => ({ + id: b.id, + fromId: b.fromId, + placeholder: b.props.placeholder, + isMovingWithParent: b.props.isMovingWithParent + })), + isInSlideshow: targetSlideshow ? this.isSlideInSlideshow(current, targetSlideshow) : false + }); + if (currentBinding && currentSlideshow && !this.isSlideInSlideshow(current, currentSlideshow)) { + logger.debug("shape", "✅ onTranslate: Moving out of slideshow", { + slideId: current.id, + slideshowId: currentSlideshow.id + }); + this.editor.deleteBindings(currentBindings); + return current; + } + if (!targetSlideshow) { + return current; + } + const index = this.getBindingIndexForPosition(current, targetSlideshow, pageAnchor); + if (currentBinding && currentBinding.fromId === targetSlideshow.id) { + if (currentBinding.props.index !== index) { + logger.debug("shape", "✅ onTranslate: Updating binding index", { + slideId: current.id, + slideshowId: targetSlideshow.id, + oldIndex: currentBinding.props.index, + newIndex: index + }); + this.editor.updateBinding({ + id: currentBinding.id, + type: currentBinding.type, + fromId: currentBinding.fromId, + toId: currentBinding.toId, + props: { + ...currentBinding.props, + index, + isMovingWithParent: true + } + }); + } + } else if (this.isSlideInSlideshow(current, targetSlideshow)) { + logger.debug("shape", "✅ onTranslate: Creating new placeholder binding", { + slideId: current.id, + slideshowId: targetSlideshow.id, + index + }); + this.editor.createBinding({ + type: "cc-slide-layout", + fromId: targetSlideshow.id, + toId: current.id, + props: { + index, + isMovingWithParent: true, + placeholder: true + } + }); + } + return current; + }; + isSlideInSlideshow(slide, slideshow) { + const slideCenter = this.editor.getShapePageTransform(slide).applyToPoint({ + x: slide.props.w / 2, + y: slide.props.h / 2 + }); + const slideshowBounds = this.editor.getShapeGeometry(slideshow).bounds; + const padding = CC_SLIDESHOW_STYLE_CONSTANTS.SLIDE_SPACING / 4; + return slideCenter.x >= slideshow.x + padding && slideCenter.x <= slideshow.x + slideshowBounds.width - padding && slideCenter.y >= slideshow.y + padding && slideCenter.y <= slideshow.y + slideshowBounds.height - padding; + } + onTranslateEnd = (shape) => { + const pageAnchor = this.editor.getShapePageTransform(shape).applyToPoint({ x: shape.props.w / 2, y: shape.props.h / 2 }); + const targetSlideshow = this.getTargetSlideshow(shape, pageAnchor); + const bindings = this.editor.getBindingsToShape(shape.id, "cc-slide-layout"); + logger.debug("shape", "✅ onTranslateEnd", { + shape, + hasTargetSlideshow: !!targetSlideshow, + targetSlideshowId: targetSlideshow?.id, + bindings: bindings.map((b) => ({ + id: b.id, + fromId: b.fromId, + placeholder: b.props.placeholder, + isMovingWithParent: b.props.isMovingWithParent + })), + isInSlideshow: targetSlideshow ? this.isSlideInSlideshow(shape, targetSlideshow) : false + }); + if (targetSlideshow && this.isSlideInSlideshow(shape, targetSlideshow)) { + const index = this.getBindingIndexForPosition(shape, targetSlideshow, pageAnchor); + const existingBinding = bindings[0]; + if (existingBinding && existingBinding.fromId === targetSlideshow.id) { + logger.debug("shape", "✅ onTranslateEnd: Updating existing binding", { + slideId: shape.id, + slideshowId: targetSlideshow.id, + bindingId: existingBinding.id, + index + }); + this.editor.updateBinding({ + id: existingBinding.id, + type: existingBinding.type, + fromId: existingBinding.fromId, + toId: existingBinding.toId, + props: { + index, + isMovingWithParent: true, + placeholder: false + } + }); + } else { + logger.debug("shape", "✅ onTranslateEnd: Creating new binding", { + slideId: shape.id, + slideshowId: targetSlideshow.id, + index + }); + this.editor.deleteBindings(bindings); + this.editor.createBinding({ + type: "cc-slide-layout", + fromId: targetSlideshow.id, + toId: shape.id, + props: { + index, + isMovingWithParent: true, + placeholder: false + } + }); + } + } else { + logger.debug("shape", "✅ onTranslateEnd: Removing bindings", { + slideId: shape.id, + bindings: bindings.map((b) => b.id) + }); + this.editor.deleteBindings(bindings); + } + }; + renderContent = (shape) => { + return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: { + width: "100%", + height: "100%", + position: "relative", + backgroundColor: "white", + borderRadius: "4px", + overflow: "hidden" + }, children: shape.props.imageData && /* @__PURE__ */ jsxRuntimeExports.jsx( + "img", + { + src: shape.props.imageData, + alt: shape.props.title, + style: { + width: "100%", + height: `100%`, + objectFit: "contain", + position: "absolute", + top: 0, + left: 0 + } + } + ) }); + }; +} + +var n,l$1,u$1,i$3,t,r$1,o,f$1,e$1,c$2={},s=[],a$1=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i;function h(n,l){for(var u in l)n[u]=l[u];return n}function v$1(n){var l=n.parentNode;l&&l.removeChild(n);}function y(l,u,i){var t,r,o,f={};for(o in u)"key"==o?t=u[o]:"ref"==o?r=u[o]:f[o]=u[o];if(arguments.length>2&&(f.children=arguments.length>3?n.call(arguments,2):i),"function"==typeof l&&null!=l.defaultProps)for(o in l.defaultProps) void 0===f[o]&&(f[o]=l.defaultProps[o]);return p(l,f,t,r,null)}function p(n,i,t,r,o){var f={type:n,props:i,key:t,ref:r,__k:null,__:null,__b:0,__e:null,__d:void 0,__c:null,__h:null,constructor:void 0,__v:null==o?++u$1:o};return null==o&&null!=l$1.vnode&&l$1.vnode(f),f}function d(){return {current:null}}function _(n){return n.children}function k$1(n,l,u,i,t){var r;for(r in u)"children"===r||"key"===r||r in l||g$2(n,r,null,u[r],i);for(r in l)t&&"function"!=typeof l[r]||"children"===r||"key"===r||"value"===r||"checked"===r||u[r]===l[r]||g$2(n,r,l[r],u[r],i);}function b$1(n,l,u){"-"===l[0]?n.setProperty(l,null==u?"":u):n[l]=null==u?"":"number"!=typeof u||a$1.test(l)?u:u+"px";}function g$2(n,l,u,i,t){var r;n:if("style"===l)if("string"==typeof u)n.style.cssText=u;else {if("string"==typeof i&&(n.style.cssText=i=""),i)for(l in i)u&&l in u||b$1(n.style,l,"");if(u)for(l in u)i&&u[l]===i[l]||b$1(n.style,l,u[l]);}else if("o"===l[0]&&"n"===l[1])r=l!==(l=l.replace(/Capture$/,"")),l=l.toLowerCase()in n?l.toLowerCase().slice(2):l.slice(2),n.l||(n.l={}),n.l[l+r]=u,u?i||n.addEventListener(l,r?w$2:m$1,r):n.removeEventListener(l,r?w$2:m$1,r);else if("dangerouslySetInnerHTML"!==l){if(t)l=l.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if("width"!==l&&"height"!==l&&"href"!==l&&"list"!==l&&"form"!==l&&"tabIndex"!==l&&"download"!==l&&l in n)try{n[l]=null==u?"":u;break n}catch(n){}"function"==typeof u||(null==u||false===u&&-1==l.indexOf("-")?n.removeAttribute(l):n.setAttribute(l,u));}}function m$1(n){t=true;try{return this.l[n.type+!1](l$1.event?l$1.event(n):n)}finally{t=false;}}function w$2(n){t=true;try{return this.l[n.type+!0](l$1.event?l$1.event(n):n)}finally{t=false;}}function x$1(n,l){this.props=n,this.context=l;}function A(n,l){if(null==l)return n.__?A(n.__,n.__.__k.indexOf(n)+1):null;for(var u;ll&&r$1.sort(function(n,l){return n.__v.__b-l.__v.__b}));$$1.__r=0;}function H$1(n,l,u,i,t,r,o,f,e,a){var h,v,y,d,k,b,g,m=i&&i.__k||s,w=m.length;for(u.__k=[],h=0;h0?p(d.type,d.props,d.key,d.ref?d.ref:null,d.__v):d)){if(d.__=u,d.__b=u.__b+1,null===(y=m[h])||y&&d.key==y.key&&d.type===y.type)m[h]=void 0;else for(v=0;v=0;l--)if((u=n.__k[l])&&(i=L$1(u)))return i;return null}function M(n,u,i,t,r,o,f,e,c){var s,a,v,y,p,d,k,b,g,m,w,A,P,C,T,$=u.type;if(void 0!==u.constructor)return null;null!=i.__h&&(c=i.__h,e=u.__e=i.__e,u.__h=null,o=[e]),(s=l$1.__b)&&s(u);try{n:if("function"==typeof $){if(b=u.props,g=(s=$.contextType)&&t[s.__c],m=s?g?g.props.value:s.__:t,i.__c?k=(a=u.__c=i.__c).__=a.__E:("prototype"in $&&$.prototype.render?u.__c=a=new $(b,m):(u.__c=a=new x$1(b,m),a.constructor=$,a.render=B$1),g&&g.sub(a),a.props=b,a.state||(a.state={}),a.context=m,a.__n=t,v=a.__d=!0,a.__h=[],a._sb=[]),null==a.__s&&(a.__s=a.state),null!=$.getDerivedStateFromProps&&(a.__s==a.state&&(a.__s=h({},a.__s)),h(a.__s,$.getDerivedStateFromProps(b,a.__s))),y=a.props,p=a.state,a.__v=u,v)null==$.getDerivedStateFromProps&&null!=a.componentWillMount&&a.componentWillMount(),null!=a.componentDidMount&&a.__h.push(a.componentDidMount);else {if(null==$.getDerivedStateFromProps&&b!==y&&null!=a.componentWillReceiveProps&&a.componentWillReceiveProps(b,m),!a.__e&&null!=a.shouldComponentUpdate&&!1===a.shouldComponentUpdate(b,a.__s,m)||u.__v===i.__v){for(u.__v!==i.__v&&(a.props=b,a.state=a.__s,a.__d=!1),u.__e=i.__e,u.__k=i.__k,u.__k.forEach(function(n){n&&(n.__=u);}),w=0;w3;)e.pop()();if(e[1]>>1,1),e.i.removeChild(n);}}),D$1(y(P,{context:e.context},n.__v),e.l)):e.l&&e.componentWillUnmount();}function j(n,e){var r=y($,{__v:n,i:e});return r.containerInfo=e,r}(V.prototype=new x$1).__a=function(n){var t=this,e=F(t.__v),r=t.o.get(n);return r[0]++,function(u){var o=function(){t.props.revealOrder?(r.push(u),W(t,n,r)):u();};e?e(o):o();}},V.prototype.render=function(n){this.u=null,this.o=new Map;var t=j$2(n.children);n.revealOrder&&"b"===n.revealOrder[0]&&t.reverse();for(var e=t.length;e--;)this.o.set(t[e],this.u=[1,0,this.u]);return n.children},V.prototype.componentDidUpdate=V.prototype.componentDidMount=function(){var n=this;this.o.forEach(function(t,e){W(n,e,t);});};var z="undefined"!=typeof Symbol&&Symbol.for&&Symbol.for("react.element")||60103,B=/^(?:accent|alignment|arabic|baseline|cap|clip(?!PathU)|color|dominant|fill|flood|font|glyph(?!R)|horiz|image|letter|lighting|marker(?!H|W|U)|overline|paint|pointer|shape|stop|strikethrough|stroke|text(?!L)|transform|underline|unicode|units|v|vector|vert|word|writing|x(?!C))[A-Z]/,H="undefined"!=typeof document,Z=function(n){return ("undefined"!=typeof Symbol&&"symbol"==typeof Symbol()?/fil|che|rad/i:/fil|che|ra/i).test(n)};x$1.prototype.isReactComponent={},["componentWillMount","componentWillReceiveProps","componentWillUpdate"].forEach(function(t){Object.defineProperty(x$1.prototype,t,{configurable:true,get:function(){return this["UNSAFE_"+t]},set:function(n){Object.defineProperty(this,t,{configurable:true,writable:true,value:n});}});});var G=l$1.event;function J(){}function K(){return this.cancelBubble}function Q(){return this.defaultPrevented}l$1.event=function(n){return G&&(n=G(n)),n.persist=J,n.isPropagationStopped=K,n.isDefaultPrevented=Q,n.nativeEvent=n};var nn={configurable:true,get:function(){return this.class}},tn=l$1.vnode;l$1.vnode=function(n){var t=n.type,e=n.props,u=e;if("string"==typeof t){var o=-1===t.indexOf("-");for(var i in u={},e){var l=e[i];H&&"children"===i&&"noscript"===t||"value"===i&&"defaultValue"in e&&null==l||("defaultValue"===i&&"value"in e&&null==e.value?i="value":"download"===i&&true===l?l="":/ondoubleclick/i.test(i)?i="ondblclick":/^onchange(textarea|input)/i.test(i+t)&&!Z(e.type)?i="oninput":/^onfocus$/i.test(i)?i="onfocusin":/^onblur$/i.test(i)?i="onfocusout":/^on(Ani|Tra|Tou|BeforeInp|Compo)/.test(i)?i=i.toLowerCase():o&&B.test(i)?i=i.replace(/[A-Z0-9]/g,"-$&").toLowerCase():null===l&&(l=void 0),/^oninput$/i.test(i)&&(i=i.toLowerCase(),u[i]&&(i="oninputCapture")),u[i]=l);}"select"==t&&u.multiple&&Array.isArray(u.value)&&(u.value=j$2(e.children).forEach(function(n){n.props.selected=-1!=u.value.indexOf(n.props.value);})),"select"==t&&null!=u.defaultValue&&(u.value=j$2(e.children).forEach(function(n){n.props.selected=u.multiple?-1!=u.defaultValue.indexOf(n.props.value):u.defaultValue==n.props.value;})),n.props=u,e.class!=e.className&&(nn.enumerable="className"in e,null!=e.className&&(u.class=e.className),Object.defineProperty(u,"className",nn));}n.$$typeof=z,tn&&tn(n);};var en=l$1.__r;l$1.__r=function(n){en&&en(n),n.__c;}; + +const styleTexts = []; +const styleEls = new Map(); +function injectStyles(styleText) { + styleTexts.push(styleText); + styleEls.forEach((styleEl) => { + appendStylesTo(styleEl, styleText); + }); +} +function ensureElHasStyles(el) { + if (el.isConnected && // sometimes true if SSR system simulates DOM + el.getRootNode // sometimes undefined if SSR system simulates DOM + ) { + registerStylesRoot(el.getRootNode()); + } +} +function registerStylesRoot(rootNode) { + let styleEl = styleEls.get(rootNode); + if (!styleEl || !styleEl.isConnected) { + styleEl = rootNode.querySelector('style[data-fullcalendar]'); + if (!styleEl) { + styleEl = document.createElement('style'); + styleEl.setAttribute('data-fullcalendar', ''); + const nonce = getNonceValue(); + if (nonce) { + styleEl.nonce = nonce; + } + const parentEl = rootNode === document ? document.head : rootNode; + const insertBefore = rootNode === document + ? parentEl.querySelector('script,link[rel=stylesheet],link[as=style],style') + : parentEl.firstChild; + parentEl.insertBefore(styleEl, insertBefore); + } + styleEls.set(rootNode, styleEl); + hydrateStylesRoot(styleEl); + } +} +function hydrateStylesRoot(styleEl) { + for (const styleText of styleTexts) { + appendStylesTo(styleEl, styleText); + } +} +function appendStylesTo(styleEl, styleText) { + const { sheet } = styleEl; + const ruleCnt = sheet.cssRules.length; + styleText.split('}').forEach((styleStr, i) => { + styleStr = styleStr.trim(); + if (styleStr) { + sheet.insertRule(styleStr + '}', ruleCnt + i); + } + }); +} +// nonce +// ------------------------------------------------------------------------------------------------- +let queriedNonceValue; +function getNonceValue() { + if (queriedNonceValue === undefined) { + queriedNonceValue = queryNonceValue(); + } + return queriedNonceValue; +} +/* +TODO: discourage meta tag and instead put nonce attribute on placeholder + + ` + } + ); +} + +function getFrameHeadingSide(editor, shape) { + const pageRotation = canonicalizeRotation(editor.getShapePageTransform(shape.id).rotation()); + const offsetRotation = pageRotation + Math.PI / 4; + const scaledRotation = (offsetRotation * (2 / Math.PI) + 4) % 4; + return Math.floor(scaledRotation); +} +function getFrameHeadingInfo(editor, shape, opts) { + const spans = editor.textMeasure.measureTextSpans( + defaultEmptyAs(shape.props.name, "Frame") + String.fromCharCode(8203), + opts + ); + const firstSpan = spans[0]; + const lastSpan = last$1(spans); + const labelTextWidth = lastSpan.box.w + lastSpan.box.x - firstSpan.box.x; + return { + box: new Box(0, -opts.height, labelTextWidth, opts.height), + spans + }; +} +function getFrameHeadingOpts(shape, color) { + return { + fontSize: 12, + fontFamily: "Inter, sans-serif", + textAlign: "start", + width: shape.props.w, + height: 32, + padding: 0, + lineHeight: 1, + fontStyle: "normal", + fontWeight: "normal", + overflow: "truncate-ellipsis", + verticalTextAlign: "middle", + fill: color, + offsetY: -34, + offsetX: 2 + }; +} +function getFrameHeadingTranslation(shape, side, isSvg) { + const u = isSvg ? "" : "px"; + const r = isSvg ? "" : "deg"; + let labelTranslate; + switch (side) { + case 0: + labelTranslate = ``; + break; + case 3: + labelTranslate = `translate(${toDomPrecision(shape.props.w)}${u}, 0${u}) rotate(90${r})`; + break; + case 2: + labelTranslate = `translate(${toDomPrecision(shape.props.w)}${u}, ${toDomPrecision( + shape.props.h + )}${u}) rotate(180${r})`; + break; + case 1: + labelTranslate = `translate(0${u}, ${toDomPrecision(shape.props.h)}${u}) rotate(270${r})`; + break; + default: + throw Error("labelSide out of bounds"); + } + return labelTranslate; +} + +const FrameLabelInput = reactExports.forwardRef(({ id, name, isEditing }, ref) => { + const editor = useEditor(); + const handleKeyDown = reactExports.useCallback( + (e) => { + if (e.key === "Enter" && !e.nativeEvent.isComposing) { + stopEventPropagation(e); + e.currentTarget.blur(); + editor.setEditingShape(null); + } + }, + [editor] + ); + const handleBlur = reactExports.useCallback( + (e) => { + const shape = editor.getShape(id); + if (!shape) return; + const name2 = shape.props.name; + const value = e.currentTarget.value.trim(); + if (name2 === value) return; + editor.updateShapes([ + { + id, + type: "frame", + props: { name: value } + } + ]); + }, + [id, editor] + ); + const handleChange = reactExports.useCallback( + (e) => { + const shape = editor.getShape(id); + if (!shape) return; + const name2 = shape.props.name; + const value = e.currentTarget.value; + if (name2 === value) return; + editor.updateShapes([ + { + id, + type: "frame", + props: { name: value } + } + ]); + }, + [id, editor] + ); + return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `tl-frame-label ${isEditing ? "tl-frame-label__editing" : ""}`, children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx( + "input", + { + className: "tl-frame-name-input", + ref, + style: { display: isEditing ? void 0 : "none" }, + value: name, + autoFocus: true, + onKeyDown: handleKeyDown, + onBlur: handleBlur, + onChange: handleChange + } + ), + defaultEmptyAs(name, "Frame") + String.fromCharCode(8203) + ] }); +}); + +function FrameHeading({ + id, + name, + width, + height +}) { + const editor = useEditor(); + const { side, translation } = useValue( + "shape rotation", + () => { + const shape = editor.getShape(id); + if (!shape) { + return { + side: 0, + translation: "translate(0, 0)" + }; + } + const labelSide = getFrameHeadingSide(editor, shape); + return { + side: labelSide, + translation: getFrameHeadingTranslation(shape, labelSide, false) + }; + }, + [editor, id] + ); + const rInput = reactExports.useRef(null); + const isEditing = useIsEditing(id); + reactExports.useEffect(() => { + const el = rInput.current; + if (el && isEditing) { + el.focus(); + el.select(); + } + }, [rInput, isEditing]); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + "div", + { + className: "tl-frame-heading", + style: { + overflow: isEditing ? "visible" : "hidden", + maxWidth: `calc(var(--tl-zoom) * ${side === 0 || side === 2 ? Math.ceil(width) : Math.ceil(height)}px + var(--space-5))`, + bottom: "100%", + transform: `${translation} scale(var(--tl-scale)) translateX(calc(-1 * var(--space-3))` + }, + children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tl-frame-heading-hit-area", children: /* @__PURE__ */ jsxRuntimeExports.jsx(FrameLabelInput, { ref: rInput, id, name, isEditing }) }) + } + ); +} + +function defaultEmptyAs(str, dflt) { + if (str.match(/^\s*$/)) { + return dflt; + } + return str; +} +class FrameShapeUtil extends BaseBoxShapeUtil { + static type = "frame"; + static props = frameShapeProps; + static migrations = frameShapeMigrations; + canEdit() { + return true; + } + getDefaultProps() { + return { w: 160 * 2, h: 90 * 2, name: "" }; + } + getGeometry(shape) { + const { editor } = this; + const z = editor.getZoomLevel(); + const opts = getFrameHeadingOpts(shape, "black"); + const headingInfo = getFrameHeadingInfo(editor, shape, opts); + const labelSide = getFrameHeadingSide(editor, shape); + let x, y, w, h; + const { w: hw, h: hh } = headingInfo.box; + const scaledW = Math.min(hw, shape.props.w * z); + const scaledH = Math.min(hh, shape.props.h * z); + switch (labelSide) { + case 0: { + x = -8 / z; + y = (-hh - 4) / z; + w = (scaledW + 16) / z; + h = hh / z; + break; + } + case 1: { + x = (-hh - 4) / z; + h = (scaledH + 16) / z; + y = shape.props.h - h + 8 / z; + w = hh / z; + break; + } + case 2: { + x = shape.props.w - (scaledW + 8) / z; + y = shape.props.h + 4 / z; + w = (scaledH + 16) / z; + h = hh / z; + break; + } + case 3: { + x = shape.props.w + 4 / z; + h = (scaledH + 16) / z; + y = -8 / z; + w = hh / z; + break; + } + } + return new Group2d({ + children: [ + new Rectangle2d({ + width: shape.props.w, + height: shape.props.h, + isFilled: false + }), + new Rectangle2d({ + x, + y, + width: w, + height: h, + isFilled: true, + isLabel: true + }) + ] + }); + } + getText(shape) { + return shape.props.name; + } + component(shape) { + const bounds = this.editor.getShapeGeometry(shape).bounds; + const theme = useDefaultColorTheme(); + const isCreating = useValue( + "is creating this shape", + () => { + const resizingState = this.editor.getStateDescendant("select.resizing"); + if (!resizingState) return false; + if (!resizingState.getIsActive()) return false; + const info = resizingState?.info; + if (!info) return false; + return info.isCreating && this.editor.getOnlySelectedShapeId() === shape.id; + }, + [shape.id] + ); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(SVGContainer, { children: /* @__PURE__ */ jsxRuntimeExports.jsx( + "rect", + { + className: classNames("tl-frame__body", { "tl-frame__creating": isCreating }), + width: bounds.width, + height: bounds.height, + fill: theme.solid, + stroke: theme.text + } + ) }), + isCreating ? null : /* @__PURE__ */ jsxRuntimeExports.jsx( + FrameHeading, + { + id: shape.id, + name: shape.props.name, + width: bounds.width, + height: bounds.height + } + ) + ] }); + } + toSvg(shape, ctx) { + const theme = getDefaultColorTheme({ isDarkMode: ctx.isDarkMode }); + const labelSide = getFrameHeadingSide(this.editor, shape); + const labelTranslate = getFrameHeadingTranslation(shape, labelSide, true); + const opts = getFrameHeadingOpts(shape, theme.text); + const { box: labelBounds, spans } = getFrameHeadingInfo(this.editor, shape, opts); + const text = createTextJsxFromSpans(this.editor, spans, opts); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx( + "rect", + { + width: shape.props.w, + height: shape.props.h, + fill: theme.solid, + stroke: theme.black.solid, + strokeWidth: 1, + rx: 1, + ry: 1 + } + ), + /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { transform: labelTranslate, children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx( + "rect", + { + x: labelBounds.x - 8, + y: labelBounds.y - 4, + width: labelBounds.width + 20, + height: labelBounds.height, + fill: theme.background, + rx: 4, + ry: 4 + } + ), + text + ] }) + ] }); + } + indicator(shape) { + const bounds = this.editor.getShapeGeometry(shape).bounds; + return /* @__PURE__ */ jsxRuntimeExports.jsx( + "rect", + { + width: toDomPrecision(bounds.width), + height: toDomPrecision(bounds.height), + className: `tl-frame-indicator` + } + ); + } + canReceiveNewChildrenOfType(shape, _type) { + return !shape.isLocked; + } + providesBackgroundForChildren() { + return true; + } + canDropShapes(shape, _shapes) { + return !shape.isLocked; + } + onDragShapesOver(frame, shapes) { + if (!shapes.every((child) => child.parentId === frame.id)) { + this.editor.reparentShapes(shapes, frame.id); + } + } + onDragShapesOut(_shape, shapes) { + const parent = this.editor.getShape(_shape.parentId); + const isInGroup = parent && this.editor.isShapeOfType(parent, "group"); + if (isInGroup) { + this.editor.reparentShapes(shapes, parent.id); + } else { + this.editor.reparentShapes(shapes, this.editor.getCurrentPageId()); + } + } + onResize(shape, info) { + return resizeBox(shape, info); + } + getInterpolatedProps(startShape, endShape, t) { + return { + ...(t > 0.5 ? endShape.props : startShape.props), + w: lerp(startShape.props.w, endShape.props.w, t), + h: lerp(startShape.props.h, endShape.props.h, t) + }; + } +} + +function getOvalPerimeter(h, w) { + if (h > w) return (PI$1 * (w / 2) + (h - w)) * 2; + else return (PI$1 * (h / 2) + (w - h)) * 2; +} +function getHeartPath(w, h) { + return getHeartParts(w, h).map((c, i) => c.getSvgPathData(i === 0)).join(" ") + " Z"; +} +function getDrawHeartPath(w, h, sw, id) { + const o = w / 4; + const k = h / 4; + const random = rng(id); + const mutDistance = sw * 0.75; + const mut = (v) => v.addXY(random() * mutDistance, random() * mutDistance); + const A = new Vec(w / 2, h); + const B = new Vec(0, k * 1.2); + const C = new Vec(w / 2, k * 0.9); + const D = new Vec(w, k * 1.2); + const Am = mut(new Vec(w / 2, h)); + const Bm = mut(new Vec(0, k * 1.2)); + const Cm = mut(new Vec(w / 2, k * 0.9)); + const Dm = mut(new Vec(w, k * 1.2)); + const parts = [ + new CubicBezier2d({ + start: A, + cp1: new Vec(o * 1.5, k * 3), + cp2: new Vec(0, k * 2.5), + end: B + }), + new CubicBezier2d({ + start: B, + cp1: new Vec(0, -k * 0.32), + cp2: new Vec(o * 1.85, -k * 0.32), + end: C + }), + new CubicBezier2d({ + start: C, + cp1: new Vec(o * 2.15, -k * 0.32), + cp2: new Vec(w, -k * 0.32), + end: D + }), + new CubicBezier2d({ + start: D, + cp1: new Vec(w, k * 2.5), + cp2: new Vec(o * 2.5, k * 3), + end: Am + }), + new CubicBezier2d({ + start: Am, + cp1: new Vec(o * 1.5, k * 3), + cp2: new Vec(0, k * 2.5), + end: Bm + }), + new CubicBezier2d({ + start: Bm, + cp1: new Vec(0, -k * 0.32), + cp2: new Vec(o * 1.85, -k * 0.32), + end: Cm + }), + new CubicBezier2d({ + start: Cm, + cp1: new Vec(o * 2.15, -k * 0.32), + cp2: new Vec(w, -k * 0.32), + end: Dm + }), + new CubicBezier2d({ + start: Dm, + cp1: new Vec(w, k * 2.5), + cp2: new Vec(o * 2.5, k * 3), + end: A + }) + ]; + return parts.map((c, i) => c.getSvgPathData(i === 0)).join(" ") + " Z"; +} +function getHeartParts(w, h) { + const o = w / 4; + const k = h / 4; + return [ + new CubicBezier2d({ + start: new Vec(w / 2, h), + cp1: new Vec(o * 1.5, k * 3), + cp2: new Vec(0, k * 2.5), + end: new Vec(0, k * 1.2) + }), + new CubicBezier2d({ + start: new Vec(0, k * 1.2), + cp1: new Vec(0, -k * 0.32), + cp2: new Vec(o * 1.85, -k * 0.32), + end: new Vec(w / 2, k * 0.9) + }), + new CubicBezier2d({ + start: new Vec(w / 2, k * 0.9), + cp1: new Vec(o * 2.15, -k * 0.32), + cp2: new Vec(w, -k * 0.32), + end: new Vec(w, k * 1.2) + }), + new CubicBezier2d({ + start: new Vec(w, k * 1.2), + cp1: new Vec(w, k * 2.5), + cp2: new Vec(o * 2.5, k * 3), + end: new Vec(w / 2, h) + }) + ]; +} +function getEllipseStrokeOptions(strokeWidth) { + return { + size: 1 + strokeWidth, + thinning: 0.25, + end: { taper: strokeWidth }, + start: { taper: strokeWidth }, + streamline: 0, + smoothing: 1, + simulatePressure: false + }; +} +function getEllipseStrokePoints(id, width, height, strokeWidth) { + const getRandom = rng(id); + const rx = width / 2; + const ry = height / 2; + const perimeter = perimeterOfEllipse(rx, ry); + const points = []; + const start = PI2 * getRandom(); + const length = PI2 + HALF_PI / 2 + Math.abs(getRandom()) * HALF_PI; + const count = Math.max(16, perimeter / 10); + for (let i = 0; i < count; i++) { + const t = i / (count - 1); + const r = start + t * length; + const c = Math.cos(r); + const s = Math.sin(r); + points.push( + new Vec( + rx * c + width * 0.5 + 0.05 * getRandom(), + ry * s + height / 2 + 0.05 * getRandom(), + Math.min( + 1, + 0.5 + Math.abs(0.5 - (getRandom() > 0 ? EASINGS.easeInOutSine(t) : EASINGS.easeInExpo(t))) / 2 + ) + ) + ); + } + return getStrokePoints(points, getEllipseStrokeOptions(strokeWidth)); +} +function getEllipseDrawIndicatorPath(id, width, height, strokeWidth) { + return getSvgPathFromStrokePoints(getEllipseStrokePoints(id, width, height, strokeWidth)); +} +function getRoundedInkyPolygonPath(points) { + let polylineA = `M`; + const len = points.length; + let p0; + let p1; + let p2; + for (let i = 0, n = len; i < n; i += 3) { + p0 = points[i]; + p1 = points[i + 1]; + p2 = points[i + 2]; + polylineA += `${precise(p0)}L${precise(p1)}Q${precise(p2)}`; + } + polylineA += `${precise(points[0])}`; + return polylineA; +} +function getRoundedPolygonPoints(id, outline, offset, roundness, passes) { + const results = []; + const random = rng(id); + let p0 = outline[0]; + let p1; + const len = outline.length; + for (let i = 0, n = len * passes; i < n; i++) { + p1 = Vec.AddXY(outline[(i + 1) % len], random() * offset, random() * offset); + const delta = Vec.Sub(p1, p0); + const distance = Vec.Len(delta); + const vector = Vec.Div(delta, distance).mul(Math.min(distance / 4, roundness)); + results.push(Vec.Add(p0, vector), Vec.Add(p1, vector.neg()), p1); + p0 = p1; + } + return results; +} +function getPillPoints(width, height, numPoints) { + const radius = Math.min(width, height) / 2; + const longSide = Math.max(width, height) - radius * 2; + const circumference = Math.PI * (radius * 2) + 2 * longSide; + const spacing = circumference / numPoints; + const sections = width > height ? [ + { + type: "straight", + start: new Vec(radius, 0), + delta: new Vec(1, 0) + }, + { + type: "arc", + center: new Vec(width - radius, radius), + startAngle: -PI$1 / 2 + }, + { + type: "straight", + start: new Vec(width - radius, height), + delta: new Vec(-1, 0) + }, + { + type: "arc", + center: new Vec(radius, radius), + startAngle: PI$1 / 2 + } + ] : [ + { + type: "straight", + start: new Vec(width, radius), + delta: new Vec(0, 1) + }, + { + type: "arc", + center: new Vec(radius, height - radius), + startAngle: 0 + }, + { + type: "straight", + start: new Vec(0, height - radius), + delta: new Vec(0, -1) + }, + { + type: "arc", + center: new Vec(radius, radius), + startAngle: PI$1 + } + ]; + let sectionOffset = 0; + const points = []; + for (let i = 0; i < numPoints; i++) { + const section = sections[0]; + if (section.type === "straight") { + points.push(Vec.Add(section.start, Vec.Mul(section.delta, sectionOffset))); + } else { + points.push( + getPointOnCircle(section.center, radius, section.startAngle + sectionOffset / radius) + ); + } + sectionOffset += spacing; + let sectionLength = section.type === "straight" ? longSide : PI$1 * radius; + while (sectionOffset > sectionLength) { + sectionOffset -= sectionLength; + sections.push(sections.shift()); + sectionLength = sections[0].type === "straight" ? longSide : PI$1 * radius; + } + } + return points; +} +const SIZES = { + s: 50, + m: 70, + l: 100, + xl: 130 +}; +const BUMP_PROTRUSION = 0.2; +function getCloudArcs(width, height, seed, size, scale) { + const getRandom = rng(seed); + const pillCircumference = getOvalPerimeter(width, height); + const numBumps = Math.max( + Math.ceil(pillCircumference / SIZES[size]), + 6, + Math.ceil(pillCircumference / Math.min(width, height)) + ); + const targetBumpProtrusion = pillCircumference / numBumps * BUMP_PROTRUSION; + const innerWidth = Math.max(width - targetBumpProtrusion * 2, 1); + const innerHeight = Math.max(height - targetBumpProtrusion * 2, 1); + const innerCircumference = getOvalPerimeter(innerWidth, innerHeight); + const distanceBetweenPointsOnPerimeter = innerCircumference / numBumps; + const paddingX = (width - innerWidth) / 2; + const paddingY = (height - innerHeight) / 2; + const bumpPoints = getPillPoints(innerWidth, innerHeight, numBumps).map((p) => { + return p.addXY(paddingX, paddingY); + }); + const maxWiggleX = width < 20 ? 0 : targetBumpProtrusion * 0.3; + const maxWiggleY = height < 20 ? 0 : targetBumpProtrusion * 0.3; + const wiggledPoints = bumpPoints.slice(0); + for (let i = 0; i < Math.floor(numBumps / 2); i++) { + wiggledPoints[i] = Vec.AddXY( + wiggledPoints[i], + getRandom() * maxWiggleX * scale, + getRandom() * maxWiggleY * scale + ); + wiggledPoints[numBumps - i - 1] = Vec.AddXY( + wiggledPoints[numBumps - i - 1], + getRandom() * maxWiggleX * scale, + getRandom() * maxWiggleY * scale + ); + } + const arcs = []; + for (let i = 0; i < wiggledPoints.length; i++) { + const j = i === wiggledPoints.length - 1 ? 0 : i + 1; + const leftWigglePoint = wiggledPoints[i]; + const rightWigglePoint = wiggledPoints[j]; + const leftPoint = bumpPoints[i]; + const rightPoint = bumpPoints[j]; + const distanceBetweenOriginalPoints = Vec.Dist(leftPoint, rightPoint); + const curvatureOffset = distanceBetweenPointsOnPerimeter - distanceBetweenOriginalPoints; + const distanceBetweenWigglePoints = Vec.Dist(leftWigglePoint, rightWigglePoint); + const relativeSize = distanceBetweenWigglePoints / distanceBetweenOriginalPoints; + const finalDistance = (Math.max(paddingX, paddingY) + curvatureOffset) * relativeSize; + const arcPoint = Vec.Lrp(leftPoint, rightPoint, 0.5).add( + Vec.Sub(rightPoint, leftPoint).uni().per().mul(finalDistance) + ); + if (arcPoint.x < 0) { + arcPoint.x = 0; + } else if (arcPoint.x > width) { + arcPoint.x = width; + } + if (arcPoint.y < 0) { + arcPoint.y = 0; + } else if (arcPoint.y > height) { + arcPoint.y = height; + } + const center = centerOfCircleFromThreePoints(leftWigglePoint, rightWigglePoint, arcPoint); + const radius = Vec.Dist( + center ? center : Vec.Average([leftWigglePoint, rightWigglePoint]), + leftWigglePoint + ); + arcs.push({ + leftPoint: leftWigglePoint, + rightPoint: rightWigglePoint, + arcPoint, + center, + radius + }); + } + return arcs; +} +function cloudOutline(width, height, seed, size, scale) { + const path = []; + const arcs = getCloudArcs(width, height, seed, size, scale); + for (const { center, radius, leftPoint, rightPoint } of arcs) { + path.push(...getPointsOnArc(leftPoint, rightPoint, center, radius, 10)); + } + return path; +} +function getCloudPath(width, height, seed, size, scale) { + const arcs = getCloudArcs(width, height, seed, size, scale); + let path = `M${arcs[0].leftPoint.toFixed()}`; + for (const { leftPoint, rightPoint, radius, center } of arcs) { + if (center === null) { + path += ` L${rightPoint.toFixed()}`; + continue; + } + const arc = Vec.Clockwise(leftPoint, rightPoint, center) ? "0" : "1"; + path += ` A${toDomPrecision(radius)},${toDomPrecision(radius)} 0 ${arc},1 ${rightPoint.toFixed()}`; + } + path += " Z"; + return path; +} +const DRAW_OFFSETS = { + s: 0.5, + m: 0.7, + l: 0.9, + xl: 1.6 +}; +function inkyCloudSvgPath(width, height, seed, size, scale) { + const getRandom = rng(seed); + const mutMultiplier = DRAW_OFFSETS[size] * scale; + const arcs = getCloudArcs(width, height, seed, size, scale); + const avgArcLengthSquared = arcs.reduce((sum, arc) => sum + Vec.Dist2(arc.leftPoint, arc.rightPoint), 0) / arcs.length; + const shouldMutatePoints = avgArcLengthSquared > (mutMultiplier * 15) ** 2; + const mutPoint = shouldMutatePoints ? (p) => Vec.AddXY(p, getRandom() * mutMultiplier * 2, getRandom() * mutMultiplier * 2) : (p) => p; + let pathA = `M${arcs[0].leftPoint.toFixed()}`; + let leftMutPoint = mutPoint(arcs[0].leftPoint); + let pathB = `M${leftMutPoint.toFixed()}`; + for (const { leftPoint, center, rightPoint, radius, arcPoint } of arcs) { + if (center === null) { + pathA += ` L${rightPoint.toFixed()}`; + const rightMutPoint2 = mutPoint(rightPoint); + pathB += ` L${rightMutPoint2.toFixed()}`; + leftMutPoint = rightMutPoint2; + continue; + } + const arc = Vec.Clockwise(leftPoint, rightPoint, center) ? "0" : "1"; + pathA += ` A${toDomPrecision(radius)},${toDomPrecision(radius)} 0 ${arc},1 ${rightPoint.toFixed()}`; + const rightMutPoint = mutPoint(rightPoint); + const mutArcPoint = mutPoint(arcPoint); + const mutCenter = centerOfCircleFromThreePoints(leftMutPoint, rightMutPoint, mutArcPoint); + if (!mutCenter) { + pathB += ` L${rightMutPoint.toFixed()}`; + leftMutPoint = rightMutPoint; + continue; + } + const mutRadius = Math.abs(Vec.Dist(mutCenter, leftMutPoint)); + pathB += ` A${toDomPrecision(mutRadius)},${toDomPrecision( + mutRadius + )} 0 ${arc},1 ${rightMutPoint.toFixed()}`; + leftMutPoint = rightMutPoint; + } + return pathA + pathB + " Z"; +} + +function getLines(props, sw) { + switch (props.geo) { + case "x-box": { + return getXBoxLines(props.w, props.h, sw, props.dash); + } + case "check-box": { + return getCheckBoxLines(props.w, props.h); + } + default: { + return void 0; + } + } +} +function getXBoxLines(w, h, sw, dash) { + const inset = dash === "draw" ? 0.62 : 0; + if (dash === "dashed") { + return [ + [new Vec(0, 0), new Vec(w / 2, h / 2)], + [new Vec(w, h), new Vec(w / 2, h / 2)], + [new Vec(0, h), new Vec(w / 2, h / 2)], + [new Vec(w, 0), new Vec(w / 2, h / 2)] + ]; + } + const clampX = (x) => Math.max(0, Math.min(w, x)); + const clampY = (y) => Math.max(0, Math.min(h, y)); + return [ + [ + new Vec(clampX(sw * inset), clampY(sw * inset)), + new Vec(clampX(w - sw * inset), clampY(h - sw * inset)) + ], + [ + new Vec(clampX(sw * inset), clampY(h - sw * inset)), + new Vec(clampX(w - sw * inset), clampY(sw * inset)) + ] + ]; +} +function getCheckBoxLines(w, h) { + const size = Math.min(w, h) * 0.82; + const ox = (w - size) / 2; + const oy = (h - size) / 2; + const clampX = (x) => Math.max(0, Math.min(w, x)); + const clampY = (y) => Math.max(0, Math.min(h, y)); + return [ + [ + new Vec(clampX(ox + size * 0.25), clampY(oy + size * 0.52)), + new Vec(clampX(ox + size * 0.45), clampY(oy + size * 0.82)) + ], + [ + new Vec(clampX(ox + size * 0.45), clampY(oy + size * 0.82)), + new Vec(clampX(ox + size * 0.82), clampY(oy + size * 0.22)) + ] + ]; +} + +function GeoShapeBody({ + shape, + shouldScale, + forceSolid +}) { + const scaleToUse = shouldScale ? shape.props.scale : 1; + const editor = useEditor(); + const theme = useDefaultColorTheme(); + const { id, props } = shape; + const { w, color, fill, dash, growY, size, scale } = props; + const strokeWidth = STROKE_SIZES[size] * scaleToUse; + const h = props.h + growY; + switch (props.geo) { + case "cloud": { + if (dash === "solid") { + const d = getCloudPath(w, h, id, size, scale); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ShapeFill, { theme, d, color, fill, scale: scaleToUse }), + /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d, stroke: theme[color].solid, strokeWidth, fill: "none" }) + ] }); + } else if (dash === "draw") { + const d = inkyCloudSvgPath(w, h, id, size, scale); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ShapeFill, { theme, d, color, fill, scale: scaleToUse }), + /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d, stroke: theme[color].solid, strokeWidth, fill: "none" }) + ] }); + } else { + const d = getCloudPath(w, h, id, size, scale); + const arcs = getCloudArcs(w, h, id, size, scale); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ShapeFill, { theme, d, color, fill, scale: scaleToUse }), + /* @__PURE__ */ jsxRuntimeExports.jsx( + "g", + { + strokeWidth, + stroke: theme[color].solid, + fill: "none", + pointerEvents: "all", + children: arcs.map(({ leftPoint, rightPoint, center, radius }, i) => { + const arcLength = center ? radius * canonicalizeRotation( + canonicalizeRotation(Vec.Angle(center, rightPoint)) - canonicalizeRotation(Vec.Angle(center, leftPoint)) + ) : Vec.Dist(leftPoint, rightPoint); + const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( + arcLength, + strokeWidth, + { + style: dash, + start: "outset", + end: "outset", + forceSolid + } + ); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + "path", + { + d: center ? `M${leftPoint.x},${leftPoint.y}A${radius},${radius},0,0,1,${rightPoint.x},${rightPoint.y}` : `M${leftPoint.x},${leftPoint.y}L${rightPoint.x},${rightPoint.y}`, + strokeDasharray, + strokeDashoffset + }, + i + ); + }) + } + ) + ] }); + } + } + case "ellipse": { + const geometry = shouldScale ? ( + // cached + (editor.getShapeGeometry(shape)) + ) : ( + // not cached + (editor.getShapeUtil(shape).getGeometry(shape)) + ); + const d = geometry.getSvgPathData(true); + if (dash === "dashed" || dash === "dotted") { + const perimeter = geometry.length; + const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( + perimeter < 64 ? perimeter * 2 : perimeter, + strokeWidth, + { + style: dash, + snap: 4, + closed: true, + forceSolid + } + ); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ShapeFill, { theme, d, color, fill, scale: scaleToUse }), + /* @__PURE__ */ jsxRuntimeExports.jsx( + "path", + { + d, + strokeWidth, + fill: "none", + stroke: theme[color].solid, + strokeDasharray, + strokeDashoffset + } + ) + ] }); + } else { + const geometry2 = shouldScale ? ( + // cached + (editor.getShapeGeometry(shape)) + ) : ( + // not cached + (editor.getShapeUtil(shape).getGeometry(shape)) + ); + const d2 = geometry2.getSvgPathData(true); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ShapeFill, { theme, d: d2, color, fill, scale: scaleToUse }), + /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: d2, stroke: theme[color].solid, strokeWidth, fill: "none" }) + ] }); + } + } + case "oval": { + const geometry = shouldScale ? ( + // cached + (editor.getShapeGeometry(shape)) + ) : ( + // not cached + (editor.getShapeUtil(shape).getGeometry(shape)) + ); + const d = geometry.getSvgPathData(true); + if (dash === "dashed" || dash === "dotted") { + const perimeter = geometry.getLength(); + const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( + perimeter < 64 ? perimeter * 2 : perimeter, + strokeWidth, + { + style: dash, + snap: 4, + start: "outset", + end: "outset", + closed: true, + forceSolid + } + ); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ShapeFill, { theme, d, color, fill, scale: scaleToUse }), + /* @__PURE__ */ jsxRuntimeExports.jsx( + "path", + { + d, + strokeWidth, + fill: "none", + stroke: theme[color].solid, + strokeDasharray, + strokeDashoffset + } + ) + ] }); + } else { + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ShapeFill, { theme, d, color, fill, scale: scaleToUse }), + /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d, stroke: theme[color].solid, strokeWidth, fill: "none" }) + ] }); + } + } + case "heart": { + if (dash === "dashed" || dash === "dotted" || dash === "solid") { + const d = getHeartPath(w, h); + const curves = getHeartParts(w, h); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ShapeFill, { theme, d, color, fill, scale: scaleToUse }), + curves.map((c, i) => { + const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( + c.length, + strokeWidth, + { + style: dash, + snap: 1, + start: "outset", + end: "outset", + closed: true, + forceSolid + } + ); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + "path", + { + d: c.getSvgPathData(), + strokeWidth, + fill: "none", + stroke: theme[color].solid, + strokeDasharray, + strokeDashoffset, + pointerEvents: "all" + }, + `curve_${i}` + ); + }) + ] }); + } else { + const d = getDrawHeartPath(w, h, strokeWidth, shape.id); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ShapeFill, { theme, d, color, fill, scale: scaleToUse }), + /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d, stroke: theme[color].solid, strokeWidth, fill: "none" }) + ] }); + } + } + default: { + const geometry = shouldScale ? ( + // cached + (editor.getShapeGeometry(shape)) + ) : ( + // not cached + (editor.getShapeUtil(shape).getGeometry(shape)) + ); + const outline = geometry instanceof Group2d ? geometry.children[0].vertices : geometry.vertices; + const lines = getLines(shape.props, strokeWidth); + if (dash === "solid") { + let d = "M" + outline[0] + "L" + outline.slice(1) + "Z"; + if (lines) { + for (const [A, B] of lines) { + d += `M${A.x},${A.y}L${B.x},${B.y}`; + } + } + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ShapeFill, { theme, d, color, fill, scale: scaleToUse }), + /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d, stroke: theme[color].solid, strokeWidth, fill: "none" }) + ] }); + } else if (dash === "dashed" || dash === "dotted") { + const d = "M" + outline[0] + "L" + outline.slice(1) + "Z"; + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ShapeFill, { theme, d, color, fill, scale: scaleToUse }), + /* @__PURE__ */ jsxRuntimeExports.jsxs( + "g", + { + strokeWidth, + stroke: theme[color].solid, + fill: "none", + pointerEvents: "all", + children: [ + Array.from(Array(outline.length)).map((_, i) => { + const A = Vec.ToFixed(outline[i]); + const B = Vec.ToFixed(outline[(i + 1) % outline.length]); + const dist = Vec.Dist(A, B); + const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( + dist, + strokeWidth, + { + style: dash, + start: "outset", + end: "outset", + forceSolid + } + ); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + "line", + { + x1: A.x, + y1: A.y, + x2: B.x, + y2: B.y, + strokeDasharray, + strokeDashoffset + }, + i + ); + }), + lines && lines.map(([A, B], i) => { + const dist = Vec.Dist(A, B); + const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( + dist, + strokeWidth, + { + style: dash, + start: "skip", + end: "skip", + snap: dash === "dotted" ? 4 : void 0, + forceSolid + } + ); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + "path", + { + d: `M${A.x},${A.y}L${B.x},${B.y}`, + stroke: theme[color].solid, + strokeWidth, + fill: "none", + strokeDasharray, + strokeDashoffset + }, + `line_fg_${i}` + ); + }) + ] + } + ) + ] }); + } else if (dash === "draw") { + let d = getRoundedInkyPolygonPath( + getRoundedPolygonPoints(id, outline, strokeWidth / 3, strokeWidth * 2, 2) + ); + if (lines) { + for (const [A, B] of lines) { + d += `M${A.toFixed()}L${B.toFixed()}`; + } + } + const innerPathData = getRoundedInkyPolygonPath( + getRoundedPolygonPoints(id, outline, 0, strokeWidth * 2, 1) + ); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx( + ShapeFill, + { + theme, + d: innerPathData, + color, + fill, + scale: scaleToUse + } + ), + /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d, stroke: theme[color].solid, strokeWidth, fill: "none" }) + ] }); + } + } + } +} + +const MIN_SIZE_WITH_LABEL = 17 * 3; +class GeoShapeUtil extends BaseBoxShapeUtil { + static type = "geo"; + static props = geoShapeProps; + static migrations = geoShapeMigrations; + canEdit() { + return true; + } + getDefaultProps() { + return { + w: 100, + h: 100, + geo: "rectangle", + color: "black", + labelColor: "black", + fill: "none", + dash: "draw", + size: "m", + font: "draw", + text: "", + align: "middle", + verticalAlign: "middle", + growY: 0, + url: "", + scale: 1 + }; + } + getGeometry(shape) { + const w = Math.max(1, shape.props.w); + const h = Math.max(1, shape.props.h + shape.props.growY); + const cx = w / 2; + const cy = h / 2; + const isFilled = shape.props.fill !== "none"; + let body; + switch (shape.props.geo) { + case "cloud": { + body = new Polygon2d({ + points: cloudOutline(w, h, shape.id, shape.props.size, shape.props.scale), + isFilled + }); + break; + } + case "triangle": { + body = new Polygon2d({ + points: [new Vec(cx, 0), new Vec(w, h), new Vec(0, h)], + isFilled + }); + break; + } + case "diamond": { + body = new Polygon2d({ + points: [new Vec(cx, 0), new Vec(w, cy), new Vec(cx, h), new Vec(0, cy)], + isFilled + }); + break; + } + case "pentagon": { + body = new Polygon2d({ + points: getPolygonVertices(w, h, 5), + isFilled + }); + break; + } + case "hexagon": { + body = new Polygon2d({ + points: getPolygonVertices(w, h, 6), + isFilled + }); + break; + } + case "octagon": { + body = new Polygon2d({ + points: getPolygonVertices(w, h, 8), + isFilled + }); + break; + } + case "ellipse": { + body = new Ellipse2d({ + width: w, + height: h, + isFilled + }); + break; + } + case "oval": { + body = new Stadium2d({ + width: w, + height: h, + isFilled + }); + break; + } + case "star": { + const sides = 5; + const step = PI2 / sides / 2; + const rightMostIndex = Math.floor(sides / 4) * 2; + const leftMostIndex = sides * 2 - rightMostIndex; + const topMostIndex = 0; + const bottomMostIndex = Math.floor(sides / 2) * 2; + const maxX = Math.cos(-HALF_PI + rightMostIndex * step) * w / 2; + const minX = Math.cos(-HALF_PI + leftMostIndex * step) * w / 2; + const minY = Math.sin(-HALF_PI + topMostIndex * step) * h / 2; + const maxY = Math.sin(-HALF_PI + bottomMostIndex * step) * h / 2; + const diffX = w - Math.abs(maxX - minX); + const diffY = h - Math.abs(maxY - minY); + const offsetX = w / 2 + minX - (w / 2 - maxX); + const offsetY = h / 2 + minY - (h / 2 - maxY); + const ratio = 1; + const cx2 = (w - offsetX) / 2; + const cy2 = (h - offsetY) / 2; + const ox = (w + diffX) / 2; + const oy = (h + diffY) / 2; + const ix = ox * ratio / 2; + const iy = oy * ratio / 2; + body = new Polygon2d({ + points: Array.from(Array(sides * 2)).map((_, i) => { + const theta = -HALF_PI + i * step; + return new Vec( + cx2 + (i % 2 ? ix : ox) * Math.cos(theta), + cy2 + (i % 2 ? iy : oy) * Math.sin(theta) + ); + }), + isFilled + }); + break; + } + case "rhombus": { + const offset = Math.min(w * 0.38, h * 0.38); + body = new Polygon2d({ + points: [new Vec(offset, 0), new Vec(w, 0), new Vec(w - offset, h), new Vec(0, h)], + isFilled + }); + break; + } + case "rhombus-2": { + const offset = Math.min(w * 0.38, h * 0.38); + body = new Polygon2d({ + points: [new Vec(0, 0), new Vec(w - offset, 0), new Vec(w, h), new Vec(offset, h)], + isFilled + }); + break; + } + case "trapezoid": { + const offset = Math.min(w * 0.38, h * 0.38); + body = new Polygon2d({ + points: [new Vec(offset, 0), new Vec(w - offset, 0), new Vec(w, h), new Vec(0, h)], + isFilled + }); + break; + } + case "arrow-right": { + const ox = Math.min(w, h) * 0.38; + const oy = h * 0.16; + body = new Polygon2d({ + points: [ + new Vec(0, oy), + new Vec(w - ox, oy), + new Vec(w - ox, 0), + new Vec(w, h / 2), + new Vec(w - ox, h), + new Vec(w - ox, h - oy), + new Vec(0, h - oy) + ], + isFilled + }); + break; + } + case "arrow-left": { + const ox = Math.min(w, h) * 0.38; + const oy = h * 0.16; + body = new Polygon2d({ + points: [ + new Vec(ox, 0), + new Vec(ox, oy), + new Vec(w, oy), + new Vec(w, h - oy), + new Vec(ox, h - oy), + new Vec(ox, h), + new Vec(0, h / 2) + ], + isFilled + }); + break; + } + case "arrow-up": { + const ox = w * 0.16; + const oy = Math.min(w, h) * 0.38; + body = new Polygon2d({ + points: [ + new Vec(w / 2, 0), + new Vec(w, oy), + new Vec(w - ox, oy), + new Vec(w - ox, h), + new Vec(ox, h), + new Vec(ox, oy), + new Vec(0, oy) + ], + isFilled + }); + break; + } + case "arrow-down": { + const ox = w * 0.16; + const oy = Math.min(w, h) * 0.38; + body = new Polygon2d({ + points: [ + new Vec(ox, 0), + new Vec(w - ox, 0), + new Vec(w - ox, h - oy), + new Vec(w, h - oy), + new Vec(w / 2, h), + new Vec(0, h - oy), + new Vec(ox, h - oy) + ], + isFilled + }); + break; + } + case "check-box": + case "x-box": + case "rectangle": { + body = new Rectangle2d({ + width: w, + height: h, + isFilled + }); + break; + } + case "heart": { + const parts = getHeartParts(w, h); + const points = parts.reduce((acc, part) => { + acc.push(...part.vertices); + return acc; + }, []); + body = new Polygon2d({ + points, + isFilled + }); + break; + } + default: { + exhaustiveSwitchError(shape.props.geo); + } + } + const unscaledlabelSize = getUnscaledLabelSize(this.editor, shape); + const unscaledW = w / shape.props.scale; + const unscaledH = h / shape.props.scale; + const unscaledminWidth = Math.min(100, unscaledW / 2); + const unscaledMinHeight = Math.min( + LABEL_FONT_SIZES[shape.props.size] * TEXT_PROPS.lineHeight + LABEL_PADDING * 2, + unscaledH / 2 + ); + const unscaledLabelWidth = Math.min( + unscaledW, + Math.max(unscaledlabelSize.w, Math.min(unscaledminWidth, Math.max(1, unscaledW - 8))) + ); + const unscaledLabelHeight = Math.min( + unscaledH, + Math.max(unscaledlabelSize.h, Math.min(unscaledMinHeight, Math.max(1, unscaledH - 8))) + ); + const lines = getLines(shape.props, STROKE_SIZES[shape.props.size] * shape.props.scale); + const edges = lines ? lines.map((line) => new Polyline2d({ points: line })) : []; + return new Group2d({ + children: [ + body, + new Rectangle2d({ + x: shape.props.align === "start" ? 0 : shape.props.align === "end" ? (unscaledW - unscaledLabelWidth) * shape.props.scale : (unscaledW - unscaledLabelWidth) / 2 * shape.props.scale, + y: shape.props.verticalAlign === "start" ? 0 : shape.props.verticalAlign === "end" ? (unscaledH - unscaledLabelHeight) * shape.props.scale : (unscaledH - unscaledLabelHeight) / 2 * shape.props.scale, + width: unscaledLabelWidth * shape.props.scale, + height: unscaledLabelHeight * shape.props.scale, + isFilled: true, + isLabel: true + }), + ...edges + ] + }); + } + getHandleSnapGeometry(shape) { + const geometry = this.getGeometry(shape); + const outline = geometry.children[0]; + switch (shape.props.geo) { + case "arrow-down": + case "arrow-left": + case "arrow-right": + case "arrow-up": + case "check-box": + case "diamond": + case "hexagon": + case "octagon": + case "pentagon": + case "rectangle": + case "rhombus": + case "rhombus-2": + case "star": + case "trapezoid": + case "triangle": + case "x-box": + return { outline, points: [...outline.getVertices(), geometry.bounds.center] }; + case "cloud": + case "ellipse": + case "heart": + case "oval": + return { outline, points: [geometry.bounds.center] }; + default: + exhaustiveSwitchError(shape.props.geo); + } + } + getText(shape) { + return shape.props.text; + } + onEditEnd(shape) { + const { + id, + type, + props: { text } + } = shape; + if (text.trimEnd() !== shape.props.text) { + this.editor.updateShapes([ + { + id, + type, + props: { + text: text.trimEnd() + } + } + ]); + } + } + component(shape) { + const { id, type, props } = shape; + const { fill, font, align, verticalAlign, size, text } = props; + const theme = useDefaultColorTheme(); + const { editor } = this; + const isOnlySelected = useValue( + "isGeoOnlySelected", + () => shape.id === editor.getOnlySelectedShapeId(), + [] + ); + const isEditingAnything = editor.getEditingShapeId() !== null; + const showHtmlContainer = isEditingAnything || shape.props.text; + const isForceSolid = useValue( + "force solid", + () => { + return editor.getZoomLevel() < 0.2; + }, + [editor] + ); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(SVGContainer, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(GeoShapeBody, { shape, shouldScale: true, forceSolid: isForceSolid }) }), + showHtmlContainer && /* @__PURE__ */ jsxRuntimeExports.jsx( + HTMLContainer, + { + style: { + overflow: "hidden", + width: shape.props.w, + height: shape.props.h + props.growY + }, + children: /* @__PURE__ */ jsxRuntimeExports.jsx( + TextLabel, + { + shapeId: id, + type, + font, + fontSize: LABEL_FONT_SIZES[size] * shape.props.scale, + lineHeight: TEXT_PROPS.lineHeight, + padding: LABEL_PADDING * shape.props.scale, + fill, + align, + verticalAlign, + text, + isSelected: isOnlySelected, + labelColor: theme[props.labelColor].solid, + wrap: true + } + ) + } + ), + shape.props.url && /* @__PURE__ */ jsxRuntimeExports.jsx(HyperlinkButton, { url: shape.props.url }) + ] }); + } + indicator(shape) { + const { id, props } = shape; + const { w, size } = props; + const h = props.h + props.growY; + const strokeWidth = STROKE_SIZES[size]; + const geometry = this.editor.getShapeGeometry(shape); + switch (props.geo) { + case "ellipse": { + if (props.dash === "draw") { + return /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: getEllipseDrawIndicatorPath(id, w, h, strokeWidth) }); + } + return /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: geometry.getSvgPathData(true) }); + } + case "heart": { + return /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: getHeartPath(w, h) }); + } + case "oval": { + return /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: geometry.getSvgPathData(true) }); + } + case "cloud": { + return /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: getCloudPath(w, h, id, size, shape.props.scale) }); + } + default: { + const geometry2 = this.editor.getShapeGeometry(shape); + const outline = geometry2 instanceof Group2d ? geometry2.children[0].vertices : geometry2.vertices; + let path; + if (props.dash === "draw") { + const polygonPoints = getRoundedPolygonPoints( + id, + outline, + 0, + strokeWidth * 2 * shape.props.scale, + 1 + ); + path = getRoundedInkyPolygonPath(polygonPoints); + } else { + path = "M" + outline[0] + "L" + outline.slice(1) + "Z"; + } + const lines = getLines(shape.props, strokeWidth); + if (lines) { + for (const [A, B] of lines) { + path += `M${A.x},${A.y}L${B.x},${B.y}`; + } + } + return /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: path }); + } + } + } + toSvg(shape, ctx) { + const newShape = { + ...shape, + props: { + ...shape.props, + w: shape.props.w / shape.props.scale, + h: shape.props.h / shape.props.scale + } + }; + const props = newShape.props; + ctx.addExportDef(getFillDefForExport(props.fill)); + let textEl; + if (props.text) { + ctx.addExportDef(getFontDefForExport(props.font)); + const theme = getDefaultColorTheme(ctx); + const bounds = new Box(0, 0, props.w, props.h + props.growY); + textEl = /* @__PURE__ */ jsxRuntimeExports.jsx( + SvgTextLabel, + { + fontSize: LABEL_FONT_SIZES[props.size], + font: props.font, + align: props.align, + verticalAlign: props.verticalAlign, + text: props.text, + labelColor: theme[props.labelColor].solid, + bounds, + padding: 16 + } + ); + } + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(GeoShapeBody, { shouldScale: false, shape: newShape, forceSolid: false }), + textEl + ] }); + } + getCanvasSvgDefs() { + return [getFillDefForCanvas()]; + } + onResize(shape, { handle, newPoint, scaleX, scaleY, initialShape }) { + const unscaledInitialW = initialShape.props.w / initialShape.props.scale; + const unscaledInitialH = initialShape.props.h / initialShape.props.scale; + const unscaledGrowY = initialShape.props.growY / initialShape.props.scale; + let unscaledW = unscaledInitialW * scaleX; + let unscaledH = (unscaledInitialH + unscaledGrowY) * scaleY; + let overShrinkX = 0; + let overShrinkY = 0; + const min = MIN_SIZE_WITH_LABEL; + if (shape.props.text.trim()) { + let newW = Math.max(Math.abs(unscaledW), min); + let newH = Math.max(Math.abs(unscaledH), min); + if (newW < min && newH === min) newW = min; + if (newW === min && newH < min) newH = min; + const unscaledLabelSize = getUnscaledLabelSize(this.editor, { + ...shape, + props: { + ...shape.props, + w: newW * shape.props.scale, + h: newH * shape.props.scale + } + }); + const nextW = Math.max(Math.abs(unscaledW), unscaledLabelSize.w) * Math.sign(unscaledW); + const nextH = Math.max(Math.abs(unscaledH), unscaledLabelSize.h) * Math.sign(unscaledH); + overShrinkX = Math.abs(nextW) - Math.abs(unscaledW); + overShrinkY = Math.abs(nextH) - Math.abs(unscaledH); + unscaledW = nextW; + unscaledH = nextH; + } + const scaledW = unscaledW * shape.props.scale; + const scaledH = unscaledH * shape.props.scale; + const offset = new Vec(0, 0); + if (scaleX < 0) { + offset.x += scaledW; + } + if (handle === "left" || handle === "top_left" || handle === "bottom_left") { + offset.x += scaleX < 0 ? overShrinkX : -overShrinkX; + } + if (scaleY < 0) { + offset.y += scaledH; + } + if (handle === "top" || handle === "top_left" || handle === "top_right") { + offset.y += scaleY < 0 ? overShrinkY : -overShrinkY; + } + const { x, y } = offset.rot(shape.rotation).add(newPoint); + return { + x, + y, + props: { + w: Math.max(Math.abs(scaledW), 1), + h: Math.max(Math.abs(scaledH), 1), + growY: 0 + } + }; + } + onBeforeCreate(shape) { + if (!shape.props.text) { + if (shape.props.growY) { + return { + ...shape, + props: { + ...shape.props, + growY: 0 + } + }; + } else { + return; + } + } + const unscaledPrevHeight = shape.props.h / shape.props.scale; + const unscaledNextHeight = getUnscaledLabelSize(this.editor, shape).h; + let growY = null; + if (unscaledNextHeight > unscaledPrevHeight) { + growY = unscaledNextHeight - unscaledPrevHeight; + } else { + if (shape.props.growY) { + growY = 0; + } + } + if (growY !== null) { + return { + ...shape, + props: { + ...shape.props, + // scale the growY + growY: growY * shape.props.scale + } + }; + } + } + onBeforeUpdate(prev, next) { + const prevText = prev.props.text; + const nextText = next.props.text; + if (prevText === nextText && prev.props.font === next.props.font && prev.props.size === next.props.size) { + return; + } + if (prevText && !nextText) { + return { + ...next, + props: { + ...next.props, + growY: 0 + } + }; + } + const unscaledPrevWidth = prev.props.w / prev.props.scale; + const unscaledPrevHeight = prev.props.h / prev.props.scale; + const unscaledPrevGrowY = prev.props.growY / prev.props.scale; + const unscaledNextLabelSize = getUnscaledLabelSize(this.editor, next); + if (!prevText && nextText && nextText.length === 1) { + let unscaledW = Math.max(unscaledPrevWidth, unscaledNextLabelSize.w); + let unscaledH = Math.max(unscaledPrevHeight, unscaledNextLabelSize.h); + const min = MIN_SIZE_WITH_LABEL; + if (unscaledPrevWidth < min && unscaledPrevHeight < min) { + unscaledW = Math.max(unscaledW, min); + unscaledH = Math.max(unscaledH, min); + unscaledW = Math.max(unscaledW, unscaledH); + unscaledH = Math.max(unscaledW, unscaledH); + } + return { + ...next, + props: { + ...next.props, + // Scale the results + w: unscaledW * next.props.scale, + h: unscaledH * next.props.scale, + growY: 0 + } + }; + } + let growY = null; + if (unscaledNextLabelSize.h > unscaledPrevHeight) { + growY = unscaledNextLabelSize.h - unscaledPrevHeight; + } else { + if (unscaledPrevGrowY) { + growY = 0; + } + } + if (growY !== null) { + const unscaledNextWidth = next.props.w / next.props.scale; + return { + ...next, + props: { + ...next.props, + // Scale the results + growY: growY * next.props.scale, + w: Math.max(unscaledNextWidth, unscaledNextLabelSize.w) * next.props.scale + } + }; + } + if (unscaledNextLabelSize.w > unscaledPrevWidth) { + return { + ...next, + props: { + ...next.props, + // Scale the results + w: unscaledNextLabelSize.w * next.props.scale + } + }; + } + } + onDoubleClick(shape) { + if (this.editor.inputs.altKey) { + switch (shape.props.geo) { + case "rectangle": { + return { + ...shape, + props: { + geo: "check-box" + } + }; + } + case "check-box": { + return { + ...shape, + props: { + geo: "rectangle" + } + }; + } + } + } + return; + } + getInterpolatedProps(startShape, endShape, t) { + return { + ...(t > 0.5 ? endShape.props : startShape.props), + w: lerp(startShape.props.w, endShape.props.w, t), + h: lerp(startShape.props.h, endShape.props.h, t), + scale: lerp(startShape.props.scale, endShape.props.scale, t) + }; + } +} +function getUnscaledLabelSize(editor, shape) { + const { text, font, size, w } = shape.props; + if (!text) { + return { w: 0, h: 0 }; + } + const minSize = editor.textMeasure.measureText("w", { + ...TEXT_PROPS, + fontFamily: FONT_FAMILIES[font], + fontSize: LABEL_FONT_SIZES[size], + maxWidth: 100 + // ? + }); + const sizes = { + s: 2, + m: 3.5, + l: 5, + xl: 10 + }; + const textSize = editor.textMeasure.measureText(text, { + ...TEXT_PROPS, + fontFamily: FONT_FAMILIES[font], + fontSize: LABEL_FONT_SIZES[size], + minWidth: minSize.w, + maxWidth: Math.max( + // Guard because a DOM nodes can't be less 0 + 0, + // A 'w' width that we're setting as the min-width + Math.ceil(minSize.w + sizes[size]), + // The actual text size + Math.ceil(w / shape.props.scale - LABEL_PADDING * 2) + ) + }); + return { + w: textSize.w + LABEL_PADDING * 2, + h: textSize.h + LABEL_PADDING * 2 + }; +} + +function useColorSpace() { + const [supportsP3, setSupportsP3] = reactExports.useState(false); + reactExports.useEffect(() => { + const supportsSyntax = CSS.supports("color", "color(display-p3 1 1 1)"); + const query = matchMedia("(color-gamut: p3)"); + setSupportsP3(supportsSyntax && query.matches); + const onChange = () => setSupportsP3(supportsSyntax && query.matches); + query.addEventListener("change", onChange); + return () => query.removeEventListener("change", onChange); + }, []); + const forceSrgb = useValue(debugFlags.forceSrgb); + return forceSrgb || !supportsP3 ? "srgb" : "p3"; +} + +const OVERLAY_OPACITY = 0.35; +const UNDERLAY_OPACITY = 0.82; +class HighlightShapeUtil extends ShapeUtil { + static type = "highlight"; + static props = highlightShapeProps; + static migrations = highlightShapeMigrations; + hideResizeHandles(shape) { + return getIsDot(shape); + } + hideRotateHandle(shape) { + return getIsDot(shape); + } + hideSelectionBoundsFg(shape) { + return getIsDot(shape); + } + getDefaultProps() { + return { + segments: [], + color: "black", + size: "m", + isComplete: false, + isPen: false, + scale: 1 + }; + } + getGeometry(shape) { + const strokeWidth = getStrokeWidth(shape); + if (getIsDot(shape)) { + return new Circle2d({ + x: -strokeWidth / 2, + y: -strokeWidth / 2, + radius: strokeWidth / 2, + isFilled: true + }); + } + const { strokePoints, sw } = getHighlightStrokePoints(shape, strokeWidth, true); + const opts = getHighlightFreehandSettings({ strokeWidth: sw, showAsComplete: true }); + setStrokePointRadii(strokePoints, opts); + return new Polygon2d({ + points: getStrokeOutlinePoints(strokePoints, opts), + isFilled: true + }); + } + component(shape) { + const forceSolid = useHighlightForceSolid(this.editor, shape); + const strokeWidth = getStrokeWidth(shape); + return /* @__PURE__ */ jsxRuntimeExports.jsx(SVGContainer, { children: /* @__PURE__ */ jsxRuntimeExports.jsx( + HighlightRenderer, + { + shape, + forceSolid, + strokeWidth, + opacity: OVERLAY_OPACITY + } + ) }); + } + backgroundComponent(shape) { + const forceSolid = useHighlightForceSolid(this.editor, shape); + const strokeWidth = getStrokeWidth(shape); + return /* @__PURE__ */ jsxRuntimeExports.jsx(SVGContainer, { children: /* @__PURE__ */ jsxRuntimeExports.jsx( + HighlightRenderer, + { + shape, + forceSolid, + strokeWidth, + opacity: UNDERLAY_OPACITY + } + ) }); + } + indicator(shape) { + const forceSolid = useHighlightForceSolid(this.editor, shape); + const strokeWidth = getStrokeWidth(shape); + const { strokePoints, sw } = getHighlightStrokePoints(shape, strokeWidth, forceSolid); + const allPointsFromSegments = getPointsFromSegments(shape.props.segments); + let strokePath; + if (strokePoints.length < 2) { + strokePath = getIndicatorDot(allPointsFromSegments[0], sw); + } else { + strokePath = getSvgPathFromStrokePoints(strokePoints, false); + } + return /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: strokePath }); + } + toSvg(shape) { + const strokeWidth = getStrokeWidth(shape); + const forceSolid = strokeWidth < 1.5; + const scaleFactor = 1 / shape.props.scale; + return /* @__PURE__ */ jsxRuntimeExports.jsx("g", { transform: `scale(${scaleFactor})`, children: /* @__PURE__ */ jsxRuntimeExports.jsx( + HighlightRenderer, + { + forceSolid, + strokeWidth, + shape, + opacity: OVERLAY_OPACITY + } + ) }); + } + toBackgroundSvg(shape) { + const strokeWidth = getStrokeWidth(shape); + const forceSolid = strokeWidth < 1.5; + const scaleFactor = 1 / shape.props.scale; + return /* @__PURE__ */ jsxRuntimeExports.jsx("g", { transform: `scale(${scaleFactor})`, children: /* @__PURE__ */ jsxRuntimeExports.jsx( + HighlightRenderer, + { + forceSolid, + strokeWidth, + shape, + opacity: UNDERLAY_OPACITY + } + ) }); + } + onResize(shape, info) { + const { scaleX, scaleY } = info; + const newSegments = []; + for (const segment of shape.props.segments) { + newSegments.push({ + ...segment, + points: segment.points.map(({ x, y, z }) => { + return { + x: scaleX * x, + y: scaleY * y, + z + }; + }) + }); + } + return { + props: { + segments: newSegments + } + }; + } + getInterpolatedProps(startShape, endShape, t) { + return { + ...(t > 0.5 ? endShape.props : startShape.props), + ...endShape.props, + segments: interpolateSegments(startShape.props.segments, endShape.props.segments, t), + scale: lerp(startShape.props.scale, endShape.props.scale, t) + }; + } +} +function getShapeDot(point) { + const r = 0.1; + return `M ${point.x} ${point.y} m -${r}, 0 a ${r},${r} 0 1,0 ${r * 2},0 a ${r},${r} 0 1,0 -${r * 2},0`; +} +function getIndicatorDot(point, sw) { + const r = sw / 2; + return `M ${point.x} ${point.y} m -${r}, 0 a ${r},${r} 0 1,0 ${r * 2},0 a ${r},${r} 0 1,0 -${r * 2},0`; +} +function getHighlightStrokePoints(shape, strokeWidth, forceSolid) { + const allPointsFromSegments = getPointsFromSegments(shape.props.segments); + const showAsComplete = shape.props.isComplete || last$1(shape.props.segments)?.type === "straight"; + let sw = strokeWidth; + if (!forceSolid && !shape.props.isPen && allPointsFromSegments.length === 1) { + sw += rng(shape.id)() * (strokeWidth / 6); + } + const options = getHighlightFreehandSettings({ + strokeWidth: sw, + showAsComplete + }); + const strokePoints = getStrokePoints(allPointsFromSegments, options); + return { strokePoints, sw }; +} +function getStrokeWidth(shape) { + return FONT_SIZES[shape.props.size] * 1.12 * shape.props.scale; +} +function getIsDot(shape) { + return shape.props.segments.length === 1 && shape.props.segments[0].points.length < 2; +} +function HighlightRenderer({ + strokeWidth, + forceSolid, + shape, + opacity +}) { + const theme = useDefaultColorTheme(); + const allPointsFromSegments = getPointsFromSegments(shape.props.segments); + let sw = strokeWidth; + if (!forceSolid && !shape.props.isPen && allPointsFromSegments.length === 1) { + sw += rng(shape.id)() * (sw / 6); + } + const options = getHighlightFreehandSettings({ + strokeWidth: sw, + showAsComplete: shape.props.isComplete || last$1(shape.props.segments)?.type === "straight" + }); + const strokePoints = getStrokePoints(allPointsFromSegments, options); + const solidStrokePath = strokePoints.length > 1 ? getSvgPathFromStrokePoints(strokePoints, false) : getShapeDot(shape.props.segments[0].points[0]); + const colorSpace = useColorSpace(); + const color = theme[shape.props.color].highlight[colorSpace]; + return /* @__PURE__ */ jsxRuntimeExports.jsx( + "path", + { + d: solidStrokePath, + strokeLinecap: "round", + fill: "none", + pointerEvents: "all", + stroke: color, + strokeWidth: sw, + opacity + } + ); +} +function useHighlightForceSolid(editor, shape) { + return useValue( + "forceSolid", + () => { + const sw = getStrokeWidth(shape); + const zoomLevel = editor.getZoomLevel(); + if (sw / zoomLevel < 1.5) { + return true; + } + return false; + }, + [editor] + ); +} + +function BrokenAssetIcon() { + return /* @__PURE__ */ jsxRuntimeExports.jsxs( + "svg", + { + width: "15", + height: "15", + viewBox: "0 0 30 30", + xmlns: "http://www.w3.org/2000/svg", + fill: "none", + stroke: "currentColor", + strokeLinecap: "round", + strokeLinejoin: "round", + children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M3,11 L3,3 11,3", strokeWidth: "2" }), + /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M19,27 L27,27 L27,19", strokeWidth: "2" }), + /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M27,3 L3,27", strokeWidth: "2" }) + ] + } + ); +} + +function useImageOrVideoAsset({ + shapeId, + assetId +}) { + const editor = useEditor(); + const isExport = !!useSvgExportContext(); + const isReady = useDelaySvgExport(); + const resolveAssetUrlDebounced = reactExports.useMemo(() => debounce(resolveAssetUrl, 500), []); + const [result, setResult] = reactExports.useState(() => ({ + asset: assetId ? editor.getAsset(assetId) ?? null : null, + url: null + })); + const didAlreadyResolve = reactExports.useRef(false); + const previousUrl = reactExports.useRef(null); + reactExports.useEffect(() => { + if (!assetId) return; + let isCancelled = false; + let cancelDebounceFn; + const cleanupEffectScheduler = react("update state", () => { + if (!isExport && editor.getCulledShapes().has(shapeId)) return; + const asset = editor.getAsset(assetId); + if (!asset) return; + const shape = editor.getShape(shapeId); + if (!shape) return; + if (!asset.props.src) { + const preview = editor.getTemporaryAssetPreview(asset.id); + if (preview) { + if (previousUrl.current !== preview) { + previousUrl.current = preview; + setResult((prev) => ({ ...prev, isPlaceholder: true, url: preview })); + isReady(); + } + return; + } + } + const screenScale = editor.getZoomLevel() * (shape.props.w / asset.props.w); + function resolve(asset2, url) { + if (isCancelled) return; + if (previousUrl.current === url) return; + didAlreadyResolve.current = true; + previousUrl.current = url; + setResult({ asset: asset2, url }); + isReady(); + } + if (didAlreadyResolve.current) { + resolveAssetUrlDebounced( + editor, + assetId, + screenScale, + isExport, + (url) => resolve(asset, url) + ); + cancelDebounceFn = resolveAssetUrlDebounced.cancel; + } else { + resolveAssetUrl(editor, assetId, screenScale, isExport, (url) => resolve(asset, url)); + } + }); + return () => { + cleanupEffectScheduler(); + cancelDebounceFn?.(); + isCancelled = true; + }; + }, [editor, assetId, isExport, isReady, shapeId, resolveAssetUrlDebounced]); + return result; +} +function resolveAssetUrl(editor, assetId, screenScale, isExport, callback) { + editor.resolveAssetUrl(assetId, { + screenScale, + shouldResolveToOriginal: isExport + }).then((url) => { + callback(url); + }); +} + +function usePrefersReducedMotion() { + const [prefersReducedMotion, setPrefersReducedMotion] = reactExports.useState(false); + reactExports.useEffect(() => { + if (typeof window === "undefined" || !("matchMedia" in window)) return; + const mql = window.matchMedia("(prefers-reduced-motion: reduce)"); + const handler = () => { + setPrefersReducedMotion(mql.matches); + }; + handler(); + mql.addEventListener("change", handler); + return () => mql.removeEventListener("change", handler); + }, []); + return prefersReducedMotion; +} + +async function getDataURIFromURL(url) { + const response = await fetch(url); + const blob = await response.blob(); + return FileHelpers.blobToDataUrl(blob); +} +class ImageShapeUtil extends BaseBoxShapeUtil { + static type = "image"; + static props = imageShapeProps; + static migrations = imageShapeMigrations; + isAspectRatioLocked() { + return true; + } + canCrop() { + return true; + } + getDefaultProps() { + return { + w: 100, + h: 100, + assetId: null, + playing: true, + url: "", + crop: null, + flipX: false, + flipY: false + }; + } + onResize(shape, info) { + let resized = resizeBox(shape, info); + const { flipX, flipY } = info.initialShape.props; + const { scaleX, scaleY, mode } = info; + resized = { + ...resized, + props: { + ...resized.props, + flipX: scaleX < 0 !== flipX, + flipY: scaleY < 0 !== flipY + } + }; + if (!shape.props.crop) return resized; + const flipCropHorizontally = ( + // We used the flip horizontally feature + (// We resized the shape past it's bounds, so it flipped + mode === "scale_shape" && scaleX === -1 || mode === "resize_bounds" && flipX !== resized.props.flipX) + ); + const flipCropVertically = ( + // We used the flip vertically feature + (// We resized the shape past it's bounds, so it flipped + mode === "scale_shape" && scaleY === -1 || mode === "resize_bounds" && flipY !== resized.props.flipY) + ); + const { topLeft, bottomRight } = shape.props.crop; + resized.props.crop = { + topLeft: { + x: flipCropHorizontally ? 1 - bottomRight.x : topLeft.x, + y: flipCropVertically ? 1 - bottomRight.y : topLeft.y + }, + bottomRight: { + x: flipCropHorizontally ? 1 - topLeft.x : bottomRight.x, + y: flipCropVertically ? 1 - topLeft.y : bottomRight.y + } + }; + return resized; + } + component(shape) { + return /* @__PURE__ */ jsxRuntimeExports.jsx(ImageShape, { shape }); + } + indicator(shape) { + const isCropping = this.editor.getCroppingShapeId() === shape.id; + if (isCropping) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { width: toDomPrecision(shape.props.w), height: toDomPrecision(shape.props.h) }); + } + async toSvg(shape) { + if (!shape.props.assetId) return null; + const asset = this.editor.getAsset(shape.props.assetId); + if (!asset) return null; + let src = await this.editor.resolveAssetUrl(shape.props.assetId, { + shouldResolveToOriginal: true + }); + if (!src) return null; + if (src.startsWith("blob:") || src.startsWith("http") || src.startsWith("/") || src.startsWith("./")) { + src = (await getDataURIFromURL(src)) || ""; + } + return /* @__PURE__ */ jsxRuntimeExports.jsx(SvgImage, { shape, src }); + } + onDoubleClickEdge(shape) { + const props = shape.props; + if (!props) return; + if (this.editor.getCroppingShapeId() !== shape.id) { + return; + } + const crop = structuredClone(props.crop) || { + topLeft: { x: 0, y: 0 }, + bottomRight: { x: 1, y: 1 } + }; + const w = 1 / (crop.bottomRight.x - crop.topLeft.x) * shape.props.w; + const h = 1 / (crop.bottomRight.y - crop.topLeft.y) * shape.props.h; + const pointDelta = new Vec(crop.topLeft.x * w, crop.topLeft.y * h).rot(shape.rotation); + const partial = { + id: shape.id, + type: shape.type, + x: shape.x - pointDelta.x, + y: shape.y - pointDelta.y, + props: { + crop: { + topLeft: { x: 0, y: 0 }, + bottomRight: { x: 1, y: 1 } + }, + w, + h + } + }; + this.editor.updateShapes([partial]); + } + getInterpolatedProps(startShape, endShape, t) { + function interpolateCrop(startShape2, endShape2) { + if (startShape2.props.crop === null && endShape2.props.crop === null) return null; + const startTL = startShape2.props.crop?.topLeft || { x: 0, y: 0 }; + const startBR = startShape2.props.crop?.bottomRight || { x: 1, y: 1 }; + const endTL = endShape2.props.crop?.topLeft || { x: 0, y: 0 }; + const endBR = endShape2.props.crop?.bottomRight || { x: 1, y: 1 }; + return { + topLeft: { x: lerp(startTL.x, endTL.x, t), y: lerp(startTL.y, endTL.y, t) }, + bottomRight: { x: lerp(startBR.x, endBR.x, t), y: lerp(startBR.y, endBR.y, t) } + }; + } + return { + ...(t > 0.5 ? endShape.props : startShape.props), + w: lerp(startShape.props.w, endShape.props.w, t), + h: lerp(startShape.props.h, endShape.props.h, t), + crop: interpolateCrop(startShape, endShape) + }; + } +} +const ImageShape = reactExports.memo(function ImageShape2({ shape }) { + const editor = useEditor(); + const { asset, url } = useImageOrVideoAsset({ + shapeId: shape.id, + assetId: shape.props.assetId + }); + const prefersReducedMotion = usePrefersReducedMotion(); + const [staticFrameSrc, setStaticFrameSrc] = reactExports.useState(""); + const [loadedUrl, setLoadedUrl] = reactExports.useState(null); + const isAnimated = getIsAnimated(editor, shape); + reactExports.useEffect(() => { + if (url && isAnimated) { + let cancelled = false; + const image = Image(); + image.onload = () => { + if (cancelled) return; + const canvas = document.createElement("canvas"); + canvas.width = image.width; + canvas.height = image.height; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + ctx.drawImage(image, 0, 0); + setStaticFrameSrc(canvas.toDataURL()); + setLoadedUrl(url); + }; + image.crossOrigin = "anonymous"; + image.src = url; + return () => { + cancelled = true; + }; + } + }, [editor, isAnimated, prefersReducedMotion, url]); + const showCropPreview = useValue( + "show crop preview", + () => shape.id === editor.getOnlySelectedShapeId() && editor.getCroppingShapeId() === shape.id && editor.isIn("select.crop"), + [editor, shape.id] + ); + const reduceMotion = prefersReducedMotion && (asset?.props.mimeType?.includes("video") || isAnimated); + const containerStyle = getCroppedContainerStyle(shape); + const nextSrc = url === loadedUrl ? null : url; + const loadedSrc = reduceMotion ? staticFrameSrc : loadedUrl; + if (!url && !asset?.props.src) { + return /* @__PURE__ */ jsxRuntimeExports.jsxs( + HTMLContainer, + { + id: shape.id, + style: { + overflow: "hidden", + width: shape.props.w, + height: shape.props.h, + color: "var(--color-text-3)", + backgroundColor: "var(--color-low)", + border: "1px solid var(--color-low-border)" + }, + children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx( + "div", + { + className: classNames("tl-image-container", asset && "tl-image-container-loading"), + style: containerStyle, + children: asset ? null : /* @__PURE__ */ jsxRuntimeExports.jsx(BrokenAssetIcon, {}) + } + ), + "url" in shape.props && shape.props.url && /* @__PURE__ */ jsxRuntimeExports.jsx(HyperlinkButton, { url: shape.props.url }) + ] + } + ); + } + const crossOrigin = isAnimated ? "anonymous" : void 0; + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + showCropPreview && loadedSrc && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { style: containerStyle, children: /* @__PURE__ */ jsxRuntimeExports.jsx( + "img", + { + className: "tl-image", + style: { ...getFlipStyle(shape), opacity: 0.1 }, + crossOrigin, + src: loadedSrc, + referrerPolicy: "strict-origin-when-cross-origin", + draggable: false + } + ) }), + /* @__PURE__ */ jsxRuntimeExports.jsxs( + HTMLContainer, + { + id: shape.id, + style: { overflow: "hidden", width: shape.props.w, height: shape.props.h }, + children: [ + /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: classNames("tl-image-container"), style: containerStyle, children: [ + loadedSrc && /* @__PURE__ */ jsxRuntimeExports.jsx( + "img", + { + className: "tl-image", + style: getFlipStyle(shape), + crossOrigin, + src: loadedSrc, + referrerPolicy: "strict-origin-when-cross-origin", + draggable: false + }, + loadedSrc + ), + nextSrc && /* @__PURE__ */ jsxRuntimeExports.jsx( + "img", + { + className: "tl-image", + style: getFlipStyle(shape), + crossOrigin, + src: nextSrc, + referrerPolicy: "strict-origin-when-cross-origin", + draggable: false, + onLoad: () => setLoadedUrl(nextSrc) + }, + nextSrc + ) + ] }), + shape.props.url && /* @__PURE__ */ jsxRuntimeExports.jsx(HyperlinkButton, { url: shape.props.url }) + ] + } + ) + ] }); +}); +function getIsAnimated(editor, shape) { + const asset = shape.props.assetId ? editor.getAsset(shape.props.assetId) : void 0; + if (!asset) return false; + return "mimeType" in asset.props && MediaHelpers.isAnimatedImageType(asset?.props.mimeType) || "isAnimated" in asset.props && asset.props.isAnimated; +} +function getCroppedContainerStyle(shape) { + const crop = shape.props.crop; + const topLeft = crop?.topLeft; + if (!topLeft) { + return { + width: shape.props.w, + height: shape.props.h + }; + } + const w = 1 / (crop.bottomRight.x - crop.topLeft.x) * shape.props.w; + const h = 1 / (crop.bottomRight.y - crop.topLeft.y) * shape.props.h; + const offsetX = -topLeft.x * w; + const offsetY = -topLeft.y * h; + return { + transform: `translate(${offsetX}px, ${offsetY}px)`, + width: w, + height: h + }; +} +function getFlipStyle(shape, size) { + const { flipX, flipY } = shape.props; + if (!flipX && !flipY) return void 0; + const scale = `scale(${flipX ? -1 : 1}, ${flipY ? -1 : 1})`; + const translate = size ? `translate(${flipX ? size.width : 0}px, ${flipY ? size.height : 0}px)` : ""; + return { + transform: `${translate} ${scale}`, + // in SVG, flipping around the center doesn't work so we use explicit width/height + transformOrigin: size ? "0 0" : "center center" + }; +} +function SvgImage({ shape, src }) { + const cropClipId = useUniqueSafeId(); + const containerStyle = getCroppedContainerStyle(shape); + const crop = shape.props.crop; + if (containerStyle.transform && crop) { + const { transform: cropTransform, width, height } = containerStyle; + const croppedWidth = (crop.bottomRight.x - crop.topLeft.x) * width; + const croppedHeight = (crop.bottomRight.y - crop.topLeft.y) * height; + const points = [ + new Vec(0, 0), + new Vec(croppedWidth, 0), + new Vec(croppedWidth, croppedHeight), + new Vec(0, croppedHeight) + ]; + const flip = getFlipStyle(shape, { width, height }); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx("defs", { children: /* @__PURE__ */ jsxRuntimeExports.jsx("clipPath", { id: cropClipId, children: /* @__PURE__ */ jsxRuntimeExports.jsx("polygon", { points: points.map((p) => `${p.x},${p.y}`).join(" ") }) }) }), + /* @__PURE__ */ jsxRuntimeExports.jsx("g", { clipPath: `url(#${cropClipId})`, children: /* @__PURE__ */ jsxRuntimeExports.jsx( + "image", + { + href: src, + width, + height, + style: flip ? { ...flip, transform: `${cropTransform} ${flip.transform}` } : { transform: cropTransform } + } + ) }) + ] }); + } else { + return /* @__PURE__ */ jsxRuntimeExports.jsx( + "image", + { + href: src, + width: shape.props.w, + height: shape.props.h, + style: getFlipStyle(shape, { width: shape.props.w, height: shape.props.h }) + } + ); + } +} + +function getLineDrawFreehandOptions(strokeWidth) { + return { + size: strokeWidth, + thinning: 0.4, + streamline: 0, + smoothing: 0.5, + simulatePressure: true, + last: true + }; +} +function getLineStrokePoints(shape, spline, strokeWidth) { + const points = spline.vertices; + const options = getLineDrawFreehandOptions(strokeWidth); + return getStrokePoints(points, options); +} +function getLineDrawStrokeOutlinePoints(shape, spline, strokeWidth) { + const options = getLineDrawFreehandOptions(strokeWidth); + return getStrokeOutlinePoints( + setStrokePointRadii(getLineStrokePoints(shape, spline, strokeWidth), options), + options + ); +} +function getLineDrawPath(shape, spline, strokeWidth) { + const stroke = getLineDrawStrokeOutlinePoints(shape, spline, strokeWidth); + return getSvgPathFromPoints(stroke); +} +function getLineIndicatorPath(shape, spline, strokeWidth) { + if (shape.props.dash === "draw") { + const strokePoints = getLineStrokePoints(shape, spline, strokeWidth); + return getSvgPathFromStrokePoints(strokePoints); + } + return spline.getSvgPathData(); +} + +function getDrawLinePathData(id, outline, strokeWidth) { + let innerPathData = `M ${precise(outline[0])}L`; + let outerPathData2 = `M ${precise(outline[0])}L`; + const offset = strokeWidth / 3; + const roundness = strokeWidth * 2; + const random = rng(id); + let p0 = outline[0]; + let p1; + let s0 = outline[0]; + let s1; + const len = outline.length; + for (let i = 0, n = len - 1; i < n; i++) { + p1 = outline[i + 1]; + s1 = Vec.AddXY(outline[i + 1], random() * offset, random() * offset); + const delta = Vec.Sub(p1, p0); + const distance = Vec.Len(delta); + const vector = Vec.Div(delta, distance).mul(Math.min(distance / 4, roundness)); + const q0 = Vec.Add(p0, vector); + const q1 = Vec.Add(p1, vector.neg()); + const sDelta = Vec.Sub(s1, s0); + const sDistance = Vec.Len(sDelta); + const sVector = Vec.Div(sDelta, sDistance).mul(Math.min(sDistance / 4, roundness)); + const sq0 = Vec.Add(s0, sVector); + const sq1 = Vec.Add(s1, sVector.neg()); + if (i === n - 1) { + innerPathData += `${precise(q0)}L ${precise(p1)}`; + outerPathData2 += `${precise(sq0)}L ${precise(s1)}`; + } else { + innerPathData += `${precise(q0)}L ${precise(q1)}Q ${precise(p1)}`; + outerPathData2 += `${precise(sq0)}L ${precise(sq1)}Q ${precise(s1)}`; + p0 = p1; + s0 = s1; + } + } + return [innerPathData, innerPathData + outerPathData2]; +} + +const handlesCache = new WeakCache(); +class LineShapeUtil extends ShapeUtil { + static type = "line"; + static props = lineShapeProps; + static migrations = lineShapeMigrations; + hideResizeHandles() { + return true; + } + hideRotateHandle() { + return true; + } + hideSelectionBoundsFg() { + return true; + } + hideSelectionBoundsBg() { + return true; + } + getDefaultProps() { + const [start, end] = getIndices(2); + return { + dash: "draw", + size: "m", + color: "black", + spline: "line", + points: { + [start]: { id: start, index: start, x: 0, y: 0 }, + [end]: { id: end, index: end, x: 0.1, y: 0.1 } + }, + scale: 1 + }; + } + getGeometry(shape) { + return getGeometryForLineShape(shape); + } + getHandles(shape) { + return handlesCache.get(shape.props, () => { + const spline = getGeometryForLineShape(shape); + const points = linePointsToArray(shape); + const results = points.map((point) => ({ + ...point, + id: point.index, + type: "vertex", + canSnap: true + })); + for (let i = 0; i < points.length - 1; i++) { + const index = getIndexBetween(points[i].index, points[i + 1].index); + const segment = spline.segments[i]; + const point = segment.midPoint(); + results.push({ + id: index, + type: "create", + index, + x: point.x, + y: point.y, + canSnap: true + }); + } + return results.sort(sortByIndex$1); + }); + } + // Events + onResize(shape, info) { + const { scaleX, scaleY } = info; + return { + props: { + points: mapObjectMapValues(shape.props.points, (_, { id, index, x, y }) => ({ + id, + index, + x: x * scaleX, + y: y * scaleY + })) + } + }; + } + onBeforeCreate(next) { + const { + props: { points } + } = next; + const pointKeys = Object.keys(points); + if (pointKeys.length < 2) { + return; + } + const firstPoint = points[pointKeys[0]]; + const allSame = pointKeys.every((key) => { + const point = points[key]; + return point.x === firstPoint.x && point.y === firstPoint.y; + }); + if (allSame) { + const lastKey = pointKeys[pointKeys.length - 1]; + points[lastKey] = { + ...points[lastKey], + x: points[lastKey].x + 0.1, + y: points[lastKey].y + 0.1 + }; + return next; + } + return; + } + onHandleDrag(shape, { handle }) { + if (handle.type !== "vertex") return; + const newPoint = maybeSnapToGrid(new Vec(handle.x, handle.y), this.editor); + return { + ...shape, + props: { + ...shape.props, + points: { + ...shape.props.points, + [handle.id]: { id: handle.id, index: handle.index, x: newPoint.x, y: newPoint.y } + } + } + }; + } + component(shape) { + return /* @__PURE__ */ jsxRuntimeExports.jsx(SVGContainer, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(LineShapeSvg, { shape }) }); + } + indicator(shape) { + const strokeWidth = STROKE_SIZES$1[shape.props.size] * shape.props.scale; + const spline = getGeometryForLineShape(shape); + const { dash } = shape.props; + let path; + if (shape.props.spline === "line") { + const outline = spline.points; + if (dash === "solid" || dash === "dotted" || dash === "dashed") { + path = "M" + outline[0] + "L" + outline.slice(1); + } else { + const [innerPathData] = getDrawLinePathData(shape.id, outline, strokeWidth); + path = innerPathData; + } + } else { + path = getLineIndicatorPath(shape, spline, strokeWidth); + } + return /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: path }); + } + toSvg(shape) { + return /* @__PURE__ */ jsxRuntimeExports.jsx(LineShapeSvg, { shouldScale: true, shape }); + } + getHandleSnapGeometry(shape) { + const points = linePointsToArray(shape); + return { + points, + getSelfSnapPoints: (handle) => { + const index = this.getHandles(shape).filter((h) => h.type === "vertex").findIndex((h) => h.id === handle.id); + return points.filter((_, i) => Math.abs(i - index) > 1).map(Vec.From); + }, + getSelfSnapOutline: (handle) => { + const index = this.getHandles(shape).filter((h) => h.type === "vertex").findIndex((h) => h.id === handle.id); + const segments = getGeometryForLineShape(shape).segments.filter( + (_, i) => i !== index - 1 && i !== index + ); + if (!segments.length) return null; + return new Group2d({ children: segments }); + } + }; + } + getInterpolatedProps(startShape, endShape, t) { + const startPoints = linePointsToArray(startShape); + const endPoints = linePointsToArray(endShape); + const pointsToUseStart = []; + const pointsToUseEnd = []; + let index = ZERO_INDEX_KEY; + if (startPoints.length > endPoints.length) { + for (let i = 0; i < startPoints.length; i++) { + pointsToUseStart[i] = { ...startPoints[i] }; + if (endPoints[i] === void 0) { + pointsToUseEnd[i] = { ...endPoints[endPoints.length - 1], id: index }; + } else { + pointsToUseEnd[i] = { ...endPoints[i], id: index }; + } + index = getIndexAbove(index); + } + } else if (endPoints.length > startPoints.length) { + for (let i = 0; i < endPoints.length; i++) { + pointsToUseEnd[i] = { ...endPoints[i] }; + if (startPoints[i] === void 0) { + pointsToUseStart[i] = { + ...startPoints[startPoints.length - 1], + id: index + }; + } else { + pointsToUseStart[i] = { ...startPoints[i], id: index }; + } + index = getIndexAbove(index); + } + } else { + for (let i = 0; i < endPoints.length; i++) { + pointsToUseStart[i] = startPoints[i]; + pointsToUseEnd[i] = endPoints[i]; + } + } + return { + ...(t > 0.5 ? endShape.props : startShape.props), + points: Object.fromEntries( + pointsToUseStart.map((point, i) => { + const endPoint = pointsToUseEnd[i]; + return [ + point.id, + { + ...point, + x: lerp(point.x, endPoint.x, t), + y: lerp(point.y, endPoint.y, t) + } + ]; + }) + ), + scale: lerp(startShape.props.scale, endShape.props.scale, t) + }; + } +} +function linePointsToArray(shape) { + return Object.values(shape.props.points).sort(sortByIndex$1); +} +function getGeometryForLineShape(shape) { + const points = linePointsToArray(shape).map(Vec.From); + switch (shape.props.spline) { + case "cubic": { + return new CubicSpline2d({ points }); + } + case "line": { + return new Polyline2d({ points }); + } + } +} +function LineShapeSvg({ + shape, + shouldScale = false, + forceSolid = false +}) { + const theme = useDefaultColorTheme(); + const spline = getGeometryForLineShape(shape); + const { dash, color, size } = shape.props; + const scaleFactor = 1 / shape.props.scale; + const scale = shouldScale ? scaleFactor : 1; + const strokeWidth = STROKE_SIZES$1[size] * shape.props.scale; + if (shape.props.spline === "line") { + if (dash === "solid") { + const outline = spline.points; + const pathData = "M" + outline[0] + "L" + outline.slice(1); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + "path", + { + d: pathData, + stroke: theme[color].solid, + strokeWidth, + fill: "none", + transform: `scale(${scale})` + } + ); + } + if (dash === "dashed" || dash === "dotted") { + return /* @__PURE__ */ jsxRuntimeExports.jsx("g", { stroke: theme[color].solid, strokeWidth, transform: `scale(${scale})`, children: spline.segments.map((segment, i) => { + const { strokeDasharray, strokeDashoffset } = forceSolid ? { strokeDasharray: "none", strokeDashoffset: "none" } : getPerfectDashProps(segment.length, strokeWidth, { + style: dash, + start: i > 0 ? "outset" : "none", + end: i < spline.segments.length - 1 ? "outset" : "none" + }); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + "path", + { + strokeDasharray, + strokeDashoffset, + d: segment.getSvgPathData(true), + fill: "none" + }, + i + ); + }) }); + } + if (dash === "draw") { + const outline = spline.points; + const [_, outerPathData] = getDrawLinePathData(shape.id, outline, strokeWidth); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + "path", + { + d: outerPathData, + stroke: theme[color].solid, + strokeWidth, + fill: "none", + transform: `scale(${scale})` + } + ); + } + } + if (shape.props.spline === "cubic") { + const splinePath = spline.getSvgPathData(); + if (dash === "solid") { + return /* @__PURE__ */ jsxRuntimeExports.jsx( + "path", + { + strokeWidth, + stroke: theme[color].solid, + fill: "none", + d: splinePath, + transform: `scale(${scale})` + } + ); + } + if (dash === "dashed" || dash === "dotted") { + return /* @__PURE__ */ jsxRuntimeExports.jsx("g", { stroke: theme[color].solid, strokeWidth, transform: `scale(${scale})`, children: spline.segments.map((segment, i) => { + const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( + segment.length, + strokeWidth, + { + style: dash, + start: i > 0 ? "outset" : "none", + end: i < spline.segments.length - 1 ? "outset" : "none", + forceSolid + } + ); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + "path", + { + strokeDasharray, + strokeDashoffset, + d: segment.getSvgPathData(), + fill: "none" + }, + i + ); + }) }); + } + if (dash === "draw") { + return /* @__PURE__ */ jsxRuntimeExports.jsx( + "path", + { + d: getLineDrawPath(shape, spline, strokeWidth), + strokeWidth: 1, + stroke: theme[color].solid, + fill: theme[color].solid, + transform: `scale(${scale})` + } + ); + } + } +} + +class NoteShapeUtil extends ShapeUtil { + static type = "note"; + static props = noteShapeProps; + static migrations = noteShapeMigrations; + canEdit() { + return true; + } + hideResizeHandles() { + return true; + } + hideSelectionBoundsFg() { + return false; + } + getDefaultProps() { + return { + color: "black", + size: "m", + text: "", + font: "draw", + align: "middle", + verticalAlign: "middle", + labelColor: "black", + growY: 0, + fontSizeAdjustment: 0, + url: "", + scale: 1 + }; + } + getGeometry(shape) { + const { labelHeight, labelWidth } = getLabelSize(this.editor, shape); + const { scale } = shape.props; + const lh = labelHeight * scale; + const lw = labelWidth * scale; + const nw = NOTE_SIZE * scale; + const nh = getNoteHeight(shape); + return new Group2d({ + children: [ + new Rectangle2d({ width: nw, height: nh, isFilled: true }), + new Rectangle2d({ + x: shape.props.align === "start" ? 0 : shape.props.align === "end" ? nw - lw : (nw - lw) / 2, + y: shape.props.verticalAlign === "start" ? 0 : shape.props.verticalAlign === "end" ? nh - lh : (nh - lh) / 2, + width: lw, + height: lh, + isFilled: true, + isLabel: true + }) + ] + }); + } + getHandles(shape) { + const { scale } = shape.props; + const isCoarsePointer = this.editor.getInstanceState().isCoarsePointer; + if (isCoarsePointer) return []; + const zoom = this.editor.getZoomLevel(); + if (zoom * scale < 0.25) return []; + const nh = getNoteHeight(shape); + const nw = NOTE_SIZE * scale; + const offset = CLONE_HANDLE_MARGIN / zoom * scale; + if (zoom * scale < 0.5) { + return [ + { + id: "bottom", + index: "a3", + type: "clone", + x: nw / 2, + y: nh + offset + } + ]; + } + return [ + { + id: "top", + index: "a1", + type: "clone", + x: nw / 2, + y: -offset + }, + { + id: "right", + index: "a2", + type: "clone", + x: nw + offset, + y: nh / 2 + }, + { + id: "bottom", + index: "a3", + type: "clone", + x: nw / 2, + y: nh + offset + }, + { + id: "left", + index: "a4", + type: "clone", + x: -offset, + y: nh / 2 + } + ]; + } + getText(shape) { + return shape.props.text; + } + component(shape) { + const { + id, + type, + props: { + labelColor, + scale, + color, + font, + size, + align, + text, + verticalAlign, + fontSizeAdjustment + } + } = shape; + const handleKeyDown = useNoteKeydownHandler(id); + const theme = useDefaultColorTheme(); + const nw = NOTE_SIZE * scale; + const nh = getNoteHeight(shape); + const rotation = useValue( + "shape rotation", + () => this.editor.getShapePageTransform(id)?.rotation() ?? 0, + [this.editor] + ); + const hideShadows = useValue("zoom", () => this.editor.getZoomLevel() < 0.35 / scale, [ + scale, + this.editor + ]); + const isDarkMode = useValue("dark mode", () => this.editor.user.getIsDarkMode(), [this.editor]); + const isSelected = shape.id === this.editor.getOnlySelectedShapeId(); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx( + "div", + { + id, + className: "tl-note__container", + style: { + width: nw, + height: nh, + backgroundColor: theme[color].note.fill, + borderBottom: hideShadows ? isDarkMode ? `${2 * scale}px solid rgb(20, 20, 20)` : `${2 * scale}px solid rgb(144, 144, 144)` : "none", + boxShadow: hideShadows ? "none" : getNoteShadow(shape.id, rotation, scale) + }, + children: /* @__PURE__ */ jsxRuntimeExports.jsx( + TextLabel, + { + shapeId: id, + type, + font, + fontSize: (fontSizeAdjustment || LABEL_FONT_SIZES[size]) * scale, + lineHeight: TEXT_PROPS.lineHeight, + align, + verticalAlign, + text, + isNote: true, + isSelected, + labelColor: labelColor === "black" ? theme[color].note.text : theme[labelColor].fill, + wrap: true, + padding: 16 * scale, + onKeyDown: handleKeyDown + } + ) + } + ), + "url" in shape.props && shape.props.url && /* @__PURE__ */ jsxRuntimeExports.jsx(HyperlinkButton, { url: shape.props.url }) + ] }); + } + indicator(shape) { + const { scale } = shape.props; + return /* @__PURE__ */ jsxRuntimeExports.jsx( + "rect", + { + rx: scale, + width: toDomPrecision(NOTE_SIZE * scale), + height: toDomPrecision(getNoteHeight(shape)) + } + ); + } + toSvg(shape, ctx) { + if (shape.props.text) ctx.addExportDef(getFontDefForExport(shape.props.font)); + const theme = getDefaultColorTheme({ isDarkMode: ctx.isDarkMode }); + const bounds = getBoundsForSVG(shape); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: 5, y: 5, rx: 1, width: NOTE_SIZE - 10, height: bounds.h, fill: "rgba(0,0,0,.1)" }), + /* @__PURE__ */ jsxRuntimeExports.jsx( + "rect", + { + rx: 1, + width: NOTE_SIZE, + height: bounds.h, + fill: theme[shape.props.color].note.fill + } + ), + /* @__PURE__ */ jsxRuntimeExports.jsx( + SvgTextLabel, + { + fontSize: shape.props.fontSizeAdjustment || LABEL_FONT_SIZES[shape.props.size], + font: shape.props.font, + align: shape.props.align, + verticalAlign: shape.props.verticalAlign, + text: shape.props.text, + labelColor: theme[shape.props.color].note.text, + bounds, + stroke: false + } + ) + ] }); + } + onBeforeCreate(next) { + return getNoteSizeAdjustments(this.editor, next); + } + onBeforeUpdate(prev, next) { + if (prev.props.text === next.props.text && prev.props.font === next.props.font && prev.props.size === next.props.size) { + return; + } + return getNoteSizeAdjustments(this.editor, next); + } + onEditEnd(shape) { + const { + id, + type, + props: { text } + } = shape; + if (text.trimEnd() !== shape.props.text) { + this.editor.updateShapes([ + { + id, + type, + props: { + text: text.trimEnd() + } + } + ]); + } + } + getInterpolatedProps(startShape, endShape, t) { + return { + ...(t > 0.5 ? endShape.props : startShape.props), + scale: lerp(startShape.props.scale, endShape.props.scale, t) + }; + } +} +function getNoteSizeAdjustments(editor, shape) { + const { labelHeight, fontSizeAdjustment } = getLabelSize(editor, shape); + const growY = Math.max(0, labelHeight - NOTE_SIZE); + if (growY !== shape.props.growY || fontSizeAdjustment !== shape.props.fontSizeAdjustment) { + return { + ...shape, + props: { + ...shape.props, + growY, + fontSizeAdjustment + } + }; + } +} +function getNoteLabelSize(editor, shape) { + const { text } = shape.props; + if (!text) { + const minHeight = LABEL_FONT_SIZES[shape.props.size] * TEXT_PROPS.lineHeight + LABEL_PADDING * 2; + return { labelHeight: minHeight, labelWidth: 100, fontSizeAdjustment: 0 }; + } + const unadjustedFontSize = LABEL_FONT_SIZES[shape.props.size]; + let fontSizeAdjustment = 0; + let iterations = 0; + let labelHeight = NOTE_SIZE; + let labelWidth = NOTE_SIZE; + const FUZZ = 1; + do { + fontSizeAdjustment = Math.min(unadjustedFontSize, unadjustedFontSize - iterations); + const nextTextSize = editor.textMeasure.measureText(text, { + ...TEXT_PROPS, + fontFamily: FONT_FAMILIES[shape.props.font], + fontSize: fontSizeAdjustment, + maxWidth: NOTE_SIZE - LABEL_PADDING * 2 - FUZZ, + disableOverflowWrapBreaking: true + }); + labelHeight = nextTextSize.h + LABEL_PADDING * 2; + labelWidth = nextTextSize.w + LABEL_PADDING * 2; + if (fontSizeAdjustment <= 14) { + const nextTextSizeWithOverflowBreak = editor.textMeasure.measureText(text, { + ...TEXT_PROPS, + fontFamily: FONT_FAMILIES[shape.props.font], + fontSize: fontSizeAdjustment, + maxWidth: NOTE_SIZE - LABEL_PADDING * 2 - FUZZ + }); + labelHeight = nextTextSizeWithOverflowBreak.h + LABEL_PADDING * 2; + labelWidth = nextTextSizeWithOverflowBreak.w + LABEL_PADDING * 2; + break; + } + if (nextTextSize.scrollWidth.toFixed(0) === nextTextSize.w.toFixed(0)) { + break; + } + } while (iterations++ < 50); + return { + labelHeight, + labelWidth, + fontSizeAdjustment + }; +} +const labelSizesForNote = new WeakCache(); +function getLabelSize(editor, shape) { + return labelSizesForNote.get(shape, () => getNoteLabelSize(editor, shape)); +} +function useNoteKeydownHandler(id) { + const editor = useEditor(); + const translation = useCurrentTranslation(); + return reactExports.useCallback( + (e) => { + const shape = editor.getShape(id); + if (!shape) return; + const isTab = e.key === "Tab"; + const isCmdEnter = (e.metaKey || e.ctrlKey) && e.key === "Enter"; + if (isTab || isCmdEnter) { + e.preventDefault(); + const pageTransform = editor.getShapePageTransform(id); + const pageRotation = pageTransform.rotation(); + const isRTL = !!(translation.dir === "rtl" || isRightToLeftLanguage(shape.props.text)); + const offsetLength = (NOTE_SIZE + editor.options.adjacentShapeMargin + // If we're growing down, we need to account for the current shape's growY + (isCmdEnter && !e.shiftKey ? shape.props.growY : 0)) * shape.props.scale; + const adjacentCenter = new Vec( + isTab ? e.shiftKey != isRTL ? -1 : 1 : 0, + isCmdEnter ? e.shiftKey ? -1 : 1 : 0 + ).mul(offsetLength).add(NOTE_CENTER_OFFSET.clone().mul(shape.props.scale)).rot(pageRotation).add(pageTransform.point()); + const newNote = getNoteShapeForAdjacentPosition(editor, shape, adjacentCenter, pageRotation); + if (newNote) { + editor.markHistoryStoppingPoint("editing adjacent shape"); + startEditingShapeWithLabel( + editor, + newNote, + true + /* selectAll */ + ); + } + } + }, + [id, editor, translation.dir] + ); +} +function getNoteHeight(shape) { + return (NOTE_SIZE + shape.props.growY) * shape.props.scale; +} +function getNoteShadow(id, rotation, scale) { + const random = rng(id); + const lift = Math.abs(random()) + 0.5; + const oy = Math.cos(rotation); + const a = 5 * scale; + const b = 4 * scale; + const c = 6 * scale; + const d = 7 * scale; + return `0px ${a - lift}px ${a}px -${a}px rgba(15, 23, 31, .6), + 0px ${(b + lift * d) * Math.max(0, oy)}px ${c + lift * d}px -${b + lift * c}px rgba(15, 23, 31, ${(0.3 + lift * 0.1).toFixed(2)}), + 0px ${48 * scale}px ${10 * scale}px -${10 * scale}px inset rgba(15, 23, 44, ${((0.022 + random() * 5e-3) * ((1 + oy) / 2)).toFixed(2)})`; +} +function getBoundsForSVG(shape) { + return new Box(0, 0, NOTE_SIZE, NOTE_SIZE + shape.props.growY); +} + +function resizeScaled(shape, { + initialBounds, + scaleX, + scaleY, + newPoint +}) { + const scaleDelta = Math.max(0.01, Math.min(Math.abs(scaleX), Math.abs(scaleY))); + const offset = new Vec(0, 0); + if (scaleX < 0) { + offset.x = -(initialBounds.width * scaleDelta); + } + if (scaleY < 0) { + offset.y = -(initialBounds.height * scaleDelta); + } + const { x, y } = Vec.Add(newPoint, offset.rot(shape.rotation)); + return { + x, + y, + props: { + scale: scaleDelta * shape.props.scale + } + }; +} + +const sizeCache = new WeakCache(); +class TextShapeUtil extends ShapeUtil { + static type = "text"; + static props = textShapeProps; + static migrations = textShapeMigrations; + getDefaultProps() { + return { + color: "black", + size: "m", + w: 8, + text: "", + font: "draw", + textAlign: "start", + autoSize: true, + scale: 1 + }; + } + getMinDimensions(shape) { + return sizeCache.get(shape.props, (props) => getTextSize(this.editor, props)); + } + getGeometry(shape) { + const { scale } = shape.props; + const { width, height } = this.getMinDimensions(shape); + return new Rectangle2d({ + width: width * scale, + height: height * scale, + isFilled: true, + isLabel: true + }); + } + getText(shape) { + return shape.props.text; + } + canEdit() { + return true; + } + isAspectRatioLocked() { + return true; + } + // WAIT NO THIS IS HARD CODED IN THE RESIZE HANDLER + component(shape) { + const { + id, + props: { font, size, text, color, scale, textAlign } + } = shape; + const { width, height } = this.getMinDimensions(shape); + const isSelected = shape.id === this.editor.getOnlySelectedShapeId(); + const theme = useDefaultColorTheme(); + const handleKeyDown = useTextShapeKeydownHandler(id); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + TextLabel, + { + shapeId: id, + classNamePrefix: "tl-text-shape", + type: "text", + font, + fontSize: FONT_SIZES[size], + lineHeight: TEXT_PROPS.lineHeight, + align: textAlign, + verticalAlign: "middle", + text, + labelColor: theme[color].solid, + isSelected, + textWidth: width, + textHeight: height, + style: { + transform: `scale(${scale})`, + transformOrigin: "top left" + }, + wrap: true, + onKeyDown: handleKeyDown + } + ); + } + indicator(shape) { + const bounds = this.editor.getShapeGeometry(shape).bounds; + const editor = useEditor(); + if (shape.props.autoSize && editor.getEditingShapeId() === shape.id) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { width: toDomPrecision(bounds.width), height: toDomPrecision(bounds.height) }); + } + toSvg(shape, ctx) { + if (shape.props.text) ctx.addExportDef(getFontDefForExport(shape.props.font)); + const bounds = this.editor.getShapeGeometry(shape).bounds; + const width = bounds.width / (shape.props.scale ?? 1); + const height = bounds.height / (shape.props.scale ?? 1); + const theme = getDefaultColorTheme(ctx); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + SvgTextLabel, + { + fontSize: FONT_SIZES[shape.props.size], + font: shape.props.font, + align: shape.props.textAlign, + verticalAlign: "middle", + text: shape.props.text, + labelColor: theme[shape.props.color].solid, + bounds: new Box(0, 0, width, height), + padding: 0 + } + ); + } + onResize(shape, info) { + const { newPoint, initialBounds, initialShape, scaleX, handle } = info; + if (info.mode === "scale_shape" || handle !== "right" && handle !== "left") { + return { + id: shape.id, + type: shape.type, + ...resizeScaled(shape, info) + }; + } else { + const nextWidth = Math.max(1, Math.abs(initialBounds.width * scaleX)); + const { x, y } = scaleX < 0 ? Vec.Sub(newPoint, Vec.FromAngle(shape.rotation).mul(nextWidth)) : newPoint; + return { + id: shape.id, + type: shape.type, + x, + y, + props: { + w: nextWidth / initialShape.props.scale, + autoSize: false + } + }; + } + } + onEditEnd(shape) { + const { + id, + type, + props: { text } + } = shape; + const trimmedText = shape.props.text.trimEnd(); + if (trimmedText.length === 0) { + this.editor.deleteShapes([shape.id]); + } else { + if (trimmedText !== shape.props.text) { + this.editor.updateShapes([ + { + id, + type, + props: { + text: text.trimEnd() + } + } + ]); + } + } + } + onBeforeUpdate(prev, next) { + if (!next.props.autoSize) return; + const styleDidChange = prev.props.size !== next.props.size || prev.props.textAlign !== next.props.textAlign || prev.props.font !== next.props.font || prev.props.scale !== 1 && next.props.scale === 1; + const textDidChange = prev.props.text !== next.props.text; + if (!styleDidChange && !textDidChange) return; + const boundsA = this.getMinDimensions(prev); + const boundsB = getTextSize(this.editor, next.props); + const wA = boundsA.width * prev.props.scale; + const hA = boundsA.height * prev.props.scale; + const wB = boundsB.width * next.props.scale; + const hB = boundsB.height * next.props.scale; + let delta; + switch (next.props.textAlign) { + case "middle": { + delta = new Vec((wB - wA) / 2, textDidChange ? 0 : (hB - hA) / 2); + break; + } + case "end": { + delta = new Vec(wB - wA, textDidChange ? 0 : (hB - hA) / 2); + break; + } + default: { + if (textDidChange) break; + delta = new Vec(0, (hB - hA) / 2); + break; + } + } + if (delta) { + delta.rot(next.rotation); + const { x, y } = next; + return { + ...next, + x: x - delta.x, + y: y - delta.y, + props: { ...next.props, w: wB } + }; + } else { + return { + ...next, + props: { ...next.props, w: wB } + }; + } + } + // todo: The edge doubleclicking feels like a mistake more often than + // not, especially on multiline text. Removed June 16 2024 + // override onDoubleClickEdge = (shape: TLTextShape) => { + // // If the shape has a fixed width, set it to autoSize. + // if (!shape.props.autoSize) { + // return { + // id: shape.id, + // type: shape.type, + // props: { + // autoSize: true, + // }, + // } + // } + // // If the shape is scaled, reset the scale to 1. + // if (shape.props.scale !== 1) { + // return { + // id: shape.id, + // type: shape.type, + // props: { + // scale: 1, + // }, + // } + // } + // } +} +function getTextSize(editor, props) { + const { font, text, autoSize, size, w } = props; + const minWidth = autoSize ? 16 : Math.max(16, w); + const fontSize = FONT_SIZES[size]; + const cw = autoSize ? null : ( + // `measureText` floors the number so we need to do the same here to avoid issues. + (Math.floor(Math.max(minWidth, w))) + ); + const result = editor.textMeasure.measureText(text, { + ...TEXT_PROPS, + fontFamily: FONT_FAMILIES[font], + fontSize, + maxWidth: cw + }); + if (autoSize) { + result.w += 1; + } + return { + width: Math.max(minWidth, result.w), + height: Math.max(fontSize, result.h) + }; +} +function useTextShapeKeydownHandler(id) { + const editor = useEditor(); + return reactExports.useCallback( + (e) => { + if (editor.getEditingShapeId() !== id) return; + switch (e.key) { + case "Enter": { + if (e.ctrlKey || e.metaKey) { + editor.complete(); + } + break; + } + case "Tab": { + preventDefault(e); + if (e.shiftKey) { + TextHelpers.unindent(e.currentTarget); + } else { + TextHelpers.indent(e.currentTarget); + } + break; + } + } + }, + [editor, id] + ); +} + +class VideoShapeUtil extends BaseBoxShapeUtil { + static type = "video"; + static props = videoShapeProps; + static migrations = videoShapeMigrations; + canEdit() { + return true; + } + isAspectRatioLocked() { + return true; + } + getDefaultProps() { + return { + w: 100, + h: 100, + assetId: null, + time: 0, + playing: true, + url: "" + }; + } + component(shape) { + return /* @__PURE__ */ jsxRuntimeExports.jsx(VideoShape, { shape }); + } + indicator(shape) { + return /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { width: toDomPrecision(shape.props.w), height: toDomPrecision(shape.props.h) }); + } + async toSvg(shape) { + const image = await serializeVideo(this.editor, shape); + if (!image) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsx("image", { href: image, width: shape.props.w, height: shape.props.h }); + } +} +const VideoShape = reactExports.memo(function VideoShape2({ shape }) { + const editor = useEditor(); + const showControls = editor.getShapeGeometry(shape).bounds.w * editor.getZoomLevel() >= 110; + const isEditing = useIsEditing(shape.id); + const prefersReducedMotion = usePrefersReducedMotion(); + const { Spinner } = useEditorComponents(); + const { asset, url } = useImageOrVideoAsset({ + shapeId: shape.id, + assetId: shape.props.assetId + }); + const rVideo = reactExports.useRef(null); + const [isLoaded, setIsLoaded] = reactExports.useState(false); + const [isFullscreen, setIsFullscreen] = reactExports.useState(false); + reactExports.useEffect(() => { + const fullscreenChange = () => setIsFullscreen(document.fullscreenElement === rVideo.current); + document.addEventListener("fullscreenchange", fullscreenChange); + return () => document.removeEventListener("fullscreenchange", fullscreenChange); + }); + const handleLoadedData = reactExports.useCallback((e) => { + const video = e.currentTarget; + if (!video) return; + setIsLoaded(true); + }, []); + reactExports.useEffect(() => { + const video = rVideo.current; + if (!video) return; + if (isEditing) { + if (document.activeElement !== video) { + video.focus(); + } + } + }, [isEditing, isLoaded]); + reactExports.useEffect(() => { + if (prefersReducedMotion) { + const video = rVideo.current; + if (!video) return; + video.pause(); + video.currentTime = 0; + } + }, [rVideo, prefersReducedMotion]); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx( + HTMLContainer, + { + id: shape.id, + style: { + color: "var(--color-text-3)", + backgroundColor: asset ? "transparent" : "var(--color-low)", + border: asset ? "none" : "1px solid var(--color-low-border)" + }, + children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tl-counter-scaled", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tl-video-container", children: !asset ? /* @__PURE__ */ jsxRuntimeExports.jsx(BrokenAssetIcon, {}) : Spinner && !asset.props.src ? /* @__PURE__ */ jsxRuntimeExports.jsx(Spinner, {}) : url ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx( + "video", + { + ref: rVideo, + style: isEditing ? { pointerEvents: "all" } : !isLoaded ? { display: "none" } : void 0, + className: classNames("tl-video", `tl-video-shape-${shape.id.split(":")[1]}`, { + "tl-video-is-fullscreen": isFullscreen + }), + width: "100%", + height: "100%", + draggable: false, + playsInline: true, + autoPlay: true, + muted: true, + loop: true, + disableRemotePlayback: true, + disablePictureInPicture: true, + controls: isEditing && showControls, + onLoadedData: handleLoadedData, + hidden: !isLoaded, + children: /* @__PURE__ */ jsxRuntimeExports.jsx("source", { src: url }) + } + ), + !isLoaded && Spinner && /* @__PURE__ */ jsxRuntimeExports.jsx(Spinner, {}) + ] }) : null }) }) + } + ), + "url" in shape.props && shape.props.url && /* @__PURE__ */ jsxRuntimeExports.jsx(HyperlinkButton, { url: shape.props.url }) + ] }); +}); +async function serializeVideo(editor, shape) { + const assetUrl = await editor.resolveAssetUrl(shape.props.assetId, { + shouldResolveToOriginal: true + }); + if (!assetUrl) return null; + const video = await MediaHelpers.loadVideo(assetUrl); + return MediaHelpers.getVideoFrameAsDataUrl(video, 0); +} + +const defaultShapeUtils = [ + TextShapeUtil, + BookmarkShapeUtil, + DrawShapeUtil, + GeoShapeUtil, + NoteShapeUtil, + LineShapeUtil, + FrameShapeUtil, + ArrowShapeUtil, + HighlightShapeUtil, + EmbedShapeUtil, + ImageShapeUtil, + VideoShapeUtil +]; + +function registerDefaultSideEffects(editor) { + return editor.sideEffects.register({ + instance_page_state: { + afterChange: (prev, next) => { + if (prev.croppingShapeId !== next.croppingShapeId) { + const isInCroppingState = editor.isIn("select.crop"); + if (!prev.croppingShapeId && next.croppingShapeId) { + if (!isInCroppingState) { + editor.setCurrentTool("select.crop.idle"); + } + } else if (prev.croppingShapeId && !next.croppingShapeId) { + if (isInCroppingState) { + editor.setCurrentTool("select.idle"); + } + } + } + if (prev.editingShapeId !== next.editingShapeId) { + if (!prev.editingShapeId && next.editingShapeId) { + if (!editor.isIn("select.editing_shape")) { + const shape = editor.getEditingShape(); + if (shape && shape.type === "text" && editor.isInAny("text.pointing", "select.resizing") && editor.getInstanceState().isToolLocked) { + editor.setCurrentTool("select.editing_shape", { + isCreatingTextWhileToolLocked: true + }); + } else { + editor.setCurrentTool("select.editing_shape"); + } + } + } else if (prev.editingShapeId && !next.editingShapeId) { + if (editor.isIn("select.editing_shape")) { + editor.setCurrentTool("select.idle"); + } + } + } + } + } + }); +} + +class Erasing extends StateNode { + static id = "erasing"; + info = {}; + scribbleId = "id"; + markId = ""; + excludedShapeIds = /* @__PURE__ */ new Set(); + onEnter(info) { + this.markId = this.editor.markHistoryStoppingPoint("erase scribble begin"); + this.info = info; + const { originPagePoint } = this.editor.inputs; + this.excludedShapeIds = new Set( + this.editor.getCurrentPageShapes().filter((shape) => { + if (this.editor.isShapeOrAncestorLocked(shape)) return true; + if (this.editor.isShapeOfType(shape, "group") || this.editor.isShapeOfType(shape, "frame")) { + const pointInShapeShape = this.editor.getPointInShapeSpace(shape, originPagePoint); + const geometry = this.editor.getShapeGeometry(shape); + return geometry.bounds.containsPoint(pointInShapeShape); + } + return false; + }).map((shape) => shape.id) + ); + const scribble = this.editor.scribbles.addScribble({ + color: "muted-1", + size: 12 + }); + this.scribbleId = scribble.id; + this.update(); + } + pushPointToScribble() { + const { x, y } = this.editor.inputs.currentPagePoint; + this.editor.scribbles.addPoint(this.scribbleId, x, y); + } + onExit() { + this.editor.setErasingShapes([]); + this.editor.scribbles.stop(this.scribbleId); + } + onPointerMove() { + this.update(); + } + onPointerUp() { + this.complete(); + } + onCancel() { + this.cancel(); + } + onComplete() { + this.complete(); + } + update() { + const { editor, excludedShapeIds } = this; + const erasingShapeIds = editor.getErasingShapeIds(); + const zoomLevel = editor.getZoomLevel(); + const currentPageShapes = editor.getCurrentPageRenderingShapesSorted(); + const { + inputs: { currentPagePoint, previousPagePoint } + } = editor; + this.pushPointToScribble(); + const erasing = new Set(erasingShapeIds); + const minDist = this.editor.options.hitTestMargin / zoomLevel; + for (const shape of currentPageShapes) { + if (editor.isShapeOfType(shape, "group")) continue; + const pageMask = editor.getShapeMask(shape.id); + if (pageMask && !pointInPolygon(currentPagePoint, pageMask)) { + continue; + } + const geometry = editor.getShapeGeometry(shape); + const pageTransform = editor.getShapePageTransform(shape); + if (!geometry || !pageTransform) continue; + const pt = pageTransform.clone().invert(); + const A = pt.applyToPoint(previousPagePoint); + const B = pt.applyToPoint(currentPagePoint); + const { bounds } = geometry; + if (bounds.minX - minDist > Math.max(A.x, B.x) || bounds.minY - minDist > Math.max(A.y, B.y) || bounds.maxX + minDist < Math.min(A.x, B.x) || bounds.maxY + minDist < Math.min(A.y, B.y)) { + continue; + } + if (geometry.hitTestLineSegment(A, B, minDist)) { + erasing.add(editor.getOutermostSelectableShape(shape).id); + } + } + this.editor.setErasingShapes([...erasing].filter((id) => !excludedShapeIds.has(id))); + } + complete() { + const { editor } = this; + editor.deleteShapes(editor.getCurrentPageState().erasingShapeIds); + this.parent.transition("idle"); + } + cancel() { + const { editor } = this; + editor.bailToMark(this.markId); + this.parent.transition("idle", this.info); + } +} + +let Idle$5 = class Idle extends StateNode { + static id = "idle"; + onPointerDown(info) { + this.parent.transition("pointing", info); + } + onCancel() { + this.editor.setCurrentTool("select"); + } +}; + +let Pointing$2 = class Pointing extends StateNode { + static id = "pointing"; + onEnter() { + const zoomLevel = this.editor.getZoomLevel(); + const currentPageShapesSorted = this.editor.getCurrentPageRenderingShapesSorted(); + const { + inputs: { currentPagePoint } + } = this.editor; + const erasing = /* @__PURE__ */ new Set(); + const initialSize = erasing.size; + for (let n = currentPageShapesSorted.length, i = n - 1; i >= 0; i--) { + const shape = currentPageShapesSorted[i]; + if (this.editor.isShapeOrAncestorLocked(shape) || this.editor.isShapeOfType(shape, "group")) { + continue; + } + if (this.editor.isPointInShape(shape, currentPagePoint, { + hitInside: false, + margin: this.editor.options.hitTestMargin / zoomLevel + })) { + const hitShape = this.editor.getOutermostSelectableShape(shape); + if (this.editor.isShapeOfType(hitShape, "frame") && erasing.size > initialSize) { + break; + } + erasing.add(hitShape.id); + } + } + this.editor.setErasingShapes([...erasing]); + } + onLongPress(info) { + this.startErasing(info); + } + onExit(_info, to) { + if (to !== "erasing") { + this.editor.setErasingShapes([]); + } + } + onPointerMove(info) { + if (this.editor.inputs.isDragging) { + this.startErasing(info); + } + } + onPointerUp() { + this.complete(); + } + onCancel() { + this.cancel(); + } + onComplete() { + this.complete(); + } + onInterrupt() { + this.cancel(); + } + startErasing(info) { + this.parent.transition("erasing", info); + } + complete() { + const erasingShapeIds = this.editor.getErasingShapeIds(); + if (erasingShapeIds.length) { + this.editor.markHistoryStoppingPoint("erase end"); + this.editor.deleteShapes(erasingShapeIds); + } + this.parent.transition("idle"); + } + cancel() { + this.parent.transition("idle"); + } +}; + +class EraserTool extends StateNode { + static id = "eraser"; + static initial = "idle"; + static isLockable = false; + static children() { + return [Idle$5, Pointing$2, Erasing]; + } + onEnter() { + this.editor.setCursor({ type: "cross", rotation: 0 }); + } +} + +class Dragging extends StateNode { + static id = "dragging"; + initialCamera = new Vec(); + onEnter() { + this.initialCamera = Vec.From(this.editor.getCamera()); + this.update(); + } + onPointerMove() { + this.update(); + } + onPointerUp() { + this.complete(); + } + onCancel() { + this.parent.transition("idle"); + } + onComplete() { + this.complete(); + } + update() { + const { initialCamera, editor } = this; + const { currentScreenPoint, originScreenPoint } = editor.inputs; + const delta = Vec.Sub(currentScreenPoint, originScreenPoint).div(editor.getZoomLevel()); + if (delta.len2() === 0) return; + editor.setCamera(initialCamera.clone().add(delta)); + } + complete() { + const { editor } = this; + const { pointerVelocity } = editor.inputs; + const velocityAtPointerUp = Math.min(pointerVelocity.len(), 2); + if (velocityAtPointerUp > 0.1) { + this.editor.slideCamera({ speed: velocityAtPointerUp, direction: pointerVelocity }); + } + this.parent.transition("idle"); + } +} + +let Idle$4 = class Idle extends StateNode { + static id = "idle"; + onEnter() { + this.editor.setCursor({ type: "grab", rotation: 0 }); + } + onPointerDown(info) { + this.parent.transition("pointing", info); + } + onCancel() { + this.editor.setCurrentTool("select"); + } +}; + +let Pointing$1 = class Pointing extends StateNode { + static id = "pointing"; + onEnter() { + this.editor.stopCameraAnimation(); + this.editor.setCursor({ type: "grabbing", rotation: 0 }); + } + onLongPress() { + this.startDragging(); + } + onPointerMove() { + if (this.editor.inputs.isDragging) { + this.startDragging(); + } + } + startDragging() { + this.parent.transition("dragging"); + } + onPointerUp() { + this.complete(); + } + onCancel() { + this.complete(); + } + onComplete() { + this.complete(); + } + onInterrupt() { + this.complete(); + } + complete() { + this.parent.transition("idle"); + } +}; + +class HandTool extends StateNode { + static id = "hand"; + static initial = "idle"; + static isLockable = false; + static children() { + return [Idle$4, Pointing$1, Dragging]; + } + onDoubleClick(info) { + if (info.phase === "settle") { + const { currentScreenPoint } = this.editor.inputs; + this.editor.zoomIn(currentScreenPoint, { + animation: { duration: 220, easing: EASINGS.easeOutQuint } + }); + } + } + onTripleClick(info) { + if (info.phase === "settle") { + const { currentScreenPoint } = this.editor.inputs; + this.editor.zoomOut(currentScreenPoint, { + animation: { duration: 320, easing: EASINGS.easeOutQuint } + }); + } + } + onQuadrupleClick(info) { + if (info.phase === "settle") { + const zoomLevel = this.editor.getZoomLevel(); + const { + inputs: { currentScreenPoint } + } = this.editor; + if (zoomLevel === 1) { + this.editor.zoomToFit({ animation: { duration: 400, easing: EASINGS.easeOutQuint } }); + } else { + this.editor.resetZoom(currentScreenPoint, { + animation: { duration: 320, easing: EASINGS.easeOutQuint } + }); + } + } + } +} + +let Idle$3 = class Idle extends StateNode { + static id = "idle"; + onPointerDown(info) { + this.parent.transition("lasering", info); + } +}; + +class Lasering extends StateNode { + static id = "lasering"; + scribbleId = "id"; + onEnter() { + const scribble = this.editor.scribbles.addScribble({ + color: "laser", + opacity: 0.7, + size: 4, + delay: this.editor.options.laserDelayMs, + shrink: 0.05, + taper: true + }); + this.scribbleId = scribble.id; + this.pushPointToScribble(); + } + onExit() { + this.editor.scribbles.stop(this.scribbleId); + } + onPointerMove() { + this.pushPointToScribble(); + } + onPointerUp() { + this.complete(); + } + pushPointToScribble() { + const { x, y } = this.editor.inputs.currentPagePoint; + this.editor.scribbles.addPoint(this.scribbleId, x, y); + } + onCancel() { + this.cancel(); + } + onComplete() { + this.complete(); + } + complete() { + this.parent.transition("idle"); + } + cancel() { + this.parent.transition("idle"); + } +} + +class LaserTool extends StateNode { + static id = "laser"; + static initial = "idle"; + static children() { + return [Idle$3, Lasering]; + } + static isLockable = false; + onEnter() { + this.editor.setCursor({ type: "cross", rotation: 0 }); + } +} + +class Brushing extends StateNode { + static id = "brushing"; + info = {}; + initialSelectedShapeIds = []; + excludedShapeIds = /* @__PURE__ */ new Set(); + isWrapMode = false; + // The shape that the brush started on + initialStartShape = null; + onEnter(info) { + const { altKey, currentPagePoint } = this.editor.inputs; + this.isWrapMode = this.editor.user.getIsWrapMode(); + if (altKey) { + this.parent.transition("scribble_brushing", info); + return; + } + this.excludedShapeIds = new Set( + this.editor.getCurrentPageShapes().filter( + (shape) => this.editor.isShapeOfType(shape, "group") || this.editor.isShapeOrAncestorLocked(shape) + ).map((shape) => shape.id) + ); + this.info = info; + this.initialSelectedShapeIds = this.editor.getSelectedShapeIds().slice(); + this.initialStartShape = this.editor.getShapesAtPoint(currentPagePoint)[0]; + this.hitTestShapes(); + } + onExit() { + this.initialSelectedShapeIds = []; + this.editor.updateInstanceState({ brush: null }); + } + onTick({ elapsed }) { + const { editor } = this; + editor.edgeScrollManager.updateEdgeScrolling(elapsed); + } + onPointerMove() { + this.hitTestShapes(); + } + onPointerUp() { + this.complete(); + } + onComplete() { + this.complete(); + } + onCancel(info) { + this.editor.setSelectedShapes(this.initialSelectedShapeIds); + this.parent.transition("idle", info); + } + onKeyDown(info) { + if (this.editor.inputs.altKey) { + this.parent.transition("scribble_brushing", info); + } else { + this.hitTestShapes(); + } + } + onKeyUp() { + this.hitTestShapes(); + } + complete() { + this.hitTestShapes(); + this.parent.transition("idle"); + } + hitTestShapes() { + const { editor, excludedShapeIds, isWrapMode } = this; + const { + inputs: { originPagePoint, currentPagePoint, shiftKey, ctrlKey } + } = editor; + const results = new Set(shiftKey ? this.initialSelectedShapeIds : []); + const isWrapping = isWrapMode ? !ctrlKey : ctrlKey; + const brush = Box.FromPoints([originPagePoint, currentPagePoint]); + const { corners } = brush; + let A, B, shape, pageBounds, pageTransform, localCorners; + const currentPageShapes = editor.getCurrentPageRenderingShapesSorted(); + const currentPageId = editor.getCurrentPageId(); + testAllShapes: for (let i = 0, n = currentPageShapes.length; i < n; i++) { + shape = currentPageShapes[i]; + if (excludedShapeIds.has(shape.id) || results.has(shape.id)) continue testAllShapes; + pageBounds = editor.getShapePageBounds(shape); + if (!pageBounds) continue testAllShapes; + if (brush.contains(pageBounds)) { + this.handleHit(shape, currentPagePoint, currentPageId, results, corners); + continue testAllShapes; + } + if (isWrapping || editor.isShapeOfType(shape, "frame")) { + continue testAllShapes; + } + if (brush.collides(pageBounds)) { + pageTransform = editor.getShapePageTransform(shape); + if (!pageTransform) continue testAllShapes; + localCorners = pageTransform.clone().invert().applyToPoints(corners); + const geometry = editor.getShapeGeometry(shape); + hitTestBrushEdges: for (let i2 = 0; i2 < 4; i2++) { + A = localCorners[i2]; + B = localCorners[(i2 + 1) % 4]; + if (geometry.hitTestLineSegment(A, B, 0)) { + this.handleHit(shape, currentPagePoint, currentPageId, results, corners); + break hitTestBrushEdges; + } + } + } + } + const currentBrush = editor.getInstanceState().brush; + if (!currentBrush || !brush.equals(currentBrush)) { + editor.updateInstanceState({ brush: { ...brush.toJson() } }); + } + const current = editor.getSelectedShapeIds(); + if (current.length !== results.size || current.some((id) => !results.has(id))) { + editor.setSelectedShapes(Array.from(results)); + } + } + onInterrupt() { + this.editor.updateInstanceState({ brush: null }); + } + handleHit(shape, currentPagePoint, currentPageId, results, corners) { + if (shape.parentId === currentPageId) { + results.add(shape.id); + return; + } + const selectedShape = this.editor.getOutermostSelectableShape(shape); + const pageMask = this.editor.getShapeMask(selectedShape.id); + if (pageMask && !polygonsIntersect(pageMask, corners) && !pointInPolygon(currentPagePoint, pageMask)) { + return; + } + results.add(selectedShape.id); + } +} + +const CursorTypeMap = { + bottom: "ns-resize", + top: "ns-resize", + left: "ew-resize", + right: "ew-resize", + bottom_left: "nesw-resize", + bottom_right: "nwse-resize", + top_left: "nwse-resize", + top_right: "nesw-resize", + bottom_left_rotate: "swne-rotate", + bottom_right_rotate: "senw-rotate", + top_left_rotate: "nwse-rotate", + top_right_rotate: "nesw-rotate", + mobile_rotate: "grabbing" +}; +class PointingResizeHandle extends StateNode { + static id = "pointing_resize_handle"; + info = {}; + updateCursor() { + const selected = this.editor.getSelectedShapes(); + const cursorType = CursorTypeMap[this.info.handle]; + this.editor.setCursor({ + type: cursorType, + rotation: selected.length === 1 ? this.editor.getSelectionRotation() : 0 + }); + } + onEnter(info) { + this.info = info; + this.updateCursor(); + } + onPointerMove() { + if (this.editor.inputs.isDragging) { + this.startResizing(); + } + } + onLongPress() { + this.startResizing(); + } + startResizing() { + if (this.editor.getIsReadonly()) return; + this.parent.transition("resizing", this.info); + } + onPointerUp() { + this.complete(); + } + onCancel() { + this.cancel(); + } + onComplete() { + this.cancel(); + } + onInterrupt() { + this.cancel(); + } + complete() { + if (this.info.onInteractionEnd) { + this.editor.setCurrentTool(this.info.onInteractionEnd, {}); + } else { + this.parent.transition("idle"); + } + } + cancel() { + if (this.info.onInteractionEnd) { + this.editor.setCurrentTool(this.info.onInteractionEnd, {}); + } else { + this.parent.transition("idle"); + } + } +} + +const MIN_CROP_SIZE = 8; + +class Cropping extends StateNode { + static id = "cropping"; + info = {}; + markId = ""; + snapshot = {}; + onEnter(info) { + this.info = info; + this.markId = this.editor.markHistoryStoppingPoint("cropping"); + this.snapshot = this.createSnapshot(); + this.updateShapes(); + } + onPointerMove() { + this.updateShapes(); + } + onPointerUp() { + this.complete(); + } + onComplete() { + this.complete(); + } + onCancel() { + this.cancel(); + } + updateCursor() { + const selectedShape = this.editor.getSelectedShapes()[0]; + if (!selectedShape) return; + const cursorType = CursorTypeMap[this.info.handle]; + this.editor.setCursor({ type: cursorType, rotation: this.editor.getSelectionRotation() }); + } + getDefaultCrop() { + return { + topLeft: { x: 0, y: 0 }, + bottomRight: { x: 1, y: 1 } + }; + } + updateShapes() { + const { shape, cursorHandleOffset } = this.snapshot; + if (!shape) return; + const util = this.editor.getShapeUtil("image"); + if (!util) return; + const props = shape.props; + const currentPagePoint = this.editor.inputs.currentPagePoint.clone().sub(cursorHandleOffset); + const originPagePoint = this.editor.inputs.originPagePoint.clone().sub(cursorHandleOffset); + const change = currentPagePoint.clone().sub(originPagePoint).rot(-shape.rotation); + const crop = props.crop ?? this.getDefaultCrop(); + const newCrop = structuredClone(crop); + const newPoint = new Vec(shape.x, shape.y); + const pointDelta = new Vec(0, 0); + const w = 1 / (crop.bottomRight.x - crop.topLeft.x) * props.w; + const h = 1 / (crop.bottomRight.y - crop.topLeft.y) * props.h; + let hasCropChanged = false; + switch (this.info.handle) { + case "top": + case "top_left": + case "top_right": { + if (h < MIN_CROP_SIZE) break; + hasCropChanged = true; + newCrop.topLeft.y = newCrop.topLeft.y + change.y / h; + const heightAfterCrop = h * (newCrop.bottomRight.y - newCrop.topLeft.y); + if (heightAfterCrop < MIN_CROP_SIZE) { + newCrop.topLeft.y = newCrop.bottomRight.y - MIN_CROP_SIZE / h; + pointDelta.y = (newCrop.topLeft.y - crop.topLeft.y) * h; + } else { + if (newCrop.topLeft.y <= 0) { + newCrop.topLeft.y = 0; + pointDelta.y = (newCrop.topLeft.y - crop.topLeft.y) * h; + } else { + pointDelta.y = change.y; + } + } + break; + } + case "bottom": + case "bottom_left": + case "bottom_right": { + if (h < MIN_CROP_SIZE) break; + hasCropChanged = true; + newCrop.bottomRight.y = Math.min(1, newCrop.bottomRight.y + change.y / h); + const heightAfterCrop = h * (newCrop.bottomRight.y - newCrop.topLeft.y); + if (heightAfterCrop < MIN_CROP_SIZE) { + newCrop.bottomRight.y = newCrop.topLeft.y + MIN_CROP_SIZE / h; + } + break; + } + } + switch (this.info.handle) { + case "left": + case "top_left": + case "bottom_left": { + if (w < MIN_CROP_SIZE) break; + hasCropChanged = true; + newCrop.topLeft.x = newCrop.topLeft.x + change.x / w; + const widthAfterCrop = w * (newCrop.bottomRight.x - newCrop.topLeft.x); + if (widthAfterCrop < MIN_CROP_SIZE) { + newCrop.topLeft.x = newCrop.bottomRight.x - MIN_CROP_SIZE / w; + pointDelta.x = (newCrop.topLeft.x - crop.topLeft.x) * w; + } else { + if (newCrop.topLeft.x <= 0) { + newCrop.topLeft.x = 0; + pointDelta.x = (newCrop.topLeft.x - crop.topLeft.x) * w; + } else { + pointDelta.x = change.x; + } + } + break; + } + case "right": + case "top_right": + case "bottom_right": { + if (w < MIN_CROP_SIZE) break; + hasCropChanged = true; + newCrop.bottomRight.x = Math.min(1, newCrop.bottomRight.x + change.x / w); + const widthAfterCrop = w * (newCrop.bottomRight.x - newCrop.topLeft.x); + if (widthAfterCrop < MIN_CROP_SIZE) { + newCrop.bottomRight.x = newCrop.topLeft.x + MIN_CROP_SIZE / w; + } + break; + } + } + if (!hasCropChanged) return; + newPoint.add(pointDelta.rot(shape.rotation)); + const partial = { + id: shape.id, + type: shape.type, + x: newPoint.x, + y: newPoint.y, + props: { + crop: newCrop, + w: (newCrop.bottomRight.x - newCrop.topLeft.x) * w, + h: (newCrop.bottomRight.y - newCrop.topLeft.y) * h + } + }; + this.editor.updateShapes([partial]); + this.updateCursor(); + } + complete() { + this.updateShapes(); + kickoutOccludedShapes(this.editor, [this.snapshot.shape.id]); + if (this.info.onInteractionEnd) { + this.editor.setCurrentTool(this.info.onInteractionEnd, this.info); + } else { + this.editor.setCroppingShape(null); + this.editor.setCurrentTool("select.idle"); + } + } + cancel() { + this.editor.bailToMark(this.markId); + if (this.info.onInteractionEnd) { + this.editor.setCurrentTool(this.info.onInteractionEnd, this.info); + } else { + this.editor.setCroppingShape(null); + this.editor.setCurrentTool("select.idle"); + } + } + createSnapshot() { + const selectionRotation = this.editor.getSelectionRotation(); + const { + inputs: { originPagePoint } + } = this.editor; + const shape = this.editor.getOnlySelectedShape(); + const selectionBounds = this.editor.getSelectionRotatedPageBounds(); + const dragHandlePoint = Vec.RotWith( + selectionBounds.getHandlePoint(this.info.handle), + selectionBounds.point, + selectionRotation + ); + const cursorHandleOffset = Vec.Sub(originPagePoint, dragHandlePoint); + return { + shape, + cursorHandleOffset + }; + } +} + +function getHitShapeOnCanvasPointerDown(editor, hitLabels = false) { + const zoomLevel = editor.getZoomLevel(); + const { + inputs: { currentPagePoint } + } = editor; + return ( + // hovered shape at point + (// selected shape at point + editor.getShapeAtPoint(currentPagePoint, { + hitInside: false, + hitLabels, + margin: editor.options.hitTestMargin / zoomLevel, + renderingOnly: true + }) ?? editor.getSelectedShapeAtPoint(currentPagePoint)) + ); +} + +function getTranslateCroppedImageChange(editor, shape, delta) { + if (!shape) { + throw Error("Needs to translate a cropped shape!"); + } + const { crop: oldCrop } = shape.props; + if (!oldCrop) { + return; + } + const flatten = editor.inputs.shiftKey ? Math.abs(delta.x) < Math.abs(delta.y) ? "x" : "y" : null; + if (flatten === "x") { + delta.x = 0; + } else if (flatten === "y") { + delta.y = 0; + } + delta.rot(-shape.rotation); + const w = 1 / (oldCrop.bottomRight.x - oldCrop.topLeft.x) * shape.props.w; + const h = 1 / (oldCrop.bottomRight.y - oldCrop.topLeft.y) * shape.props.h; + const yCrop = oldCrop.bottomRight.y - oldCrop.topLeft.y; + const xCrop = oldCrop.bottomRight.x - oldCrop.topLeft.x; + const newCrop = structuredClone(oldCrop); + newCrop.topLeft.x = Math.min(1 - xCrop, Math.max(0, newCrop.topLeft.x - delta.x / w)); + newCrop.topLeft.y = Math.min(1 - yCrop, Math.max(0, newCrop.topLeft.y - delta.y / h)); + newCrop.bottomRight.x = newCrop.topLeft.x + xCrop; + newCrop.bottomRight.y = newCrop.topLeft.y + yCrop; + const partial = { + id: shape.id, + type: shape.type, + props: { + crop: newCrop + } + }; + return partial; +} + +let Idle$2 = class Idle extends StateNode { + static id = "idle"; + onEnter() { + this.editor.setCursor({ type: "default", rotation: 0 }); + const onlySelectedShape = this.editor.getOnlySelectedShape(); + if (onlySelectedShape) { + this.editor.setCroppingShape(onlySelectedShape.id); + } + } + onExit() { + this.editor.setCursor({ type: "default", rotation: 0 }); + } + onCancel() { + this.editor.setCroppingShape(null); + this.editor.setCurrentTool("select.idle", {}); + } + onPointerDown(info) { + if (info.accelKey) { + this.cancel(); + this.editor.root.handleEvent(info); + return; + } + switch (info.target) { + case "canvas": { + const hitShape = getHitShapeOnCanvasPointerDown(this.editor); + if (hitShape && !this.editor.isShapeOfType(hitShape, "group")) { + this.onPointerDown({ + ...info, + shape: hitShape, + target: "shape" + }); + return; + } + this.cancel(); + this.editor.root.handleEvent(info); + break; + } + case "shape": { + if (info.shape.id === this.editor.getCroppingShapeId()) { + this.editor.setCurrentTool("select.crop.pointing_crop", info); + return; + } else { + if (this.editor.getShapeUtil(info.shape)?.canCrop(info.shape)) { + this.editor.setCroppingShape(info.shape.id); + this.editor.setSelectedShapes([info.shape.id]); + this.editor.setCurrentTool("select.crop.pointing_crop", info); + } else { + this.cancel(); + this.editor.root.handleEvent(info); + } + } + break; + } + case "selection": { + switch (info.handle) { + case "mobile_rotate": + case "top_left_rotate": + case "top_right_rotate": + case "bottom_left_rotate": + case "bottom_right_rotate": { + this.editor.setCurrentTool("select.pointing_rotate_handle", { + ...info, + onInteractionEnd: "select.crop.idle" + }); + break; + } + case "top": + case "right": + case "bottom": + case "left": + case "top_left": + case "top_right": + case "bottom_left": + case "bottom_right": { + this.editor.setCurrentTool("select.crop.pointing_crop_handle", { + ...info, + onInteractionEnd: "select.crop.idle" + }); + break; + } + default: { + this.cancel(); + } + } + break; + } + } + } + onDoubleClick(info) { + if (this.editor.inputs.shiftKey || info.phase !== "up") return; + const croppingShapeId = this.editor.getCroppingShapeId(); + if (!croppingShapeId) return; + const shape = this.editor.getShape(croppingShapeId); + if (!shape) return; + const util = this.editor.getShapeUtil(shape); + if (!util) return; + if (info.target === "selection") { + util.onDoubleClickEdge?.(shape); + return; + } + this.cancel(); + this.editor.root.handleEvent(info); + } + onKeyDown() { + this.nudgeCroppingImage(false); + } + onKeyRepeat() { + this.nudgeCroppingImage(true); + } + onKeyUp(info) { + switch (info.code) { + case "Enter": { + this.editor.setCroppingShape(null); + this.editor.setCurrentTool("select.idle", {}); + break; + } + } + } + cancel() { + this.editor.setCroppingShape(null); + this.editor.setCurrentTool("select.idle", {}); + } + nudgeCroppingImage(ephemeral = false) { + const { + editor: { + inputs: { keys } + } + } = this; + const shiftKey = keys.has("ShiftLeft"); + const delta = new Vec(0, 0); + if (keys.has("ArrowLeft")) delta.x += 1; + if (keys.has("ArrowRight")) delta.x -= 1; + if (keys.has("ArrowUp")) delta.y += 1; + if (keys.has("ArrowDown")) delta.y -= 1; + if (delta.equals(new Vec(0, 0))) return; + if (shiftKey) delta.mul(10); + const shape = this.editor.getShape(this.editor.getCroppingShapeId()); + if (!shape) return; + const partial = getTranslateCroppedImageChange(this.editor, shape, delta); + if (partial) { + if (!ephemeral) { + this.editor.markHistoryStoppingPoint("translate crop"); + } + this.editor.updateShapes([partial]); + } + } +}; + +class PointingCrop extends StateNode { + static id = "pointing_crop"; + onCancel() { + this.editor.setCurrentTool("select.crop.idle", {}); + } + onPointerMove(info) { + if (this.editor.inputs.isDragging) { + this.editor.setCurrentTool("select.crop.translating_crop", info); + } + } + onPointerUp(info) { + this.editor.setCurrentTool("select.crop.idle", info); + } +} + +class PointingCropHandle extends StateNode { + static id = "pointing_crop_handle"; + info = {}; + onEnter(info) { + this.info = info; + this.parent.setCurrentToolIdMask(info.onInteractionEnd); + const selectedShape = this.editor.getSelectedShapes()[0]; + if (!selectedShape) return; + const cursorType = CursorTypeMap[this.info.handle]; + this.editor.setCursor({ type: cursorType, rotation: this.editor.getSelectionRotation() }); + this.editor.setCroppingShape(selectedShape.id); + } + onExit() { + this.editor.setCursor({ type: "default", rotation: 0 }); + this.parent.setCurrentToolIdMask(void 0); + } + onPointerMove() { + if (this.editor.inputs.isDragging) { + this.startCropping(); + } + } + onLongPress() { + this.startCropping(); + } + startCropping() { + if (this.editor.getIsReadonly()) return; + this.parent.transition("cropping", { + ...this.info, + onInteractionEnd: this.info.onInteractionEnd + }); + } + onPointerUp() { + if (this.info.onInteractionEnd) { + this.editor.setCurrentTool(this.info.onInteractionEnd, this.info); + } else { + this.editor.setCroppingShape(null); + this.editor.setCurrentTool("select.idle"); + } + } + onCancel() { + this.cancel(); + } + onComplete() { + this.cancel(); + } + onInterrupt() { + this.cancel(); + } + cancel() { + if (this.info.onInteractionEnd) { + this.editor.setCurrentTool(this.info.onInteractionEnd, this.info); + } else { + this.editor.setCroppingShape(null); + this.editor.setCurrentTool("select.idle"); + } + } +} + +class TranslatingCrop extends StateNode { + static id = "translating_crop"; + info = {}; + markId = ""; + snapshot = {}; + onEnter(info) { + this.info = info; + this.snapshot = this.createSnapshot(); + this.markId = this.editor.markHistoryStoppingPoint("translating_crop"); + this.editor.setCursor({ type: "move", rotation: 0 }); + this.updateShapes(); + } + onExit() { + this.editor.setCursor({ type: "default", rotation: 0 }); + } + onPointerMove() { + this.updateShapes(); + } + onPointerUp() { + this.complete(); + } + onComplete() { + this.complete(); + } + onCancel() { + this.cancel(); + } + onKeyDown(info) { + switch (info.key) { + case "Alt": + case "Shift": { + this.updateShapes(); + return; + } + } + } + onKeyUp(info) { + switch (info.key) { + case "Enter": { + this.complete(); + return; + } + case "Alt": + case "Shift": { + this.updateShapes(); + } + } + } + complete() { + this.updateShapes(); + this.editor.setCurrentTool("select.crop.idle", this.info); + } + cancel() { + this.editor.bailToMark(this.markId); + this.editor.setCurrentTool("select.crop.idle", this.info); + } + createSnapshot() { + const shape = this.editor.getOnlySelectedShape(); + return { shape }; + } + updateShapes() { + const shape = this.snapshot.shape; + if (!shape) return; + const { originPagePoint, currentPagePoint } = this.editor.inputs; + const delta = currentPagePoint.clone().sub(originPagePoint); + const partial = getTranslateCroppedImageChange(this.editor, shape, delta); + if (partial) { + this.editor.updateShapes([partial]); + } + } +} + +class Crop extends StateNode { + static id = "crop"; + static initial = "idle"; + static children() { + return [Idle$2, TranslatingCrop, PointingCrop, PointingCropHandle, Cropping]; + } + markId = ""; + onEnter() { + this.didExit = false; + this.markId = this.editor.markHistoryStoppingPoint("crop"); + } + didExit = false; + onExit() { + if (!this.didExit) { + this.didExit = true; + this.editor.squashToMark(this.markId); + } + } + onCancel() { + if (!this.didExit) { + this.didExit = true; + this.editor.bailToMark(this.markId); + } + } +} + +class DraggingHandle extends StateNode { + static id = "dragging_handle"; + shapeId = ""; + initialHandle = {}; + initialAdjacentHandle = null; + initialPagePoint = {}; + markId = ""; + initialPageTransform; + initialPageRotation; + info = {}; + isPrecise = false; + isPreciseId = null; + pointingId = null; + onEnter(info) { + const { shape, isCreating, creatingMarkId, handle } = info; + this.info = info; + this.parent.setCurrentToolIdMask(info.onInteractionEnd); + this.shapeId = shape.id; + this.markId = ""; + if (isCreating) { + if (creatingMarkId) { + this.markId = creatingMarkId; + } else { + const markId = this.editor.getMarkIdMatching( + `creating:${this.editor.getOnlySelectedShapeId()}` + ); + if (markId) { + this.markId = markId; + } + } + } else { + this.markId = this.editor.markHistoryStoppingPoint("dragging handle"); + } + this.initialHandle = structuredClone(handle); + if (this.editor.isShapeOfType(shape, "line")) { + if (this.initialHandle.type === "create") { + this.editor.updateShape({ + ...shape, + props: { + points: { + ...shape.props.points, + [handle.index]: { id: handle.index, index: handle.index, x: handle.x, y: handle.y } + } + } + }); + const handlesAfter = this.editor.getShapeHandles(shape); + const handleAfter = handlesAfter.find((h) => h.index === handle.index); + this.initialHandle = structuredClone(handleAfter); + } + } + this.initialPageTransform = this.editor.getShapePageTransform(shape); + this.initialPageRotation = this.initialPageTransform.rotation(); + this.initialPagePoint = this.editor.inputs.originPagePoint.clone(); + this.editor.setCursor({ type: isCreating ? "cross" : "grabbing", rotation: 0 }); + const handles = this.editor.getShapeHandles(shape).sort(sortByIndex$1); + const index = handles.findIndex((h) => h.id === info.handle.id); + this.initialAdjacentHandle = null; + for (let i = index + 1; i < handles.length; i++) { + const handle2 = handles[i]; + if (handle2.type === "vertex" && handle2.id !== "middle" && handle2.id !== info.handle.id) { + this.initialAdjacentHandle = handle2; + break; + } + } + if (!this.initialAdjacentHandle) { + for (let i = handles.length - 1; i >= 0; i--) { + const handle2 = handles[i]; + if (handle2.type === "vertex" && handle2.id !== "middle" && handle2.id !== info.handle.id) { + this.initialAdjacentHandle = handle2; + break; + } + } + } + if (this.editor.isShapeOfType(shape, "arrow")) { + const initialBinding = getArrowBindings(this.editor, shape)[info.handle.id]; + this.isPrecise = false; + if (initialBinding) { + this.editor.setHintingShapes([initialBinding.toId]); + this.isPrecise = initialBinding.props.isPrecise; + if (this.isPrecise) { + this.isPreciseId = initialBinding.toId; + } else { + this.resetExactTimeout(); + } + } else { + this.editor.setHintingShapes([]); + } + } + this.update(); + this.editor.select(this.shapeId); + } + // Only relevant to arrows + exactTimeout = -1; + // Only relevant to arrows + resetExactTimeout() { + if (this.exactTimeout !== -1) { + this.clearExactTimeout(); + } + this.exactTimeout = this.editor.timers.setTimeout(() => { + if (this.getIsActive() && !this.isPrecise) { + this.isPrecise = true; + this.isPreciseId = this.pointingId; + this.update(); + } + this.exactTimeout = -1; + }, 750); + } + // Only relevant to arrows + clearExactTimeout() { + if (this.exactTimeout !== -1) { + clearTimeout(this.exactTimeout); + this.exactTimeout = -1; + } + } + onPointerMove() { + this.update(); + } + onKeyDown() { + this.update(); + } + onKeyUp() { + this.update(); + } + onPointerUp() { + this.complete(); + } + onComplete() { + this.update(); + this.complete(); + } + onCancel() { + this.cancel(); + } + onExit() { + this.parent.setCurrentToolIdMask(void 0); + this.editor.setHintingShapes([]); + this.editor.snaps.clearIndicators(); + this.editor.setCursor({ type: "default", rotation: 0 }); + } + complete() { + this.editor.snaps.clearIndicators(); + kickoutOccludedShapes(this.editor, [this.shapeId]); + const { onInteractionEnd } = this.info; + if (this.editor.getInstanceState().isToolLocked && onInteractionEnd) { + this.editor.setCurrentTool(onInteractionEnd, { shapeId: this.shapeId }); + return; + } + this.parent.transition("idle"); + } + cancel() { + this.editor.bailToMark(this.markId); + this.editor.snaps.clearIndicators(); + const { onInteractionEnd } = this.info; + if (onInteractionEnd) { + this.editor.setCurrentTool(onInteractionEnd, { shapeId: this.shapeId }); + return; + } + this.parent.transition("idle"); + } + update() { + const { editor, shapeId, initialPagePoint } = this; + const { initialHandle, initialPageRotation, initialAdjacentHandle } = this; + const hintingShapeIds = this.editor.getHintingShapeIds(); + const isSnapMode = this.editor.user.getIsSnapMode(); + const { + snaps, + inputs: { currentPagePoint, shiftKey, ctrlKey, altKey, pointerVelocity } + } = editor; + const initial = this.info.shape; + const shape = editor.getShape(shapeId); + if (!shape) return; + const util = editor.getShapeUtil(shape); + let point = currentPagePoint.clone().sub(initialPagePoint).rot(-initialPageRotation).add(initialHandle); + if (shiftKey && initialAdjacentHandle && initialHandle.id !== "middle") { + const angle = Vec.Angle(initialAdjacentHandle, point); + const snappedAngle = snapAngle(angle, 24); + const angleDifference = snappedAngle - angle; + point = Vec.RotWith(point, initialAdjacentHandle, angleDifference); + } + editor.snaps.clearIndicators(); + let nextHandle = { ...initialHandle, x: point.x, y: point.y }; + if (initialHandle.canSnap && (isSnapMode ? !ctrlKey : ctrlKey)) { + const pageTransform = editor.getShapePageTransform(shape.id); + if (!pageTransform) throw Error("Expected a page transform"); + const snap = snaps.handles.snapHandle({ currentShapeId: shapeId, handle: nextHandle }); + if (snap) { + snap.nudge.rot(-editor.getShapeParentTransform(shape).rotation()); + point.add(snap.nudge); + nextHandle = { ...initialHandle, x: point.x, y: point.y }; + } + } + const changes = util.onHandleDrag?.(shape, { + handle: nextHandle, + isPrecise: this.isPrecise || altKey, + initial + }); + const next = { id: shape.id, type: shape.type, ...changes }; + if (initialHandle.type === "vertex" && this.editor.isShapeOfType(shape, "arrow")) { + const bindingAfter = getArrowBindings(editor, shape)[initialHandle.id]; + if (bindingAfter) { + if (hintingShapeIds[0] !== bindingAfter.toId) { + editor.setHintingShapes([bindingAfter.toId]); + this.pointingId = bindingAfter.toId; + this.isPrecise = pointerVelocity.len() < 0.5 || altKey; + this.isPreciseId = this.isPrecise ? bindingAfter.toId : null; + this.resetExactTimeout(); + } + } else { + if (hintingShapeIds.length > 0) { + editor.setHintingShapes([]); + this.pointingId = null; + this.isPrecise = false; + this.isPreciseId = null; + this.resetExactTimeout(); + } + } + } + if (changes) { + editor.updateShapes([next]); + } + } +} + +function getTextLabels(geometry) { + if (geometry.isLabel) { + return [geometry]; + } + if (geometry instanceof Group2d) { + return geometry.children.filter((child) => child.isLabel); + } + return []; +} + +class EditingShape extends StateNode { + static id = "editing_shape"; + hitShapeForPointerUp = null; + info = {}; + onEnter(info) { + const editingShape = this.editor.getEditingShape(); + if (!editingShape) throw Error("Entered editing state without an editing shape"); + this.hitShapeForPointerUp = null; + this.info = info; + if (info.isCreatingTextWhileToolLocked) { + this.parent.setCurrentToolIdMask("text"); + } + updateHoveredShapeId(this.editor); + this.editor.select(editingShape); + } + onExit() { + const { editingShapeId } = this.editor.getCurrentPageState(); + if (!editingShapeId) return; + this.editor.setEditingShape(null); + updateHoveredShapeId.cancel(); + const shape = this.editor.getShape(editingShapeId); + const util = this.editor.getShapeUtil(shape); + util.onEditEnd?.(shape); + if (this.info.isCreatingTextWhileToolLocked) { + this.parent.setCurrentToolIdMask(void 0); + this.editor.setCurrentTool("text", {}); + } + } + onPointerMove(info) { + if (this.hitShapeForPointerUp && this.editor.inputs.isDragging) { + if (this.editor.getIsReadonly()) return; + if (this.hitShapeForPointerUp.isLocked) return; + this.editor.select(this.hitShapeForPointerUp); + this.parent.transition("translating", info); + this.hitShapeForPointerUp = null; + return; + } + switch (info.target) { + case "shape": + case "canvas": { + updateHoveredShapeId(this.editor); + return; + } + } + } + onPointerDown(info) { + this.hitShapeForPointerUp = null; + switch (info.target) { + case "shape": { + const { shape: selectingShape } = info; + const editingShape = this.editor.getEditingShape(); + if (!editingShape) { + throw Error("Expected an editing shape!"); + } + const geometry = this.editor.getShapeUtil(selectingShape).getGeometry(selectingShape); + const textLabels = getTextLabels(geometry); + const textLabel = textLabels.length === 1 ? textLabels[0] : void 0; + const isEmptyTextShape = this.editor.isShapeOfType(editingShape, "text") && editingShape.props.text.trim() === ""; + if (textLabel && !isEmptyTextShape) { + const pointInShapeSpace = this.editor.getPointInShapeSpace( + selectingShape, + this.editor.inputs.currentPagePoint + ); + if (textLabel.bounds.containsPoint(pointInShapeSpace, 0) && textLabel.hitTestPoint(pointInShapeSpace)) { + if (selectingShape.id === editingShape.id) { + return; + } else { + this.hitShapeForPointerUp = selectingShape; + this.editor.markHistoryStoppingPoint("editing on pointer up"); + this.editor.select(selectingShape.id); + return; + } + } + } else { + if (selectingShape.id === editingShape.id) { + if (this.editor.isShapeOfType(selectingShape, "frame")) { + this.editor.setEditingShape(null); + this.parent.transition("idle", info); + } + } else { + this.parent.transition("pointing_shape", info); + return; + } + return; + } + break; + } + } + this.parent.transition("idle", info); + this.editor.root.handleEvent(info); + } + onPointerUp(info) { + const hitShape = this.hitShapeForPointerUp; + if (!hitShape) return; + this.hitShapeForPointerUp = null; + const util = this.editor.getShapeUtil(hitShape); + if (hitShape.isLocked) return; + if (this.editor.getIsReadonly()) { + if (!util.canEditInReadOnly(hitShape)) { + this.parent.transition("pointing_shape", info); + return; + } + } + this.editor.select(hitShape.id); + this.editor.setEditingShape(hitShape.id); + updateHoveredShapeId(this.editor); + } + onComplete(info) { + this.parent.transition("idle", info); + } + onCancel(info) { + this.parent.transition("idle", info); + } +} + +function getShouldEnterCropMode(editor) { + const onlySelectedShape = editor.getOnlySelectedShape(); + return !!(onlySelectedShape && !editor.isShapeOrAncestorLocked(onlySelectedShape) && editor.getShapeUtil(onlySelectedShape).canCrop(onlySelectedShape)); +} + +function selectOnCanvasPointerUp(editor, info) { + const selectedShapeIds = editor.getSelectedShapeIds(); + const { currentPagePoint } = editor.inputs; + const { shiftKey, altKey, accelKey } = info; + const additiveSelectionKey = shiftKey || accelKey; + const hitShape = editor.getShapeAtPoint(currentPagePoint, { + hitInside: false, + margin: editor.options.hitTestMargin / editor.getZoomLevel(), + hitLabels: true, + renderingOnly: true, + filter: (shape) => !shape.isLocked + }); + if (hitShape) { + const outermostSelectableShape = editor.getOutermostSelectableShape(hitShape); + if (additiveSelectionKey && !altKey) { + editor.cancelDoubleClick(); + if (selectedShapeIds.includes(outermostSelectableShape.id)) { + editor.markHistoryStoppingPoint("deselecting shape"); + editor.deselect(outermostSelectableShape); + } else { + editor.markHistoryStoppingPoint("shift selecting shape"); + editor.setSelectedShapes([...selectedShapeIds, outermostSelectableShape.id]); + } + } else { + let shapeToSelect = void 0; + if (outermostSelectableShape === hitShape) { + shapeToSelect = hitShape; + } else { + if (outermostSelectableShape.id === editor.getFocusedGroupId() || selectedShapeIds.includes(outermostSelectableShape.id)) { + shapeToSelect = hitShape; + } else { + shapeToSelect = outermostSelectableShape; + } + } + if (shapeToSelect && !selectedShapeIds.includes(shapeToSelect.id)) { + editor.markHistoryStoppingPoint("selecting shape"); + editor.select(shapeToSelect.id); + } + } + } else { + if (additiveSelectionKey) { + return; + } else { + if (selectedShapeIds.length > 0) { + editor.markHistoryStoppingPoint("selecting none"); + editor.selectNone(); + } + const focusedGroupId = editor.getFocusedGroupId(); + if (isShapeId(focusedGroupId)) { + const groupShape = editor.getShape(focusedGroupId); + if (!editor.isPointInShape(groupShape, currentPagePoint, { margin: 0, hitInside: true })) { + editor.setFocusedGroup(null); + } + } + } + } +} + +const SKIPPED_KEYS_FOR_AUTO_EDITING = [ + "Delete", + "Backspace", + "[", + "]", + "Enter", + " ", + "Shift", + "Tab" +]; +let Idle$1 = class Idle extends StateNode { + static id = "idle"; + onEnter() { + this.parent.setCurrentToolIdMask(void 0); + updateHoveredShapeId(this.editor); + this.editor.setCursor({ type: "default", rotation: 0 }); + } + onExit() { + updateHoveredShapeId.cancel(); + } + onPointerMove() { + updateHoveredShapeId(this.editor); + } + onPointerDown(info) { + const shouldEnterCropMode = info.ctrlKey && getShouldEnterCropMode(this.editor); + switch (info.target) { + case "canvas": { + const hitShape = getHitShapeOnCanvasPointerDown(this.editor); + if (hitShape && !hitShape.isLocked) { + this.onPointerDown({ + ...info, + shape: hitShape, + target: "shape" + }); + return; + } + const selectedShapeIds = this.editor.getSelectedShapeIds(); + const onlySelectedShape = this.editor.getOnlySelectedShape(); + const { + inputs: { currentPagePoint } + } = this.editor; + if (selectedShapeIds.length > 1 || onlySelectedShape && !this.editor.getShapeUtil(onlySelectedShape).hideSelectionBoundsBg(onlySelectedShape)) { + if (isPointInRotatedSelectionBounds(this.editor, currentPagePoint)) { + this.onPointerDown({ + ...info, + target: "selection" + }); + return; + } + } + this.parent.transition("pointing_canvas", info); + break; + } + case "shape": { + const { shape } = info; + if (this.isOverArrowLabelTest(shape)) { + this.parent.transition("pointing_arrow_label", info); + break; + } + if (this.editor.isShapeOrAncestorLocked(shape)) { + this.parent.transition("pointing_canvas", info); + break; + } + this.parent.transition("pointing_shape", info); + break; + } + case "handle": { + if (this.editor.getIsReadonly()) break; + if (this.editor.inputs.altKey) { + this.parent.transition("pointing_shape", info); + } else { + this.parent.transition("pointing_handle", info); + } + break; + } + case "selection": { + switch (info.handle) { + case "mobile_rotate": + case "top_left_rotate": + case "top_right_rotate": + case "bottom_left_rotate": + case "bottom_right_rotate": { + if (info.accelKey) { + this.parent.transition("brushing", info); + break; + } + this.parent.transition("pointing_rotate_handle", info); + break; + } + case "top": + case "right": + case "bottom": + case "left": + case "top_left": + case "top_right": + case "bottom_left": + case "bottom_right": { + if (shouldEnterCropMode) { + this.parent.transition("crop.pointing_crop_handle", info); + } else { + if (info.accelKey) { + this.parent.transition("brushing", info); + break; + } + this.parent.transition("pointing_resize_handle", info); + } + break; + } + default: { + const hoveredShape = this.editor.getHoveredShape(); + if (hoveredShape && !this.editor.getSelectedShapeIds().includes(hoveredShape.id) && !hoveredShape.isLocked) { + this.onPointerDown({ + ...info, + shape: hoveredShape, + target: "shape" + }); + return; + } + this.parent.transition("pointing_selection", info); + } + } + break; + } + } + } + onDoubleClick(info) { + if (this.editor.inputs.shiftKey || info.phase !== "up") return; + if (info.ctrlKey || info.shiftKey) return; + switch (info.target) { + case "canvas": { + const hoveredShape = this.editor.getHoveredShape(); + const hitShape = hoveredShape && !this.editor.isShapeOfType(hoveredShape, "group") ? hoveredShape : this.editor.getSelectedShapeAtPoint(this.editor.inputs.currentPagePoint) ?? this.editor.getShapeAtPoint(this.editor.inputs.currentPagePoint, { + margin: this.editor.options.hitTestMargin / this.editor.getZoomLevel(), + hitInside: false + }); + const focusedGroupId = this.editor.getFocusedGroupId(); + if (hitShape) { + if (this.editor.isShapeOfType(hitShape, "group")) { + selectOnCanvasPointerUp(this.editor, info); + return; + } else { + const parent = this.editor.getShape(hitShape.parentId); + if (parent && this.editor.isShapeOfType(parent, "group")) { + if (focusedGroupId && parent.id === focusedGroupId) ; else { + selectOnCanvasPointerUp(this.editor, info); + return; + } + } + } + this.onDoubleClick({ + ...info, + shape: hitShape, + target: "shape" + }); + return; + } + if (!this.editor.inputs.shiftKey) { + this.handleDoubleClickOnCanvas(info); + } + break; + } + case "selection": { + if (this.editor.getIsReadonly()) break; + const onlySelectedShape = this.editor.getOnlySelectedShape(); + if (onlySelectedShape) { + const util = this.editor.getShapeUtil(onlySelectedShape); + if (!this.canInteractWithShapeInReadOnly(onlySelectedShape)) { + return; + } + if (info.handle === "right" || info.handle === "left" || info.handle === "top" || info.handle === "bottom") { + const change = util.onDoubleClickEdge?.(onlySelectedShape); + if (change) { + this.editor.markHistoryStoppingPoint("double click edge"); + this.editor.updateShapes([change]); + kickoutOccludedShapes(this.editor, [onlySelectedShape.id]); + return; + } + } + if (util.canCrop(onlySelectedShape) && !this.editor.isShapeOrAncestorLocked(onlySelectedShape)) { + this.parent.transition("crop", info); + return; + } + if (this.shouldStartEditingShape(onlySelectedShape)) { + this.startEditingShape( + onlySelectedShape, + info, + true + /* select all */ + ); + } + } + break; + } + case "shape": { + const { shape } = info; + const util = this.editor.getShapeUtil(shape); + if (shape.type !== "video" && shape.type !== "embed" && this.editor.getIsReadonly()) break; + if (util.onDoubleClick) { + const change = util.onDoubleClick?.(shape); + if (change) { + this.editor.updateShapes([change]); + return; + } + } + if (util.canCrop(shape) && !this.editor.isShapeOrAncestorLocked(shape)) { + this.editor.markHistoryStoppingPoint("select and crop"); + this.editor.select(info.shape?.id); + this.parent.transition("crop", info); + return; + } + if (this.shouldStartEditingShape(shape)) { + this.startEditingShape( + shape, + info, + true + /* select all */ + ); + } else { + this.handleDoubleClickOnCanvas(info); + } + break; + } + case "handle": { + if (this.editor.getIsReadonly()) break; + const { shape, handle } = info; + const util = this.editor.getShapeUtil(shape); + const changes = util.onDoubleClickHandle?.(shape, handle); + if (changes) { + this.editor.updateShapes([changes]); + } else { + if (this.shouldStartEditingShape(shape)) { + this.startEditingShape( + shape, + info, + true + /* select all */ + ); + } + } + } + } + } + onRightClick(info) { + switch (info.target) { + case "canvas": { + const hoveredShape = this.editor.getHoveredShape(); + const hitShape = hoveredShape && !this.editor.isShapeOfType(hoveredShape, "group") ? hoveredShape : this.editor.getShapeAtPoint(this.editor.inputs.currentPagePoint, { + margin: this.editor.options.hitTestMargin / this.editor.getZoomLevel(), + hitInside: false, + hitLabels: true, + hitLocked: true, + hitFrameInside: true, + renderingOnly: true + }); + if (hitShape) { + this.onRightClick({ + ...info, + shape: hitShape, + target: "shape" + }); + return; + } + const selectedShapeIds = this.editor.getSelectedShapeIds(); + const onlySelectedShape = this.editor.getOnlySelectedShape(); + const { + inputs: { currentPagePoint } + } = this.editor; + if (selectedShapeIds.length > 1 || onlySelectedShape && !this.editor.getShapeUtil(onlySelectedShape).hideSelectionBoundsBg(onlySelectedShape)) { + if (isPointInRotatedSelectionBounds(this.editor, currentPagePoint)) { + this.onRightClick({ + ...info, + target: "selection" + }); + return; + } + } + this.editor.selectNone(); + break; + } + case "shape": { + const { selectedShapeIds } = this.editor.getCurrentPageState(); + const { shape } = info; + const targetShape = this.editor.getOutermostSelectableShape( + shape, + (parent) => !selectedShapeIds.includes(parent.id) + ); + if (!selectedShapeIds.includes(targetShape.id) && !this.editor.findShapeAncestor( + targetShape, + (shape2) => selectedShapeIds.includes(shape2.id) + )) { + this.editor.markHistoryStoppingPoint("selecting shape"); + this.editor.setSelectedShapes([targetShape.id]); + } + break; + } + } + } + onCancel() { + if (this.editor.getFocusedGroupId() !== this.editor.getCurrentPageId() && this.editor.getSelectedShapeIds().length > 0) { + this.editor.popFocusedGroupId(); + } else { + this.editor.markHistoryStoppingPoint("clearing selection"); + this.editor.selectNone(); + } + } + onKeyDown(info) { + switch (info.code) { + case "ArrowLeft": + case "ArrowRight": + case "ArrowUp": + case "ArrowDown": { + this.nudgeSelectedShapes(false); + return; + } + } + if (debugFlags["editOnType"].get()) { + if (!SKIPPED_KEYS_FOR_AUTO_EDITING.includes(info.key) && !info.altKey && !info.ctrlKey) { + const onlySelectedShape = this.editor.getOnlySelectedShape(); + if (onlySelectedShape && // If it's a note shape, then edit on type + this.editor.isShapeOfType(onlySelectedShape, "note") && // If it's not locked or anything + this.shouldStartEditingShape(onlySelectedShape)) { + this.startEditingShape( + onlySelectedShape, + { + ...info, + target: "shape", + shape: onlySelectedShape + }, + true + /* select all */ + ); + return; + } + } + } + } + onKeyRepeat(info) { + switch (info.code) { + case "ArrowLeft": + case "ArrowRight": + case "ArrowUp": + case "ArrowDown": { + this.nudgeSelectedShapes(true); + break; + } + } + } + onKeyUp(info) { + switch (info.code) { + case "Enter": { + const selectedShapes = this.editor.getSelectedShapes(); + if (selectedShapes.every((shape) => this.editor.isShapeOfType(shape, "group"))) { + this.editor.setSelectedShapes( + selectedShapes.flatMap((shape) => this.editor.getSortedChildIdsForParent(shape.id)) + ); + return; + } + const onlySelectedShape = this.editor.getOnlySelectedShape(); + if (onlySelectedShape && this.shouldStartEditingShape(onlySelectedShape)) { + this.startEditingShape( + onlySelectedShape, + { + ...info, + target: "shape", + shape: onlySelectedShape + }, + true + /* select all */ + ); + return; + } + if (getShouldEnterCropMode(this.editor)) { + this.parent.transition("crop", info); + } + break; + } + } + } + shouldStartEditingShape(shape = this.editor.getOnlySelectedShape()) { + if (!shape) return false; + if (this.editor.isShapeOrAncestorLocked(shape) && shape.type !== "embed") return false; + if (!this.canInteractWithShapeInReadOnly(shape)) return false; + return this.editor.getShapeUtil(shape).canEdit(shape); + } + startEditingShape(shape, info, shouldSelectAll) { + if (this.editor.isShapeOrAncestorLocked(shape) && shape.type !== "embed") return; + this.editor.markHistoryStoppingPoint("editing shape"); + startEditingShapeWithLabel(this.editor, shape, shouldSelectAll); + this.parent.transition("editing_shape", info); + } + isOverArrowLabelTest(shape) { + if (!shape) return false; + const pointInShapeSpace = this.editor.getPointInShapeSpace( + shape, + this.editor.inputs.currentPagePoint + ); + if (this.editor.isShapeOfType(shape, "arrow")) { + const labelGeometry = this.editor.getShapeGeometry(shape).children[1]; + if (labelGeometry && pointInPolygon(pointInShapeSpace, labelGeometry.vertices)) { + return true; + } + } + return false; + } + handleDoubleClickOnCanvas(info) { + if (this.editor.getIsReadonly()) return; + if (!this.editor.options.createTextOnCanvasDoubleClick) return; + this.editor.markHistoryStoppingPoint("creating text shape"); + const id = createShapeId(); + const { x, y } = this.editor.inputs.currentPagePoint; + this.editor.createShapes([ + { + id, + type: "text", + x, + y, + props: { + text: "", + autoSize: true + } + } + ]); + const shape = this.editor.getShape(id); + if (!shape) return; + const util = this.editor.getShapeUtil(shape); + if (this.editor.getIsReadonly()) { + if (!util.canEditInReadOnly(shape)) { + return; + } + } + this.editor.setEditingShape(id); + this.editor.select(id); + this.parent.transition("editing_shape", info); + } + nudgeSelectedShapes(ephemeral = false) { + const { + editor: { + inputs: { keys } + } + } = this; + const shiftKey = keys.has("ShiftLeft"); + const delta = new Vec(0, 0); + if (keys.has("ArrowLeft")) delta.x -= 1; + if (keys.has("ArrowRight")) delta.x += 1; + if (keys.has("ArrowUp")) delta.y -= 1; + if (keys.has("ArrowDown")) delta.y += 1; + if (delta.equals(new Vec(0, 0))) return; + if (!ephemeral) this.editor.markHistoryStoppingPoint("nudge shapes"); + const { gridSize } = this.editor.getDocumentSettings(); + const step = this.editor.getInstanceState().isGridMode ? shiftKey ? gridSize * GRID_INCREMENT : gridSize : shiftKey ? MAJOR_NUDGE_FACTOR : MINOR_NUDGE_FACTOR; + const selectedShapeIds = this.editor.getSelectedShapeIds(); + this.editor.nudgeShapes(selectedShapeIds, delta.mul(step)); + kickoutOccludedShapes(this.editor, selectedShapeIds); + } + canInteractWithShapeInReadOnly(shape) { + if (!this.editor.getIsReadonly()) return true; + const util = this.editor.getShapeUtil(shape); + if (util.canEditInReadOnly(shape)) return true; + return false; + } +}; +const MAJOR_NUDGE_FACTOR = 10; +const MINOR_NUDGE_FACTOR = 1; +const GRID_INCREMENT = 5; +function isPointInRotatedSelectionBounds(editor, point) { + const selectionBounds = editor.getSelectionRotatedPageBounds(); + if (!selectionBounds) return false; + const selectionRotation = editor.getSelectionRotation(); + if (!selectionRotation) return selectionBounds.containsPoint(point); + return pointInPolygon( + point, + selectionBounds.corners.map((c) => Vec.RotWith(c, selectionBounds.point, selectionRotation)) + ); +} + +class PointingArrowLabel extends StateNode { + static id = "pointing_arrow_label"; + shapeId = ""; + markId = ""; + wasAlreadySelected = false; + didDrag = false; + didCtrlOnEnter = false; + info = {}; + updateCursor() { + this.editor.setCursor({ type: "grabbing", rotation: 0 }); + } + onEnter(info) { + const { shape } = info; + this.parent.setCurrentToolIdMask(info.onInteractionEnd); + this.info = info; + this.shapeId = shape.id; + this.didDrag = false; + this.didCtrlOnEnter = info.accelKey; + this.wasAlreadySelected = this.editor.getOnlySelectedShapeId() === shape.id; + this.updateCursor(); + const geometry = this.editor.getShapeGeometry(shape); + const labelGeometry = geometry.children[1]; + if (!labelGeometry) { + throw Error(`Expected to find an arrow label geometry for shape: ${shape.id}`); + } + const { currentPagePoint } = this.editor.inputs; + const pointInShapeSpace = this.editor.getPointInShapeSpace(shape, currentPagePoint); + this._labelDragOffset = Vec.Sub(labelGeometry.center, pointInShapeSpace); + this.markId = this.editor.markHistoryStoppingPoint("label-drag start"); + this.editor.setSelectedShapes([this.shapeId]); + } + onExit() { + this.parent.setCurrentToolIdMask(void 0); + this.editor.setCursor({ type: "default", rotation: 0 }); + } + _labelDragOffset = new Vec(0, 0); + onPointerMove() { + const { isDragging } = this.editor.inputs; + if (!isDragging) return; + if (this.didCtrlOnEnter) { + this.parent.transition("brushing", this.info); + return; + } + const shape = this.editor.getShape(this.shapeId); + if (!shape) return; + const info = getArrowInfo(this.editor, shape); + const groupGeometry = this.editor.getShapeGeometry(shape); + const bodyGeometry = groupGeometry.children[0]; + const pointInShapeSpace = this.editor.getPointInShapeSpace( + shape, + this.editor.inputs.currentPagePoint + ); + const nearestPoint = bodyGeometry.nearestPoint( + Vec.Add(pointInShapeSpace, this._labelDragOffset) + ); + let nextLabelPosition; + if (info.isStraight) { + const lineLength = Vec.Dist(info.start.point, info.end.point); + const segmentLength = Vec.Dist(info.end.point, nearestPoint); + nextLabelPosition = 1 - segmentLength / lineLength; + } else { + const { _center, measure, angleEnd, angleStart } = groupGeometry.children[0]; + nextLabelPosition = getPointInArcT(measure, angleStart, angleEnd, _center.angle(nearestPoint)); + } + if (isNaN(nextLabelPosition)) { + nextLabelPosition = 0.5; + } + this.didDrag = true; + this.editor.updateShape({ + id: shape.id, + type: shape.type, + props: { labelPosition: nextLabelPosition } + }); + } + onPointerUp() { + const shape = this.editor.getShape(this.shapeId); + if (!shape) return; + if (this.didDrag || !this.wasAlreadySelected) { + this.complete(); + } else if (!this.editor.getIsReadonly()) { + this.editor.setEditingShape(shape.id); + this.editor.setCurrentTool("select.editing_shape"); + } + } + onCancel() { + this.cancel(); + } + onComplete() { + this.cancel(); + } + onInterrupt() { + this.cancel(); + } + complete() { + if (this.info.onInteractionEnd) { + this.editor.setCurrentTool(this.info.onInteractionEnd, {}); + } else { + this.parent.transition("idle"); + } + } + cancel() { + this.editor.bailToMark(this.markId); + if (this.info.onInteractionEnd) { + this.editor.setCurrentTool(this.info.onInteractionEnd, {}); + } else { + this.parent.transition("idle"); + } + } +} + +class PointingCanvas extends StateNode { + static id = "pointing_canvas"; + onEnter(info) { + const additiveSelectionKey = info.shiftKey || info.accelKey; + if (!additiveSelectionKey) { + if (this.editor.getSelectedShapeIds().length > 0) { + this.editor.markHistoryStoppingPoint("selecting none"); + this.editor.selectNone(); + } + } + } + onPointerMove(info) { + if (this.editor.inputs.isDragging) { + this.parent.transition("brushing", info); + } + } + onPointerUp(info) { + selectOnCanvasPointerUp(this.editor, info); + this.complete(); + } + onComplete() { + this.complete(); + } + onInterrupt() { + this.parent.transition("idle"); + } + complete() { + this.parent.transition("idle"); + } +} + +class PointingHandle extends StateNode { + static id = "pointing_handle"; + didCtrlOnEnter = false; + info = {}; + onEnter(info) { + this.info = info; + this.didCtrlOnEnter = info.accelKey; + const { shape } = info; + if (this.editor.isShapeOfType(shape, "arrow")) { + const initialBinding = getArrowBindings(this.editor, shape)[info.handle.id]; + if (initialBinding) { + this.editor.setHintingShapes([initialBinding.toId]); + } + } + this.editor.setCursor({ type: "grabbing", rotation: 0 }); + } + onExit() { + this.editor.setHintingShapes([]); + this.editor.setCursor({ type: "default", rotation: 0 }); + } + onPointerUp() { + const { shape, handle } = this.info; + if (this.editor.isShapeOfType(shape, "note")) { + const { editor } = this; + const nextNote = getNoteForPit(editor, shape, handle, false); + if (nextNote) { + startEditingShapeWithLabel( + editor, + nextNote, + true + /* selectAll */ + ); + return; + } + } + this.parent.transition("idle", this.info); + } + onPointerMove(info) { + const { editor } = this; + if (editor.inputs.isDragging) { + if (this.didCtrlOnEnter) { + this.parent.transition("brushing", info); + } else { + this.startDraggingHandle(); + } + } + } + onLongPress() { + this.startDraggingHandle(); + } + startDraggingHandle() { + const { editor } = this; + if (editor.getIsReadonly()) return; + const { shape, handle } = this.info; + if (editor.isShapeOfType(shape, "note")) { + const nextNote = getNoteForPit(editor, shape, handle, true); + if (nextNote) { + const centeredOnPointer = editor.getPointInParentSpace(nextNote, editor.inputs.originPagePoint).sub(Vec.Rot(NOTE_CENTER_OFFSET.clone().mul(shape.props.scale), nextNote.rotation)); + editor.updateShape({ ...nextNote, x: centeredOnPointer.x, y: centeredOnPointer.y }); + editor.setHoveredShape(nextNote.id).select(nextNote.id).setCurrentTool("select.translating", { + ...this.info, + target: "shape", + shape: editor.getShape(nextNote), + onInteractionEnd: "note", + isCreating: true, + onCreate: () => { + startEditingShapeWithLabel( + editor, + nextNote, + true + /* selectAll */ + ); + } + }); + return; + } + } + this.parent.transition("dragging_handle", this.info); + } + onCancel() { + this.cancel(); + } + onComplete() { + this.cancel(); + } + onInterrupt() { + this.cancel(); + } + cancel() { + this.parent.transition("idle"); + } +} +function getNoteForPit(editor, shape, handle, forceNew) { + const pageTransform = editor.getShapePageTransform(shape.id); + const pagePoint = pageTransform.point(); + const pageRotation = pageTransform.rotation(); + const pits = getNoteAdjacentPositions( + editor, + pagePoint, + pageRotation, + shape.props.growY, + 0, + shape.props.scale + ); + const pit = pits[handle.index]; + if (pit) { + return getNoteShapeForAdjacentPosition(editor, shape, pit, pageRotation, forceNew); + } +} + +class PointingRotateHandle extends StateNode { + static id = "pointing_rotate_handle"; + info = {}; + updateCursor() { + this.editor.setCursor({ + type: CursorTypeMap[this.info.handle], + rotation: this.editor.getSelectionRotation() + }); + } + onEnter(info) { + this.parent.setCurrentToolIdMask(info.onInteractionEnd); + this.info = info; + this.updateCursor(); + } + onExit() { + this.parent.setCurrentToolIdMask(void 0); + this.editor.setCursor({ type: "default", rotation: 0 }); + } + onPointerMove() { + if (this.editor.inputs.isDragging) { + this.startRotating(); + } + } + onLongPress() { + this.startRotating(); + } + startRotating() { + if (this.editor.getIsReadonly()) return; + this.parent.transition("rotating", this.info); + } + onPointerUp() { + this.complete(); + } + onCancel() { + this.cancel(); + } + onComplete() { + this.cancel(); + } + onInterrupt() { + this.cancel(); + } + complete() { + if (this.info.onInteractionEnd) { + this.editor.setCurrentTool(this.info.onInteractionEnd, {}); + } else { + this.parent.transition("idle"); + } + } + cancel() { + if (this.info.onInteractionEnd) { + this.editor.setCurrentTool(this.info.onInteractionEnd, {}); + } else { + this.parent.transition("idle"); + } + } +} + +class PointingSelection extends StateNode { + static id = "pointing_selection"; + info = {}; + onEnter(info) { + this.info = info; + } + onPointerUp(info) { + selectOnCanvasPointerUp(this.editor, info); + this.parent.transition("idle", info); + } + onPointerMove(info) { + if (this.editor.inputs.isDragging) { + this.startTranslating(info); + } + } + onLongPress(info) { + this.startTranslating(info); + } + startTranslating(info) { + if (this.editor.getIsReadonly()) return; + this.parent.transition("translating", info); + } + onDoubleClick(info) { + const hoveredShape = this.editor.getHoveredShape(); + const hitShape = hoveredShape && !this.editor.isShapeOfType(hoveredShape, "group") ? hoveredShape : this.editor.getShapeAtPoint(this.editor.inputs.currentPagePoint, { + hitInside: true, + margin: 0, + renderingOnly: true + }); + if (hitShape) { + this.parent.transition("idle"); + this.parent.onDoubleClick?.({ + ...info, + target: "shape", + shape: this.editor.getShape(hitShape) + }); + return; + } + } + onCancel() { + this.cancel(); + } + onComplete() { + this.cancel(); + } + onInterrupt() { + this.cancel(); + } + cancel() { + this.parent.transition("idle"); + } +} + +class PointingShape extends StateNode { + static id = "pointing_shape"; + hitShape = {}; + hitShapeForPointerUp = {}; + isDoubleClick = false; + didCtrlOnEnter = false; + didSelectOnEnter = false; + onEnter(info) { + const selectedShapeIds = this.editor.getSelectedShapeIds(); + const selectionBounds = this.editor.getSelectionRotatedPageBounds(); + const focusedGroupId = this.editor.getFocusedGroupId(); + const { + inputs: { currentPagePoint } + } = this.editor; + const { shiftKey, altKey, accelKey } = info; + this.hitShape = info.shape; + this.isDoubleClick = false; + this.didCtrlOnEnter = accelKey; + const outermostSelectingShape = this.editor.getOutermostSelectableShape(info.shape); + const selectedAncestor = this.editor.findShapeAncestor( + outermostSelectingShape, + (parent) => selectedShapeIds.includes(parent.id) + ); + if (this.didCtrlOnEnter || // If the shape has an onClick handler + this.editor.getShapeUtil(info.shape).onClick || // ...or if the shape is the focused layer (e.g. group) + outermostSelectingShape.id === focusedGroupId || // ...or if the shape is within the selection + selectedShapeIds.includes(outermostSelectingShape.id) || // ...or if an ancestor of the shape is selected + selectedAncestor || // ...or if the current point is NOT within the selection bounds + selectedShapeIds.length > 1 && selectionBounds?.containsPoint(currentPagePoint)) { + this.didSelectOnEnter = false; + this.hitShapeForPointerUp = outermostSelectingShape; + return; + } + this.didSelectOnEnter = true; + if (shiftKey && !altKey) { + this.editor.cancelDoubleClick(); + if (!selectedShapeIds.includes(outermostSelectingShape.id)) { + this.editor.markHistoryStoppingPoint("shift selecting shape"); + this.editor.setSelectedShapes([...selectedShapeIds, outermostSelectingShape.id]); + } + } else { + this.editor.markHistoryStoppingPoint("selecting shape"); + this.editor.setSelectedShapes([outermostSelectingShape.id]); + } + } + onPointerUp(info) { + const selectedShapeIds = this.editor.getSelectedShapeIds(); + const focusedGroupId = this.editor.getFocusedGroupId(); + const zoomLevel = this.editor.getZoomLevel(); + const { + inputs: { currentPagePoint } + } = this.editor; + const additiveSelectionKey = info.shiftKey || info.accelKey; + const hitShape = this.editor.getShapeAtPoint(currentPagePoint, { + margin: this.editor.options.hitTestMargin / zoomLevel, + hitInside: true, + renderingOnly: true + }) ?? this.hitShape; + const selectingShape = hitShape ? this.editor.getOutermostSelectableShape(hitShape) : this.hitShapeForPointerUp; + if (selectingShape) { + const util = this.editor.getShapeUtil(selectingShape); + if (util.onClick) { + const change = util.onClick?.(selectingShape); + if (change) { + this.editor.markHistoryStoppingPoint("shape on click"); + this.editor.updateShapes([change]); + this.parent.transition("idle", info); + return; + } + } + if (selectingShape.id === focusedGroupId) { + if (selectedShapeIds.length > 0) { + this.editor.markHistoryStoppingPoint("clearing shape ids"); + this.editor.setSelectedShapes([]); + } else { + this.editor.popFocusedGroupId(); + } + this.parent.transition("idle", info); + return; + } + } + if (!this.didSelectOnEnter) { + const outermostSelectableShape = this.editor.getOutermostSelectableShape( + hitShape, + // if a group is selected, we want to stop before reaching that group + // so we can drill down into the group + (parent) => !selectedShapeIds.includes(parent.id) + ); + if (selectedShapeIds.includes(outermostSelectableShape.id)) { + if (additiveSelectionKey) { + this.editor.markHistoryStoppingPoint("deselecting on pointer up"); + this.editor.deselect(selectingShape); + } else { + if (selectedShapeIds.includes(selectingShape.id)) { + if (selectedShapeIds.length === 1) { + const geometry = this.editor.getShapeUtil(selectingShape).getGeometry(selectingShape); + const textLabels = getTextLabels(geometry); + const textLabel = textLabels.length === 1 ? textLabels[0] : void 0; + if (textLabel) { + const pointInShapeSpace = this.editor.getPointInShapeSpace( + selectingShape, + currentPagePoint + ); + if (textLabel.bounds.containsPoint(pointInShapeSpace, 0) && textLabel.hitTestPoint(pointInShapeSpace)) { + this.editor.run(() => { + this.editor.markHistoryStoppingPoint("editing on pointer up"); + this.editor.select(selectingShape.id); + const util = this.editor.getShapeUtil(selectingShape); + if (this.editor.getIsReadonly()) { + if (!util.canEditInReadOnly(selectingShape)) { + return; + } + } + this.editor.setEditingShape(selectingShape.id); + this.editor.setCurrentTool("select.editing_shape"); + if (this.isDoubleClick) { + this.editor.emit("select-all-text", { shapeId: selectingShape.id }); + } + }); + return; + } + } + } + this.editor.markHistoryStoppingPoint("selecting on pointer up"); + this.editor.select(selectingShape.id); + } else { + this.editor.markHistoryStoppingPoint("selecting on pointer up"); + this.editor.select(selectingShape); + } + } + } else if (additiveSelectionKey) { + const ancestors = this.editor.getShapeAncestors(outermostSelectableShape); + this.editor.markHistoryStoppingPoint("shift deselecting on pointer up"); + this.editor.setSelectedShapes([ + ...this.editor.getSelectedShapeIds().filter((id) => !ancestors.find((a) => a.id === id)), + outermostSelectableShape.id + ]); + } else { + this.editor.markHistoryStoppingPoint("selecting on pointer up"); + this.editor.setSelectedShapes([outermostSelectableShape.id]); + } + } + this.parent.transition("idle", info); + } + onDoubleClick() { + this.isDoubleClick = true; + } + onPointerMove(info) { + if (this.editor.inputs.isDragging) { + if (this.didCtrlOnEnter) { + this.parent.transition("brushing", info); + } else { + this.startTranslating(info); + } + } + } + onLongPress(info) { + this.startTranslating(info); + } + startTranslating(info) { + if (this.editor.getIsReadonly()) return; + this.editor.focus(); + this.parent.transition("translating", info); + } + onCancel() { + this.cancel(); + } + onComplete() { + this.cancel(); + } + onInterrupt() { + this.cancel(); + } + cancel() { + this.parent.transition("idle"); + } +} + +class Resizing extends StateNode { + static id = "resizing"; + info = {}; + markId = ""; + // A switch to detect when the user is holding ctrl + didHoldCommand = false; + // we transition into the resizing state from the geo pointing state, which starts with a shape of size w: 1, h: 1, + // so if the user drags x: +50, y: +50 after mouseDown, the shape will be w: 51, h: 51, which is too many pixels, alas + // so we allow passing a further offset into this state to negate such issues + creationCursorOffset = { x: 0, y: 0 }; + snapshot = {}; + onEnter(info) { + const { isCreating = false, creatingMarkId, creationCursorOffset = { x: 0, y: 0 } } = info; + this.info = info; + this.didHoldCommand = false; + this.parent.setCurrentToolIdMask(info.onInteractionEnd); + this.creationCursorOffset = creationCursorOffset; + this.snapshot = this._createSnapshot(); + this.markId = ""; + if (isCreating) { + if (creatingMarkId) { + this.markId = creatingMarkId; + } else { + const markId = this.editor.getMarkIdMatching( + `creating:${this.editor.getOnlySelectedShapeId()}` + ); + if (markId) { + this.markId = markId; + } + } + } else { + this.markId = this.editor.markHistoryStoppingPoint("starting resizing"); + } + if (isCreating) { + this.editor.setCursor({ type: "cross", rotation: 0 }); + } + this.handleResizeStart(); + this.updateShapes(); + } + onTick({ elapsed }) { + const { editor } = this; + editor.edgeScrollManager.updateEdgeScrolling(elapsed); + } + onPointerMove() { + this.updateShapes(); + } + onKeyDown() { + this.updateShapes(); + } + onKeyUp() { + this.updateShapes(); + } + onPointerUp() { + this.complete(); + } + onComplete() { + this.complete(); + } + onCancel() { + this.cancel(); + } + cancel() { + this.editor.bailToMark(this.markId); + if (this.info.onInteractionEnd) { + this.editor.setCurrentTool(this.info.onInteractionEnd, {}); + } else { + this.parent.transition("idle"); + } + } + complete() { + kickoutOccludedShapes(this.editor, this.snapshot.selectedShapeIds); + this.handleResizeEnd(); + if (this.info.isCreating && this.info.onCreate) { + this.info.onCreate?.(this.editor.getOnlySelectedShape()); + return; + } + if (this.editor.getInstanceState().isToolLocked && this.info.onInteractionEnd) { + this.editor.setCurrentTool(this.info.onInteractionEnd, {}); + return; + } + this.parent.transition("idle"); + } + handleResizeStart() { + const { shapeSnapshots } = this.snapshot; + const changes = []; + shapeSnapshots.forEach(({ shape }) => { + const util = this.editor.getShapeUtil(shape); + const change = util.onResizeStart?.(shape); + if (change) { + changes.push(change); + } + }); + if (changes.length > 0) { + this.editor.updateShapes(changes); + } + } + handleResizeEnd() { + const { shapeSnapshots } = this.snapshot; + const changes = []; + shapeSnapshots.forEach(({ shape }) => { + const current = this.editor.getShape(shape.id); + const util = this.editor.getShapeUtil(shape); + const change = util.onResizeEnd?.(shape, current); + if (change) { + changes.push(change); + } + }); + if (changes.length > 0) { + this.editor.updateShapes(changes); + } + } + updateShapes() { + const { altKey, shiftKey } = this.editor.inputs; + const { + frames, + shapeSnapshots, + selectionBounds, + cursorHandleOffset, + selectedShapeIds, + selectionRotation, + canShapesDeform + } = this.snapshot; + let isAspectRatioLocked = shiftKey || !canShapesDeform; + if (shapeSnapshots.size === 1) { + const onlySnapshot = [...shapeSnapshots.values()][0]; + if (this.editor.isShapeOfType(onlySnapshot.shape, "text")) { + isAspectRatioLocked = !(this.info.handle === "left" || this.info.handle === "right"); + } + } + const { ctrlKey } = this.editor.inputs; + const currentPagePoint = this.editor.inputs.currentPagePoint.clone().sub(cursorHandleOffset).sub(this.creationCursorOffset); + const originPagePoint = this.editor.inputs.originPagePoint.clone().sub(cursorHandleOffset); + if (this.editor.getInstanceState().isGridMode && !ctrlKey) { + const { gridSize } = this.editor.getDocumentSettings(); + currentPagePoint.snapToGrid(gridSize); + } + const dragHandle = this.info.handle; + const scaleOriginHandle = rotateSelectionHandle(dragHandle, Math.PI); + this.editor.snaps.clearIndicators(); + const shouldSnap = this.editor.user.getIsSnapMode() ? !ctrlKey : ctrlKey; + if (shouldSnap && selectionRotation % HALF_PI === 0) { + const { nudge } = this.editor.snaps.shapeBounds.snapResizeShapes({ + dragDelta: Vec.Sub(currentPagePoint, originPagePoint), + initialSelectionPageBounds: this.snapshot.initialSelectionPageBounds, + handle: rotateSelectionHandle(dragHandle, selectionRotation), + isAspectRatioLocked, + isResizingFromCenter: altKey + }); + currentPagePoint.add(nudge); + } + const scaleOriginPage = Vec.RotWith( + altKey ? selectionBounds.center : selectionBounds.getHandlePoint(scaleOriginHandle), + selectionBounds.point, + selectionRotation + ); + const distanceFromScaleOriginNow = Vec.Sub(currentPagePoint, scaleOriginPage).rot( + -selectionRotation + ); + const distanceFromScaleOriginAtStart = Vec.Sub(originPagePoint, scaleOriginPage).rot( + -selectionRotation + ); + const scale = Vec.DivV(distanceFromScaleOriginNow, distanceFromScaleOriginAtStart); + if (!Number.isFinite(scale.x)) scale.x = 1; + if (!Number.isFinite(scale.y)) scale.y = 1; + const isXLocked = dragHandle === "top" || dragHandle === "bottom"; + const isYLocked = dragHandle === "left" || dragHandle === "right"; + if (isAspectRatioLocked) { + if (isYLocked) { + scale.y = Math.abs(scale.x); + } else if (isXLocked) { + scale.x = Math.abs(scale.y); + } else if (Math.abs(scale.x) > Math.abs(scale.y)) { + scale.y = Math.abs(scale.x) * (scale.y < 0 ? -1 : 1); + } else { + scale.x = Math.abs(scale.y) * (scale.x < 0 ? -1 : 1); + } + } else { + if (isXLocked) { + scale.x = 1; + } + if (isYLocked) { + scale.y = 1; + } + } + if (!this.info.isCreating) { + this.updateCursor({ + dragHandle, + isFlippedX: scale.x < 0, + isFlippedY: scale.y < 0, + rotation: selectionRotation + }); + } + for (const id of shapeSnapshots.keys()) { + const snapshot = shapeSnapshots.get(id); + this.editor.resizeShape(id, scale, { + initialShape: snapshot.shape, + initialBounds: snapshot.bounds, + initialPageTransform: snapshot.pageTransform, + dragHandle, + mode: selectedShapeIds.length === 1 && id === selectedShapeIds[0] ? "resize_bounds" : "scale_shape", + scaleOrigin: scaleOriginPage, + isAspectRatioLocked, + scaleAxisRotation: selectionRotation, + skipStartAndEndCallbacks: true + }); + } + if (this.editor.inputs.ctrlKey) { + this.didHoldCommand = true; + for (const { id, children } of frames) { + if (!children.length) continue; + const initial = shapeSnapshots.get(id).shape; + const current = this.editor.getShape(id); + if (!(initial && current)) continue; + const dx = current.x - initial.x; + const dy = current.y - initial.y; + const delta = new Vec(dx, dy).rot(-initial.rotation); + if (delta.x !== 0 || delta.y !== 0) { + for (const child of children) { + this.editor.updateShape({ + id: child.id, + type: child.type, + x: child.x - delta.x, + y: child.y - delta.y + }); + } + } + } + } else if (this.didHoldCommand) { + this.didHoldCommand = false; + for (const { children } of frames) { + if (!children.length) continue; + for (const child of children) { + this.editor.updateShape({ + id: child.id, + type: child.type, + x: child.x, + y: child.y + }); + } + } + } + } + // --- + updateCursor({ + dragHandle, + isFlippedX, + isFlippedY, + rotation + }) { + const nextCursor = { ...this.editor.getInstanceState().cursor }; + switch (dragHandle) { + case "top_left": + case "bottom_right": { + nextCursor.type = "nwse-resize"; + if (isFlippedX !== isFlippedY) { + nextCursor.type = "nesw-resize"; + } + break; + } + case "top_right": + case "bottom_left": { + nextCursor.type = "nesw-resize"; + if (isFlippedX !== isFlippedY) { + nextCursor.type = "nwse-resize"; + } + break; + } + } + nextCursor.rotation = rotation; + this.editor.setCursor(nextCursor); + } + onExit() { + this.parent.setCurrentToolIdMask(void 0); + this.editor.setCursor({ type: "default", rotation: 0 }); + this.editor.snaps.clearIndicators(); + } + _createSnapshot() { + const selectedShapeIds = this.editor.getSelectedShapeIds(); + const selectionRotation = this.editor.getSelectionRotation(); + const { + inputs: { originPagePoint } + } = this.editor; + const selectionBounds = this.editor.getSelectionRotatedPageBounds(); + const dragHandlePoint = Vec.RotWith( + selectionBounds.getHandlePoint(this.info.handle), + selectionBounds.point, + selectionRotation + ); + const cursorHandleOffset = Vec.Sub(originPagePoint, dragHandlePoint); + const shapeSnapshots = /* @__PURE__ */ new Map(); + const frames = []; + selectedShapeIds.forEach((id) => { + const shape = this.editor.getShape(id); + if (shape) { + if (shape.type === "frame") { + frames.push({ + id, + children: compact( + this.editor.getSortedChildIdsForParent(shape).map((id2) => this.editor.getShape(id2)) + ) + }); + } + shapeSnapshots.set(shape.id, this._createShapeSnapshot(shape)); + if (this.editor.isShapeOfType(shape, "frame") && selectedShapeIds.length === 1) + return; + this.editor.visitDescendants(shape.id, (descendantId) => { + const descendent = this.editor.getShape(descendantId); + if (descendent) { + shapeSnapshots.set(descendent.id, this._createShapeSnapshot(descendent)); + if (this.editor.isShapeOfType(descendent, "frame")) { + return false; + } + } + }); + } + }); + const canShapesDeform = ![...shapeSnapshots.values()].some( + (shape) => !areAnglesCompatible(shape.pageRotation, selectionRotation) || shape.isAspectRatioLocked + ); + return { + shapeSnapshots, + selectionBounds, + cursorHandleOffset, + selectionRotation, + selectedShapeIds, + canShapesDeform, + initialSelectionPageBounds: this.editor.getSelectionPageBounds(), + frames + }; + } + _createShapeSnapshot(shape) { + const pageTransform = this.editor.getShapePageTransform(shape); + const util = this.editor.getShapeUtil(shape); + return { + shape, + bounds: this.editor.getShapeGeometry(shape).bounds, + pageTransform, + pageRotation: Mat.Decompose(pageTransform).rotation, + isAspectRatioLocked: util.isAspectRatioLocked(shape) + }; + } +} +const ORDERED_SELECTION_HANDLES = [ + "top", + "top_right", + "right", + "bottom_right", + "bottom", + "bottom_left", + "left", + "top_left" +]; +function rotateSelectionHandle(handle, rotation) { + rotation = rotation % PI2; + const numSteps = Math.round(rotation / (PI$1 / 4)); + const currentIndex = ORDERED_SELECTION_HANDLES.indexOf(handle); + return ORDERED_SELECTION_HANDLES[(currentIndex + numSteps) % ORDERED_SELECTION_HANDLES.length]; +} + +const ONE_DEGREE = Math.PI / 180; +class Rotating extends StateNode { + static id = "rotating"; + snapshot = {}; + info = {}; + markId = ""; + onEnter(info) { + this.info = info; + this.parent.setCurrentToolIdMask(info.onInteractionEnd); + this.markId = this.editor.markHistoryStoppingPoint("rotate start"); + const snapshot = getRotationSnapshot({ + editor: this.editor, + ids: this.editor.getSelectedShapeIds() + }); + if (!snapshot) return this.parent.transition("idle", this.info); + this.snapshot = snapshot; + const newSelectionRotation = this._getRotationFromPointerPosition({ + snapToNearestDegree: false + }); + applyRotationToSnapshotShapes({ + editor: this.editor, + delta: this._getRotationFromPointerPosition({ snapToNearestDegree: false }), + snapshot: this.snapshot, + stage: "start" + }); + this.editor.setCursor({ + type: CursorTypeMap[this.info.handle], + rotation: newSelectionRotation + this.snapshot.initialShapesRotation + }); + } + onExit() { + this.editor.setCursor({ type: "default", rotation: 0 }); + this.parent.setCurrentToolIdMask(void 0); + this.snapshot = {}; + } + onPointerMove() { + this.update(); + } + onKeyDown() { + this.update(); + } + onKeyUp() { + this.update(); + } + onPointerUp() { + this.complete(); + } + onComplete() { + this.complete(); + } + onCancel() { + this.cancel(); + } + // --- + update() { + const newSelectionRotation = this._getRotationFromPointerPosition({ + snapToNearestDegree: false + }); + applyRotationToSnapshotShapes({ + editor: this.editor, + delta: newSelectionRotation, + snapshot: this.snapshot, + stage: "update" + }); + this.editor.setCursor({ + type: CursorTypeMap[this.info.handle], + rotation: newSelectionRotation + this.snapshot.initialShapesRotation + }); + } + cancel() { + this.editor.bailToMark(this.markId); + if (this.info.onInteractionEnd) { + this.editor.setCurrentTool(this.info.onInteractionEnd, this.info); + } else { + this.parent.transition("idle", this.info); + } + } + complete() { + applyRotationToSnapshotShapes({ + editor: this.editor, + delta: this._getRotationFromPointerPosition({ snapToNearestDegree: true }), + snapshot: this.snapshot, + stage: "end" + }); + kickoutOccludedShapes( + this.editor, + this.snapshot.shapeSnapshots.map((s) => s.shape.id) + ); + if (this.info.onInteractionEnd) { + this.editor.setCurrentTool(this.info.onInteractionEnd, this.info); + } else { + this.parent.transition("idle", this.info); + } + } + _getRotationFromPointerPosition({ snapToNearestDegree }) { + const selectionRotation = this.editor.getSelectionRotation(); + const selectionBounds = this.editor.getSelectionRotatedPageBounds(); + const { + inputs: { shiftKey, currentPagePoint } + } = this.editor; + const { initialCursorAngle, initialShapesRotation } = this.snapshot; + if (!selectionBounds) return initialShapesRotation; + const selectionPageCenter = selectionBounds.center.clone().rotWith(selectionBounds.point, selectionRotation); + const preSnapRotationDelta = selectionPageCenter.angle(currentPagePoint) - initialCursorAngle; + let newSelectionRotation = initialShapesRotation + preSnapRotationDelta; + if (shiftKey) { + newSelectionRotation = snapAngle(newSelectionRotation, 24); + } else if (snapToNearestDegree) { + newSelectionRotation = Math.round(newSelectionRotation / ONE_DEGREE) * ONE_DEGREE; + if (this.editor.getInstanceState().isCoarsePointer) { + const snappedToRightAngle = snapAngle(newSelectionRotation, 4); + const angleToRightAngle = shortAngleDist(newSelectionRotation, snappedToRightAngle); + if (Math.abs(angleToRightAngle) < degreesToRadians(5)) { + newSelectionRotation = snappedToRightAngle; + } + } + } + return newSelectionRotation - initialShapesRotation; + } +} + +class ScribbleBrushing extends StateNode { + static id = "scribble_brushing"; + hits = /* @__PURE__ */ new Set(); + size = 0; + scribbleId = "id"; + initialSelectedShapeIds = /* @__PURE__ */ new Set(); + newlySelectedShapeIds = /* @__PURE__ */ new Set(); + onEnter() { + this.initialSelectedShapeIds = new Set( + this.editor.inputs.shiftKey ? this.editor.getSelectedShapeIds() : [] + ); + this.newlySelectedShapeIds = /* @__PURE__ */ new Set(); + this.size = 0; + this.hits.clear(); + const scribbleItem = this.editor.scribbles.addScribble({ + color: "selection-stroke", + opacity: 0.32, + size: 12 + }); + this.scribbleId = scribbleItem.id; + this.updateScribbleSelection(true); + this.editor.updateInstanceState({ brush: null }); + } + onExit() { + this.editor.scribbles.stop(this.scribbleId); + } + onPointerMove() { + this.updateScribbleSelection(true); + } + onPointerUp() { + this.complete(); + } + onKeyDown() { + this.updateScribbleSelection(false); + } + onKeyUp() { + if (!this.editor.inputs.altKey) { + this.parent.transition("brushing"); + } else { + this.updateScribbleSelection(false); + } + } + onCancel() { + this.cancel(); + } + onComplete() { + this.complete(); + } + pushPointToScribble() { + const { x, y } = this.editor.inputs.currentPagePoint; + this.editor.scribbles.addPoint(this.scribbleId, x, y); + } + updateScribbleSelection(addPoint) { + const { editor } = this; + const currentPageShapes = this.editor.getCurrentPageRenderingShapesSorted(); + const { + inputs: { shiftKey, originPagePoint, previousPagePoint, currentPagePoint } + } = this.editor; + const { newlySelectedShapeIds, initialSelectedShapeIds } = this; + if (addPoint) { + this.pushPointToScribble(); + } + const shapes = currentPageShapes; + let shape, geometry, A, B; + const minDist = 0; + for (let i = 0, n = shapes.length; i < n; i++) { + shape = shapes[i]; + if (editor.isShapeOfType(shape, "group") || newlySelectedShapeIds.has(shape.id) || editor.isShapeOrAncestorLocked(shape)) { + continue; + } + geometry = editor.getShapeGeometry(shape); + if (editor.isShapeOfType(shape, "frame") && geometry.bounds.containsPoint(editor.getPointInShapeSpace(shape, originPagePoint))) { + continue; + } + const pageTransform = editor.getShapePageTransform(shape); + if (!geometry || !pageTransform) continue; + const pt = pageTransform.clone().invert(); + A = pt.applyToPoint(previousPagePoint); + B = pt.applyToPoint(currentPagePoint); + const { bounds } = geometry; + if (bounds.minX - minDist > Math.max(A.x, B.x) || bounds.minY - minDist > Math.max(A.y, B.y) || bounds.maxX + minDist < Math.min(A.x, B.x) || bounds.maxY + minDist < Math.min(A.y, B.y)) { + continue; + } + if (geometry.hitTestLineSegment(A, B, minDist)) { + const outermostShape = this.editor.getOutermostSelectableShape(shape); + const pageMask = this.editor.getShapeMask(outermostShape.id); + if (pageMask) { + const intersection = intersectLineSegmentPolygon( + previousPagePoint, + currentPagePoint, + pageMask + ); + if (intersection !== null) { + const isInMask = pointInPolygon(currentPagePoint, pageMask); + if (!isInMask) continue; + } + } + newlySelectedShapeIds.add(outermostShape.id); + } + } + const current = editor.getSelectedShapeIds(); + const next = new Set( + shiftKey ? [...newlySelectedShapeIds, ...initialSelectedShapeIds] : [...newlySelectedShapeIds] + ); + if (current.length !== next.size || current.some((id) => !next.has(id))) { + this.editor.setSelectedShapes(Array.from(next)); + } + } + complete() { + this.updateScribbleSelection(true); + this.parent.transition("idle"); + } + cancel() { + this.editor.setSelectedShapes([...this.initialSelectedShapeIds]); + this.parent.transition("idle"); + } +} + +var __create$2 = Object.create; +var __defProp$2 = Object.defineProperty; +var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor; +var __knownSymbol$2 = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name); +var __typeError$2 = (msg) => { + throw TypeError(msg); +}; +var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __decoratorStart$2 = (base) => [, , , __create$2(null)]; +var __decoratorStrings$2 = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"]; +var __expectFn$2 = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError$2("Function expected") : fn; +var __decoratorContext$2 = (kind, name, done, metadata, fns) => ({ kind: __decoratorStrings$2[kind], name, metadata, addInitializer: (fn) => done._ ? __typeError$2("Already initialized") : fns.push(__expectFn$2(fn || null)) }); +var __decoratorMetadata$2 = (array, target) => __defNormalProp$2(target, __knownSymbol$2("metadata"), array[3]); +var __runInitializers$2 = (array, flags, self, value) => { + for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) fns[i].call(self) ; + return value; +}; +var __decorateElement$2 = (array, flags, name, decorators, target, extra) => { + var it, done, ctx, access, k = flags & 7, s = false, p = false; + var j = 2 , key = __decoratorStrings$2[k + 5]; + var extraInitializers = array[j] || (array[j] = []); + var desc = ((target = target.prototype), __getOwnPropDesc$2(target , name)); + for (var i = decorators.length - 1; i >= 0; i--) { + ctx = __decoratorContext$2(k, name, done = {}, array[3], extraInitializers); + { + ctx.static = s, ctx.private = p, access = ctx.access = { has: (x) => name in x }; + access.get = (x) => x[name]; + } + it = (0, decorators[i])(desc[key] , ctx), done._ = 1; + __expectFn$2(it) && (desc[key] = it ); + } + return desc && __defProp$2(target, name, desc), target; +}; +var __publicField$2 = (obj, key, value) => __defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value); +var _dispose_dec, _init$2; +const INITIAL_POINTER_LAG_DURATION = 20; +const FAST_POINTER_LAG_DURATION = 100; +_dispose_dec = [bind$2]; +class DragAndDropManager { + constructor(editor) { + this.editor = editor; + __runInitializers$2(_init$2, 5, this); + __publicField$2(this, "prevDroppingShapeId", null); + __publicField$2(this, "droppingNodeTimer", null); + __publicField$2(this, "first", true); + editor.disposables.add(this.dispose); + } + updateDroppingNode(movingShapes, cb) { + if (this.first) { + this.editor.setHintingShapes( + movingShapes.map((s) => this.editor.findShapeAncestor(s, (v) => v.type !== "group")).filter((s) => s) + ); + this.prevDroppingShapeId = this.editor.getDroppingOverShape(this.editor.inputs.originPagePoint, movingShapes)?.id ?? null; + this.first = false; + } + if (this.droppingNodeTimer === null) { + this.setDragTimer(movingShapes, INITIAL_POINTER_LAG_DURATION, cb); + } else if (this.editor.inputs.pointerVelocity.len() > 0.5) { + clearTimeout(this.droppingNodeTimer); + this.setDragTimer(movingShapes, FAST_POINTER_LAG_DURATION, cb); + } + } + setDragTimer(movingShapes, duration, cb) { + this.droppingNodeTimer = this.editor.timers.setTimeout(() => { + this.editor.run(() => { + this.handleDrag(this.editor.inputs.currentPagePoint, movingShapes, cb); + }); + this.droppingNodeTimer = null; + }, duration); + } + handleDrag(point, movingShapes, cb) { + movingShapes = compact(movingShapes.map((shape) => this.editor.getShape(shape.id))); + const nextDroppingShapeId = this.editor.getDroppingOverShape(point, movingShapes)?.id ?? null; + if (nextDroppingShapeId === this.prevDroppingShapeId) { + this.hintParents(movingShapes); + return; + } + const { prevDroppingShapeId } = this; + const prevDroppingShape = prevDroppingShapeId && this.editor.getShape(prevDroppingShapeId); + const nextDroppingShape = nextDroppingShapeId && this.editor.getShape(nextDroppingShapeId); + if (prevDroppingShape) { + this.editor.getShapeUtil(prevDroppingShape).onDragShapesOut?.(prevDroppingShape, movingShapes); + } + if (nextDroppingShape) { + this.editor.getShapeUtil(nextDroppingShape).onDragShapesOver?.(nextDroppingShape, movingShapes); + } + this.hintParents(movingShapes); + cb?.(); + this.prevDroppingShapeId = nextDroppingShapeId; + } + hintParents(movingShapes) { + const shapesGroupedByAncestor = /* @__PURE__ */ new Map(); + for (const shape of movingShapes) { + const ancestor = this.editor.findShapeAncestor(shape, (v) => v.type !== "group"); + if (!ancestor) continue; + if (!shapesGroupedByAncestor.has(ancestor.id)) { + shapesGroupedByAncestor.set(ancestor.id, []); + } + shapesGroupedByAncestor.get(ancestor.id).push(shape.id); + } + const hintingShapes = []; + for (const [ancestorId, shapeIds] of shapesGroupedByAncestor) { + const ancestor = this.editor.getShape(ancestorId); + if (!ancestor) continue; + if (getOccludedChildren(this.editor, ancestor).length < shapeIds.length) { + hintingShapes.push(ancestor.id); + } + } + this.editor.setHintingShapes(hintingShapes); + } + dropShapes(shapes) { + const { prevDroppingShapeId } = this; + this.handleDrag(this.editor.inputs.currentPagePoint, shapes); + if (prevDroppingShapeId) { + const shape = this.editor.getShape(prevDroppingShapeId); + if (!shape) return; + this.editor.getShapeUtil(shape).onDropShapesOver?.(shape, shapes); + } + } + clear() { + this.prevDroppingShapeId = null; + if (this.droppingNodeTimer !== null) { + clearTimeout(this.droppingNodeTimer); + } + this.droppingNodeTimer = null; + this.editor.setHintingShapes([]); + this.first = true; + } + dispose() { + this.clear(); + } +} +_init$2 = __decoratorStart$2(); +__decorateElement$2(_init$2, 1, "dispose", _dispose_dec, DragAndDropManager); +__decoratorMetadata$2(_init$2, DragAndDropManager); + +var __create$1 = Object.create; +var __defProp$1 = Object.defineProperty; +var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor; +var __knownSymbol$1 = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name); +var __typeError$1 = (msg) => { + throw TypeError(msg); +}; +var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __decoratorStart$1 = (base) => [, , , __create$1(base?.[__knownSymbol$1("metadata")] ?? null)]; +var __decoratorStrings$1 = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"]; +var __expectFn$1 = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError$1("Function expected") : fn; +var __decoratorContext$1 = (kind, name, done, metadata, fns) => ({ kind: __decoratorStrings$1[kind], name, metadata, addInitializer: (fn) => done._ ? __typeError$1("Already initialized") : fns.push(__expectFn$1(fn || null)) }); +var __decoratorMetadata$1 = (array, target) => __defNormalProp$1(target, __knownSymbol$1("metadata"), array[3]); +var __runInitializers$1 = (array, flags, self, value) => { + for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) fns[i].call(self) ; + return value; +}; +var __decorateElement$1 = (array, flags, name, decorators, target, extra) => { + var it, done, ctx, access, k = flags & 7, s = false, p = false; + var j = 2 , key = __decoratorStrings$1[k + 5]; + var extraInitializers = array[j] || (array[j] = []); + var desc = ((target = target.prototype), __getOwnPropDesc$1(target , name)); + for (var i = decorators.length - 1; i >= 0; i--) { + ctx = __decoratorContext$1(k, name, done = {}, array[3], extraInitializers); + { + ctx.static = s, ctx.private = p, access = ctx.access = { has: (x) => name in x }; + access.get = (x) => x[name]; + } + it = (0, decorators[i])(desc[key] , ctx), done._ = 1; + __expectFn$1(it) && (desc[key] = it ); + } + return desc && __defProp$1(target, name, desc), target; +}; +var __publicField$1 = (obj, key, value) => __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value); +var _updateParentTransforms_dec, _a, _init$1; +class Translating extends (_a = StateNode, _updateParentTransforms_dec = [bind$2], _a) { + constructor() { + super(...arguments); + __runInitializers$1(_init$1, 5, this); + __publicField$1(this, "info", {}); + __publicField$1(this, "selectionSnapshot", {}); + __publicField$1(this, "snapshot", {}); + __publicField$1(this, "markId", ""); + __publicField$1(this, "isCloning", false); + __publicField$1(this, "isCreating", false); + __publicField$1(this, "dragAndDropManager", new DragAndDropManager(this.editor)); + } + onCreate(_shape) { + return; + } + onEnter(info) { + const { isCreating = false, creatingMarkId, onCreate = () => void 0 } = info; + if (!this.editor.getSelectedShapeIds()?.length) { + this.parent.transition("idle"); + return; + } + this.info = info; + this.parent.setCurrentToolIdMask(info.onInteractionEnd); + this.isCreating = isCreating; + this.markId = ""; + if (isCreating) { + if (creatingMarkId) { + this.markId = creatingMarkId; + } else { + const markId = this.editor.getMarkIdMatching( + `creating:${this.editor.getOnlySelectedShapeId()}` + ); + if (markId) { + this.markId = markId; + } + } + } else { + this.markId = this.editor.markHistoryStoppingPoint("translating"); + } + this.onCreate = onCreate; + this.isCloning = false; + this.info = info; + this.editor.setCursor({ type: "move", rotation: 0 }); + this.selectionSnapshot = getTranslatingSnapshot(this.editor); + if (!this.isCreating) { + if (this.editor.inputs.altKey) { + this.startCloning(); + return; + } + } + this.snapshot = this.selectionSnapshot; + this.handleStart(); + this.updateShapes(); + } + onExit() { + this.parent.setCurrentToolIdMask(void 0); + this.selectionSnapshot = {}; + this.snapshot = {}; + this.editor.snaps.clearIndicators(); + this.editor.setCursor({ type: "default", rotation: 0 }); + this.dragAndDropManager.clear(); + } + onTick({ elapsed }) { + const { editor } = this; + this.dragAndDropManager.updateDroppingNode( + this.snapshot.movingShapes, + this.updateParentTransforms + ); + editor.edgeScrollManager.updateEdgeScrolling(elapsed); + } + onPointerMove() { + this.updateShapes(); + } + onKeyDown() { + if (this.editor.inputs.altKey && !this.isCloning) { + this.startCloning(); + return; + } + this.updateShapes(); + } + onKeyUp() { + if (!this.editor.inputs.altKey && this.isCloning) { + this.stopCloning(); + return; + } + this.updateShapes(); + } + onPointerUp() { + this.complete(); + } + onComplete() { + this.complete(); + } + onCancel() { + this.cancel(); + } + startCloning() { + if (this.isCreating) return; + this.isCloning = true; + this.reset(); + this.markId = this.editor.markHistoryStoppingPoint("translate cloning"); + this.editor.duplicateShapes(Array.from(this.editor.getSelectedShapeIds())); + this.snapshot = getTranslatingSnapshot(this.editor); + this.handleStart(); + this.updateShapes(); + } + stopCloning() { + this.isCloning = false; + this.snapshot = this.selectionSnapshot; + this.reset(); + this.markId = this.editor.markHistoryStoppingPoint("translate"); + this.updateShapes(); + } + reset() { + this.editor.bailToMark(this.markId); + } + complete() { + this.updateShapes(); + this.dragAndDropManager.dropShapes(this.snapshot.movingShapes); + kickoutOccludedShapes( + this.editor, + this.snapshot.movingShapes.map((s) => s.id) + ); + this.handleEnd(); + if (this.editor.getInstanceState().isToolLocked && this.info.onInteractionEnd) { + this.editor.setCurrentTool(this.info.onInteractionEnd); + } else { + if (this.isCreating) { + this.onCreate?.(this.editor.getOnlySelectedShape()); + } else { + this.parent.transition("idle"); + } + } + } + cancel() { + this.reset(); + if (this.info.onInteractionEnd) { + this.editor.setCurrentTool(this.info.onInteractionEnd); + } else { + this.parent.transition("idle", this.info); + } + } + handleStart() { + const { movingShapes } = this.snapshot; + const changes = []; + movingShapes.forEach((shape) => { + const util = this.editor.getShapeUtil(shape); + const change = util.onTranslateStart?.(shape); + if (change) { + changes.push(change); + } + }); + if (changes.length > 0) { + this.editor.updateShapes(changes); + } + this.editor.setHoveredShape(null); + } + handleEnd() { + const { movingShapes } = this.snapshot; + if (this.isCloning && movingShapes.length > 0) { + const currentAveragePagePoint = Vec.Average( + movingShapes.map((s) => this.editor.getShapePageTransform(s.id).point()) + ); + const offset = Vec.Sub(currentAveragePagePoint, this.selectionSnapshot.averagePagePoint); + if (!Vec.IsNaN(offset)) { + this.editor.updateInstanceState({ + duplicateProps: { + shapeIds: movingShapes.map((s) => s.id), + offset: { x: offset.x, y: offset.y } + } + }); + } + } + const changes = []; + movingShapes.forEach((shape) => { + const current = this.editor.getShape(shape.id); + const util = this.editor.getShapeUtil(shape); + const change = util.onTranslateEnd?.(shape, current); + if (change) { + changes.push(change); + } + }); + if (changes.length > 0) { + this.editor.updateShapes(changes); + } + } + updateShapes() { + const { snapshot } = this; + this.dragAndDropManager.updateDroppingNode(snapshot.movingShapes, this.updateParentTransforms); + moveShapesToPoint({ + editor: this.editor, + snapshot + }); + const { movingShapes } = snapshot; + const changes = []; + movingShapes.forEach((shape) => { + const current = this.editor.getShape(shape.id); + const util = this.editor.getShapeUtil(shape); + const change = util.onTranslate?.(shape, current); + if (change) { + changes.push(change); + } + }); + if (changes.length > 0) { + this.editor.updateShapes(changes); + } + } + updateParentTransforms() { + const { + editor, + snapshot: { shapeSnapshots } + } = this; + shapeSnapshots.forEach((shapeSnapshot) => { + const shape = editor.getShape(shapeSnapshot.shape.id); + if (!shape) return null; + const parentTransform = isPageId(shape.parentId) ? null : Mat.Inverse(editor.getShapePageTransform(shape.parentId)); + shapeSnapshot.parentTransform = parentTransform; + }); + } +} +_init$1 = __decoratorStart$1(_a); +__decorateElement$1(_init$1, 1, "updateParentTransforms", _updateParentTransforms_dec, Translating); +__decoratorMetadata$1(_init$1, Translating); +__publicField$1(Translating, "id", "translating"); +function getTranslatingSnapshot(editor) { + const movingShapes = []; + const pagePoints = []; + const selectedShapeIds = editor.getSelectedShapeIds(); + const shapeSnapshots = compact( + selectedShapeIds.map((id) => { + const shape = editor.getShape(id); + if (!shape) return null; + movingShapes.push(shape); + const pageTransform = editor.getShapePageTransform(id); + const pagePoint = pageTransform.point(); + const pageRotation = pageTransform.rotation(); + pagePoints.push(pagePoint); + const parentTransform = PageRecordType.isId(shape.parentId) ? null : Mat.Inverse(editor.getShapePageTransform(shape.parentId)); + return { + shape, + pagePoint, + pageRotation, + parentTransform + }; + }) + ); + const onlySelectedShape = editor.getOnlySelectedShape(); + let initialSnapPoints = []; + if (onlySelectedShape) { + initialSnapPoints = editor.snaps.shapeBounds.getSnapPoints(onlySelectedShape.id); + } else { + const selectionPageBounds = editor.getSelectionPageBounds(); + if (selectionPageBounds) { + initialSnapPoints = selectionPageBounds.cornersAndCenter.map((p, i) => ({ + id: "selection:" + i, + x: p.x, + y: p.y + })); + } + } + let noteAdjacentPositions; + let noteSnapshot; + const { originPagePoint } = editor.inputs; + const allHoveredNotes = shapeSnapshots.filter( + (s) => editor.isShapeOfType(s.shape, "note") && editor.isPointInShape(s.shape, originPagePoint) + ); + if (allHoveredNotes.length === 0) ; else if (allHoveredNotes.length === 1) { + noteSnapshot = allHoveredNotes[0]; + } else { + const allShapesSorted = editor.getCurrentPageShapesSorted(); + noteSnapshot = allHoveredNotes.map((s) => ({ + snapshot: s, + index: allShapesSorted.findIndex((shape) => shape.id === s.shape.id) + })).sort((a, b) => b.index - a.index)[0]?.snapshot; + } + if (noteSnapshot) { + noteAdjacentPositions = getAvailableNoteAdjacentPositions( + editor, + noteSnapshot.pageRotation, + noteSnapshot.shape.props.scale, + noteSnapshot.shape.props.growY ?? 0 + ); + } + return { + averagePagePoint: Vec.Average(pagePoints), + movingShapes, + shapeSnapshots, + initialPageBounds: editor.getSelectionPageBounds(), + initialSnapPoints, + noteAdjacentPositions, + noteSnapshot + }; +} +function moveShapesToPoint({ + editor, + snapshot +}) { + const { inputs } = editor; + const { + noteSnapshot, + noteAdjacentPositions, + initialPageBounds, + initialSnapPoints, + shapeSnapshots, + averagePagePoint + } = snapshot; + const isGridMode = editor.getInstanceState().isGridMode; + const gridSize = editor.getDocumentSettings().gridSize; + const delta = Vec.Sub(inputs.currentPagePoint, inputs.originPagePoint); + const flatten = editor.inputs.shiftKey ? Math.abs(delta.x) < Math.abs(delta.y) ? "x" : "y" : null; + if (flatten === "x") { + delta.x = 0; + } else if (flatten === "y") { + delta.y = 0; + } + editor.snaps.clearIndicators(); + const isSnapping = editor.user.getIsSnapMode() ? !inputs.ctrlKey : inputs.ctrlKey; + let snappedToPit = false; + if (isSnapping && editor.inputs.pointerVelocity.len() < 0.5) { + const { nudge } = editor.snaps.shapeBounds.snapTranslateShapes({ + dragDelta: delta, + initialSelectionPageBounds: initialPageBounds, + lockedAxis: flatten, + initialSelectionSnapPoints: initialSnapPoints + }); + delta.add(nudge); + } else { + if (noteSnapshot && noteAdjacentPositions) { + const { scale } = noteSnapshot.shape.props; + const pageCenter = noteSnapshot.pagePoint.clone().add(delta).add(NOTE_CENTER_OFFSET.clone().mul(scale).rot(noteSnapshot.pageRotation)); + let min = NOTE_ADJACENT_POSITION_SNAP_RADIUS / editor.getZoomLevel(); + let offset = new Vec(0, 0); + for (const pit of noteAdjacentPositions) { + const deltaToPit = Vec.Sub(pageCenter, pit); + const dist = deltaToPit.len(); + if (dist < min) { + snappedToPit = true; + min = dist; + offset = deltaToPit; + } + } + delta.sub(offset); + } + } + const averageSnappedPoint = Vec.Add(averagePagePoint, delta); + const snapIndicators = editor.snaps.getIndicators(); + if (isGridMode && !inputs.ctrlKey && !snappedToPit && snapIndicators.length === 0) { + averageSnappedPoint.snapToGrid(gridSize); + } + const averageSnap = Vec.Sub(averageSnappedPoint, averagePagePoint); + editor.updateShapes( + compact( + shapeSnapshots.map(({ shape, pagePoint, parentTransform }) => { + const newPagePoint = Vec.Add(pagePoint, averageSnap); + const newLocalPoint = parentTransform ? Mat.applyToPoint(parentTransform, newPagePoint) : newPagePoint; + return { + id: shape.id, + type: shape.type, + x: newLocalPoint.x, + y: newLocalPoint.y + }; + }) + ) + ); +} + +class SelectTool extends StateNode { + static id = "select"; + static initial = "idle"; + static isLockable = false; + reactor = void 0; + static children() { + return [ + Crop, + Cropping, + Idle$1, + PointingCanvas, + PointingShape, + Translating, + Brushing, + ScribbleBrushing, + PointingCropHandle, + PointingSelection, + PointingResizeHandle, + EditingShape, + Resizing, + Rotating, + PointingRotateHandle, + PointingArrowLabel, + PointingHandle, + DraggingHandle + ]; + } + // We want to clean up the duplicate props when the selection changes + cleanUpDuplicateProps() { + const selectedShapeIds = this.editor.getSelectedShapeIds(); + const instance = this.editor.getInstanceState(); + if (!instance.duplicateProps) return; + const duplicatedShapes = new Set(instance.duplicateProps.shapeIds); + if (selectedShapeIds.length === duplicatedShapes.size && selectedShapeIds.every((shapeId) => duplicatedShapes.has(shapeId))) { + return; + } + this.editor.updateInstanceState({ + duplicateProps: null + }); + } + onEnter() { + this.reactor = react("clean duplicate props", () => { + try { + this.cleanUpDuplicateProps(); + } catch (e) { + { + console.error(e); + } + } + }); + } + onExit() { + this.reactor?.(); + if (this.editor.getCurrentPageState().editingShapeId) { + this.editor.setEditingShape(null); + } + } +} + +class Idle extends StateNode { + static id = "idle"; + info = {}; + onEnter(info) { + this.info = info; + } + onPointerDown() { + this.parent.transition("pointing", this.info); + } +} + +class Pointing extends StateNode { + static id = "pointing"; + info = {}; + onEnter(info) { + this.info = info; + } + onPointerUp() { + this.complete(); + } + onPointerMove() { + if (this.editor.inputs.isDragging) { + this.parent.transition("zoom_brushing", this.info); + } + } + onCancel() { + this.cancel(); + } + complete() { + const { currentScreenPoint } = this.editor.inputs; + if (this.editor.inputs.altKey) { + this.editor.zoomOut(currentScreenPoint, { animation: { duration: 220 } }); + } else { + this.editor.zoomIn(currentScreenPoint, { animation: { duration: 220 } }); + } + this.parent.transition("idle", this.info); + } + cancel() { + this.parent.transition("idle", this.info); + } +} + +class ZoomBrushing extends StateNode { + static id = "zoom_brushing"; + info = {}; + zoomBrush = new Box(); + onEnter(info) { + this.info = info; + this.update(); + } + onExit() { + this.editor.updateInstanceState({ zoomBrush: null }); + } + onPointerMove() { + this.update(); + } + onPointerUp() { + this.complete(); + } + onCancel() { + this.cancel(); + } + update() { + const { + inputs: { originPagePoint, currentPagePoint } + } = this.editor; + this.zoomBrush.setTo(Box.FromPoints([originPagePoint, currentPagePoint])); + this.editor.updateInstanceState({ zoomBrush: this.zoomBrush.toJson() }); + } + cancel() { + this.parent.transition("idle", this.info); + } + complete() { + const { zoomBrush } = this; + const threshold = 8 / this.editor.getZoomLevel(); + if (zoomBrush.width < threshold && zoomBrush.height < threshold) { + const point = this.editor.inputs.currentScreenPoint; + if (this.editor.inputs.altKey) { + this.editor.zoomOut(point, { animation: { duration: 220 } }); + } else { + this.editor.zoomIn(point, { animation: { duration: 220 } }); + } + } else { + const targetZoom = this.editor.inputs.altKey ? this.editor.getZoomLevel() / 2 : void 0; + this.editor.zoomToBounds(zoomBrush, { targetZoom, animation: { duration: 220 } }); + } + this.parent.transition("idle", this.info); + } +} + +class ZoomTool extends StateNode { + static id = "zoom"; + static initial = "idle"; + static children() { + return [Idle, ZoomBrushing, Pointing]; + } + static isLockable = false; + info = {}; + onEnter(info) { + this.info = info; + this.parent.setCurrentToolIdMask(info.onInteractionEnd); + this.updateCursor(); + } + onExit() { + this.parent.setCurrentToolIdMask(void 0); + this.editor.updateInstanceState({ zoomBrush: null, cursor: { type: "default", rotation: 0 } }); + this.parent.setCurrentToolIdMask(void 0); + } + onKeyDown() { + this.updateCursor(); + } + onKeyUp(info) { + this.updateCursor(); + if (info.code === "KeyZ") { + this.complete(); + } + } + onInterrupt() { + this.complete(); + } + complete() { + if (this.info.onInteractionEnd && this.info.onInteractionEnd !== "select") { + this.editor.setCurrentTool(this.info.onInteractionEnd, this.info); + } else { + this.parent.transition("select"); + } + } + updateCursor() { + if (this.editor.inputs.altKey) { + this.editor.setCursor({ type: "zoom-out", rotation: 0 }); + } else { + this.editor.setCursor({ type: "zoom-in", rotation: 0 }); + } + } +} + +const defaultTools = [EraserTool, HandTool, LaserTool, ZoomTool, SelectTool]; + +function FollowingIndicator() { + const editor = useEditor(); + const followingUserId = useValue("follow", () => editor.getInstanceState().followingUserId, [ + editor + ]); + if (!followingUserId) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsx(FollowingIndicatorInner, { userId: followingUserId }); +} +function FollowingIndicatorInner({ userId }) { + const presence = usePresence$1(userId); + if (!presence) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tlui-following-indicator", style: { borderColor: presence.color } }); +} + +let defaultEditorAssetUrls = { + fonts: { + draw: `${getDefaultCdnBaseUrl()}/fonts/Shantell_Sans-Tldrawish.woff2`, + serif: `${getDefaultCdnBaseUrl()}/fonts/IBMPlexSerif-Medium.woff2`, + sansSerif: `${getDefaultCdnBaseUrl()}/fonts/IBMPlexSans-Medium.woff2`, + monospace: `${getDefaultCdnBaseUrl()}/fonts/IBMPlexMono-Medium.woff2` + } +}; +function useDefaultEditorAssetsWithOverrides(overrides) { + return reactExports.useMemo(() => { + if (!overrides) return defaultEditorAssetUrls; + return { + fonts: { ...defaultEditorAssetUrls.fonts, ...overrides?.fonts } + }; + }, [overrides]); +} + +const iconTypes = [ + "align-bottom", + "align-center-horizontal", + "align-center-vertical", + "align-left", + "align-right", + "align-top", + "arrow-left", + "arrowhead-arrow", + "arrowhead-bar", + "arrowhead-diamond", + "arrowhead-dot", + "arrowhead-none", + "arrowhead-square", + "arrowhead-triangle-inverted", + "arrowhead-triangle", + "blob", + "bring-forward", + "bring-to-front", + "broken", + "check-circle", + "check", + "chevron-down", + "chevron-left", + "chevron-right", + "chevron-up", + "chevrons-ne", + "chevrons-sw", + "clipboard-copied", + "clipboard-copy", + "color", + "cross-2", + "cross-circle", + "dash-dashed", + "dash-dotted", + "dash-draw", + "dash-solid", + "disconnected", + "discord", + "distribute-horizontal", + "distribute-vertical", + "dot", + "dots-horizontal", + "dots-vertical", + "drag-handle-dots", + "duplicate", + "edit", + "external-link", + "fill-fill", + "fill-none", + "fill-pattern", + "fill-semi", + "fill-solid", + "follow", + "following", + "font-draw", + "font-mono", + "font-sans", + "font-serif", + "geo-arrow-down", + "geo-arrow-left", + "geo-arrow-right", + "geo-arrow-up", + "geo-check-box", + "geo-cloud", + "geo-diamond", + "geo-ellipse", + "geo-heart", + "geo-hexagon", + "geo-octagon", + "geo-oval", + "geo-pentagon", + "geo-rectangle", + "geo-rhombus-2", + "geo-rhombus", + "geo-star", + "geo-trapezoid", + "geo-triangle", + "geo-x-box", + "github", + "group", + "horizontal-align-end", + "horizontal-align-middle", + "horizontal-align-start", + "info-circle", + "leading", + "link", + "lock", + "menu", + "minus", + "mixed", + "pack", + "plus", + "question-mark-circle", + "question-mark", + "redo", + "reset-zoom", + "rotate-ccw", + "rotate-cw", + "send-backward", + "send-to-back", + "share-1", + "size-extra-large", + "size-large", + "size-medium", + "size-small", + "spline-cubic", + "spline-line", + "stack-horizontal", + "stack-vertical", + "status-offline", + "stretch-horizontal", + "stretch-vertical", + "text-align-center", + "text-align-left", + "text-align-right", + "toggle-off", + "toggle-on", + "tool-arrow", + "tool-eraser", + "tool-frame", + "tool-hand", + "tool-highlight", + "tool-laser", + "tool-line", + "tool-media", + "tool-note", + "tool-pencil", + "tool-pointer", + "tool-screenshot", + "tool-text", + "trash", + "twitter", + "undo", + "ungroup", + "unlock", + "vertical-align-end", + "vertical-align-middle", + "vertical-align-start", + "warning-triangle", + "zoom-in", + "zoom-out" +]; + +let defaultUiAssetUrls = { + ...defaultEditorAssetUrls, + icons: Object.fromEntries( + iconTypes.map((name) => [name, `${getDefaultCdnBaseUrl()}/icons/icon/0_merged.svg#${name}`]) + ), + translations: Object.fromEntries( + LANGUAGES.map((lang) => [ + lang.locale, + `${getDefaultCdnBaseUrl()}/translations/${lang.locale}.json` + ]) + ), + embedIcons: Object.fromEntries( + DEFAULT_EMBED_DEFINITIONS.map((def) => [ + def.type, + `${getDefaultCdnBaseUrl()}/embed-icons/${def.type}.png` + ]) + ) +}; +function useDefaultUiAssetUrlsWithOverrides(overrides) { + if (!overrides) return defaultUiAssetUrls; + return { + fonts: Object.assign({ ...defaultUiAssetUrls.fonts }, { ...overrides?.fonts }), + icons: Object.assign({ ...defaultUiAssetUrls.icons }, { ...overrides?.icons }), + embedIcons: Object.assign({ ...defaultUiAssetUrls.embedIcons }, { ...overrides?.embedIcons }), + translations: Object.assign( + { ...defaultUiAssetUrls.translations }, + { ...overrides?.translations } + ) + }; +} + +var POPOVER_NAME = "Popover"; +var [createPopoverContext, createPopoverScope] = createContextScope(POPOVER_NAME, [ + createPopperScope +]); +var usePopperScope = createPopperScope(); +var [PopoverProvider, usePopoverContext] = createPopoverContext(POPOVER_NAME); +var Popover = (props) => { + const { + __scopePopover, + children, + open: openProp, + defaultOpen, + onOpenChange, + modal = false + } = props; + const popperScope = usePopperScope(__scopePopover); + const triggerRef = reactExports.useRef(null); + const [hasCustomAnchor, setHasCustomAnchor] = reactExports.useState(false); + const [open, setOpen] = useControllableState({ + prop: openProp, + defaultProp: defaultOpen ?? false, + onChange: onOpenChange, + caller: POPOVER_NAME + }); + return /* @__PURE__ */ jsxRuntimeExports.jsx(Root2$4, { ...popperScope, children: /* @__PURE__ */ jsxRuntimeExports.jsx( + PopoverProvider, + { + scope: __scopePopover, + contentId: useId(), + triggerRef, + open, + onOpenChange: setOpen, + onOpenToggle: reactExports.useCallback(() => setOpen((prevOpen) => !prevOpen), [setOpen]), + hasCustomAnchor, + onCustomAnchorAdd: reactExports.useCallback(() => setHasCustomAnchor(true), []), + onCustomAnchorRemove: reactExports.useCallback(() => setHasCustomAnchor(false), []), + modal, + children + } + ) }); +}; +Popover.displayName = POPOVER_NAME; +var ANCHOR_NAME = "PopoverAnchor"; +var PopoverAnchor = reactExports.forwardRef( + (props, forwardedRef) => { + const { __scopePopover, ...anchorProps } = props; + const context = usePopoverContext(ANCHOR_NAME, __scopePopover); + const popperScope = usePopperScope(__scopePopover); + const { onCustomAnchorAdd, onCustomAnchorRemove } = context; + reactExports.useEffect(() => { + onCustomAnchorAdd(); + return () => onCustomAnchorRemove(); + }, [onCustomAnchorAdd, onCustomAnchorRemove]); + return /* @__PURE__ */ jsxRuntimeExports.jsx(Anchor, { ...popperScope, ...anchorProps, ref: forwardedRef }); + } +); +PopoverAnchor.displayName = ANCHOR_NAME; +var TRIGGER_NAME = "PopoverTrigger"; +var PopoverTrigger = reactExports.forwardRef( + (props, forwardedRef) => { + const { __scopePopover, ...triggerProps } = props; + const context = usePopoverContext(TRIGGER_NAME, __scopePopover); + const popperScope = usePopperScope(__scopePopover); + const composedTriggerRef = useComposedRefs(forwardedRef, context.triggerRef); + const trigger = /* @__PURE__ */ jsxRuntimeExports.jsx( + Primitive.button, + { + type: "button", + "aria-haspopup": "dialog", + "aria-expanded": context.open, + "aria-controls": context.contentId, + "data-state": getState(context.open), + ...triggerProps, + ref: composedTriggerRef, + onClick: composeEventHandlers(props.onClick, context.onOpenToggle) + } + ); + return context.hasCustomAnchor ? trigger : /* @__PURE__ */ jsxRuntimeExports.jsx(Anchor, { asChild: true, ...popperScope, children: trigger }); + } +); +PopoverTrigger.displayName = TRIGGER_NAME; +var PORTAL_NAME = "PopoverPortal"; +var [PortalProvider, usePortalContext] = createPopoverContext(PORTAL_NAME, { + forceMount: void 0 +}); +var PopoverPortal = (props) => { + const { __scopePopover, forceMount, children, container } = props; + const context = usePopoverContext(PORTAL_NAME, __scopePopover); + return /* @__PURE__ */ jsxRuntimeExports.jsx(PortalProvider, { scope: __scopePopover, forceMount, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Presence, { present: forceMount || context.open, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Portal$3, { asChild: true, container, children }) }) }); +}; +PopoverPortal.displayName = PORTAL_NAME; +var CONTENT_NAME = "PopoverContent"; +var PopoverContent = reactExports.forwardRef( + (props, forwardedRef) => { + const portalContext = usePortalContext(CONTENT_NAME, props.__scopePopover); + const { forceMount = portalContext.forceMount, ...contentProps } = props; + const context = usePopoverContext(CONTENT_NAME, props.__scopePopover); + return /* @__PURE__ */ jsxRuntimeExports.jsx(Presence, { present: forceMount || context.open, children: context.modal ? /* @__PURE__ */ jsxRuntimeExports.jsx(PopoverContentModal, { ...contentProps, ref: forwardedRef }) : /* @__PURE__ */ jsxRuntimeExports.jsx(PopoverContentNonModal, { ...contentProps, ref: forwardedRef }) }); + } +); +PopoverContent.displayName = CONTENT_NAME; +var Slot = createSlot("PopoverContent.RemoveScroll"); +var PopoverContentModal = reactExports.forwardRef( + (props, forwardedRef) => { + const context = usePopoverContext(CONTENT_NAME, props.__scopePopover); + const contentRef = reactExports.useRef(null); + const composedRefs = useComposedRefs(forwardedRef, contentRef); + const isRightClickOutsideRef = reactExports.useRef(false); + reactExports.useEffect(() => { + const content = contentRef.current; + if (content) return hideOthers(content); + }, []); + return /* @__PURE__ */ jsxRuntimeExports.jsx(ReactRemoveScroll, { as: Slot, allowPinchZoom: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx( + PopoverContentImpl, + { + ...props, + ref: composedRefs, + trapFocus: context.open, + disableOutsidePointerEvents: true, + onCloseAutoFocus: composeEventHandlers(props.onCloseAutoFocus, (event) => { + event.preventDefault(); + if (!isRightClickOutsideRef.current) context.triggerRef.current?.focus(); + }), + onPointerDownOutside: composeEventHandlers( + props.onPointerDownOutside, + (event) => { + const originalEvent = event.detail.originalEvent; + const ctrlLeftClick = originalEvent.button === 0 && originalEvent.ctrlKey === true; + const isRightClick = originalEvent.button === 2 || ctrlLeftClick; + isRightClickOutsideRef.current = isRightClick; + }, + { checkForDefaultPrevented: false } + ), + onFocusOutside: composeEventHandlers( + props.onFocusOutside, + (event) => event.preventDefault(), + { checkForDefaultPrevented: false } + ) + } + ) }); + } +); +var PopoverContentNonModal = reactExports.forwardRef( + (props, forwardedRef) => { + const context = usePopoverContext(CONTENT_NAME, props.__scopePopover); + const hasInteractedOutsideRef = reactExports.useRef(false); + const hasPointerDownOutsideRef = reactExports.useRef(false); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + PopoverContentImpl, + { + ...props, + ref: forwardedRef, + trapFocus: false, + disableOutsidePointerEvents: false, + onCloseAutoFocus: (event) => { + props.onCloseAutoFocus?.(event); + if (!event.defaultPrevented) { + if (!hasInteractedOutsideRef.current) context.triggerRef.current?.focus(); + event.preventDefault(); + } + hasInteractedOutsideRef.current = false; + hasPointerDownOutsideRef.current = false; + }, + onInteractOutside: (event) => { + props.onInteractOutside?.(event); + if (!event.defaultPrevented) { + hasInteractedOutsideRef.current = true; + if (event.detail.originalEvent.type === "pointerdown") { + hasPointerDownOutsideRef.current = true; + } + } + const target = event.target; + const targetIsTrigger = context.triggerRef.current?.contains(target); + if (targetIsTrigger) event.preventDefault(); + if (event.detail.originalEvent.type === "focusin" && hasPointerDownOutsideRef.current) { + event.preventDefault(); + } + } + } + ); + } +); +var PopoverContentImpl = reactExports.forwardRef( + (props, forwardedRef) => { + const { + __scopePopover, + trapFocus, + onOpenAutoFocus, + onCloseAutoFocus, + disableOutsidePointerEvents, + onEscapeKeyDown, + onPointerDownOutside, + onFocusOutside, + onInteractOutside, + ...contentProps + } = props; + const context = usePopoverContext(CONTENT_NAME, __scopePopover); + const popperScope = usePopperScope(__scopePopover); + useFocusGuards(); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + FocusScope, + { + asChild: true, + loop: true, + trapped: trapFocus, + onMountAutoFocus: onOpenAutoFocus, + onUnmountAutoFocus: onCloseAutoFocus, + children: /* @__PURE__ */ jsxRuntimeExports.jsx( + DismissableLayer, + { + asChild: true, + disableOutsidePointerEvents, + onInteractOutside, + onEscapeKeyDown, + onPointerDownOutside, + onFocusOutside, + onDismiss: () => context.onOpenChange(false), + children: /* @__PURE__ */ jsxRuntimeExports.jsx( + Content$1, + { + "data-state": getState(context.open), + role: "dialog", + id: context.contentId, + ...popperScope, + ...contentProps, + ref: forwardedRef, + style: { + ...contentProps.style, + // re-namespace exposed content custom properties + ...{ + "--radix-popover-content-transform-origin": "var(--radix-popper-transform-origin)", + "--radix-popover-content-available-width": "var(--radix-popper-available-width)", + "--radix-popover-content-available-height": "var(--radix-popper-available-height)", + "--radix-popover-trigger-width": "var(--radix-popper-anchor-width)", + "--radix-popover-trigger-height": "var(--radix-popper-anchor-height)" + } + } + } + ) + } + ) + } + ); + } +); +var CLOSE_NAME = "PopoverClose"; +var PopoverClose = reactExports.forwardRef( + (props, forwardedRef) => { + const { __scopePopover, ...closeProps } = props; + const context = usePopoverContext(CLOSE_NAME, __scopePopover); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + Primitive.button, + { + type: "button", + ...closeProps, + ref: forwardedRef, + onClick: composeEventHandlers(props.onClick, () => context.onOpenChange(false)) + } + ); + } +); +PopoverClose.displayName = CLOSE_NAME; +var ARROW_NAME = "PopoverArrow"; +var PopoverArrow = reactExports.forwardRef( + (props, forwardedRef) => { + const { __scopePopover, ...arrowProps } = props; + const popperScope = usePopperScope(__scopePopover); + return /* @__PURE__ */ jsxRuntimeExports.jsx(Arrow, { ...popperScope, ...arrowProps, ref: forwardedRef }); + } +); +PopoverArrow.displayName = ARROW_NAME; +function getState(open) { + return open ? "open" : "closed"; +} +var Root2 = Popover; +var Trigger = PopoverTrigger; +var Portal = PopoverPortal; +var Content2 = PopoverContent; + +function TldrawUiPopover({ id, children, onOpenChange, open }) { + const [isOpen, handleOpenChange] = useMenuIsOpen(id, onOpenChange); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + Root2, + { + onOpenChange: handleOpenChange, + open: open || isOpen, + children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tlui-popover", children }) + } + ); +} +function TldrawUiPopoverTrigger({ children }) { + return /* @__PURE__ */ jsxRuntimeExports.jsx(Trigger, { asChild: true, dir: "ltr", children }); +} +function TldrawUiPopoverContent({ + side, + children, + align = "center", + sideOffset = 8, + alignOffset = 0, + disableEscapeKeyDown = false +}) { + const container = useContainer(); + return /* @__PURE__ */ jsxRuntimeExports.jsx(Portal, { container, children: /* @__PURE__ */ jsxRuntimeExports.jsx( + Content2, + { + className: "tlui-popover__content", + side, + sideOffset, + align, + alignOffset, + dir: "ltr", + onEscapeKeyDown: (e) => disableEscapeKeyDown && e.preventDefault(), + children + } + ) }); +} + +function shapesWithUnboundArrows(editor) { + const selectedShapeIds = editor.getSelectedShapeIds(); + const selectedShapes = selectedShapeIds.map((id) => { + return editor.getShape(id); + }); + return selectedShapes.filter((shape) => { + if (!shape) return false; + if (editor.isShapeOfType(shape, "arrow")) { + const bindings = getArrowBindings(editor, shape); + if (bindings.start || bindings.end) return false; + } + return true; + }); +} +const useThreeStackableItems = () => { + const editor = useEditor(); + return useValue("threeStackableItems", () => shapesWithUnboundArrows(editor).length > 2, [editor]); +}; +const useIsInSelectState = () => { + const editor = useEditor(); + return useValue("isInSelectState", () => editor.isIn("select"), [editor]); +}; +const useAllowGroup = () => { + const editor = useEditor(); + return useValue( + "allow group", + () => { + const selectedShapes = editor.getSelectedShapes(); + if (selectedShapes.length < 2) return false; + for (const shape of selectedShapes) { + if (editor.isShapeOfType(shape, "arrow")) { + const bindings = getArrowBindings(editor, shape); + if (bindings.start) { + if (!selectedShapes.some((s) => s.id === bindings.start.toId)) { + return false; + } + } + if (bindings.end) { + if (!selectedShapes.some((s) => s.id === bindings.end.toId)) { + return false; + } + } + } + } + return true; + }, + [editor] + ); +}; +const useAllowUngroup = () => { + const editor = useEditor(); + return useValue( + "allowUngroup", + () => editor.getSelectedShapeIds().some((id) => editor.getShape(id)?.type === "group"), + [editor] + ); +}; +const showMenuPaste = typeof window !== "undefined" && "navigator" in window && Boolean(navigator.clipboard) && Boolean(navigator.clipboard.read); +function useAnySelectedShapesCount(min, max) { + const editor = useEditor(); + return useValue( + "selectedShapes", + () => { + const len = editor.getSelectedShapes().length; + { + { + return len >= min; + } + } + }, + [editor, min, max] + ); +} +function useUnlockedSelectedShapesCount(min, max) { + const editor = useEditor(); + return useValue( + "selectedShapes", + () => { + const len = editor.getSelectedShapes().filter((s) => !editor.isShapeOrAncestorLocked(s)).length; + if (min === void 0) { + { + return len; + } + } else { + { + return len >= min; + } + } + }, + [editor] + ); +} +function useShowAutoSizeToggle() { + const editor = useEditor(); + return useValue( + "showAutoSizeToggle", + () => { + const selectedShapes = editor.getSelectedShapes(); + return selectedShapes.length === 1 && editor.isShapeOfType(selectedShapes[0], "text") && selectedShapes[0].props.autoSize === false; + }, + [editor] + ); +} +function useHasLinkShapeSelected() { + const editor = useEditor(); + return useValue( + "hasLinkShapeSelected", + () => { + const onlySelectedShape = editor.getOnlySelectedShape(); + return !!(onlySelectedShape && onlySelectedShape.type !== "embed" && "url" in onlySelectedShape.props && !onlySelectedShape.isLocked); + }, + [editor] + ); +} +function useOnlyFlippableShape() { + const editor = useEditor(); + return useValue( + "onlyFlippableShape", + () => { + const shape = editor.getOnlySelectedShape(); + return shape && (editor.isShapeOfType(shape, "group") || editor.isShapeOfType(shape, "image") || editor.isShapeOfType(shape, "arrow") || editor.isShapeOfType(shape, "line") || editor.isShapeOfType(shape, "draw")); + }, + [editor] + ); +} +function useCanRedo() { + const editor = useEditor(); + return useValue("useCanRedo", () => editor.getCanRedo(), [editor]); +} +function useCanUndo() { + const editor = useEditor(); + return useValue("useCanUndo", () => editor.getCanUndo(), [editor]); +} + +function DefaultActionsMenuContent() { + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(AlignMenuItems, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(DistributeMenuItems, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(StackMenuItems, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ReorderMenuItems, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomOrRotateMenuItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCWMenuItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(EditLinkMenuItem$1, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(GroupOrUngroupMenuItem, {}) + ] }); +} +function AlignMenuItems() { + const twoSelected = useUnlockedSelectedShapesCount(2); + const isInSelectState = useIsInSelectState(); + const enabled = twoSelected && isInSelectState; + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "align-left", disabled: !enabled }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "align-center-horizontal", disabled: !enabled }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "align-right", disabled: !enabled }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "stretch-horizontal", disabled: !enabled }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "align-top", disabled: !enabled }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "align-center-vertical", disabled: !enabled }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "align-bottom", disabled: !enabled }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "stretch-vertical", disabled: !enabled }) + ] }); +} +function DistributeMenuItems() { + const threeSelected = useUnlockedSelectedShapesCount(3); + const isInSelectState = useIsInSelectState(); + const enabled = threeSelected && isInSelectState; + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "distribute-horizontal", disabled: !enabled }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "distribute-vertical", disabled: !enabled }) + ] }); +} +function StackMenuItems() { + const threeStackableItems = useThreeStackableItems(); + const isInSelectState = useIsInSelectState(); + const enabled = threeStackableItems && isInSelectState; + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "stack-horizontal", disabled: !enabled }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "stack-vertical", disabled: !enabled }) + ] }); +} +function ReorderMenuItems() { + const oneSelected = useUnlockedSelectedShapesCount(1); + const isInSelectState = useIsInSelectState(); + const enabled = oneSelected && isInSelectState; + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "send-to-back", disabled: !enabled }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "send-backward", disabled: !enabled }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "bring-forward", disabled: !enabled }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "bring-to-front", disabled: !enabled }) + ] }); +} +function ZoomOrRotateMenuItem() { + const breakpoint = useBreakpoint(); + return breakpoint < PORTRAIT_BREAKPOINT.TABLET_SM ? /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomTo100MenuItem$1, {}) : /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCCWMenuItem, {}); +} +function ZoomTo100MenuItem$1() { + const editor = useEditor(); + const isZoomedTo100 = useValue("zoom is 1", () => editor.getZoomLevel() === 1, [editor]); + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "zoom-to-100", disabled: isZoomedTo100 }); +} +function RotateCCWMenuItem() { + const oneSelected = useUnlockedSelectedShapesCount(1); + const isInSelectState = useIsInSelectState(); + const enabled = oneSelected && isInSelectState; + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "rotate-ccw", disabled: !enabled }); +} +function RotateCWMenuItem() { + const oneSelected = useUnlockedSelectedShapesCount(1); + const isInSelectState = useIsInSelectState(); + const enabled = oneSelected && isInSelectState; + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "rotate-cw", disabled: !enabled }); +} +function EditLinkMenuItem$1() { + const showEditLink = useHasLinkShapeSelected(); + const isInSelectState = useIsInSelectState(); + const enabled = showEditLink && isInSelectState; + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "edit-link", disabled: !enabled }); +} +function GroupOrUngroupMenuItem() { + const allowGroup = useAllowGroup(); + const allowUngroup = useAllowUngroup(); + return allowGroup ? /* @__PURE__ */ jsxRuntimeExports.jsx(GroupMenuItem$1, {}) : allowUngroup ? /* @__PURE__ */ jsxRuntimeExports.jsx(UngroupMenuItem$1, {}) : /* @__PURE__ */ jsxRuntimeExports.jsx(GroupMenuItem$1, {}); +} +function GroupMenuItem$1() { + const twoSelected = useUnlockedSelectedShapesCount(2); + const isInSelectState = useIsInSelectState(); + const enabled = twoSelected && isInSelectState; + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "group", disabled: !enabled }); +} +function UngroupMenuItem$1() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "ungroup" }); +} + +const DefaultActionsMenu = reactExports.memo(function DefaultActionsMenu2({ + children +}) { + const msg = useTranslation(); + const breakpoint = useBreakpoint(); + const isReadonlyMode = useReadonly(); + const ref = reactExports.useRef(null); + usePassThroughWheelEvents(ref); + const editor = useEditor(); + const isInAcceptableReadonlyState = useValue( + "should display quick actions when in readonly", + () => editor.isInAny("hand", "zoom"), + [editor] + ); + const content = children ?? /* @__PURE__ */ jsxRuntimeExports.jsx(DefaultActionsMenuContent, {}); + if (isReadonlyMode && !isInAcceptableReadonlyState) return; + return /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiPopover, { id: "actions-menu", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiPopoverTrigger, { children: /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButton, + { + type: "icon", + "data-testid": "actions-menu.button", + title: msg("actions-menu.title"), + children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonIcon, { icon: "dots-vertical", small: true }) + } + ) }), + /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiPopoverContent, + { + side: breakpoint >= PORTRAIT_BREAKPOINT.TABLET ? "bottom" : "top", + sideOffset: 6, + children: /* @__PURE__ */ jsxRuntimeExports.jsx( + "div", + { + ref, + className: "tlui-actions-menu tlui-buttons__grid", + "data-testid": "actions-menu.content", + children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuContextProvider, { type: "icons", sourceId: "actions-menu", children: content }) + } + ) + } + ) + ] }); +}); + +function ToggleAutoSizeMenuItem() { + const shouldDisplay = useShowAutoSizeToggle(); + if (!shouldDisplay) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "toggle-auto-size" }); +} +function EditLinkMenuItem() { + const shouldDisplay = useHasLinkShapeSelected(); + if (!shouldDisplay) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "edit-link" }); +} +function DuplicateMenuItem() { + const shouldDisplay = useUnlockedSelectedShapesCount(1); + if (!shouldDisplay) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "duplicate" }); +} +function FlattenMenuItem() { + const editor = useEditor(); + const shouldDisplay = useValue( + "should display flatten option", + () => { + const selectedShapeIds = editor.getSelectedShapeIds(); + if (selectedShapeIds.length === 0) return false; + const onlySelectedShape = editor.getOnlySelectedShape(); + if (onlySelectedShape && editor.isShapeOfType(onlySelectedShape, "image")) { + return false; + } + return true; + }, + [editor] + ); + if (!shouldDisplay) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "flatten-to-image" }); +} +function GroupMenuItem() { + const shouldDisplay = useAllowGroup(); + if (!shouldDisplay) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "group" }); +} +function UngroupMenuItem() { + const shouldDisplay = useAllowUngroup(); + if (!shouldDisplay) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "ungroup" }); +} +function RemoveFrameMenuItem() { + const editor = useEditor(); + const shouldDisplay = useValue( + "allow unframe", + () => { + const selectedShapes = editor.getSelectedShapes(); + if (selectedShapes.length === 0) return false; + return selectedShapes.every((shape) => editor.isShapeOfType(shape, "frame")); + }, + [editor] + ); + if (!shouldDisplay) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "remove-frame" }); +} +function FitFrameToContentMenuItem() { + const editor = useEditor(); + const shouldDisplay = useValue( + "allow fit frame to content", + () => { + const onlySelectedShape = editor.getOnlySelectedShape(); + if (!onlySelectedShape) return false; + return editor.isShapeOfType(onlySelectedShape, "frame") && editor.getSortedChildIdsForParent(onlySelectedShape).length > 0; + }, + [editor] + ); + if (!shouldDisplay) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "fit-frame-to-content" }); +} +function ToggleLockMenuItem() { + const editor = useEditor(); + const shouldDisplay = useValue("selected shapes", () => editor.getSelectedShapes().length > 0, [ + editor + ]); + if (!shouldDisplay) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "toggle-lock" }); +} +function ToggleTransparentBgMenuItem() { + const editor = useEditor(); + const isTransparentBg = useValue( + "isTransparentBg", + () => !editor.getInstanceState().exportBackground, + [editor] + ); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiMenuActionCheckboxItem, + { + actionId: "toggle-transparent", + checked: isTransparentBg, + toggle: true + } + ); +} +function UnlockAllMenuItem() { + const editor = useEditor(); + const shouldDisplay = useValue("any shapes", () => editor.getCurrentPageShapeIds().size > 0, [ + editor + ]); + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "unlock-all", disabled: !shouldDisplay }); +} +function ZoomTo100MenuItem() { + const editor = useEditor(); + const isZoomedTo100 = useValue("zoomed to 100", () => editor.getZoomLevel() === 1, [editor]); + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "zoom-to-100", noClose: true, disabled: isZoomedTo100 }); +} +function ZoomToFitMenuItem() { + const editor = useEditor(); + const hasShapes = useValue("has shapes", () => editor.getCurrentPageShapeIds().size > 0, [editor]); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiMenuActionItem, + { + actionId: "zoom-to-fit", + disabled: !hasShapes, + "data-testid": "minimap.zoom-menu.zoom-to-fit", + noClose: true + } + ); +} +function ZoomToSelectionMenuItem() { + const editor = useEditor(); + const hasSelected = useValue("has shapes", () => editor.getSelectedShapeIds().length > 0, [ + editor + ]); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiMenuActionItem, + { + actionId: "zoom-to-selection", + disabled: !hasSelected, + "data-testid": "minimap.zoom-menu.zoom-to-selection", + noClose: true + } + ); +} +function ClipboardMenuGroup() { + return /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuGroup, { id: "clipboard", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(CutMenuItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(CopyMenuItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(PasteMenuItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(DuplicateMenuItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(DeleteMenuItem, {}) + ] }); +} +function CopyAsMenuGroup() { + const editor = useEditor(); + const atLeastOneShapeOnPage = useValue( + "atLeastOneShapeOnPage", + () => editor.getCurrentPageShapeIds().size > 0, + [editor] + ); + return /* @__PURE__ */ jsxRuntimeExports.jsxs( + TldrawUiMenuSubmenu, + { + id: "copy-as", + label: "context-menu.copy-as", + size: "small", + disabled: !atLeastOneShapeOnPage, + children: [ + /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuGroup, { id: "copy-as-group", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "copy-as-svg" }), + Boolean(window.navigator.clipboard?.write) && /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "copy-as-png" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "copy-as-json" }) + ] }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuGroup, { id: "copy-as-bg", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ToggleTransparentBgMenuItem, {}) }) + ] + } + ); +} +function CutMenuItem() { + const shouldDisplay = useUnlockedSelectedShapesCount(1); + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "cut", disabled: !shouldDisplay }); +} +function CopyMenuItem() { + const shouldDisplay = useAnySelectedShapesCount(1); + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "copy", disabled: !shouldDisplay }); +} +function PasteMenuItem() { + const shouldDisplay = showMenuPaste; + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "paste", disabled: !shouldDisplay }); +} +function ConversionsMenuGroup() { + const editor = useEditor(); + const atLeastOneShapeOnPage = useValue( + "atLeastOneShapeOnPage", + () => editor.getCurrentPageShapeIds().size > 0, + [editor] + ); + if (!atLeastOneShapeOnPage) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuGroup, { id: "conversions", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(CopyAsMenuGroup, {}), + /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuSubmenu, { id: "export-as", label: "context-menu.export-as", size: "small", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuGroup, { id: "export-as-group", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "export-as-svg" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "export-as-png" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "export-as-json" }) + ] }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuGroup, { id: "export-as-bg", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ToggleTransparentBgMenuItem, {}) }) + ] }) + ] }); +} +function SelectAllMenuItem() { + const editor = useEditor(); + const atLeastOneShapeOnPage = useValue( + "atLeastOneShapeOnPage", + () => editor.getCurrentPageShapeIds().size > 0, + [editor] + ); + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "select-all", disabled: !atLeastOneShapeOnPage }); +} +function DeleteMenuItem() { + const oneSelected = useUnlockedSelectedShapesCount(1); + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "delete", disabled: !oneSelected }); +} +function EditMenuSubmenu() { + const isReadonlyMode = useReadonly(); + if (!useAnySelectedShapesCount(1)) return null; + if (isReadonlyMode) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuSubmenu, { id: "edit", label: "context-menu.edit", size: "small", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(GroupMenuItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(UngroupMenuItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(FlattenMenuItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(EditLinkMenuItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(FitFrameToContentMenuItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(RemoveFrameMenuItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ConvertToEmbedMenuItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ConvertToBookmarkMenuItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ToggleAutoSizeMenuItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ToggleLockMenuItem, {}) + ] }); +} +function ArrangeMenuSubmenu() { + const twoSelected = useUnlockedSelectedShapesCount(2); + const onlyFlippableShapeSelected = useOnlyFlippableShape(); + const isReadonlyMode = useReadonly(); + if (isReadonlyMode) return null; + if (!(twoSelected || onlyFlippableShapeSelected)) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuSubmenu, { id: "arrange", label: "context-menu.arrange", size: "small", children: [ + twoSelected && /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuGroup, { id: "align", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "align-left" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "align-center-horizontal" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "align-right" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "align-top" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "align-center-vertical" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "align-bottom" }) + ] }), + /* @__PURE__ */ jsxRuntimeExports.jsx(DistributeMenuGroup, {}), + twoSelected && /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuGroup, { id: "stretch", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "stretch-horizontal" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "stretch-vertical" }) + ] }), + (twoSelected || onlyFlippableShapeSelected) && /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuGroup, { id: "flip", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "flip-horizontal" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "flip-vertical" }) + ] }), + /* @__PURE__ */ jsxRuntimeExports.jsx(OrderMenuGroup, {}) + ] }); +} +function DistributeMenuGroup() { + const threeSelected = useUnlockedSelectedShapesCount(3); + if (!threeSelected) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuGroup, { id: "distribute", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "distribute-horizontal" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "distribute-vertical" }) + ] }); +} +function OrderMenuGroup() { + const twoSelected = useUnlockedSelectedShapesCount(2); + const threeStackableItems = useThreeStackableItems(); + if (!twoSelected) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuGroup, { id: "order", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "pack" }), + threeStackableItems && /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "stack-horizontal" }), + threeStackableItems && /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "stack-vertical" }) + ] }); +} +function ReorderMenuSubmenu() { + const isReadonlyMode = useReadonly(); + const oneSelected = useUnlockedSelectedShapesCount(1); + if (isReadonlyMode) return null; + if (!oneSelected) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuSubmenu, { id: "reorder", label: "context-menu.reorder", size: "small", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuGroup, { id: "reorder", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "bring-to-front" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "bring-forward" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "send-backward" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "send-to-back" }) + ] }) }); +} +function MoveToPageMenu() { + const editor = useEditor(); + const pages = useValue("pages", () => editor.getPages(), [editor]); + const currentPageId = useValue("current page id", () => editor.getCurrentPageId(), [editor]); + const { addToast } = useToasts(); + const trackEvent = useUiEvents(); + const isReadonlyMode = useReadonly(); + const oneSelected = useUnlockedSelectedShapesCount(1); + if (!oneSelected) return null; + if (isReadonlyMode) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuSubmenu, { id: "move-to-page", label: "context-menu.move-to-page", size: "small", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuGroup, { id: "pages", children: pages.map((page) => /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiMenuItem, + { + id: page.id, + disabled: currentPageId === page.id, + label: page.name.length > 30 ? `${page.name.slice(0, 30)}\u2026` : page.name, + onSelect: () => { + editor.markHistoryStoppingPoint("move_shapes_to_page"); + editor.moveShapesToPage(editor.getSelectedShapeIds(), page.id); + const toPage = editor.getPage(page.id); + if (toPage) { + addToast({ + title: "Changed Page", + description: `Moved to ${toPage.name}.`, + actions: [ + { + label: "Go Back", + type: "primary", + onClick: () => { + editor.markHistoryStoppingPoint("change-page"); + editor.setCurrentPage(currentPageId); + } + } + ] + }); + } + trackEvent("move-to-page", { source: "context-menu" }); + } + }, + page.id + )) }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuGroup, { id: "new-page", children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "move-to-new-page" }) }) + ] }); +} +function ConvertToBookmarkMenuItem() { + const editor = useEditor(); + const oneEmbedSelected = useValue( + "oneEmbedSelected", + () => { + const onlySelectedShape = editor.getOnlySelectedShape(); + if (!onlySelectedShape) return false; + return !!(editor.isShapeOfType(onlySelectedShape, "embed") && onlySelectedShape.props.url && !editor.isShapeOrAncestorLocked(onlySelectedShape)); + }, + [editor] + ); + if (!oneEmbedSelected) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "convert-to-bookmark" }); +} +function ConvertToEmbedMenuItem() { + const editor = useEditor(); + const getEmbedDefinition = useGetEmbedDefinition(); + const oneEmbeddableBookmarkSelected = useValue( + "oneEmbeddableBookmarkSelected", + () => { + const onlySelectedShape = editor.getOnlySelectedShape(); + if (!onlySelectedShape) return false; + return !!(editor.isShapeOfType(onlySelectedShape, "bookmark") && onlySelectedShape.props.url && getEmbedDefinition(onlySelectedShape.props.url) && !editor.isShapeOrAncestorLocked(onlySelectedShape)); + }, + [editor] + ); + if (!oneEmbeddableBookmarkSelected) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "convert-to-embed" }); +} +function ToggleSnapModeItem() { + const editor = useEditor(); + const isSnapMode = useValue("isSnapMode", () => editor.user.getIsSnapMode(), [editor]); + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionCheckboxItem, { actionId: "toggle-snap-mode", checked: isSnapMode }); +} +function ToggleToolLockItem() { + const editor = useEditor(); + const isToolLock = useValue("isToolLock", () => editor.getInstanceState().isToolLocked, [editor]); + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionCheckboxItem, { actionId: "toggle-tool-lock", checked: isToolLock }); +} +function ToggleGridItem() { + const editor = useEditor(); + const isGridMode = useValue("isGridMode", () => editor.getInstanceState().isGridMode, [editor]); + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionCheckboxItem, { actionId: "toggle-grid", checked: isGridMode }); +} +function ToggleWrapModeItem() { + const editor = useEditor(); + const isWrapMode = useValue("isWrapMode", () => editor.user.getIsWrapMode(), [editor]); + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionCheckboxItem, { actionId: "toggle-wrap-mode", checked: isWrapMode }); +} +function ToggleFocusModeItem() { + const editor = useEditor(); + const isFocusMode = useValue("isFocusMode", () => editor.getInstanceState().isFocusMode, [editor]); + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionCheckboxItem, { actionId: "toggle-focus-mode", checked: isFocusMode }); +} +function ToggleEdgeScrollingItem() { + const editor = useEditor(); + const edgeScrollSpeed = useValue("edgeScrollSpeed", () => editor.user.getEdgeScrollSpeed(), [ + editor + ]); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiMenuActionCheckboxItem, + { + actionId: "toggle-edge-scrolling", + checked: edgeScrollSpeed === 1 + } + ); +} +function ToggleReduceMotionItem() { + const editor = useEditor(); + const animationSpeed = useValue("animationSpeed", () => editor.user.getAnimationSpeed(), [editor]); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiMenuActionCheckboxItem, + { + actionId: "toggle-reduce-motion", + checked: animationSpeed === 0 + } + ); +} +function ToggleDebugModeItem() { + const editor = useEditor(); + const isDebugMode = useValue("isDebugMode", () => editor.getInstanceState().isDebugMode, [editor]); + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionCheckboxItem, { actionId: "toggle-debug-mode", checked: isDebugMode }); +} +function ToggleDynamicSizeModeItem() { + const editor = useEditor(); + const isDynamicResizeMode = useValue( + "dynamic resize", + () => editor.user.getIsDynamicResizeMode(), + [editor] + ); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiMenuActionCheckboxItem, + { + actionId: "toggle-dynamic-size-mode", + checked: isDynamicResizeMode + } + ); +} +function TogglePasteAtCursorItem() { + const editor = useEditor(); + const pasteAtCursor = useValue("paste at cursor", () => editor.user.getIsPasteAtCursorMode(), [ + editor + ]); + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionCheckboxItem, { actionId: "toggle-paste-at-cursor", checked: pasteAtCursor }); +} +function CursorChatItem() { + const editor = useEditor(); + const shouldShow = useValue( + "show cursor chat", + () => editor.getCurrentToolId() === "select" && !editor.getInstanceState().isCoarsePointer, + [editor] + ); + if (!shouldShow) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "open-cursor-chat" }); +} + +function DefaultContextMenuContent() { + const editor = useEditor(); + const showCollaborationUi = useShowCollaborationUi(); + const selectToolActive = useValue( + "isSelectToolActive", + () => editor.getCurrentToolId() === "select", + [editor] + ); + const isSinglePageMode = useValue("isSinglePageMode", () => editor.options.maxPages <= 1, [ + editor + ]); + if (!selectToolActive) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + showCollaborationUi && /* @__PURE__ */ jsxRuntimeExports.jsx(CursorChatItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuGroup, { id: "modify", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(EditMenuSubmenu, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ArrangeMenuSubmenu, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ReorderMenuSubmenu, {}), + !isSinglePageMode && /* @__PURE__ */ jsxRuntimeExports.jsx(MoveToPageMenu, {}) + ] }), + /* @__PURE__ */ jsxRuntimeExports.jsx(ClipboardMenuGroup, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ConversionsMenuGroup, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuGroup, { id: "select-all", children: /* @__PURE__ */ jsxRuntimeExports.jsx(SelectAllMenuItem, {}) }) + ] }); +} + +const DefaultContextMenu = reactExports.memo(function DefaultContextMenu2({ + children, + disabled = false +}) { + const editor = useEditor(); + const { Canvas } = useEditorComponents(); + const cb = reactExports.useCallback( + (isOpen2) => { + if (!isOpen2) { + const onlySelectedShape = editor.getOnlySelectedShape(); + if (onlySelectedShape && editor.isShapeOrAncestorLocked(onlySelectedShape)) { + editor.setSelectedShapes([]); + } + } else { + if (editor.getInstanceState().isCoarsePointer) { + const selectedShapes = editor.getSelectedShapes(); + const { + inputs: { currentPagePoint } + } = editor; + const shapesAtPoint = editor.getShapesAtPoint(currentPagePoint); + if ( + // if there are no selected shapes + !editor.getSelectedShapes().length || // OR if none of the shapes at the point include the selected shape + !shapesAtPoint.some((s) => selectedShapes.includes(s)) + ) { + const lockedShapes = shapesAtPoint.filter((s) => editor.isShapeOrAncestorLocked(s)); + if (lockedShapes.length) { + editor.select(...lockedShapes.map((s) => s.id)); + } + } + } + } + }, + [editor] + ); + const container = useContainer(); + const [isOpen, handleOpenChange] = useMenuIsOpen("context menu", cb); + const content = children ?? /* @__PURE__ */ jsxRuntimeExports.jsx(DefaultContextMenuContent, {}); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(Root2$3, { dir: "ltr", onOpenChange: handleOpenChange, modal: false, children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(Trigger$2, { onContextMenu: void 0, dir: "ltr", disabled, children: Canvas ? /* @__PURE__ */ jsxRuntimeExports.jsx(Canvas, {}) : null }), + isOpen && /* @__PURE__ */ jsxRuntimeExports.jsx(Portal2$1, { container, children: /* @__PURE__ */ jsxRuntimeExports.jsx( + Content2$2, + { + className: "tlui-menu scrollable", + "data-testid": "context-menu", + alignOffset: -4, + collisionPadding: 4, + onContextMenu: preventDefault, + children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuContextProvider, { type: "context-menu", sourceId: "context-menu", children: content }) + } + ) }) + ] }); +}); + +const CHAT_MESSAGE_TIMEOUT_CLOSING = 2e3; +const CHAT_MESSAGE_TIMEOUT_CHATTING = 5e3; +const CursorChatBubble = track(function CursorChatBubble2() { + const editor = useEditor(); + const { isChatting, chatMessage } = editor.getInstanceState(); + const rTimeout = reactExports.useRef(-1); + const [value, setValue] = reactExports.useState(""); + reactExports.useEffect(() => { + const closingUp = !isChatting && chatMessage; + if (closingUp || isChatting) { + const duration = isChatting ? CHAT_MESSAGE_TIMEOUT_CHATTING : CHAT_MESSAGE_TIMEOUT_CLOSING; + rTimeout.current = editor.timers.setTimeout(() => { + editor.updateInstanceState({ chatMessage: "", isChatting: false }); + setValue(""); + editor.focus(); + }, duration); + } + return () => { + clearTimeout(rTimeout.current); + }; + }, [editor, chatMessage, isChatting]); + if (isChatting) + return /* @__PURE__ */ jsxRuntimeExports.jsx(CursorChatInput, { value, setValue, chatMessage }); + return chatMessage.trim() ? /* @__PURE__ */ jsxRuntimeExports.jsx(NotEditingChatMessage, { chatMessage }) : null; +}); +function usePositionBubble(ref) { + const editor = useEditor(); + reactExports.useLayoutEffect(() => { + const elm = ref.current; + if (!elm) return; + const { x, y } = editor.inputs.currentScreenPoint; + ref.current?.style.setProperty("transform", `translate(${x}px, ${y}px)`); + function positionChatBubble(e) { + const { minX, minY } = editor.getViewportScreenBounds(); + ref.current?.style.setProperty( + "transform", + `translate(${e.clientX - minX}px, ${e.clientY - minY}px)` + ); + } + window.addEventListener("pointermove", positionChatBubble); + return () => { + window.removeEventListener("pointermove", positionChatBubble); + }; + }, [ref, editor]); +} +const NotEditingChatMessage = ({ chatMessage }) => { + const editor = useEditor(); + const ref = reactExports.useRef(null); + usePositionBubble(ref); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + "div", + { + ref, + className: "tl-cursor-chat tl-cursor-chat__bubble", + style: { backgroundColor: editor.user.getColor() }, + children: chatMessage + } + ); +}; +const CursorChatInput = track(function CursorChatInput2({ + chatMessage, + value, + setValue +}) { + const editor = useEditor(); + const msg = useTranslation(); + const ref = reactExports.useRef(null); + const placeholder = chatMessage || msg("cursor-chat.type-to-chat"); + usePositionBubble(ref); + reactExports.useLayoutEffect(() => { + const elm = ref.current; + if (!elm) return; + const textMeasurement = editor.textMeasure.measureText(value || placeholder, { + fontFamily: "var(--font-body)", + fontSize: 12, + fontWeight: "500", + fontStyle: "normal", + maxWidth: null, + lineHeight: 1, + padding: "6px" + }); + elm.style.setProperty("width", textMeasurement.w + "px"); + }, [editor, value, placeholder]); + reactExports.useLayoutEffect(() => { + const raf = editor.timers.requestAnimationFrame(() => { + ref.current?.focus(); + }); + return () => { + cancelAnimationFrame(raf); + }; + }, [editor]); + const stopChatting = reactExports.useCallback(() => { + editor.updateInstanceState({ isChatting: false }); + editor.focus(); + }, [editor]); + const handleChange = reactExports.useCallback( + (e) => { + const { value: value2 } = e.target; + setValue(value2.slice(0, 64)); + editor.updateInstanceState({ chatMessage: value2 }); + }, + [editor, setValue] + ); + const handleKeyDown = reactExports.useCallback( + (e) => { + const elm = ref.current; + if (!elm) return; + const { value: currentValue } = elm; + switch (e.key) { + case "Enter": { + preventDefault(e); + e.stopPropagation(); + if (!currentValue) { + stopChatting(); + return; + } + setValue(""); + break; + } + case "Escape": { + preventDefault(e); + e.stopPropagation(); + stopChatting(); + break; + } + } + }, + [stopChatting, setValue] + ); + const handlePaste = reactExports.useCallback((e) => { + e.stopPropagation(); + }, []); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + "input", + { + ref, + className: `tl-cursor-chat`, + style: { backgroundColor: editor.user.getColor() }, + onBlur: stopChatting, + onChange: handleChange, + onKeyDown: handleKeyDown, + onPaste: handlePaste, + value, + placeholder, + spellCheck: false + } + ); +}); + +function TldrawUiButtonCheck({ checked }) { + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiIcon, { icon: checked ? "check" : "none", className: "tlui-button__icon", small: true }); +} + +function DefaultDebugMenuContent() { + const editor = useEditor(); + const { addToast } = useToasts(); + const { addDialog } = useDialogs(); + const [error, setError] = React.useState(false); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuGroup, { id: "items", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiMenuItem, + { + id: "add-toast", + onSelect: () => { + addToast({ + id: uniqueId(), + title: "Something good happened", + description: "Hey, attend to this thing over here. It might be important!", + keepOpen: true, + severity: "success" + // icon?: string + // title?: string + // description?: string + // actions?: TLUiToastAction[] + }); + addToast({ + id: uniqueId(), + title: "Something happened", + description: "Hey, attend to this thing over here. It might be important!", + keepOpen: true, + severity: "info", + actions: [ + { + label: "Primary", + type: "primary", + onClick: () => { + } + }, + { + label: "Normal", + type: "normal", + onClick: () => { + } + }, + { + label: "Danger", + type: "danger", + onClick: () => { + } + } + ] + // icon?: string + // title?: string + // description?: string + // actions?: TLUiToastAction[] + }); + addToast({ + id: uniqueId(), + title: "Something maybe bad happened", + description: "Hey, attend to this thing over here. It might be important!", + keepOpen: true, + severity: "warning", + actions: [ + { + label: "Primary", + type: "primary", + onClick: () => { + } + }, + { + label: "Normal", + type: "normal", + onClick: () => { + } + }, + { + label: "Danger", + type: "danger", + onClick: () => { + } + } + ] + }); + addToast({ + id: uniqueId(), + title: "Something bad happened", + severity: "error", + keepOpen: true + }); + }, + label: untranslated("Show toast") + } + ), + /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiMenuItem, + { + id: "show-dialog", + label: "Show dialog", + onSelect: () => { + addDialog({ + component: ({ onClose }) => /* @__PURE__ */ jsxRuntimeExports.jsx( + ExampleDialog, + { + displayDontShowAgain: true, + onCancel: () => onClose(), + onContinue: () => onClose() + } + ), + onClose: () => { + } + }); + } + } + ), + /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiMenuItem, + { + id: "create-shapes", + label: "Create 100 shapes", + onSelect: () => createNShapes(editor, 100) + } + ), + /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiMenuItem, + { + id: "count-nodes", + label: "Count shapes / nodes", + onSelect: () => { + const selectedShapes = editor.getSelectedShapes(); + const shapes = selectedShapes.length === 0 ? editor.getRenderingShapes() : selectedShapes; + window.alert( + `Shapes ${shapes.length}, DOM nodes:${document.querySelector(".tl-shapes").querySelectorAll("*")?.length}` + ); + } + } + ), + (() => { + if (error) throw Error("oh no!"); + return null; + })(), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuItem, { id: "throw-error", onSelect: () => setError(true), label: "Throw error" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuItem, { id: "hard-reset", onSelect: hardResetEditor, label: "Hard reset" }) + ] }), + /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuGroup, { id: "flags", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(DebugFlags, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(FeatureFlags, {}) + ] }) + ] }); +} +function DebugFlags() { + const items = Object.values(debugFlags); + if (!items.length) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuSubmenu, { id: "debug flags", label: "Debug Flags", children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuGroup, { id: "debug flags", children: items.map((flag) => /* @__PURE__ */ jsxRuntimeExports.jsx(DebugFlagToggle, { flag }, flag.name)) }) }); +} +function FeatureFlags() { + const items = Object.values(featureFlags); + if (!items.length) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuSubmenu, { id: "feature flags", label: "Feature Flags", children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuGroup, { id: "feature flags", children: items.map((flag) => /* @__PURE__ */ jsxRuntimeExports.jsx(DebugFlagToggle, { flag }, flag.name)) }) }); +} +function ExampleDialog({ + title = "title", + body = "hello hello hello", + cancel = "Cancel", + confirm = "Continue", + displayDontShowAgain = false, + onCancel, + onContinue +}) { + const [dontShowAgain, setDontShowAgain] = React.useState(false); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiDialogHeader, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiDialogTitle, { children: title }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiDialogCloseButton, {}) + ] }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiDialogBody, { style: { maxWidth: 350 }, children: body }), + /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiDialogFooter, { className: "tlui-dialog__footer__actions", children: [ + displayDontShowAgain && /* @__PURE__ */ jsxRuntimeExports.jsxs( + TldrawUiButton, + { + type: "normal", + onClick: () => setDontShowAgain(!dontShowAgain), + style: { marginRight: "auto" }, + children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonCheck, { checked: dontShowAgain }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonLabel, { children: "Don\u2019t show again" }) + ] + } + ), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButton, { type: "normal", onClick: onCancel, children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonLabel, { children: cancel }) }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButton, { type: "primary", onClick: async () => onContinue(), children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonLabel, { children: confirm }) }) + ] }) + ] }); +} +const DebugFlagToggle = track(function DebugFlagToggle2({ + flag, + onChange +}) { + const value = flag.get(); + return ( + /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiMenuCheckboxItem, + { + id: flag.name, + title: flag.name, + label: flag.name.replace(/([a-z0-9])([A-Z])/g, (m) => `${m[0]} ${m[1].toLowerCase()}`).replace(/^[a-z]/, (m) => m.toUpperCase()), + checked: value, + onSelect: () => { + flag.set(!value); + onChange?.(!value); + } + } + ) + ); +}); +let t = 0; +function createNShapes(editor, n) { + const shapesToCreate = Array(n); + const cols = Math.floor(Math.sqrt(n)); + for (let i = 0; i < n; i++) { + t++; + shapesToCreate[i] = { + id: createShapeId("box" + t), + type: "geo", + x: i % cols * 132, + y: Math.floor(i / cols) * 132 + }; + } + editor.run(() => { + editor.createShapes(shapesToCreate).setSelectedShapes(shapesToCreate.map((s) => s.id)); + }); +} + +function DefaultDebugMenu({ children }) { + const content = children ?? /* @__PURE__ */ jsxRuntimeExports.jsx(DefaultDebugMenuContent, {}); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiDropdownMenuRoot, { id: "debug", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiDropdownMenuTrigger, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButton, { type: "icon", title: "Debug menu", children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonIcon, { icon: "dots-horizontal" }) }) }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiDropdownMenuContent, { side: "top", align: "end", alignOffset: 0, children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuContextProvider, { type: "menu", sourceId: "debug-panel", children: content }) }) + ] }); +} + +const DefaultDebugPanel = reactExports.memo(function DefaultDebugPanel2() { + const { DebugMenu } = useTldrawUiComponents(); + const ref = reactExports.useRef(null); + usePassThroughWheelEvents(ref); + return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ref, className: "tlui-debug-panel", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(CurrentState, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(FPS, {}), + DebugMenu && /* @__PURE__ */ jsxRuntimeExports.jsx(DebugMenu, {}) + ] }); +}); +function useTick(isEnabled = true) { + const [_, setTick] = reactExports.useState(0); + const editor = useEditor(); + reactExports.useEffect(() => { + if (!isEnabled) return; + const update = () => setTick((tick) => tick + 1); + editor.on("tick", update); + return () => { + editor.off("tick", update); + }; + }, [editor, isEnabled]); +} +const CurrentState = track(function CurrentState2() { + useTick(); + const editor = useEditor(); + const path = editor.getPath(); + const hoverShape = editor.getHoveredShape(); + const selectedShape = editor.getOnlySelectedShape(); + const shape = path === "select.idle" || !path.includes("select.") ? hoverShape : selectedShape; + const shapeInfo = shape && path.includes("select.") ? ` / ${shape.type || ""}${"geo" in shape.props ? " / " + shape.props.geo : ""} / [${Vec.ToInt(editor.getPointInShapeSpace(shape, editor.inputs.currentPagePoint))}]` : ""; + const ruler = path.startsWith("select.") && !path.includes(".idle") ? ` / [${Vec.ToInt(editor.inputs.originPagePoint)}] \u2192 [${Vec.ToInt( + editor.inputs.currentPagePoint + )}] = ${Vec.Dist(editor.inputs.originPagePoint, editor.inputs.currentPagePoint).toFixed(0)}` : ""; + return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tlui-debug-panel__current-state", children: `${path}${shapeInfo}${ruler}` }); +}); +function FPS() { + const editor = useEditor(); + const showFps = useValue("show_fps", () => debugFlags.showFps.get(), [debugFlags]); + const fpsRef = reactExports.useRef(null); + reactExports.useEffect(() => { + if (!showFps) return; + const TICK_LENGTH = 250; + let maxKnownFps = 0; + let raf = -1; + let start = performance.now(); + let currentTickLength = 0; + let framesInCurrentTick = 0; + let isSlow = false; + function loop() { + framesInCurrentTick++; + currentTickLength = performance.now() - start; + if (currentTickLength > TICK_LENGTH) { + const fps = Math.round( + framesInCurrentTick * (TICK_LENGTH / currentTickLength) * (1e3 / TICK_LENGTH) + ); + if (fps > maxKnownFps) { + maxKnownFps = fps; + } + const slowFps = maxKnownFps * 0.75; + if (fps < slowFps && !isSlow || fps >= slowFps && isSlow) { + isSlow = !isSlow; + } + fpsRef.current.innerHTML = `FPS ${fps.toString()}`; + fpsRef.current.className = `tlui-debug-panel__fps` + (isSlow ? ` tlui-debug-panel__fps__slow` : ``); + currentTickLength -= TICK_LENGTH; + framesInCurrentTick = 0; + start = performance.now(); + } + raf = editor.timers.requestAnimationFrame(loop); + } + loop(); + return () => { + cancelAnimationFrame(raf); + }; + }, [showFps, editor]); + if (!showFps) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { ref: fpsRef }); +} + +const DefaultMenuPanel = reactExports.memo(function MenuPanel() { + const breakpoint = useBreakpoint(); + const ref = reactExports.useRef(null); + usePassThroughWheelEvents(ref); + const { MainMenu, QuickActions, ActionsMenu, PageMenu } = useTldrawUiComponents(); + const editor = useEditor(); + const isSinglePageMode = useValue("isSinglePageMode", () => editor.options.maxPages <= 1, [ + editor + ]); + const showQuickActions = editor.options.actionShortcutsLocation === "menu" ? true : editor.options.actionShortcutsLocation === "toolbar" ? false : breakpoint >= PORTRAIT_BREAKPOINT.TABLET; + if (!MainMenu && !PageMenu && !showQuickActions) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { ref, className: "tlui-menu-zone", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "tlui-buttons__horizontal", children: [ + MainMenu && /* @__PURE__ */ jsxRuntimeExports.jsx(MainMenu, {}), + PageMenu && !isSinglePageMode && /* @__PURE__ */ jsxRuntimeExports.jsx(PageMenu, {}), + showQuickActions ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + QuickActions && /* @__PURE__ */ jsxRuntimeExports.jsx(QuickActions, {}), + ActionsMenu && /* @__PURE__ */ jsxRuntimeExports.jsx(ActionsMenu, {}) + ] }) : null + ] }) }); +}); + +function BackToContent() { + const editor = useEditor(); + const actions = useActions(); + const [showBackToContent, setShowBackToContent] = reactExports.useState(false); + const rIsShowing = reactExports.useRef(false); + useQuickReactor( + "toggle showback to content", + () => { + const showBackToContentPrev = rIsShowing.current; + const shapeIds = editor.getCurrentPageShapeIds(); + let showBackToContentNow = false; + if (shapeIds.size) { + showBackToContentNow = shapeIds.size === editor.getCulledShapes().size; + } + if (showBackToContentPrev !== showBackToContentNow) { + setShowBackToContent(showBackToContentNow); + rIsShowing.current = showBackToContentNow; + } + }, + [editor] + ); + if (!showBackToContent) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiMenuActionItem, + { + actionId: "back-to-content", + onSelect: () => { + actions["back-to-content"].onSelect("helper-buttons"); + setShowBackToContent(false); + } + } + ); +} + +function ExitPenMode() { + const editor = useEditor(); + const isPenMode = useValue("is pen mode", () => editor.getInstanceState().isPenMode, [editor]); + if (!isPenMode) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "exit-pen-mode" }); +} + +function StopFollowing() { + const editor = useEditor(); + const actions = useActions(); + const followingUser = useValue( + "is following user", + () => !!editor.getInstanceState().followingUserId, + [editor] + ); + if (!followingUser) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuItem, { ...actions["stop-following"] }); +} + +function DefaultHelperButtonsContent() { + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ExitPenMode, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(BackToContent, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(StopFollowing, {}) + ] }); +} + +function DefaultHelperButtons({ children }) { + const content = children ?? /* @__PURE__ */ jsxRuntimeExports.jsx(DefaultHelperButtonsContent, {}); + return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tlui-helper-buttons", children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuContextProvider, { type: "helper-buttons", sourceId: "helper-buttons", children: content }) }); +} + +function DefaultKeyboardShortcutsDialogContent() { + const showCollaborationUi = useShowCollaborationUi(); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuGroup, { label: "shortcuts-dialog.tools", id: "tools", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "toggle-tool-lock" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "insert-media" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuToolItem, { toolId: "select" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuToolItem, { toolId: "draw" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuToolItem, { toolId: "eraser" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuToolItem, { toolId: "hand" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuToolItem, { toolId: "rectangle" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuToolItem, { toolId: "ellipse" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuToolItem, { toolId: "arrow" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuToolItem, { toolId: "line" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuToolItem, { toolId: "text" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuToolItem, { toolId: "frame" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuToolItem, { toolId: "note" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuToolItem, { toolId: "laser" }), + /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiMenuItem, + { + id: "pointer-down", + label: "tool.pointer-down", + kbd: ",", + onSelect: () => { + } + } + ) + ] }), + /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuGroup, { label: "shortcuts-dialog.preferences", id: "preferences", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "toggle-dark-mode" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "toggle-focus-mode" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "toggle-grid" }) + ] }), + /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuGroup, { label: "shortcuts-dialog.edit", id: "edit", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "undo" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "redo" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "cut" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "copy" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "paste" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "select-all" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "delete" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "duplicate" }) + ] }), + /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuGroup, { label: "shortcuts-dialog.view", id: "view", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "zoom-in" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "zoom-out" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "zoom-to-100" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "zoom-to-fit" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "zoom-to-selection" }) + ] }), + /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuGroup, { label: "shortcuts-dialog.transform", id: "transform", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "bring-to-front" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "bring-forward" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "send-backward" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "send-to-back" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "group" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "ungroup" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "flip-horizontal" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "flip-vertical" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "align-top" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "align-center-vertical" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "align-bottom" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "align-left" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "align-center-horizontal" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "align-right" }) + ] }), + showCollaborationUi && /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuGroup, { label: "shortcuts-dialog.collaboration", id: "collaboration", children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "open-cursor-chat" }) }) + ] }); +} + +const DefaultKeyboardShortcutsDialog = reactExports.memo(function DefaultKeyboardShortcutsDialog2({ + children +}) { + const msg = useTranslation(); + const breakpoint = useBreakpoint(); + const content = children ?? /* @__PURE__ */ jsxRuntimeExports.jsx(DefaultKeyboardShortcutsDialogContent, {}); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiDialogHeader, { className: "tlui-shortcuts-dialog__header", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiDialogTitle, { children: msg("shortcuts-dialog.title") }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiDialogCloseButton, {}) + ] }), + /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiDialogBody, + { + className: classNames("tlui-shortcuts-dialog__body", { + "tlui-shortcuts-dialog__body__mobile": breakpoint <= PORTRAIT_BREAKPOINT.MOBILE_XS, + "tlui-shortcuts-dialog__body__tablet": breakpoint <= PORTRAIT_BREAKPOINT.TABLET + }), + children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuContextProvider, { type: "keyboard-shortcuts", sourceId: "kbd", children: content }) + } + ), + /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tlui-dialog__scrim" }) + ] }); +}); + +function LanguageMenu() { + const editor = useMaybeEditor(); + const trackEvent = useUiEvents(); + const currentLanguage = useValue("locale", () => editor?.user.getLocale(), [editor]); + if (!editor) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuSubmenu, { id: "help menu language", label: "menu.language", children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuGroup, { id: "languages", children: LANGUAGES.map(({ locale, label }) => /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiMenuCheckboxItem, + { + id: `language-${locale}`, + title: locale, + label, + checked: locale === currentLanguage, + readonlyOk: true, + onSelect: () => { + editor.user.updateUserPreferences({ locale }); + trackEvent("change-language", { source: "menu", locale }); + } + }, + locale + )) }) }); +} + +function DefaultHelpMenuContent() { + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(LanguageMenu, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(KeyboardShortcutsMenuItem, {}) + ] }); +} +function KeyboardShortcutsMenuItem() { + const { KeyboardShortcutsDialog } = useTldrawUiComponents(); + const { addDialog } = useDialogs(); + const handleSelect = reactExports.useCallback(() => { + if (KeyboardShortcutsDialog) addDialog({ component: KeyboardShortcutsDialog }); + }, [addDialog, KeyboardShortcutsDialog]); + if (!KeyboardShortcutsDialog) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiMenuItem, + { + id: "keyboard-shortcuts-button", + label: "help-menu.keyboard-shortcuts", + readonlyOk: true, + onSelect: handleSelect + } + ); +} + +function DefaultMainMenuContent() { + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(EditSubmenu, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ViewSubmenu, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ExportFileContentSubMenu, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ExtrasGroup, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(PreferencesGroup, {}) + ] }); +} +function ExportFileContentSubMenu() { + return /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuSubmenu, { id: "export-all-as", label: "context-menu.export-all-as", size: "small", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuGroup, { id: "export-all-as-group", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "export-all-as-svg" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "export-all-as-png" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "export-all-as-json" }) + ] }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuGroup, { id: "export-all-as-bg", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ToggleTransparentBgMenuItem, {}) }) + ] }); +} +function EditSubmenu() { + const editor = useEditor(); + const selectToolActive = useValue( + "isSelectToolActive", + () => editor.getCurrentToolId() === "select", + [editor] + ); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuSubmenu, { id: "edit", label: "menu.edit", disabled: !selectToolActive, children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(UndoRedoGroup, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ClipboardMenuGroup, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ConversionsMenuGroup, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(MiscMenuGroup, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(LockGroup, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuGroup, { id: "select-all", children: /* @__PURE__ */ jsxRuntimeExports.jsx(SelectAllMenuItem, {}) }) + ] }); +} +function MiscMenuGroup() { + return /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuGroup, { id: "misc", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(GroupMenuItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(UngroupMenuItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(EditLinkMenuItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ToggleAutoSizeMenuItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(RemoveFrameMenuItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(FitFrameToContentMenuItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ConvertToEmbedMenuItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ConvertToBookmarkMenuItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(FlattenMenuItem, {}) + ] }); +} +function LockGroup() { + return /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuGroup, { id: "lock", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ToggleLockMenuItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(UnlockAllMenuItem, {}) + ] }); +} +function UndoRedoGroup() { + const canUndo = useCanUndo(); + const canRedo = useCanRedo(); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuGroup, { id: "undo-redo", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "undo", disabled: !canUndo }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "redo", disabled: !canRedo }) + ] }); +} +function ViewSubmenu() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuSubmenu, { id: "view", label: "menu.view", children: /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuGroup, { id: "view-actions", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "zoom-in" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "zoom-out" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomTo100MenuItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomToFitMenuItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomToSelectionMenuItem, {}) + ] }) }); +} +function ExtrasGroup() { + return /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuGroup, { id: "extras", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "insert-embed" }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "insert-media" }) + ] }); +} +function PreferencesGroup() { + return /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuGroup, { id: "preferences", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuSubmenu, { id: "preferences", label: "menu.preferences", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuGroup, { id: "preferences-actions", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(ToggleSnapModeItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ToggleToolLockItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ToggleGridItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ToggleWrapModeItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ToggleFocusModeItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ToggleEdgeScrollingItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ToggleReduceMotionItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ToggleDynamicSizeModeItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(TogglePasteAtCursorItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ToggleDebugModeItem, {}) + ] }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuGroup, { id: "color-scheme", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ColorSchemeMenu, {}) }) + ] }), + /* @__PURE__ */ jsxRuntimeExports.jsx(LanguageMenu, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(KeyboardShortcutsMenuItem, {}) + ] }); +} + +const DefaultMainMenu = reactExports.memo(function DefaultMainMenu2({ children }) { + const container = useContainer(); + const [isOpen, onOpenChange] = useMenuIsOpen("main menu"); + const msg = useTranslation(); + const content = children ?? /* @__PURE__ */ jsxRuntimeExports.jsx(DefaultMainMenuContent, {}); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(Root2$2, { dir: "ltr", open: isOpen, onOpenChange, modal: false, children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(Trigger$1, { asChild: true, dir: "ltr", children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButton, { type: "icon", "data-testid": "main-menu.button", title: msg("menu.title"), children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonIcon, { icon: "menu", small: true }) }) }), + /* @__PURE__ */ jsxRuntimeExports.jsx(Portal2, { container, children: /* @__PURE__ */ jsxRuntimeExports.jsx( + Content2$1, + { + className: "tlui-menu", + side: "bottom", + align: "start", + collisionPadding: 4, + alignOffset: 0, + sideOffset: 6, + children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuContextProvider, { type: "menu", sourceId: "main-menu", children: content }) + } + ) }) + ] }); +}); + +const memo = {}; +function getRgba(colorString) { + if (memo[colorString]) { + return memo[colorString]; + } + const canvas = document.createElement("canvas"); + const context = canvas.getContext("2d"); + context.fillStyle = colorString; + context.fillRect(0, 0, 1, 1); + const [r, g, b, a] = context.getImageData(0, 0, 1, 1).data; + const result = new Float32Array([r / 255, g / 255, b / 255, a / 255]); + memo[colorString] = result; + return result; +} + +const numArcSegmentsPerCorner = 10; +const roundedRectangleDataSize = ( + // num triangles in corners + (// num triangles in outer rects + 4 * 6 * numArcSegmentsPerCorner + // num triangles in center rect + 12 + 4 * 12) +); +function pie(array, { + center, + radius, + numArcSegments = 20, + startAngle = 0, + endAngle = PI2, + offset = 0 +}) { + const angle = (endAngle - startAngle) / numArcSegments; + let i = offset; + for (let a = startAngle; a < endAngle; a += angle) { + array[i++] = center.x; + array[i++] = center.y; + array[i++] = center.x + Math.cos(a) * radius; + array[i++] = center.y + Math.sin(a) * radius; + array[i++] = center.x + Math.cos(a + angle) * radius; + array[i++] = center.y + Math.sin(a + angle) * radius; + } + return array; +} +function rectangle(array, offset, x, y, w, h) { + array[offset++] = x; + array[offset++] = y; + array[offset++] = x; + array[offset++] = y + h; + array[offset++] = x + w; + array[offset++] = y; + array[offset++] = x + w; + array[offset++] = y; + array[offset++] = x; + array[offset++] = y + h; + array[offset++] = x + w; + array[offset++] = y + h; +} +function roundedRectangle(data, box, radius) { + const numArcSegments = numArcSegmentsPerCorner; + radius = Math.min(radius, Math.min(box.w, box.h) / 2); + const innerBox = Box.ExpandBy(box, -radius); + if (innerBox.w <= 0 || innerBox.h <= 0) { + pie(data, { center: box.center, radius, numArcSegments: numArcSegmentsPerCorner * 4 }); + return numArcSegmentsPerCorner * 4 * 6; + } + let offset = 0; + rectangle(data, offset, innerBox.minX, innerBox.minY, innerBox.w, innerBox.h); + offset += 12; + rectangle(data, offset, innerBox.minX, box.minY, innerBox.w, radius); + offset += 12; + rectangle(data, offset, innerBox.maxX, innerBox.minY, radius, innerBox.h); + offset += 12; + rectangle(data, offset, innerBox.minX, innerBox.maxY, innerBox.w, radius); + offset += 12; + rectangle(data, offset, box.minX, innerBox.minY, radius, innerBox.h); + offset += 12; + pie(data, { + numArcSegments, + offset, + center: innerBox.point, + radius, + startAngle: PI$1, + endAngle: PI$1 * 1.5 + }); + offset += numArcSegments * 6; + pie(data, { + numArcSegments, + offset, + center: Vec.Add(innerBox.point, new Vec(innerBox.w, 0)), + radius, + startAngle: PI$1 * 1.5, + endAngle: PI2 + }); + offset += numArcSegments * 6; + pie(data, { + numArcSegments, + offset, + center: Vec.Add(innerBox.point, innerBox.size), + radius, + startAngle: 0, + endAngle: HALF_PI + }); + offset += numArcSegments * 6; + pie(data, { + numArcSegments, + offset, + center: Vec.Add(innerBox.point, new Vec(0, innerBox.h)), + radius, + startAngle: HALF_PI, + endAngle: PI$1 + }); + return roundedRectangleDataSize; +} + +function setupWebGl(canvas) { + if (!canvas) throw new Error("Canvas element not found"); + const context = canvas.getContext("webgl2", { + premultipliedAlpha: false + }); + if (!context) throw new Error("Failed to get webgl2 context"); + const vertexShaderSourceCode = `#version 300 es + precision mediump float; + + in vec2 shapeVertexPosition; + + uniform vec4 canvasPageBounds; + + // taken (with thanks) from + // https://webglfundamentals.org/webgl/lessons/webgl-2d-matrices.html + void main() { + // convert the position from pixels to 0.0 to 1.0 + vec2 zeroToOne = (shapeVertexPosition - canvasPageBounds.xy) / canvasPageBounds.zw; + + // convert from 0->1 to 0->2 + vec2 zeroToTwo = zeroToOne * 2.0; + + // convert from 0->2 to -1->+1 (clipspace) + vec2 clipSpace = zeroToTwo - 1.0; + + gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1); + }`; + const vertexShader = context.createShader(context.VERTEX_SHADER); + if (!vertexShader) { + throw new Error("Failed to create vertex shader"); + } + context.shaderSource(vertexShader, vertexShaderSourceCode); + context.compileShader(vertexShader); + if (!context.getShaderParameter(vertexShader, context.COMPILE_STATUS)) { + throw new Error("Failed to compile vertex shader"); + } + const fragmentShaderSourceCode = `#version 300 es + precision mediump float; + + uniform vec4 fillColor; + out vec4 outputColor; + + void main() { + outputColor = fillColor; + }`; + const fragmentShader = context.createShader(context.FRAGMENT_SHADER); + if (!fragmentShader) { + throw new Error("Failed to create fragment shader"); + } + context.shaderSource(fragmentShader, fragmentShaderSourceCode); + context.compileShader(fragmentShader); + if (!context.getShaderParameter(fragmentShader, context.COMPILE_STATUS)) { + throw new Error("Failed to compile fragment shader"); + } + const program = context.createProgram(); + if (!program) { + throw new Error("Failed to create program"); + } + context.attachShader(program, vertexShader); + context.attachShader(program, fragmentShader); + context.linkProgram(program); + if (!context.getProgramParameter(program, context.LINK_STATUS)) { + throw new Error("Failed to link program"); + } + context.useProgram(program); + const shapeVertexPositionAttributeLocation = context.getAttribLocation( + program, + "shapeVertexPosition" + ); + if (shapeVertexPositionAttributeLocation < 0) { + throw new Error("Failed to get shapeVertexPosition attribute location"); + } + context.enableVertexAttribArray(shapeVertexPositionAttributeLocation); + const canvasPageBoundsLocation = context.getUniformLocation(program, "canvasPageBounds"); + const fillColorLocation = context.getUniformLocation(program, "fillColor"); + const selectedShapesBuffer = context.createBuffer(); + if (!selectedShapesBuffer) throw new Error("Failed to create buffer"); + const unselectedShapesBuffer = context.createBuffer(); + if (!unselectedShapesBuffer) throw new Error("Failed to create buffer"); + return { + context, + selectedShapes: allocateBuffer(context, 1024), + unselectedShapes: allocateBuffer(context, 4096), + viewport: allocateBuffer(context, roundedRectangleDataSize), + collaborators: allocateBuffer(context, 1024), + prepareTriangles(stuff, len) { + context.bindBuffer(context.ARRAY_BUFFER, stuff.buffer); + context.bufferData(context.ARRAY_BUFFER, stuff.vertices, context.STATIC_DRAW, 0, len); + context.enableVertexAttribArray(shapeVertexPositionAttributeLocation); + context.vertexAttribPointer( + shapeVertexPositionAttributeLocation, + 2, + context.FLOAT, + false, + 0, + 0 + ); + }, + drawTrianglesTransparently(len) { + context.enable(context.BLEND); + context.blendFunc(context.SRC_ALPHA, context.ONE_MINUS_SRC_ALPHA); + context.drawArrays(context.TRIANGLES, 0, len / 2); + context.disable(context.BLEND); + }, + drawTriangles(len) { + context.drawArrays(context.TRIANGLES, 0, len / 2); + }, + setFillColor(color) { + context.uniform4fv(fillColorLocation, color); + }, + setCanvasPageBounds(bounds) { + context.uniform4fv(canvasPageBoundsLocation, bounds); + } + }; +} +function allocateBuffer(context, size) { + const buffer = context.createBuffer(); + if (!buffer) throw new Error("Failed to create buffer"); + return { buffer, vertices: new Float32Array(size) }; +} +function appendVertices(bufferStuff, offset, data) { + let len = bufferStuff.vertices.length; + while (len < offset + data.length) { + len *= 2; + } + if (len != bufferStuff.vertices.length) { + const newVertices = new Float32Array(len); + newVertices.set(bufferStuff.vertices); + bufferStuff.vertices = newVertices; + } + bufferStuff.vertices.set(data, offset); +} + +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name); +var __typeError = (msg) => { + throw TypeError(msg); +}; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __decoratorStart = (base) => [, , , __create(null)]; +var __decoratorStrings = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"]; +var __expectFn = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError("Function expected") : fn; +var __decoratorContext = (kind, name, done, metadata, fns) => ({ kind: __decoratorStrings[kind], name, metadata, addInitializer: (fn) => done._ ? __typeError("Already initialized") : fns.push(__expectFn(fn || null)) }); +var __decoratorMetadata = (array, target) => __defNormalProp(target, __knownSymbol("metadata"), array[3]); +var __runInitializers = (array, flags, self, value) => { + for (var i = 0, fns = array[flags >> 1], n = fns && fns.length; i < n; i++) fns[i].call(self) ; + return value; +}; +var __decorateElement = (array, flags, name, decorators, target, extra) => { + var it, done, ctx, access, k = flags & 7, s = false, p = false; + var j = 2 , key = __decoratorStrings[k + 5]; + var extraInitializers = array[j] || (array[j] = []); + var desc = ((target = target.prototype), __getOwnPropDesc(target , name)); + for (var i = decorators.length - 1; i >= 0; i--) { + ctx = __decoratorContext(k, name, done = {}, array[3], extraInitializers); + { + ctx.static = s, ctx.private = p, access = ctx.access = { has: (x) => name in x }; + access.get = (x) => x[name]; + } + it = (0, decorators[i])(desc[key] , ctx), done._ = 1; + __expectFn(it) && (desc[key] = it ); + } + return desc && __defProp(target, name, desc), target; +}; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +var _render_dec, _getCanvasPageBoundsArray_dec, _getZoom_dec, _getCanvasPageBounds_dec, _getCanvasClientPosition_dec, _getCanvasSize_dec, _getContentScreenBounds_dec, _getContentPageBounds_dec, _getDpr_dec, _close_dec, _init; +_close_dec = [bind$2], _getDpr_dec = [computed], _getContentPageBounds_dec = [computed], _getContentScreenBounds_dec = [computed], _getCanvasSize_dec = [computed], _getCanvasClientPosition_dec = [computed], _getCanvasPageBounds_dec = [computed], _getZoom_dec = [computed], _getCanvasPageBoundsArray_dec = [computed], _render_dec = [bind$2]; +class MinimapManager { + constructor(editor, elem, container) { + this.editor = editor; + this.elem = elem; + this.container = container; + __runInitializers(_init, 5, this); + __publicField(this, "disposables", []); + __publicField(this, "gl"); + __publicField(this, "shapeGeometryCache"); + __publicField(this, "colors"); + __publicField(this, "id", uniqueId()); + __publicField(this, "canvasBoundingClientRect", atom("canvasBoundingClientRect", new Box())); + __publicField(this, "originPagePoint", new Vec()); + __publicField(this, "originPageCenter", new Vec()); + __publicField(this, "isInViewport", false); + this.gl = setupWebGl(elem); + this.shapeGeometryCache = editor.store.createComputedCache("webgl-geometry", (r) => { + const bounds = editor.getShapeMaskedPageBounds(r.id); + if (!bounds) return null; + const arr = new Float32Array(12); + rectangle(arr, 0, bounds.x, bounds.y, bounds.w, bounds.h); + return arr; + }); + this.colors = this._getColors(); + this.disposables.push(this._listenForCanvasResize(), react("minimap render", this.render)); + } + close() { + return this.disposables.forEach((d) => d()); + } + _getColors() { + const style = getComputedStyle(this.editor.getContainer()); + return { + shapeFill: getRgba(style.getPropertyValue("--color-text-3").trim()), + selectFill: getRgba(style.getPropertyValue("--color-selected").trim()), + viewportFill: getRgba(style.getPropertyValue("--color-muted-1").trim()), + background: getRgba(style.getPropertyValue("--color-low").trim()) + }; + } + // this should be called after dark/light mode changes have propagated to the dom + updateColors() { + this.colors = this._getColors(); + } + getDpr() { + return this.editor.getInstanceState().devicePixelRatio; + } + getContentPageBounds() { + const viewportPageBounds = this.editor.getViewportPageBounds(); + const commonShapeBounds = this.editor.getCurrentPageBounds(); + return commonShapeBounds ? Box.Expand(commonShapeBounds, viewportPageBounds) : viewportPageBounds; + } + getContentScreenBounds() { + const contentPageBounds = this.getContentPageBounds(); + const topLeft = this.editor.pageToScreen(contentPageBounds.point); + const bottomRight = this.editor.pageToScreen( + new Vec(contentPageBounds.maxX, contentPageBounds.maxY) + ); + return new Box(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y); + } + _getCanvasBoundingRect() { + const { x, y, width, height } = this.elem.getBoundingClientRect(); + return new Box(x, y, width, height); + } + getCanvasScreenBounds() { + return this.canvasBoundingClientRect.get(); + } + _listenForCanvasResize() { + const observer = new ResizeObserver(() => { + const rect = this._getCanvasBoundingRect(); + this.canvasBoundingClientRect.set(rect); + }); + observer.observe(this.elem); + observer.observe(this.container); + return () => observer.disconnect(); + } + getCanvasSize() { + const rect = this.canvasBoundingClientRect.get(); + const dpr = this.getDpr(); + return new Vec(rect.width * dpr, rect.height * dpr); + } + getCanvasClientPosition() { + return this.canvasBoundingClientRect.get().point; + } + getCanvasPageBounds() { + const canvasScreenBounds = this.getCanvasScreenBounds(); + const contentPageBounds = this.getContentPageBounds(); + const aspectRatio = canvasScreenBounds.width / canvasScreenBounds.height; + let targetWidth = contentPageBounds.width; + let targetHeight = targetWidth / aspectRatio; + if (targetHeight < contentPageBounds.height) { + targetHeight = contentPageBounds.height; + targetWidth = targetHeight * aspectRatio; + } + const box = new Box(0, 0, targetWidth, targetHeight); + box.center = contentPageBounds.center; + return box; + } + getZoom() { + return this.getCanvasPageBounds().width / this.getCanvasScreenBounds().width; + } + getCanvasPageBoundsArray() { + const { x, y, w, h } = this.getCanvasPageBounds(); + return new Float32Array([x, y, w, h]); + } + getMinimapPagePoint(clientX, clientY) { + const canvasPageBounds = this.getCanvasPageBounds(); + const canvasScreenBounds = this.getCanvasScreenBounds(); + let x = clientX - canvasScreenBounds.x; + let y = clientY - canvasScreenBounds.y; + x *= canvasPageBounds.width / canvasScreenBounds.width; + y *= canvasPageBounds.height / canvasScreenBounds.height; + x += canvasPageBounds.minX; + y += canvasPageBounds.minY; + return new Vec(x, y, 1); + } + minimapScreenPointToPagePoint(x, y, shiftKey = false, clampToBounds = false) { + const { editor } = this; + const vpPageBounds = editor.getViewportPageBounds(); + let { x: px, y: py } = this.getMinimapPagePoint(x, y); + if (clampToBounds) { + const shapesPageBounds = this.editor.getCurrentPageBounds() ?? new Box(); + const minX = shapesPageBounds.minX - vpPageBounds.width / 2; + const maxX = shapesPageBounds.maxX + vpPageBounds.width / 2; + const minY = shapesPageBounds.minY - vpPageBounds.height / 2; + const maxY = shapesPageBounds.maxY + vpPageBounds.height / 2; + const lx = Math.max(0, minX + vpPageBounds.width - px); + const rx = Math.max(0, -(maxX - vpPageBounds.width - px)); + const ly = Math.max(0, minY + vpPageBounds.height - py); + const ry = Math.max(0, -(maxY - vpPageBounds.height - py)); + px += (lx - rx) / 2; + py += (ly - ry) / 2; + px = clamp$2(px, minX, maxX); + py = clamp$2(py, minY, maxY); + } + if (shiftKey) { + const { originPagePoint } = this; + const dx = Math.abs(px - originPagePoint.x); + const dy = Math.abs(py - originPagePoint.y); + if (dx > dy) { + py = originPagePoint.y; + } else { + px = originPagePoint.x; + } + } + return new Vec(px, py); + } + render() { + const context = this.gl.context; + const canvasSize = this.getCanvasSize(); + this.gl.setCanvasPageBounds(this.getCanvasPageBoundsArray()); + this.elem.width = canvasSize.x; + this.elem.height = canvasSize.y; + context.viewport(0, 0, canvasSize.x, canvasSize.y); + context.clearColor( + this.colors.background[0], + this.colors.background[1], + this.colors.background[2], + 1 + ); + context.clear(context.COLOR_BUFFER_BIT); + const selectedShapes = new Set(this.editor.getSelectedShapeIds()); + const colors = this.colors; + let selectedShapeOffset = 0; + let unselectedShapeOffset = 0; + const ids = this.editor.getCurrentPageShapeIdsSorted(); + for (let i = 0, len = ids.length; i < len; i++) { + const shapeId = ids[i]; + const geometry = this.shapeGeometryCache.get(shapeId); + if (!geometry) continue; + const len2 = geometry.length; + if (selectedShapes.has(shapeId)) { + appendVertices(this.gl.selectedShapes, selectedShapeOffset, geometry); + selectedShapeOffset += len2; + } else { + appendVertices(this.gl.unselectedShapes, unselectedShapeOffset, geometry); + unselectedShapeOffset += len2; + } + } + this.drawShapes(this.gl.unselectedShapes, unselectedShapeOffset, colors.shapeFill); + this.drawShapes(this.gl.selectedShapes, selectedShapeOffset, colors.selectFill); + this.drawViewport(); + this.drawCollaborators(); + } + drawShapes(stuff, len, color) { + this.gl.prepareTriangles(stuff, len); + this.gl.setFillColor(color); + this.gl.drawTriangles(len); + } + drawViewport() { + const viewport = this.editor.getViewportPageBounds(); + const len = roundedRectangle(this.gl.viewport.vertices, viewport, 4 * this.getZoom()); + this.gl.prepareTriangles(this.gl.viewport, len); + this.gl.setFillColor(this.colors.viewportFill); + this.gl.drawTrianglesTransparently(len); + if (tlenv.isSafari) { + this.gl.drawTrianglesTransparently(len); + this.gl.drawTrianglesTransparently(len); + this.gl.drawTrianglesTransparently(len); + } + } + drawCollaborators() { + const collaborators = this.editor.getCollaboratorsOnCurrentPage(); + if (!collaborators.length) return; + const numSegmentsPerCircle = 20; + const dataSizePerCircle = numSegmentsPerCircle * 6; + const totalSize = dataSizePerCircle * collaborators.length; + if (this.gl.collaborators.vertices.length < totalSize) { + this.gl.collaborators.vertices = new Float32Array(totalSize); + } + const vertices = this.gl.collaborators.vertices; + let offset = 0; + const zoom = this.getZoom(); + for (const { cursor } of collaborators) { + pie(vertices, { + center: Vec.From(cursor), + radius: 3 * zoom, + offset, + numArcSegments: numSegmentsPerCircle + }); + offset += dataSizePerCircle; + } + this.gl.prepareTriangles(this.gl.collaborators, totalSize); + offset = 0; + for (const { color } of collaborators) { + this.gl.setFillColor(getRgba(color)); + this.gl.context.drawArrays(this.gl.context.TRIANGLES, offset / 2, dataSizePerCircle / 2); + offset += dataSizePerCircle; + } + } +} +_init = __decoratorStart(); +__decorateElement(_init, 1, "close", _close_dec, MinimapManager); +__decorateElement(_init, 1, "getDpr", _getDpr_dec, MinimapManager); +__decorateElement(_init, 1, "getContentPageBounds", _getContentPageBounds_dec, MinimapManager); +__decorateElement(_init, 1, "getContentScreenBounds", _getContentScreenBounds_dec, MinimapManager); +__decorateElement(_init, 1, "getCanvasSize", _getCanvasSize_dec, MinimapManager); +__decorateElement(_init, 1, "getCanvasClientPosition", _getCanvasClientPosition_dec, MinimapManager); +__decorateElement(_init, 1, "getCanvasPageBounds", _getCanvasPageBounds_dec, MinimapManager); +__decorateElement(_init, 1, "getZoom", _getZoom_dec, MinimapManager); +__decorateElement(_init, 1, "getCanvasPageBoundsArray", _getCanvasPageBoundsArray_dec, MinimapManager); +__decorateElement(_init, 1, "render", _render_dec, MinimapManager); +__decoratorMetadata(_init, MinimapManager); + +function DefaultMinimap() { + const editor = useEditor(); + const container = useContainer(); + const rCanvas = reactExports.useRef(null); + const rPointing = reactExports.useRef(false); + const minimapRef = reactExports.useRef(); + reactExports.useEffect(() => { + try { + const minimap = new MinimapManager(editor, rCanvas.current, container); + minimapRef.current = minimap; + return minimapRef.current.close; + } catch (e) { + editor.annotateError(e, { + origin: "minimap", + willCrashApp: false + }); + editor.timers.setTimeout(() => { + throw e; + }); + } + }, [editor, container]); + const onDoubleClick = reactExports.useCallback( + (e) => { + if (!editor.getCurrentPageShapeIds().size) return; + if (!minimapRef.current) return; + const point = minimapRef.current.minimapScreenPointToPagePoint( + e.clientX, + e.clientY, + false, + false + ); + const clampedPoint = minimapRef.current.minimapScreenPointToPagePoint( + e.clientX, + e.clientY, + false, + true + ); + minimapRef.current.originPagePoint.setTo(clampedPoint); + minimapRef.current.originPageCenter.setTo(editor.getViewportPageBounds().center); + editor.centerOnPoint(point, { animation: { duration: editor.options.animationMediumMs } }); + }, + [editor] + ); + const onPointerDown = reactExports.useCallback( + (e) => { + if (!minimapRef.current) return; + const elm = e.currentTarget; + setPointerCapture(elm, e); + if (!editor.getCurrentPageShapeIds().size) return; + rPointing.current = true; + minimapRef.current.isInViewport = false; + const point = minimapRef.current.minimapScreenPointToPagePoint( + e.clientX, + e.clientY, + false, + false + ); + const _vpPageBounds = editor.getViewportPageBounds(); + const commonBounds = minimapRef.current.getContentPageBounds(); + const allowedBounds = new Box( + commonBounds.x - _vpPageBounds.width / 2, + commonBounds.y - _vpPageBounds.height / 2, + commonBounds.width + _vpPageBounds.width, + commonBounds.height + _vpPageBounds.height + ); + if (allowedBounds.containsPoint(point) && !_vpPageBounds.containsPoint(point)) { + minimapRef.current.isInViewport = _vpPageBounds.containsPoint(point); + const delta = Vec.Sub(_vpPageBounds.center, _vpPageBounds.point); + const pagePoint = Vec.Add(point, delta); + minimapRef.current.originPagePoint.setTo(pagePoint); + minimapRef.current.originPageCenter.setTo(point); + editor.centerOnPoint(point, { animation: { duration: editor.options.animationMediumMs } }); + } else { + const clampedPoint = minimapRef.current.minimapScreenPointToPagePoint( + e.clientX, + e.clientY, + false, + true + ); + minimapRef.current.isInViewport = _vpPageBounds.containsPoint(clampedPoint); + minimapRef.current.originPagePoint.setTo(clampedPoint); + minimapRef.current.originPageCenter.setTo(_vpPageBounds.center); + } + function release(e2) { + if (elm) { + releasePointerCapture(elm, e2); + } + rPointing.current = false; + document.body.removeEventListener("pointerup", release); + } + document.body.addEventListener("pointerup", release); + }, + [editor] + ); + const onPointerMove = reactExports.useCallback( + (e) => { + if (!minimapRef.current) return; + const point = minimapRef.current.minimapScreenPointToPagePoint( + e.clientX, + e.clientY, + e.shiftKey, + true + ); + if (rPointing.current) { + if (minimapRef.current.isInViewport) { + const delta = minimapRef.current.originPagePoint.clone().sub(minimapRef.current.originPageCenter); + editor.centerOnPoint(Vec.Sub(point, delta)); + return; + } + editor.centerOnPoint(point); + } + const pagePoint = minimapRef.current.getMinimapPagePoint(e.clientX, e.clientY); + const screenPoint = editor.pageToScreen(pagePoint); + const info = { + type: "pointer", + target: "canvas", + name: "pointer_move", + ...getPointerInfo(e), + point: screenPoint, + isPen: editor.getInstanceState().isPenMode + }; + editor.dispatch(info); + }, + [editor] + ); + const onWheel = reactExports.useCallback( + (e) => { + const offset = normalizeWheel(e); + editor.dispatch({ + type: "wheel", + name: "wheel", + delta: offset, + point: new Vec(e.clientX, e.clientY), + shiftKey: e.shiftKey, + altKey: e.altKey, + ctrlKey: e.metaKey || e.ctrlKey, + metaKey: e.metaKey, + accelKey: isAccelKey(e) + }); + }, + [editor] + ); + const isDarkMode = useIsDarkMode(); + reactExports.useEffect(() => { + editor.timers.setTimeout(() => { + minimapRef.current?.updateColors(); + minimapRef.current?.render(); + }); + }, [isDarkMode, editor]); + return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tlui-minimap", children: /* @__PURE__ */ jsxRuntimeExports.jsx( + "canvas", + { + role: "img", + "aria-label": "minimap", + ref: rCanvas, + className: "tlui-minimap__canvas", + onDoubleClick, + onPointerMove, + onPointerDown, + onWheelCapture: onWheel + } + ) }); +} + +function useLocalStorageState(key, defaultValue) { + const [state, setState] = React.useState(defaultValue); + React.useLayoutEffect(() => { + const value = getFromLocalStorage(key); + if (value) { + try { + setState(JSON.parse(value)); + } catch { + console.error(`Could not restore value ${key} from local storage.`); + } + } + }, [key]); + const updateValue = React.useCallback( + (setter) => { + setState((s) => { + const value = typeof setter === "function" ? setter(s) : setter; + setInLocalStorage(key, JSON.stringify(value)); + return value; + }); + }, + [key] + ); + return [state, updateValue]; +} + +const DefaultNavigationPanel = reactExports.memo(function DefaultNavigationPanel2() { + const actions = useActions(); + const msg = useTranslation(); + const breakpoint = useBreakpoint(); + const ref = reactExports.useRef(null); + usePassThroughWheelEvents(ref); + const [collapsed, setCollapsed] = useLocalStorageState("minimap", true); + const toggleMinimap = reactExports.useCallback(() => { + setCollapsed((s) => !s); + }, [setCollapsed]); + const { ZoomMenu, Minimap } = useTldrawUiComponents(); + if (breakpoint < PORTRAIT_BREAKPOINT.MOBILE) { + return null; + } + return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ref, className: "tlui-navigation-panel", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tlui-buttons__horizontal", children: ZoomMenu && breakpoint < PORTRAIT_BREAKPOINT.TABLET ? /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomMenu, {}) : collapsed ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + ZoomMenu && /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomMenu, {}), + Minimap && /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButton, + { + type: "icon", + "data-testid": "minimap.toggle-button", + title: msg("navigation-zone.toggle-minimap"), + className: "tlui-navigation-panel__toggle", + onClick: toggleMinimap, + children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonIcon, { icon: collapsed ? "chevrons-ne" : "chevrons-sw" }) + } + ) + ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButton, + { + type: "icon", + "data-testid": "minimap.zoom-out", + title: `${msg(unwrapLabel(actions["zoom-out"].label))} ${kbdStr(actions["zoom-out"].kbd)}`, + onClick: () => actions["zoom-out"].onSelect("navigation-zone"), + children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonIcon, { icon: "minus" }) + } + ), + ZoomMenu && /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomMenu, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButton, + { + type: "icon", + "data-testid": "minimap.zoom-in", + title: `${msg(unwrapLabel(actions["zoom-in"].label))} ${kbdStr(actions["zoom-in"].kbd)}`, + onClick: () => actions["zoom-in"].onSelect("navigation-zone"), + children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonIcon, { icon: "plus" }) + } + ), + Minimap && /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButton, + { + type: "icon", + "data-testid": "minimap.toggle-button", + title: msg("navigation-zone.toggle-minimap"), + className: "tlui-navigation-panel__toggle", + onClick: toggleMinimap, + children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonIcon, { icon: collapsed ? "chevrons-ne" : "chevrons-sw" }) + } + ) + ] }) }), + Minimap && breakpoint >= PORTRAIT_BREAKPOINT.TABLET && !collapsed && /* @__PURE__ */ jsxRuntimeExports.jsx(Minimap, {}) + ] }); +}); + +const PageItemInput = function PageItemInput2({ + name, + id, + isCurrentPage, + onCancel +}) { + const editor = useEditor(); + const trackEvent = useUiEvents(); + const rInput = reactExports.useRef(null); + const rMark = reactExports.useRef(null); + const handleFocus = reactExports.useCallback(() => { + rMark.current = editor.markHistoryStoppingPoint("rename page"); + }, [editor]); + const handleChange = reactExports.useCallback( + (value) => { + editor.renamePage(id, value || "New Page"); + trackEvent("rename-page", { source: "page-menu" }); + }, + [editor, id, trackEvent] + ); + const handleCancel = reactExports.useCallback(() => { + if (rMark.current) { + editor.bailToMark(rMark.current); + } + onCancel(); + }, [editor, onCancel]); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiInput, + { + className: "tlui-page-menu__item__input", + ref: (el) => rInput.current = el, + defaultValue: name, + onValueChange: handleChange, + onCancel: handleCancel, + onFocus: handleFocus, + shouldManuallyMaintainScrollPositionWhenFocused: true, + autoFocus: isCurrentPage, + autoSelect: true + } + ); +}; + +const onMovePage = (editor, id, from, to, trackEvent) => { + let index; + const pages = editor.getPages(); + const below = from > to ? pages[to - 1] : pages[to]; + const above = from > to ? pages[to] : pages[to + 1]; + if (below && !above) { + index = getIndexAbove(below.index); + } else if (!below && above) { + index = getIndexBelow(pages[0].index); + } else { + index = getIndexBetween(below.index, above.index); + } + if (index !== pages[from].index) { + editor.markHistoryStoppingPoint("moving page"); + editor.updatePage({ + id, + index + }); + trackEvent("move-page", { source: "page-menu" }); + } +}; + +const PageItemSubmenu = track(function PageItemSubmenu2({ + index, + listSize, + item, + onRename +}) { + const editor = useEditor(); + const msg = useTranslation(); + const pages = editor.getPages(); + const trackEvent = useUiEvents(); + const onDuplicate = reactExports.useCallback(() => { + editor.markHistoryStoppingPoint("creating page"); + const newId = PageRecordType.createId(); + editor.duplicatePage(item.id, newId); + trackEvent("duplicate-page", { source: "page-menu" }); + }, [editor, item, trackEvent]); + const onMoveUp = reactExports.useCallback(() => { + onMovePage(editor, item.id, index, index - 1, trackEvent); + }, [editor, item, index, trackEvent]); + const onMoveDown = reactExports.useCallback(() => { + onMovePage(editor, item.id, index, index + 1, trackEvent); + }, [editor, item, index, trackEvent]); + const onDelete = reactExports.useCallback(() => { + editor.markHistoryStoppingPoint("deleting page"); + editor.deletePage(item.id); + trackEvent("delete-page", { source: "page-menu" }); + }, [editor, item, trackEvent]); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiDropdownMenuRoot, { id: `page item submenu ${index}`, children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiDropdownMenuTrigger, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButton, { type: "icon", title: msg("page-menu.submenu.title"), children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonIcon, { icon: "dots-vertical", small: true }) }) }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiDropdownMenuContent, { alignOffset: 0, side: "right", sideOffset: -4, children: /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuContextProvider, { type: "menu", sourceId: "page-menu", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiMenuGroup, { id: "modify", children: [ + onRename && /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuItem, { id: "rename", label: "page-menu.submenu.rename", onSelect: onRename }), + /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiMenuItem, + { + id: "duplicate", + label: "page-menu.submenu.duplicate-page", + onSelect: onDuplicate, + disabled: pages.length >= editor.options.maxPages + } + ), + index > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiMenuItem, + { + id: "move-up", + onSelect: onMoveUp, + label: "page-menu.submenu.move-up" + } + ), + index < listSize - 1 && /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiMenuItem, + { + id: "move-down", + label: "page-menu.submenu.move-down", + onSelect: onMoveDown + } + ) + ] }), + listSize > 1 && /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuGroup, { id: "delete", children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuItem, { id: "delete", onSelect: onDelete, label: "page-menu.submenu.delete" }) }) + ] }) }) + ] }); +}); + +const DefaultPageMenu = reactExports.memo(function DefaultPageMenu2() { + const editor = useEditor(); + const trackEvent = useUiEvents(); + const msg = useTranslation(); + const breakpoint = useBreakpoint(); + const handleOpenChange = reactExports.useCallback(() => setIsEditing(false), []); + const [isOpen, onOpenChange] = useMenuIsOpen("page-menu", handleOpenChange); + const ITEM_HEIGHT = 36; + const rSortableContainer = reactExports.useRef(null); + const pages = useValue("pages", () => editor.getPages(), [editor]); + const currentPage = useValue("currentPage", () => editor.getCurrentPage(), [editor]); + const currentPageId = useValue("currentPageId", () => editor.getCurrentPageId(), [editor]); + const isReadonlyMode = useReadonly(); + const maxPageCountReached = useValue( + "maxPageCountReached", + () => editor.getPages().length >= editor.options.maxPages, + [editor] + ); + const isCoarsePointer = useValue( + "isCoarsePointer", + () => editor.getInstanceState().isCoarsePointer, + [editor] + ); + const [isEditing, setIsEditing] = reactExports.useState(false); + const toggleEditing = reactExports.useCallback(() => { + if (isReadonlyMode) return; + setIsEditing((s) => !s); + }, [isReadonlyMode]); + const rMutables = reactExports.useRef({ + isPointing: false, + status: "idle", + pointing: null, + startY: 0, + startIndex: 0, + dragIndex: 0 + }); + const [sortablePositionItems, setSortablePositionItems] = reactExports.useState( + Object.fromEntries( + pages.map((page, i) => [page.id, { y: i * ITEM_HEIGHT, offsetY: 0, isSelected: false }]) + ) + ); + reactExports.useLayoutEffect(() => { + setSortablePositionItems( + Object.fromEntries( + pages.map((page, i) => [page.id, { y: i * ITEM_HEIGHT, offsetY: 0, isSelected: false }]) + ) + ); + }, [ITEM_HEIGHT, pages]); + reactExports.useEffect(() => { + if (!isOpen) return; + editor.timers.requestAnimationFrame(() => { + const elm = document.querySelector( + `[data-testid="page-menu-item-${currentPageId}"]` + ); + if (elm) { + const container = rSortableContainer.current; + if (!container) return; + const elmTopPosition = elm.offsetTop; + const containerScrollTopPosition = container.scrollTop; + if (elmTopPosition < containerScrollTopPosition) { + container.scrollTo({ top: elmTopPosition }); + } + const elmBottomPosition = elmTopPosition + ITEM_HEIGHT; + const containerScrollBottomPosition = container.scrollTop + container.offsetHeight; + if (elmBottomPosition > containerScrollBottomPosition) { + container.scrollTo({ top: elmBottomPosition - container.offsetHeight }); + } + } + }); + }, [ITEM_HEIGHT, currentPageId, isOpen, editor]); + const handlePointerDown = reactExports.useCallback( + (e) => { + const { clientY, currentTarget } = e; + const { + dataset: { id, index } + } = currentTarget; + if (!id || !index) return; + const mut = rMutables.current; + setPointerCapture(e.currentTarget, e); + mut.status = "pointing"; + mut.pointing = { id, index: +index }; + const current = sortablePositionItems[id]; + const dragY = current.y; + mut.startY = clientY; + mut.startIndex = Math.max(0, Math.min(Math.round(dragY / ITEM_HEIGHT), pages.length - 1)); + }, + [ITEM_HEIGHT, pages.length, sortablePositionItems] + ); + const handlePointerMove = reactExports.useCallback( + (e) => { + const mut = rMutables.current; + if (mut.status === "pointing") { + const { clientY } = e; + const offset = clientY - mut.startY; + if (Math.abs(offset) > 5) { + mut.status = "dragging"; + } + } + if (mut.status === "dragging") { + const { clientY } = e; + const offsetY = clientY - mut.startY; + const current = sortablePositionItems[mut.pointing.id]; + const { startIndex, pointing } = mut; + const dragY = current.y + offsetY; + const dragIndex = Math.max(0, Math.min(Math.round(dragY / ITEM_HEIGHT), pages.length - 1)); + const next = { ...sortablePositionItems }; + next[pointing.id] = { + y: current.y, + offsetY, + isSelected: true + }; + if (dragIndex !== mut.dragIndex) { + mut.dragIndex = dragIndex; + for (let i = 0; i < pages.length; i++) { + const item = pages[i]; + if (item.id === mut.pointing.id) { + continue; + } + let { y } = next[item.id]; + if (dragIndex === startIndex) { + y = i * ITEM_HEIGHT; + } else if (dragIndex < startIndex) { + if (dragIndex <= i && i < startIndex) { + y = (i + 1) * ITEM_HEIGHT; + } else { + y = i * ITEM_HEIGHT; + } + } else if (dragIndex > startIndex) { + if (dragIndex >= i && i > startIndex) { + y = (i - 1) * ITEM_HEIGHT; + } else { + y = i * ITEM_HEIGHT; + } + } + if (y !== next[item.id].y) { + next[item.id] = { y, offsetY: 0, isSelected: true }; + } + } + } + setSortablePositionItems(next); + } + }, + [ITEM_HEIGHT, pages, sortablePositionItems] + ); + const handlePointerUp = reactExports.useCallback( + (e) => { + const mut = rMutables.current; + if (mut.status === "dragging") { + const { id, index } = mut.pointing; + onMovePage(editor, id, index, mut.dragIndex, trackEvent); + } + releasePointerCapture(e.currentTarget, e); + mut.status = "idle"; + }, + [editor, trackEvent] + ); + const handleKeyDown = reactExports.useCallback( + (e) => { + const mut = rMutables.current; + if (e.key === "Escape") { + if (mut.status === "dragging") { + setSortablePositionItems( + Object.fromEntries( + pages.map((page, i) => [ + page.id, + { y: i * ITEM_HEIGHT, offsetY: 0, isSelected: false } + ]) + ) + ); + } + mut.status = "idle"; + } + }, + [ITEM_HEIGHT, pages] + ); + const handleCreatePageClick = reactExports.useCallback(() => { + if (isReadonlyMode) return; + editor.run(() => { + editor.markHistoryStoppingPoint("creating page"); + const newPageId = PageRecordType.createId(); + editor.createPage({ name: msg("page-menu.new-page-initial-name"), id: newPageId }); + editor.setCurrentPage(newPageId); + setIsEditing(true); + }); + trackEvent("new-page", { source: "page-menu" }); + }, [editor, msg, isReadonlyMode, trackEvent]); + const changePage = reactExports.useCallback( + (id) => { + editor.setCurrentPage(id); + trackEvent("change-page", { source: "page-menu" }); + }, + [editor, trackEvent] + ); + const renamePage = reactExports.useCallback( + (id, name) => { + editor.renamePage(id, name); + trackEvent("rename-page", { source: "page-menu" }); + }, + [editor, trackEvent] + ); + return ( + /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiPopover, { id: "pages", onOpenChange, open: isOpen, children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiPopoverTrigger, { "data-testid": "main.page-menu", children: /* @__PURE__ */ jsxRuntimeExports.jsxs( + TldrawUiButton, + { + type: "menu", + title: currentPage.name, + "data-testid": "page-menu.button", + className: "tlui-page-menu__trigger", + children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tlui-page-menu__name", children: currentPage.name }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonIcon, { icon: "chevron-down", small: true }) + ] + } + ) }), + /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiPopoverContent, + { + side: "bottom", + align: "start", + sideOffset: 6, + disableEscapeKeyDown: isEditing, + children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "tlui-page-menu__wrapper", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "tlui-page-menu__header", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tlui-page-menu__header__title", children: msg("page-menu.title") }), + !isReadonlyMode && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "tlui-buttons__horizontal", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButton, + { + type: "icon", + "data-testid": "page-menu.edit", + title: msg(isEditing ? "page-menu.edit-done" : "page-menu.edit-start"), + onClick: toggleEditing, + children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonIcon, { icon: isEditing ? "check" : "edit" }) + } + ), + /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButton, + { + type: "icon", + "data-testid": "page-menu.create", + title: msg( + maxPageCountReached ? "page-menu.max-page-count-reached" : "page-menu.create-new-page" + ), + disabled: maxPageCountReached, + onClick: handleCreatePageClick, + children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonIcon, { icon: "plus" }) + } + ) + ] }) + ] }), + /* @__PURE__ */ jsxRuntimeExports.jsx( + "div", + { + "data-testid": "page-menu.list", + className: "tlui-page-menu__list tlui-menu__group", + style: { height: ITEM_HEIGHT * pages.length + 4 }, + ref: rSortableContainer, + children: pages.map((page, index) => { + const position = sortablePositionItems[page.id] ?? { + offsetY: 0 + }; + return isEditing ? /* @__PURE__ */ jsxRuntimeExports.jsxs( + "div", + { + "data-testid": "page-menu.item", + className: "tlui-page_menu__item__sortable", + style: { + zIndex: page.id === currentPage.id ? 888 : index, + transform: `translate(0px, ${position.y + position.offsetY}px)` + }, + children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButton, + { + type: "icon", + tabIndex: -1, + className: "tlui-page_menu__item__sortable__handle", + onPointerDown: handlePointerDown, + onPointerUp: handlePointerUp, + onPointerMove: handlePointerMove, + onKeyDown: handleKeyDown, + "data-id": page.id, + "data-index": index, + children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonIcon, { icon: "drag-handle-dots" }) + } + ), + breakpoint < PORTRAIT_BREAKPOINT.TABLET_SM && isCoarsePointer ? ( + // sigh, this is a workaround for iOS Safari + // because the device and the radix popover seem + // to be fighting over scroll position. Nothing + // else seems to work! + /* @__PURE__ */ (jsxRuntimeExports.jsxs(TldrawUiButton, { + type: "normal", + className: "tlui-page-menu__item__button", + onClick: () => { + const name = window.prompt("Rename page", page.name); + if (name && name !== page.name) { + renamePage(page.id, name); + } + }, + onDoubleClick: toggleEditing, + children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonCheck, { checked: page.id === currentPage.id }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonLabel, { children: page.name }) + ] + })) + ) : /* @__PURE__ */ jsxRuntimeExports.jsx( + "div", + { + className: "tlui-page_menu__item__sortable__title", + style: { height: ITEM_HEIGHT }, + children: /* @__PURE__ */ jsxRuntimeExports.jsx( + PageItemInput, + { + id: page.id, + name: page.name, + isCurrentPage: page.id === currentPage.id, + onCancel: () => { + setIsEditing(false); + editor.menus.clearOpenMenus(); + } + } + ) + } + ), + !isReadonlyMode && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tlui-page_menu__item__submenu", "data-isediting": isEditing, children: /* @__PURE__ */ jsxRuntimeExports.jsx(PageItemSubmenu, { index, item: page, listSize: pages.length }) }) + ] + }, + page.id + "_editing" + ) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { "data-testid": "page-menu.item", className: "tlui-page-menu__item", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsxs( + TldrawUiButton, + { + type: "normal", + className: "tlui-page-menu__item__button", + onClick: () => changePage(page.id), + onDoubleClick: toggleEditing, + title: msg("page-menu.go-to-page"), + children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonCheck, { checked: page.id === currentPage.id }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonLabel, { children: page.name }) + ] + } + ), + !isReadonlyMode && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tlui-page_menu__item__submenu", children: /* @__PURE__ */ jsxRuntimeExports.jsx( + PageItemSubmenu, + { + index, + item: page, + listSize: pages.length, + onRename: () => { + if (tlenv.isIos) { + const name = window.prompt("Rename page", page.name); + if (name && name !== page.name) { + renamePage(page.id, name); + } + } else { + setIsEditing(true); + if (currentPageId !== page.id) { + changePage(page.id); + } + } + } + } + ) }) + ] }, page.id); + }) + } + ) + ] }) + } + ) + ] }) + ); +}); + +function DefaultQuickActionsContent() { + const editor = useEditor(); + const canUndo = useCanUndo(); + const canRedo = useCanRedo(); + const oneSelected = useUnlockedSelectedShapesCount(1); + const isReadonlyMode = useReadonly(); + const isInAcceptableReadonlyState = useValue( + "should display quick actions", + () => editor.isInAny("select", "hand", "zoom"), + [editor] + ); + const isInSelectState = useIsInSelectState(); + const selectDependentActionsEnabled = oneSelected && isInSelectState; + if (isReadonlyMode && !isInAcceptableReadonlyState) return; + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "undo", disabled: !canUndo }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "redo", disabled: !canRedo }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "delete", disabled: !selectDependentActionsEnabled }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuActionItem, { actionId: "duplicate", disabled: !selectDependentActionsEnabled }) + ] }); +} + +const DefaultQuickActions = reactExports.memo(function DefaultQuickActions2({ + children +}) { + const content = children ?? /* @__PURE__ */ jsxRuntimeExports.jsx(DefaultQuickActionsContent, {}); + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuContextProvider, { type: "small-icons", sourceId: "quick-actions", children: content }); +}); + +function PeopleMenuAvatar({ userId }) { + const presence = usePresence$1(userId); + if (!presence) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsx( + "div", + { + className: "tlui-people-menu__avatar", + style: { + backgroundColor: presence.color + }, + children: presence.userName === "New User" ? "" : presence.userName[0] ?? "" + }, + userId + ); +} + +const PeopleMenuItem = track(function PeopleMenuItem2({ userId }) { + const editor = useEditor(); + const msg = useTranslation(); + const trackEvent = useUiEvents(); + const presence = usePresence$1(userId); + const handleFollowClick = reactExports.useCallback(() => { + if (editor.getInstanceState().followingUserId === userId) { + editor.stopFollowingUser(); + trackEvent("stop-following", { source: "people-menu" }); + } else { + editor.startFollowingUser(userId); + trackEvent("start-following", { source: "people-menu" }); + } + }, [editor, userId, trackEvent]); + const theyAreFollowingYou = presence?.followingUserId === editor.user.getId(); + const youAreFollowingThem = editor.getInstanceState().followingUserId === userId; + if (!presence) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "tlui-people-menu__item tlui-buttons__horizontal", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsxs( + TldrawUiButton, + { + type: "menu", + className: "tlui-people-menu__item__button", + onClick: () => editor.zoomToUser(userId), + onDoubleClick: handleFollowClick, + children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiIcon, { icon: "color", color: presence.color }), + /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tlui-people-menu__name", children: presence.userName ?? "New User" }) + ] + } + ), + /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButton, + { + type: "icon", + className: "tlui-people-menu__item__follow", + title: theyAreFollowingYou ? msg("people-menu.leading") : youAreFollowingThem ? msg("people-menu.following") : msg("people-menu.follow"), + onClick: handleFollowClick, + disabled: theyAreFollowingYou, + "data-active": youAreFollowingThem || theyAreFollowingYou, + children: /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButtonIcon, + { + icon: theyAreFollowingYou ? "leading" : youAreFollowingThem ? "following" : "follow" + } + ) + } + ) + ] }); +}); + +function PeopleMenuMore({ count }) { + return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tlui-people-menu__more", children: "+" + Math.abs(count) }); +} + +const UserPresenceColorPicker = track(function UserPresenceColorPicker2() { + const editor = useEditor(); + const container = useContainer(); + const msg = useTranslation(); + const trackEvent = useUiEvents(); + const rPointing = reactExports.useRef(false); + const [isOpen, setIsOpen] = reactExports.useState(false); + const handleOpenChange = reactExports.useCallback((isOpen2) => { + setIsOpen(isOpen2); + }, []); + const value = editor.user.getColor(); + const onValueChange = reactExports.useCallback( + (item) => { + editor.user.updateUserPreferences({ color: item }); + trackEvent("set-color", { source: "people-menu" }); + }, + [editor, trackEvent] + ); + const { + handleButtonClick, + handleButtonPointerDown, + handleButtonPointerEnter, + handleButtonPointerUp + } = React.useMemo(() => { + const handlePointerUp = () => { + rPointing.current = false; + window.removeEventListener("pointerup", handlePointerUp); + }; + const handleButtonClick2 = (e) => { + const { id } = e.currentTarget.dataset; + if (!id) return; + if (value === id) return; + onValueChange(id); + }; + const handleButtonPointerDown2 = (e) => { + const { id } = e.currentTarget.dataset; + if (!id) return; + onValueChange(id); + rPointing.current = true; + window.addEventListener("pointerup", handlePointerUp); + }; + const handleButtonPointerEnter2 = (e) => { + if (!rPointing.current) return; + const { id } = e.currentTarget.dataset; + if (!id) return; + onValueChange(id); + }; + const handleButtonPointerUp2 = (e) => { + const { id } = e.currentTarget.dataset; + if (!id) return; + onValueChange(id); + }; + return { + handleButtonClick: handleButtonClick2, + handleButtonPointerDown: handleButtonPointerDown2, + handleButtonPointerEnter: handleButtonPointerEnter2, + handleButtonPointerUp: handleButtonPointerUp2 + }; + }, [value, onValueChange]); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(Root2, { onOpenChange: handleOpenChange, open: isOpen, children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(Trigger, { dir: "ltr", asChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButton, + { + type: "icon", + className: "tlui-people-menu__user__color", + style: { color: editor.user.getColor() }, + title: msg("people-menu.change-color"), + children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonIcon, { icon: "color" }) + } + ) }), + /* @__PURE__ */ jsxRuntimeExports.jsx(Portal, { container, children: /* @__PURE__ */ jsxRuntimeExports.jsx( + Content2, + { + dir: "ltr", + className: "tlui-menu tlui-people-menu__user__color-picker", + align: "start", + side: "left", + sideOffset: 8, + children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tlui-buttons__grid", children: USER_COLORS.map((item) => /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButton, + { + type: "icon", + "data-id": item, + "data-testid": item, + "aria-label": item, + "data-state": value === item ? "hinted" : void 0, + title: item, + className: "tlui-button-grid__button", + style: { color: item }, + onPointerEnter: handleButtonPointerEnter, + onPointerDown: handleButtonPointerDown, + onPointerUp: handleButtonPointerUp, + onClick: handleButtonClick, + children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonIcon, { icon: "color" }) + }, + item + )) }) + } + ) }) + ] }); +}); + +function UserPresenceEditor() { + const editor = useEditor(); + const trackEvent = useUiEvents(); + const userName = useValue("userName", () => editor.user.getName(), []); + const msg = useTranslation(); + const rOriginalName = reactExports.useRef(userName); + const rCurrentName = reactExports.useRef(userName); + const [isEditingName, setIsEditingName] = reactExports.useState(false); + const toggleEditingName = reactExports.useCallback(() => { + setIsEditingName((s) => !s); + }, []); + const handleValueChange = reactExports.useCallback( + (value) => { + rCurrentName.current = value; + editor.user.updateUserPreferences({ name: value }); + }, + [editor] + ); + const handleBlur = reactExports.useCallback(() => { + if (rOriginalName.current === rCurrentName.current) return; + trackEvent("change-user-name", { source: "people-menu" }); + rOriginalName.current = rCurrentName.current; + }, [trackEvent]); + const handleCancel = reactExports.useCallback(() => { + setIsEditingName(false); + editor.user.updateUserPreferences({ name: rOriginalName.current }); + editor.menus.clearOpenMenus(); + }, [editor]); + return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "tlui-people-menu__user", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(UserPresenceColorPicker, {}), + isEditingName ? /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiInput, + { + className: "tlui-people-menu__user__input", + defaultValue: userName, + onValueChange: handleValueChange, + onComplete: toggleEditingName, + onCancel: handleCancel, + onBlur: handleBlur, + shouldManuallyMaintainScrollPositionWhenFocused: true, + autoFocus: true, + autoSelect: true + } + ) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx( + "div", + { + className: "tlui-people-menu__user__name", + onDoubleClick: () => { + if (!isEditingName) setIsEditingName(true); + }, + children: userName + } + ), + userName === "New User" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tlui-people-menu__user__label", children: msg("people-menu.user") }) : null + ] }), + /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButton, + { + type: "icon", + className: "tlui-people-menu__user__edit", + "data-testid": "people-menu.change-name", + title: msg("people-menu.change-name"), + onClick: toggleEditingName, + children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonIcon, { icon: isEditingName ? "check" : "edit" }) + } + ) + ] }); +} + +function PeopleMenu({ displayUserWhenAlone, children }) { + const msg = useTranslation(); + const container = useContainer(); + const editor = useEditor(); + const userIds = usePeerIds(); + const userColor = useValue("user", () => editor.user.getColor(), [editor]); + const userName = useValue("user", () => editor.user.getName(), [editor]); + const [isOpen, onOpenChange] = useMenuIsOpen("people menu"); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(Root2, { onOpenChange, open: isOpen, children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(Trigger, { dir: "ltr", asChild: true, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { className: "tlui-people-menu__avatars-button", title: msg("people-menu.title"), children: [ + userIds.length > 5 && /* @__PURE__ */ jsxRuntimeExports.jsx(PeopleMenuMore, { count: userIds.length - 5 }), + /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "tlui-people-menu__avatars", children: [ + userIds.slice(-5).map((userId) => /* @__PURE__ */ jsxRuntimeExports.jsx(PeopleMenuAvatar, { userId }, userId)), + (displayUserWhenAlone || userIds.length > 0) && /* @__PURE__ */ jsxRuntimeExports.jsx( + "div", + { + className: "tlui-people-menu__avatar", + style: { + backgroundColor: userColor + }, + children: userName === "New User" ? "" : userName[0] ?? "" + } + ) + ] }) + ] }) }), + /* @__PURE__ */ jsxRuntimeExports.jsx(Portal, { container, children: /* @__PURE__ */ jsxRuntimeExports.jsx( + Content2, + { + dir: "ltr", + className: "tlui-menu", + side: "bottom", + sideOffset: 2, + collisionPadding: 4, + onEscapeKeyDown: preventDefault, + children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "tlui-people-menu__wrapper", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tlui-people-menu__section", children: /* @__PURE__ */ jsxRuntimeExports.jsx(UserPresenceEditor, {}) }), + userIds.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tlui-people-menu__section", children: userIds.map((userId) => { + return /* @__PURE__ */ jsxRuntimeExports.jsx(PeopleMenuItem, { userId }, userId + "_presence"); + }) }), + children + ] }) + } + ) }) + ] }); +} + +function DefaultSharePanel() { + return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tlui-share-zone", draggable: false, children: /* @__PURE__ */ jsxRuntimeExports.jsx(PeopleMenu, { displayUserWhenAlone: true }) }); +} + +const selectToolStyles = Object.freeze([ + DefaultColorStyle, + DefaultDashStyle, + DefaultFillStyle, + DefaultSizeStyle +]); +function useRelevantStyles(stylesToCheck = selectToolStyles) { + const editor = useEditor(); + return useValue( + "getRelevantStyles", + () => { + const styles = new SharedStyleMap(editor.getSharedStyles()); + const isInShapeSpecificTool = !!editor.root.getCurrent()?.shapeType; + const hasShapesSelected = editor.isIn("select") && editor.getSelectedShapeIds().length > 0; + if (styles.size === 0 && editor.isIn("select") && editor.getSelectedShapeIds().length === 0) { + for (const style of stylesToCheck) { + styles.applyValue(style, editor.getStyleForNextShape(style)); + } + } + if (isInShapeSpecificTool || hasShapesSelected || styles.size > 0) { + return styles; + } + return null; + }, + [editor] + ); +} + +const STYLES = { + color: [ + { value: "black", icon: "color" }, + { value: "grey", icon: "color" }, + { value: "light-violet", icon: "color" }, + { value: "violet", icon: "color" }, + { value: "blue", icon: "color" }, + { value: "light-blue", icon: "color" }, + { value: "yellow", icon: "color" }, + { value: "orange", icon: "color" }, + { value: "green", icon: "color" }, + { value: "light-green", icon: "color" }, + { value: "light-red", icon: "color" }, + { value: "red", icon: "color" } + ], + fill: [ + { value: "none", icon: "fill-none" }, + { value: "semi", icon: "fill-semi" }, + { value: "solid", icon: "fill-solid" }, + { value: "pattern", icon: "fill-pattern" } + // { value: 'fill', icon: 'fill-fill' }, + ], + dash: [ + { value: "draw", icon: "dash-draw" }, + { value: "dashed", icon: "dash-dashed" }, + { value: "dotted", icon: "dash-dotted" }, + { value: "solid", icon: "dash-solid" } + ], + size: [ + { value: "s", icon: "size-small" }, + { value: "m", icon: "size-medium" }, + { value: "l", icon: "size-large" }, + { value: "xl", icon: "size-extra-large" } + ], + font: [ + { value: "draw", icon: "font-draw" }, + { value: "sans", icon: "font-sans" }, + { value: "serif", icon: "font-serif" }, + { value: "mono", icon: "font-mono" } + ], + textAlign: [ + { value: "start", icon: "text-align-left" }, + { value: "middle", icon: "text-align-center" }, + { value: "end", icon: "text-align-right" } + ], + horizontalAlign: [ + { value: "start", icon: "horizontal-align-start" }, + { value: "middle", icon: "horizontal-align-middle" }, + { value: "end", icon: "horizontal-align-end" } + ], + verticalAlign: [ + { value: "start", icon: "vertical-align-start" }, + { value: "middle", icon: "vertical-align-middle" }, + { value: "end", icon: "vertical-align-end" } + ], + geo: [ + { value: "rectangle", icon: "geo-rectangle" }, + { value: "ellipse", icon: "geo-ellipse" }, + { value: "triangle", icon: "geo-triangle" }, + { value: "diamond", icon: "geo-diamond" }, + { value: "star", icon: "geo-star" }, + { value: "pentagon", icon: "geo-pentagon" }, + { value: "hexagon", icon: "geo-hexagon" }, + { value: "octagon", icon: "geo-octagon" }, + { value: "rhombus", icon: "geo-rhombus" }, + { value: "rhombus-2", icon: "geo-rhombus-2" }, + { value: "oval", icon: "geo-oval" }, + { value: "trapezoid", icon: "geo-trapezoid" }, + { value: "arrow-left", icon: "geo-arrow-left" }, + { value: "arrow-up", icon: "geo-arrow-up" }, + { value: "arrow-down", icon: "geo-arrow-down" }, + { value: "arrow-right", icon: "geo-arrow-right" }, + { value: "cloud", icon: "geo-cloud" }, + { value: "x-box", icon: "geo-x-box" }, + { value: "check-box", icon: "geo-check-box" }, + { value: "heart", icon: "geo-heart" } + ], + arrowheadStart: [ + { value: "none", icon: "arrowhead-none" }, + { value: "arrow", icon: "arrowhead-arrow" }, + { value: "triangle", icon: "arrowhead-triangle" }, + { value: "square", icon: "arrowhead-square" }, + { value: "dot", icon: "arrowhead-dot" }, + { value: "diamond", icon: "arrowhead-diamond" }, + { value: "inverted", icon: "arrowhead-triangle-inverted" }, + { value: "bar", icon: "arrowhead-bar" } + ], + arrowheadEnd: [ + { value: "none", icon: "arrowhead-none" }, + { value: "arrow", icon: "arrowhead-arrow" }, + { value: "triangle", icon: "arrowhead-triangle" }, + { value: "square", icon: "arrowhead-square" }, + { value: "dot", icon: "arrowhead-dot" }, + { value: "diamond", icon: "arrowhead-diamond" }, + { value: "inverted", icon: "arrowhead-triangle-inverted" }, + { value: "bar", icon: "arrowhead-bar" } + ], + spline: [ + { value: "line", icon: "spline-line" }, + { value: "cubic", icon: "spline-cubic" } + ] +}; + +const TldrawUiButtonPicker = reactExports.memo(function TldrawUiButtonPicker2(props) { + const { + uiType, + items, + title, + style, + value, + // columns = clamp(items.length, 2, 4), + onValueChange, + onHistoryMark, + theme + } = props; + const msg = useTranslation(); + const rPointing = reactExports.useRef(false); + const rPointingOriginalActiveElement = reactExports.useRef(null); + const { + handleButtonClick, + handleButtonPointerDown, + handleButtonPointerEnter, + handleButtonPointerUp + } = reactExports.useMemo(() => { + const handlePointerUp = () => { + rPointing.current = false; + window.removeEventListener("pointerup", handlePointerUp); + const origActiveEl = rPointingOriginalActiveElement.current; + if (origActiveEl && ["TEXTAREA", "INPUT"].includes(origActiveEl.nodeName)) { + origActiveEl.focus(); + } + rPointingOriginalActiveElement.current = null; + }; + const handleButtonClick2 = (e) => { + const { id } = e.currentTarget.dataset; + if (value.type === "shared" && value.value === id) return; + onHistoryMark?.("point picker item"); + onValueChange(style, id); + }; + const handleButtonPointerDown2 = (e) => { + const { id } = e.currentTarget.dataset; + onHistoryMark?.("point picker item"); + onValueChange(style, id); + rPointing.current = true; + rPointingOriginalActiveElement.current = document.activeElement; + window.addEventListener("pointerup", handlePointerUp); + }; + const handleButtonPointerEnter2 = (e) => { + if (!rPointing.current) return; + const { id } = e.currentTarget.dataset; + onValueChange(style, id); + }; + const handleButtonPointerUp2 = (e) => { + const { id } = e.currentTarget.dataset; + if (value.type === "shared" && value.value === id) return; + onValueChange(style, id); + }; + return { + handleButtonClick: handleButtonClick2, + handleButtonPointerDown: handleButtonPointerDown2, + handleButtonPointerEnter: handleButtonPointerEnter2, + handleButtonPointerUp: handleButtonPointerUp2 + }; + }, [value, onHistoryMark, onValueChange, style]); + return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { "data-testid": `style.${uiType}`, className: classNames("tlui-buttons__grid"), children: items.map((item) => /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButton, + { + type: "icon", + "data-id": item.value, + "data-testid": `style.${uiType}.${item.value}`, + "aria-label": item.value, + "data-state": value.type === "shared" && value.value === item.value ? "hinted" : void 0, + title: title + " \u2014 " + msg(`${uiType}-style.${item.value}`), + className: classNames("tlui-button-grid__button"), + style: style === DefaultColorStyle ? { color: theme[item.value].solid } : void 0, + onPointerEnter: handleButtonPointerEnter, + onPointerDown: handleButtonPointerDown, + onPointerUp: handleButtonPointerUp, + onClick: handleButtonClick, + children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonIcon, { icon: item.icon }) + }, + item.value + )) }); +}); + +// packages/core/number/src/number.ts +function clamp(value, [min, max]) { + return Math.min(max, Math.max(min, value)); +} + +// packages/react/use-previous/src/use-previous.tsx +function usePrevious(value) { + const ref = reactExports.useRef({ value, previous: value }); + return reactExports.useMemo(() => { + if (ref.current.value !== value) { + ref.current.previous = ref.current.value; + ref.current.value = value; + } + return ref.current.previous; + }, [value]); +} + +var PAGE_KEYS = ["PageUp", "PageDown"]; +var ARROW_KEYS = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"]; +var BACK_KEYS = { + "from-left": ["Home", "PageDown", "ArrowDown", "ArrowLeft"], + "from-right": ["Home", "PageDown", "ArrowDown", "ArrowRight"], + "from-bottom": ["Home", "PageDown", "ArrowDown", "ArrowLeft"], + "from-top": ["Home", "PageDown", "ArrowUp", "ArrowLeft"] +}; +var SLIDER_NAME = "Slider"; +var [Collection, useCollection, createCollectionScope] = createCollection(SLIDER_NAME); +var [createSliderContext, createSliderScope] = createContextScope(SLIDER_NAME, [ + createCollectionScope +]); +var [SliderProvider, useSliderContext] = createSliderContext(SLIDER_NAME); +var Slider = reactExports.forwardRef( + (props, forwardedRef) => { + const { + name, + min = 0, + max = 100, + step = 1, + orientation = "horizontal", + disabled = false, + minStepsBetweenThumbs = 0, + defaultValue = [min], + value, + onValueChange = () => { + }, + onValueCommit = () => { + }, + inverted = false, + form, + ...sliderProps + } = props; + const thumbRefs = reactExports.useRef(/* @__PURE__ */ new Set()); + const valueIndexToChangeRef = reactExports.useRef(0); + const isHorizontal = orientation === "horizontal"; + const SliderOrientation = isHorizontal ? SliderHorizontal : SliderVertical; + const [values = [], setValues] = useControllableState({ + prop: value, + defaultProp: defaultValue, + onChange: (value2) => { + const thumbs = [...thumbRefs.current]; + thumbs[valueIndexToChangeRef.current]?.focus(); + onValueChange(value2); + } + }); + const valuesBeforeSlideStartRef = reactExports.useRef(values); + function handleSlideStart(value2) { + const closestIndex = getClosestValueIndex(values, value2); + updateValues(value2, closestIndex); + } + function handleSlideMove(value2) { + updateValues(value2, valueIndexToChangeRef.current); + } + function handleSlideEnd() { + const prevValue = valuesBeforeSlideStartRef.current[valueIndexToChangeRef.current]; + const nextValue = values[valueIndexToChangeRef.current]; + const hasChanged = nextValue !== prevValue; + if (hasChanged) onValueCommit(values); + } + function updateValues(value2, atIndex, { commit } = { commit: false }) { + const decimalCount = getDecimalCount(step); + const snapToStep = roundValue(Math.round((value2 - min) / step) * step + min, decimalCount); + const nextValue = clamp(snapToStep, [min, max]); + setValues((prevValues = []) => { + const nextValues = getNextSortedValues(prevValues, nextValue, atIndex); + if (hasMinStepsBetweenValues(nextValues, minStepsBetweenThumbs * step)) { + valueIndexToChangeRef.current = nextValues.indexOf(nextValue); + const hasChanged = String(nextValues) !== String(prevValues); + if (hasChanged && commit) onValueCommit(nextValues); + return hasChanged ? nextValues : prevValues; + } else { + return prevValues; + } + }); + } + return /* @__PURE__ */ jsxRuntimeExports.jsx( + SliderProvider, + { + scope: props.__scopeSlider, + name, + disabled, + min, + max, + valueIndexToChangeRef, + thumbs: thumbRefs.current, + values, + orientation, + form, + children: /* @__PURE__ */ jsxRuntimeExports.jsx(Collection.Provider, { scope: props.__scopeSlider, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Collection.Slot, { scope: props.__scopeSlider, children: /* @__PURE__ */ jsxRuntimeExports.jsx( + SliderOrientation, + { + "aria-disabled": disabled, + "data-disabled": disabled ? "" : void 0, + ...sliderProps, + ref: forwardedRef, + onPointerDown: composeEventHandlers(sliderProps.onPointerDown, () => { + if (!disabled) valuesBeforeSlideStartRef.current = values; + }), + min, + max, + inverted, + onSlideStart: disabled ? void 0 : handleSlideStart, + onSlideMove: disabled ? void 0 : handleSlideMove, + onSlideEnd: disabled ? void 0 : handleSlideEnd, + onHomeKeyDown: () => !disabled && updateValues(min, 0, { commit: true }), + onEndKeyDown: () => !disabled && updateValues(max, values.length - 1, { commit: true }), + onStepKeyDown: ({ event, direction: stepDirection }) => { + if (!disabled) { + const isPageKey = PAGE_KEYS.includes(event.key); + const isSkipKey = isPageKey || event.shiftKey && ARROW_KEYS.includes(event.key); + const multiplier = isSkipKey ? 10 : 1; + const atIndex = valueIndexToChangeRef.current; + const value2 = values[atIndex]; + const stepInDirection = step * multiplier * stepDirection; + updateValues(value2 + stepInDirection, atIndex, { commit: true }); + } + } + } + ) }) }) + } + ); + } +); +Slider.displayName = SLIDER_NAME; +var [SliderOrientationProvider, useSliderOrientationContext] = createSliderContext(SLIDER_NAME, { + startEdge: "left", + endEdge: "right", + size: "width", + direction: 1 +}); +var SliderHorizontal = reactExports.forwardRef( + (props, forwardedRef) => { + const { + min, + max, + dir, + inverted, + onSlideStart, + onSlideMove, + onSlideEnd, + onStepKeyDown, + ...sliderProps + } = props; + const [slider, setSlider] = reactExports.useState(null); + const composedRefs = useComposedRefs(forwardedRef, (node) => setSlider(node)); + const rectRef = reactExports.useRef(void 0); + const direction = useDirection(dir); + const isDirectionLTR = direction === "ltr"; + const isSlidingFromLeft = isDirectionLTR && !inverted || !isDirectionLTR && inverted; + function getValueFromPointer(pointerPosition) { + const rect = rectRef.current || slider.getBoundingClientRect(); + const input = [0, rect.width]; + const output = isSlidingFromLeft ? [min, max] : [max, min]; + const value = linearScale(input, output); + rectRef.current = rect; + return value(pointerPosition - rect.left); + } + return /* @__PURE__ */ jsxRuntimeExports.jsx( + SliderOrientationProvider, + { + scope: props.__scopeSlider, + startEdge: isSlidingFromLeft ? "left" : "right", + endEdge: isSlidingFromLeft ? "right" : "left", + direction: isSlidingFromLeft ? 1 : -1, + size: "width", + children: /* @__PURE__ */ jsxRuntimeExports.jsx( + SliderImpl, + { + dir: direction, + "data-orientation": "horizontal", + ...sliderProps, + ref: composedRefs, + style: { + ...sliderProps.style, + ["--radix-slider-thumb-transform"]: "translateX(-50%)" + }, + onSlideStart: (event) => { + const value = getValueFromPointer(event.clientX); + onSlideStart?.(value); + }, + onSlideMove: (event) => { + const value = getValueFromPointer(event.clientX); + onSlideMove?.(value); + }, + onSlideEnd: () => { + rectRef.current = void 0; + onSlideEnd?.(); + }, + onStepKeyDown: (event) => { + const slideDirection = isSlidingFromLeft ? "from-left" : "from-right"; + const isBackKey = BACK_KEYS[slideDirection].includes(event.key); + onStepKeyDown?.({ event, direction: isBackKey ? -1 : 1 }); + } + } + ) + } + ); + } +); +var SliderVertical = reactExports.forwardRef( + (props, forwardedRef) => { + const { + min, + max, + inverted, + onSlideStart, + onSlideMove, + onSlideEnd, + onStepKeyDown, + ...sliderProps + } = props; + const sliderRef = reactExports.useRef(null); + const ref = useComposedRefs(forwardedRef, sliderRef); + const rectRef = reactExports.useRef(void 0); + const isSlidingFromBottom = !inverted; + function getValueFromPointer(pointerPosition) { + const rect = rectRef.current || sliderRef.current.getBoundingClientRect(); + const input = [0, rect.height]; + const output = isSlidingFromBottom ? [max, min] : [min, max]; + const value = linearScale(input, output); + rectRef.current = rect; + return value(pointerPosition - rect.top); + } + return /* @__PURE__ */ jsxRuntimeExports.jsx( + SliderOrientationProvider, + { + scope: props.__scopeSlider, + startEdge: isSlidingFromBottom ? "bottom" : "top", + endEdge: isSlidingFromBottom ? "top" : "bottom", + size: "height", + direction: isSlidingFromBottom ? 1 : -1, + children: /* @__PURE__ */ jsxRuntimeExports.jsx( + SliderImpl, + { + "data-orientation": "vertical", + ...sliderProps, + ref, + style: { + ...sliderProps.style, + ["--radix-slider-thumb-transform"]: "translateY(50%)" + }, + onSlideStart: (event) => { + const value = getValueFromPointer(event.clientY); + onSlideStart?.(value); + }, + onSlideMove: (event) => { + const value = getValueFromPointer(event.clientY); + onSlideMove?.(value); + }, + onSlideEnd: () => { + rectRef.current = void 0; + onSlideEnd?.(); + }, + onStepKeyDown: (event) => { + const slideDirection = isSlidingFromBottom ? "from-bottom" : "from-top"; + const isBackKey = BACK_KEYS[slideDirection].includes(event.key); + onStepKeyDown?.({ event, direction: isBackKey ? -1 : 1 }); + } + } + ) + } + ); + } +); +var SliderImpl = reactExports.forwardRef( + (props, forwardedRef) => { + const { + __scopeSlider, + onSlideStart, + onSlideMove, + onSlideEnd, + onHomeKeyDown, + onEndKeyDown, + onStepKeyDown, + ...sliderProps + } = props; + const context = useSliderContext(SLIDER_NAME, __scopeSlider); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + Primitive.span, + { + ...sliderProps, + ref: forwardedRef, + onKeyDown: composeEventHandlers(props.onKeyDown, (event) => { + if (event.key === "Home") { + onHomeKeyDown(event); + event.preventDefault(); + } else if (event.key === "End") { + onEndKeyDown(event); + event.preventDefault(); + } else if (PAGE_KEYS.concat(ARROW_KEYS).includes(event.key)) { + onStepKeyDown(event); + event.preventDefault(); + } + }), + onPointerDown: composeEventHandlers(props.onPointerDown, (event) => { + const target = event.target; + target.setPointerCapture(event.pointerId); + event.preventDefault(); + if (context.thumbs.has(target)) { + target.focus(); + } else { + onSlideStart(event); + } + }), + onPointerMove: composeEventHandlers(props.onPointerMove, (event) => { + const target = event.target; + if (target.hasPointerCapture(event.pointerId)) onSlideMove(event); + }), + onPointerUp: composeEventHandlers(props.onPointerUp, (event) => { + const target = event.target; + if (target.hasPointerCapture(event.pointerId)) { + target.releasePointerCapture(event.pointerId); + onSlideEnd(event); + } + }) + } + ); + } +); +var TRACK_NAME = "SliderTrack"; +var SliderTrack = reactExports.forwardRef( + (props, forwardedRef) => { + const { __scopeSlider, ...trackProps } = props; + const context = useSliderContext(TRACK_NAME, __scopeSlider); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + Primitive.span, + { + "data-disabled": context.disabled ? "" : void 0, + "data-orientation": context.orientation, + ...trackProps, + ref: forwardedRef + } + ); + } +); +SliderTrack.displayName = TRACK_NAME; +var RANGE_NAME = "SliderRange"; +var SliderRange = reactExports.forwardRef( + (props, forwardedRef) => { + const { __scopeSlider, ...rangeProps } = props; + const context = useSliderContext(RANGE_NAME, __scopeSlider); + const orientation = useSliderOrientationContext(RANGE_NAME, __scopeSlider); + const ref = reactExports.useRef(null); + const composedRefs = useComposedRefs(forwardedRef, ref); + const valuesCount = context.values.length; + const percentages = context.values.map( + (value) => convertValueToPercentage(value, context.min, context.max) + ); + const offsetStart = valuesCount > 1 ? Math.min(...percentages) : 0; + const offsetEnd = 100 - Math.max(...percentages); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + Primitive.span, + { + "data-orientation": context.orientation, + "data-disabled": context.disabled ? "" : void 0, + ...rangeProps, + ref: composedRefs, + style: { + ...props.style, + [orientation.startEdge]: offsetStart + "%", + [orientation.endEdge]: offsetEnd + "%" + } + } + ); + } +); +SliderRange.displayName = RANGE_NAME; +var THUMB_NAME = "SliderThumb"; +var SliderThumb = reactExports.forwardRef( + (props, forwardedRef) => { + const getItems = useCollection(props.__scopeSlider); + const [thumb, setThumb] = reactExports.useState(null); + const composedRefs = useComposedRefs(forwardedRef, (node) => setThumb(node)); + const index = reactExports.useMemo( + () => thumb ? getItems().findIndex((item) => item.ref.current === thumb) : -1, + [getItems, thumb] + ); + return /* @__PURE__ */ jsxRuntimeExports.jsx(SliderThumbImpl, { ...props, ref: composedRefs, index }); + } +); +var SliderThumbImpl = reactExports.forwardRef( + (props, forwardedRef) => { + const { __scopeSlider, index, name, ...thumbProps } = props; + const context = useSliderContext(THUMB_NAME, __scopeSlider); + const orientation = useSliderOrientationContext(THUMB_NAME, __scopeSlider); + const [thumb, setThumb] = reactExports.useState(null); + const composedRefs = useComposedRefs(forwardedRef, (node) => setThumb(node)); + const isFormControl = thumb ? context.form || !!thumb.closest("form") : true; + const size = useSize(thumb); + const value = context.values[index]; + const percent = value === void 0 ? 0 : convertValueToPercentage(value, context.min, context.max); + const label = getLabel(index, context.values.length); + const orientationSize = size?.[orientation.size]; + const thumbInBoundsOffset = orientationSize ? getThumbInBoundsOffset(orientationSize, percent, orientation.direction) : 0; + reactExports.useEffect(() => { + if (thumb) { + context.thumbs.add(thumb); + return () => { + context.thumbs.delete(thumb); + }; + } + }, [thumb, context.thumbs]); + return /* @__PURE__ */ jsxRuntimeExports.jsxs( + "span", + { + style: { + transform: "var(--radix-slider-thumb-transform)", + position: "absolute", + [orientation.startEdge]: `calc(${percent}% + ${thumbInBoundsOffset}px)` + }, + children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(Collection.ItemSlot, { scope: props.__scopeSlider, children: /* @__PURE__ */ jsxRuntimeExports.jsx( + Primitive.span, + { + role: "slider", + "aria-label": props["aria-label"] || label, + "aria-valuemin": context.min, + "aria-valuenow": value, + "aria-valuemax": context.max, + "aria-orientation": context.orientation, + "data-orientation": context.orientation, + "data-disabled": context.disabled ? "" : void 0, + tabIndex: context.disabled ? void 0 : 0, + ...thumbProps, + ref: composedRefs, + style: value === void 0 ? { display: "none" } : props.style, + onFocus: composeEventHandlers(props.onFocus, () => { + context.valueIndexToChangeRef.current = index; + }) + } + ) }), + isFormControl && /* @__PURE__ */ jsxRuntimeExports.jsx( + SliderBubbleInput, + { + name: name ?? (context.name ? context.name + (context.values.length > 1 ? "[]" : "") : void 0), + form: context.form, + value + }, + index + ) + ] + } + ); + } +); +SliderThumb.displayName = THUMB_NAME; +var BUBBLE_INPUT_NAME = "RadioBubbleInput"; +var SliderBubbleInput = reactExports.forwardRef( + ({ __scopeSlider, value, ...props }, forwardedRef) => { + const ref = reactExports.useRef(null); + const composedRefs = useComposedRefs(ref, forwardedRef); + const prevValue = usePrevious(value); + reactExports.useEffect(() => { + const input = ref.current; + if (!input) return; + const inputProto = window.HTMLInputElement.prototype; + const descriptor = Object.getOwnPropertyDescriptor(inputProto, "value"); + const setValue = descriptor.set; + if (prevValue !== value && setValue) { + const event = new Event("input", { bubbles: true }); + setValue.call(input, value); + input.dispatchEvent(event); + } + }, [prevValue, value]); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + Primitive.input, + { + style: { display: "none" }, + ...props, + ref: composedRefs, + defaultValue: value + } + ); + } +); +SliderBubbleInput.displayName = BUBBLE_INPUT_NAME; +function getNextSortedValues(prevValues = [], nextValue, atIndex) { + const nextValues = [...prevValues]; + nextValues[atIndex] = nextValue; + return nextValues.sort((a, b) => a - b); +} +function convertValueToPercentage(value, min, max) { + const maxSteps = max - min; + const percentPerStep = 100 / maxSteps; + const percentage = percentPerStep * (value - min); + return clamp(percentage, [0, 100]); +} +function getLabel(index, totalValues) { + if (totalValues > 2) { + return `Value ${index + 1} of ${totalValues}`; + } else if (totalValues === 2) { + return ["Minimum", "Maximum"][index]; + } else { + return void 0; + } +} +function getClosestValueIndex(values, nextValue) { + if (values.length === 1) return 0; + const distances = values.map((value) => Math.abs(value - nextValue)); + const closestDistance = Math.min(...distances); + return distances.indexOf(closestDistance); +} +function getThumbInBoundsOffset(width, left, direction) { + const halfWidth = width / 2; + const halfPercent = 50; + const offset = linearScale([0, halfPercent], [0, halfWidth]); + return (halfWidth - offset(left) * direction) * direction; +} +function getStepsBetweenValues(values) { + return values.slice(0, -1).map((value, index) => values[index + 1] - value); +} +function hasMinStepsBetweenValues(values, minStepsBetweenValues) { + if (minStepsBetweenValues > 0) { + const stepsBetweenValues = getStepsBetweenValues(values); + const actualMinStepsBetweenValues = Math.min(...stepsBetweenValues); + return actualMinStepsBetweenValues >= minStepsBetweenValues; + } + return true; +} +function linearScale(input, output) { + return (value) => { + if (input[0] === input[1] || output[0] === output[1]) return output[0]; + const ratio = (output[1] - output[0]) / (input[1] - input[0]); + return output[0] + ratio * (value - input[0]); + }; +} +function getDecimalCount(value) { + return (String(value).split(".")[1] || "").length; +} +function roundValue(value, decimalCount) { + const rounder = Math.pow(10, decimalCount); + return Math.round(value * rounder) / rounder; +} +var Root = Slider; +var Track = SliderTrack; +var Range$1 = SliderRange; +var Thumb = SliderThumb; + +const TldrawUiSlider = reactExports.memo(function Slider({ + onHistoryMark, + title, + steps, + value, + label, + onValueChange, + ["data-testid"]: testId +}) { + const msg = useTranslation(); + const handleValueChange = reactExports.useCallback( + (value2) => { + onValueChange(value2[0]); + }, + [onValueChange] + ); + const handlePointerDown = reactExports.useCallback(() => { + onHistoryMark("click slider"); + }, [onHistoryMark]); + const handlePointerUp = reactExports.useCallback(() => { + if (!value) return; + onValueChange(value); + }, [value, onValueChange]); + return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tlui-slider__container", children: /* @__PURE__ */ jsxRuntimeExports.jsxs( + Root, + { + "data-testid": testId, + className: "tlui-slider", + "area-label": "Opacity", + dir: "ltr", + min: 0, + max: steps, + step: 1, + value: value ? [value] : void 0, + onPointerDown: handlePointerDown, + onValueChange: handleValueChange, + onPointerUp: handlePointerUp, + title: title + " \u2014 " + msg(label), + children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(Track, { className: "tlui-slider__track", dir: "ltr", children: value !== null && /* @__PURE__ */ jsxRuntimeExports.jsx(Range$1, { className: "tlui-slider__range", dir: "ltr" }) }), + value !== null && /* @__PURE__ */ jsxRuntimeExports.jsx(Thumb, { className: "tlui-slider__thumb", dir: "ltr" }) + ] + } + ) }); +}); + +function DoubleDropdownPickerInner({ + label, + uiTypeA, + uiTypeB, + labelA, + labelB, + itemsA, + itemsB, + styleA, + styleB, + valueA, + valueB, + onValueChange +}) { + const msg = useTranslation(); + const iconA = reactExports.useMemo( + () => itemsA.find((item) => valueA.type === "shared" && valueA.value === item.value)?.icon ?? "mixed", + [itemsA, valueA] + ); + const iconB = reactExports.useMemo( + () => itemsB.find((item) => valueB.type === "shared" && valueB.value === item.value)?.icon ?? "mixed", + [itemsB, valueB] + ); + if (valueA === void 0 && valueB === void 0) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "tlui-style-panel__double-select-picker", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx("div", { title: msg(label), className: "tlui-style-panel__double-select-picker-label", children: msg(label) }), + /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "tlui-buttons__horizontal", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiDropdownMenuRoot, { id: `style panel ${uiTypeA} A`, children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiDropdownMenuTrigger, { children: /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButton, + { + type: "icon", + "data-testid": `style.${uiTypeA}`, + title: msg(labelA) + " \u2014 " + (valueA === null || valueA.type === "mixed" ? msg("style-panel.mixed") : msg(`${uiTypeA}-style.${valueA.value}`)), + children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonIcon, { icon: iconA, small: true, invertIcon: true }) + } + ) }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiDropdownMenuContent, { side: "left", align: "center", sideOffset: 80, alignOffset: 0, children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tlui-buttons__grid", children: itemsA.map((item, i) => { + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiDropdownMenuItem, { "data-testid": `style.${uiTypeA}.${item.value}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButton, + { + type: "icon", + onClick: () => onValueChange(styleA, item.value), + title: `${msg(labelA)} \u2014 ${msg(`${uiTypeA}-style.${item.value}`)}`, + children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonIcon, { icon: item.icon, invertIcon: true }) + }, + item.value + ) }, i); + }) }) }) + ] }), + /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiDropdownMenuRoot, { id: `style panel ${uiTypeB}`, children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiDropdownMenuTrigger, { children: /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButton, + { + type: "icon", + "data-testid": `style.${uiTypeB}`, + title: msg(labelB) + " \u2014 " + (valueB === null || valueB.type === "mixed" ? msg("style-panel.mixed") : msg(`${uiTypeB}-style.${valueB.value}`)), + children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonIcon, { icon: iconB, small: true }) + } + ) }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiDropdownMenuContent, { side: "left", align: "center", sideOffset: 116, alignOffset: 0, children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tlui-buttons__grid", children: itemsB.map((item) => { + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiDropdownMenuItem, { children: /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButton, + { + type: "icon", + title: `${msg(labelB)} \u2014 ${msg(`${uiTypeB}-style.${item.value}`)}`, + "data-testid": `style.${uiTypeB}.${item.value}`, + onClick: () => onValueChange(styleB, item.value), + children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonIcon, { icon: item.icon }) + } + ) }, item.value); + }) }) }) + ] }) + ] }) + ] }); +} +const DoubleDropdownPicker = reactExports.memo( + DoubleDropdownPickerInner +); + +function DropdownPickerInner({ + id, + label, + uiType, + stylePanelType, + style, + items, + type, + value, + onValueChange +}) { + const msg = useTranslation(); + const editor = useEditor(); + const icon = reactExports.useMemo( + () => items.find((item) => value.type === "shared" && item.value === value.value)?.icon, + [items, value] + ); + const stylePanelName = msg(`style-panel.${stylePanelType}`); + const titleStr = value.type === "mixed" ? msg("style-panel.mixed") : stylePanelName + " \u2014 " + msg(`${uiType}-style.${value.value}`); + const labelStr = label ? msg(label) : ""; + return /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiDropdownMenuRoot, { id: `style panel ${id}`, children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiDropdownMenuTrigger, { children: /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiButton, { type, "data-testid": `style.${uiType}`, title: titleStr, children: [ + labelStr && /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonLabel, { children: labelStr }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonIcon, { icon: icon ?? "mixed" }) + ] }) }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiDropdownMenuContent, { side: "left", align: "center", alignOffset: 0, children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tlui-buttons__grid", children: items.map((item) => { + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiDropdownMenuItem, { children: /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButton, + { + type: "icon", + "data-testid": `style.${uiType}.${item.value}`, + title: stylePanelName + " \u2014 " + msg(`${uiType}-style.${item.value}`), + onClick: () => { + editor.markHistoryStoppingPoint("select style dropdown item"); + onValueChange(style, item.value); + }, + children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonIcon, { icon: item.icon }) + } + ) }, item.value); + }) }) }) + ] }); +} +const DropdownPicker = reactExports.memo(DropdownPickerInner); + +function DefaultStylePanelContent({ styles }) { + const isDarkMode = useIsDarkMode(); + if (!styles) return null; + const geo = styles.get(GeoShapeGeoStyle); + const arrowheadEnd = styles.get(ArrowShapeArrowheadEndStyle); + const arrowheadStart = styles.get(ArrowShapeArrowheadStartStyle); + const spline = styles.get(LineShapeSplineStyle); + const font = styles.get(DefaultFontStyle); + const hideGeo = geo === void 0; + const hideArrowHeads = arrowheadEnd === void 0 && arrowheadStart === void 0; + const hideSpline = spline === void 0; + const hideText = font === void 0; + const theme = getDefaultColorTheme({ isDarkMode }); + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(CommonStylePickerSet, { theme, styles }), + !hideText && /* @__PURE__ */ jsxRuntimeExports.jsx(TextStylePickerSet, { theme, styles }), + !(hideGeo && hideArrowHeads && hideSpline) && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "tlui-style-panel__section", "aria-label": "style panel styles", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(GeoStylePickerSet, { styles }), + /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowheadStylePickerSet, { styles }), + /* @__PURE__ */ jsxRuntimeExports.jsx(SplineStylePickerSet, { styles }) + ] }) + ] }); +} +function useStyleChangeCallback() { + const editor = useEditor(); + const trackEvent = useUiEvents(); + return React.useMemo( + () => (function handleStyleChange(style, value) { + editor.run(() => { + if (editor.isIn("select")) { + editor.setStyleForSelectedShapes(style, value); + } + editor.setStyleForNextShapes(style, value); + editor.updateInstanceState({ isChangingStyle: true }); + }); + trackEvent("set-style", { source: "style-panel", id: style.id, value }); + }), + [editor, trackEvent] + ); +} +function CommonStylePickerSet({ styles, theme }) { + const msg = useTranslation(); + const editor = useEditor(); + const onHistoryMark = reactExports.useCallback((id) => editor.markHistoryStoppingPoint(id), [editor]); + const handleValueChange = useStyleChangeCallback(); + const color = styles.get(DefaultColorStyle); + const fill = styles.get(DefaultFillStyle); + const dash = styles.get(DefaultDashStyle); + const size = styles.get(DefaultSizeStyle); + const showPickers = fill !== void 0 || dash !== void 0 || size !== void 0; + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsxs( + "div", + { + tabIndex: -1, + className: "tlui-style-panel__section__common", + "aria-label": "style panel styles", + "data-testid": "style.panel", + children: [ + color === void 0 ? null : /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButtonPicker, + { + title: msg("style-panel.color"), + uiType: "color", + style: DefaultColorStyle, + items: STYLES.color, + value: color, + onValueChange: handleValueChange, + theme, + onHistoryMark + } + ), + /* @__PURE__ */ jsxRuntimeExports.jsx(OpacitySlider, {}) + ] + } + ), + showPickers && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "tlui-style-panel__section", "aria-label": "style panel styles", children: [ + fill === void 0 ? null : /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButtonPicker, + { + title: msg("style-panel.fill"), + uiType: "fill", + style: DefaultFillStyle, + items: STYLES.fill, + value: fill, + onValueChange: handleValueChange, + theme, + onHistoryMark + } + ), + dash === void 0 ? null : /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButtonPicker, + { + title: msg("style-panel.dash"), + uiType: "dash", + style: DefaultDashStyle, + items: STYLES.dash, + value: dash, + onValueChange: handleValueChange, + theme, + onHistoryMark + } + ), + size === void 0 ? null : /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButtonPicker, + { + title: msg("style-panel.size"), + uiType: "size", + style: DefaultSizeStyle, + items: STYLES.size, + value: size, + onValueChange: (style, value) => { + handleValueChange(style, value); + const selectedShapeIds = editor.getSelectedShapeIds(); + if (selectedShapeIds.length > 0) { + kickoutOccludedShapes(editor, selectedShapeIds); + } + }, + theme, + onHistoryMark + } + ) + ] }) + ] }); +} +function TextStylePickerSet({ theme, styles }) { + const msg = useTranslation(); + const handleValueChange = useStyleChangeCallback(); + const editor = useEditor(); + const onHistoryMark = reactExports.useCallback((id) => editor.markHistoryStoppingPoint(id), [editor]); + const font = styles.get(DefaultFontStyle); + const textAlign = styles.get(DefaultTextAlignStyle); + const labelAlign = styles.get(DefaultHorizontalAlignStyle); + const verticalLabelAlign = styles.get(DefaultVerticalAlignStyle); + if (font === void 0 && labelAlign === void 0) { + return null; + } + return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "tlui-style-panel__section", "aria-label": "style panel text", children: [ + font === void 0 ? null : /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButtonPicker, + { + title: msg("style-panel.font"), + uiType: "font", + style: DefaultFontStyle, + items: STYLES.font, + value: font, + onValueChange: handleValueChange, + theme, + onHistoryMark + } + ), + textAlign === void 0 ? null : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "tlui-style-panel__row", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButtonPicker, + { + title: msg("style-panel.align"), + uiType: "align", + style: DefaultTextAlignStyle, + items: STYLES.textAlign, + value: textAlign, + onValueChange: handleValueChange, + theme, + onHistoryMark + } + ), + /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tlui-style-panel__row__extra-button", children: /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButton, + { + type: "icon", + title: msg("style-panel.vertical-align"), + "data-testid": "vertical-align", + disabled: true, + children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonIcon, { icon: "vertical-align-middle" }) + } + ) }) + ] }), + labelAlign === void 0 ? null : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "tlui-style-panel__row", children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButtonPicker, + { + title: msg("style-panel.label-align"), + uiType: "align", + style: DefaultHorizontalAlignStyle, + items: STYLES.horizontalAlign, + value: labelAlign, + onValueChange: handleValueChange, + theme, + onHistoryMark + } + ), + /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tlui-style-panel__row__extra-button", children: verticalLabelAlign === void 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButton, + { + type: "icon", + title: msg("style-panel.vertical-align"), + "data-testid": "vertical-align", + disabled: true, + children: /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiButtonIcon, { icon: "vertical-align-middle" }) + } + ) : /* @__PURE__ */ jsxRuntimeExports.jsx( + DropdownPicker, + { + type: "icon", + id: "geo-vertical-alignment", + uiType: "verticalAlign", + stylePanelType: "vertical-align", + style: DefaultVerticalAlignStyle, + items: STYLES.verticalAlign, + value: verticalLabelAlign, + onValueChange: handleValueChange + } + ) }) + ] }) + ] }); +} +function GeoStylePickerSet({ styles }) { + const handleValueChange = useStyleChangeCallback(); + const geo = styles.get(GeoShapeGeoStyle); + if (geo === void 0) { + return null; + } + return /* @__PURE__ */ jsxRuntimeExports.jsx( + DropdownPicker, + { + id: "geo", + type: "menu", + label: "style-panel.geo", + uiType: "geo", + stylePanelType: "geo", + style: GeoShapeGeoStyle, + items: STYLES.geo, + value: geo, + onValueChange: handleValueChange + } + ); +} +function SplineStylePickerSet({ styles }) { + const handleValueChange = useStyleChangeCallback(); + const spline = styles.get(LineShapeSplineStyle); + if (spline === void 0) { + return null; + } + return /* @__PURE__ */ jsxRuntimeExports.jsx( + DropdownPicker, + { + id: "spline", + type: "menu", + label: "style-panel.spline", + uiType: "spline", + stylePanelType: "spline", + style: LineShapeSplineStyle, + items: STYLES.spline, + value: spline, + onValueChange: handleValueChange + } + ); +} +function ArrowheadStylePickerSet({ styles }) { + const handleValueChange = useStyleChangeCallback(); + const arrowheadEnd = styles.get(ArrowShapeArrowheadEndStyle); + const arrowheadStart = styles.get(ArrowShapeArrowheadStartStyle); + if (!arrowheadEnd || !arrowheadStart) { + return null; + } + return /* @__PURE__ */ jsxRuntimeExports.jsx( + DoubleDropdownPicker, + { + label: "style-panel.arrowheads", + uiTypeA: "arrowheadStart", + styleA: ArrowShapeArrowheadStartStyle, + itemsA: STYLES.arrowheadStart, + valueA: arrowheadStart, + uiTypeB: "arrowheadEnd", + styleB: ArrowShapeArrowheadEndStyle, + itemsB: STYLES.arrowheadEnd, + valueB: arrowheadEnd, + onValueChange: handleValueChange, + labelA: "style-panel.arrowhead-start", + labelB: "style-panel.arrowhead-end" + } + ); +} +const tldrawSupportedOpacities = [0.1, 0.25, 0.5, 0.75, 1]; +function OpacitySlider() { + const editor = useEditor(); + const onHistoryMark = reactExports.useCallback((id) => editor.markHistoryStoppingPoint(id), [editor]); + const opacity = useValue("opacity", () => editor.getSharedOpacity(), [editor]); + const trackEvent = useUiEvents(); + const msg = useTranslation(); + const handleOpacityValueChange = React.useCallback( + (value) => { + const item = tldrawSupportedOpacities[value]; + editor.run(() => { + if (editor.isIn("select")) { + editor.setOpacityForSelectedShapes(item); + } + editor.setOpacityForNextShapes(item); + editor.updateInstanceState({ isChangingStyle: true }); + }); + trackEvent("set-style", { source: "style-panel", id: "opacity", value }); + }, + [editor, trackEvent] + ); + if (opacity === void 0) return null; + const opacityIndex = opacity.type === "mixed" ? -1 : tldrawSupportedOpacities.indexOf( + minBy( + tldrawSupportedOpacities, + (supportedOpacity) => Math.abs(supportedOpacity - opacity.value) + ) + ); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiSlider, + { + "data-testid": "style.opacity", + value: opacityIndex >= 0 ? opacityIndex : tldrawSupportedOpacities.length - 1, + label: opacity.type === "mixed" ? "style-panel.mixed" : `opacity-style.${opacity.value}`, + onValueChange: handleOpacityValueChange, + steps: tldrawSupportedOpacities.length - 1, + title: msg("style-panel.opacity"), + onHistoryMark + } + ); +} + +const DefaultStylePanel = reactExports.memo(function DefaultStylePanel2({ + isMobile, + children +}) { + const editor = useEditor(); + const ref = reactExports.useRef(null); + usePassThroughWheelEvents(ref); + const styles = useRelevantStyles(); + const handlePointerOut = reactExports.useCallback(() => { + if (!isMobile) { + editor.updateInstanceState({ isChangingStyle: false }); + } + }, [editor, isMobile]); + const content = children ?? /* @__PURE__ */ jsxRuntimeExports.jsx(DefaultStylePanelContent, { styles }); + return /* @__PURE__ */ jsxRuntimeExports.jsx( + "div", + { + ref, + className: classNames("tlui-style-panel", { "tlui-style-panel__wrapper": !isMobile }), + "data-ismobile": isMobile, + onPointerLeave: handlePointerOut, + children: content + } + ); +}); + +function MobileStylePanel() { + const editor = useEditor(); + const msg = useTranslation(); + const relevantStyles = useRelevantStyles(); + const color = relevantStyles?.get(DefaultColorStyle); + const theme = getDefaultColorTheme({ isDarkMode: editor.user.getIsDarkMode() }); + const currentColor = (color?.type === "shared" ? theme[color.value] : theme.black).solid; + const disableStylePanel = useValue( + "disable style panel", + () => editor.isInAny("hand", "zoom", "eraser", "laser"), + [editor] + ); + const handleStylesOpenChange = reactExports.useCallback( + (isOpen) => { + if (!isOpen) { + editor.updateInstanceState({ isChangingStyle: false }); + } + }, + [editor] + ); + const { StylePanel } = useTldrawUiComponents(); + if (!StylePanel) return null; + return /* @__PURE__ */ jsxRuntimeExports.jsxs(TldrawUiPopover, { id: "mobile style menu", onOpenChange: handleStylesOpenChange, children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiPopoverTrigger, { children: /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButton, + { + type: "tool", + "data-testid": "mobile-styles.button", + style: { + color: disableStylePanel ? "var(--color-muted-1)" : currentColor + }, + title: msg("style-panel.title"), + disabled: disableStylePanel, + children: /* @__PURE__ */ jsxRuntimeExports.jsx( + TldrawUiButtonIcon, + { + icon: disableStylePanel ? "blob" : color?.type === "mixed" ? "mixed" : "blob" + } + ) + } + ) }), + /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiPopoverContent, { side: "top", align: "end", children: StylePanel && /* @__PURE__ */ jsxRuntimeExports.jsx(StylePanel, { isMobile: true }) }) + ] }); +} + +function DefaultToolbarContent() { + return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ + /* @__PURE__ */ jsxRuntimeExports.jsx(SelectToolbarItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(HandToolbarItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(DrawToolbarItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(EraserToolbarItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowToolbarItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(TextToolbarItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(NoteToolbarItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(AssetToolbarItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(RectangleToolbarItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(EllipseToolbarItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(TriangleToolbarItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(DiamondToolbarItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(HexagonToolbarItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(OvalToolbarItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(RhombusToolbarItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(StarToolbarItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(CloudToolbarItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(HeartToolbarItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(XBoxToolbarItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(CheckBoxToolbarItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowLeftToolbarItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowUpToolbarItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowDownToolbarItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowRightToolbarItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(LineToolbarItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(HighlightToolbarItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(LaserToolbarItem, {}), + /* @__PURE__ */ jsxRuntimeExports.jsx(FrameToolbarItem, {}) + ] }); +} +function useIsToolSelected(tool) { + const editor = useEditor(); + const geo = tool.meta?.geo; + return useValue( + "is tool selected", + () => { + const activeToolId = editor.getCurrentToolId(); + const geoState = editor.getSharedStyles().getAsKnownValue(GeoShapeGeoStyle); + return geo ? activeToolId === "geo" && geoState === geo : activeToolId === tool.id; + }, + [editor, tool.id, geo] + ); +} +function ToolbarItem({ tool }) { + const tools = useTools(); + const isSelected = useIsToolSelected(tools[tool]); + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuToolItem, { toolId: tool, isSelected }); +} +function SelectToolbarItem() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(ToolbarItem, { tool: "select" }); +} +function HandToolbarItem() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(ToolbarItem, { tool: "hand" }); +} +function DrawToolbarItem() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(ToolbarItem, { tool: "draw" }); +} +function EraserToolbarItem() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(ToolbarItem, { tool: "eraser" }); +} +function ArrowToolbarItem() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(ToolbarItem, { tool: "arrow" }); +} +function TextToolbarItem() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(ToolbarItem, { tool: "text" }); +} +function NoteToolbarItem() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(ToolbarItem, { tool: "note" }); +} +function AssetToolbarItem() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(TldrawUiMenuToolItem, { toolId: "asset" }); +} +function RectangleToolbarItem() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(ToolbarItem, { tool: "rectangle" }); +} +function EllipseToolbarItem() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(ToolbarItem, { tool: "ellipse" }); +} +function DiamondToolbarItem() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(ToolbarItem, { tool: "diamond" }); +} +function TriangleToolbarItem() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(ToolbarItem, { tool: "triangle" }); +} +function RhombusToolbarItem() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(ToolbarItem, { tool: "rhombus" }); +} +function HeartToolbarItem() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(ToolbarItem, { tool: "heart" }); +} +function HexagonToolbarItem() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(ToolbarItem, { tool: "hexagon" }); +} +function CloudToolbarItem() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(ToolbarItem, { tool: "cloud" }); +} +function StarToolbarItem() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(ToolbarItem, { tool: "star" }); +} +function OvalToolbarItem() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(ToolbarItem, { tool: "oval" }); +} +function XBoxToolbarItem() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(ToolbarItem, { tool: "x-box" }); +} +function CheckBoxToolbarItem() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(ToolbarItem, { tool: "check-box" }); +} +function ArrowLeftToolbarItem() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(ToolbarItem, { tool: "arrow-left" }); +} +function ArrowUpToolbarItem() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(ToolbarItem, { tool: "arrow-up" }); +} +function ArrowDownToolbarItem() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(ToolbarItem, { tool: "arrow-down" }); +} +function ArrowRightToolbarItem() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(ToolbarItem, { tool: "arrow-right" }); +} +function LineToolbarItem() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(ToolbarItem, { tool: "line" }); +} +function HighlightToolbarItem() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(ToolbarItem, { tool: "highlight" }); +} +function FrameToolbarItem() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(ToolbarItem, { tool: "frame" }); +} +function LaserToolbarItem() { + return /* @__PURE__ */ jsxRuntimeExports.jsx(ToolbarItem, { tool: "laser" }); +} + +/**! + * hotkeys-js v3.13.15 + * A simple micro-library for defining and dispatching keyboard shortcuts. It has no dependencies. + * + * Copyright (c) 2025 kenny wong + * https://github.com/jaywcjlove/hotkeys-js.git + * + * @website: https://jaywcjlove.github.io/hotkeys-js + + * Licensed under the MIT license + */ + +const isff = typeof navigator !== 'undefined' ? navigator.userAgent.toLowerCase().indexOf('firefox') > 0 : false; + +/** Bind event */ +function addEvent(object, event, method, useCapture) { + if (object.addEventListener) { + object.addEventListener(event, method, useCapture); + } else if (object.attachEvent) { + object.attachEvent("on".concat(event), method); + } +} +function removeEvent(object, event, method, useCapture) { + if (object.removeEventListener) { + object.removeEventListener(event, method, useCapture); + } else if (object.detachEvent) { + object.detachEvent("on".concat(event), method); + } +} + +/** Convert modifier keys to their corresponding key codes */ +function getMods(modifier, key) { + const mods = key.slice(0, key.length - 1); + for (let i = 0; i < mods.length; i++) mods[i] = modifier[mods[i].toLowerCase()]; + return mods; +} + +/** Process the input key string and convert it to an array */ +function getKeys$1(key) { + if (typeof key !== 'string') key = ''; + key = key.replace(/\s/g, ''); // Match any whitespace character, including spaces, tabs, form feeds, etc. + const keys = key.split(','); // Allow multiple shortcuts separated by ',' + let index = keys.lastIndexOf(''); + + // Shortcut may include ',' — special handling needed + for (; index >= 0;) { + keys[index - 1] += ','; + keys.splice(index, 1); + index = keys.lastIndexOf(''); + } + return keys; +} + +/** Compare arrays of modifier keys */ +function compareArray(a1, a2) { + const arr1 = a1.length >= a2.length ? a1 : a2; + const arr2 = a1.length >= a2.length ? a2 : a1; + let isIndex = true; + for (let i = 0; i < arr1.length; i++) { + if (arr2.indexOf(arr1[i]) === -1) isIndex = false; + } + return isIndex; +} + +// Special Keys +const _keyMap = { + backspace: 8, + '⌫': 8, + tab: 9, + clear: 12, + enter: 13, + '↩': 13, + return: 13, + esc: 27, + escape: 27, + space: 32, + left: 37, + up: 38, + right: 39, + down: 40, + /// https://w3c.github.io/uievents/#events-keyboard-key-location + arrowup: 38, + arrowdown: 40, + arrowleft: 37, + arrowright: 39, + del: 46, + delete: 46, + ins: 45, + insert: 45, + home: 36, + end: 35, + pageup: 33, + pagedown: 34, + capslock: 20, + num_0: 96, + num_1: 97, + num_2: 98, + num_3: 99, + num_4: 100, + num_5: 101, + num_6: 102, + num_7: 103, + num_8: 104, + num_9: 105, + num_multiply: 106, + num_add: 107, + num_enter: 108, + num_subtract: 109, + num_decimal: 110, + num_divide: 111, + '⇪': 20, + ',': 188, + '.': 190, + '/': 191, + '`': 192, + '-': isff ? 173 : 189, + '=': isff ? 61 : 187, + ';': isff ? 59 : 186, + '\'': 222, + '{': 219, + '}': 221, + '[': 219, + ']': 221, + '\\': 220 +}; + +// Modifier Keys +const _modifier = { + // shiftKey + '⇧': 16, + shift: 16, + // altKey + '⌥': 18, + alt: 18, + option: 18, + // ctrlKey + '⌃': 17, + ctrl: 17, + control: 17, + // metaKey + '⌘': 91, + cmd: 91, + meta: 91, + command: 91 +}; +const modifierMap = { + 16: 'shiftKey', + 18: 'altKey', + 17: 'ctrlKey', + 91: 'metaKey', + shiftKey: 16, + ctrlKey: 17, + altKey: 18, + metaKey: 91 +}; +const _mods = { + 16: false, + 18: false, + 17: false, + 91: false +}; +const _handlers = {}; + +// F1~F12 special key +for (let k = 1; k < 20; k++) { + _keyMap["f".concat(k)] = 111 + k; +} + +/** Record the pressed keys */ +let _downKeys = []; +/** Whether the window has already listened to the focus event */ +let winListendFocus = null; +/** Default hotkey scope */ +let _scope = 'all'; +/** Map to record elements with bound events */ +const elementEventMap = new Map(); + +/** Return key code */ +const code = x => _keyMap[x.toLowerCase()] || _modifier[x.toLowerCase()] || x.toUpperCase().charCodeAt(0); +const getKey = x => Object.keys(_keyMap).find(k => _keyMap[k] === x); +const getModifier = x => Object.keys(_modifier).find(k => _modifier[k] === x); + +/** Set or get the current scope (defaults to 'all') */ +function setScope(scope) { + _scope = scope || 'all'; +} +/** Get the current scope */ +function getScope() { + return _scope || 'all'; +} +/** Get the key codes of the currently pressed keys */ +function getPressedKeyCodes() { + return _downKeys.slice(0); +} +function getPressedKeyString() { + return _downKeys.map(c => getKey(c) || getModifier(c) || String.fromCharCode(c)); +} +function getAllKeyCodes() { + const result = []; + Object.keys(_handlers).forEach(k => { + _handlers[k].forEach(_ref => { + let { + key, + scope, + mods, + shortcut + } = _ref; + result.push({ + scope, + shortcut, + mods, + keys: key.split('+').map(v => code(v)) + }); + }); + }); + return result; +} + +/** hotkey is effective only when filter return true */ +function filter(event) { + const target = event.target || event.srcElement; + const { + tagName + } = target; + let flag = true; + const isInput = tagName === 'INPUT' && !['checkbox', 'radio', 'range', 'button', 'file', 'reset', 'submit', 'color'].includes(target.type); + // ignore: isContentEditable === 'true', and