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